feat: implement OpenClaw integration

Ports the OMX OpenClaw module into oh-my-openagent as a first-class integration.
This integration allows forwarding internal events (session lifecycle, tool execution) to external gateways (HTTP or command-based).

- Added `src/openclaw` directory with implementation:
  - `dispatcher.ts`: Handles HTTP/Command dispatching with interpolation
  - `types.ts`: TypeScript definitions
  - `client.ts`: Main entry point `wakeOpenClaw`
  - `index.ts`: Public API
- Added `src/config/schema/openclaw.ts` for Zod schema validation
- Updated `src/config/schema/oh-my-opencode-config.ts` to include `openclaw` config
- Added `src/hooks/openclaw-sender/index.ts` to listen for events
- Registered the hook in `src/plugin/hooks/create-session-hooks.ts`
- Added unit tests in `src/openclaw/__tests__`

Events handled:
- `session-start` (via `session.created`)
- `session-end` (via `session.deleted`)
- `session-idle` (via `session.idle`)
- `ask-user-question` (via `tool.execute.before` for `ask_user_question`)
- `stop` (via `tool.execute.before` for `stop-continuation` command)
This commit is contained in:
YeonGyu-Kim
2026-03-16 17:21:56 +09:00
parent 84fb1113f1
commit 03b346ba51
14 changed files with 1006 additions and 0 deletions
+78
View File
@@ -0,0 +1,78 @@
import { describe, it, expect } from "bun:test";
import {
interpolateInstruction,
resolveCommandTimeoutMs,
shellEscapeArg,
validateGatewayUrl,
wakeCommandGateway,
} from "../dispatcher";
import { type OpenClawCommandGatewayConfig } from "../types";
describe("OpenClaw Dispatcher", () => {
describe("validateGatewayUrl", () => {
it("accepts valid https URLs", () => {
expect(validateGatewayUrl("https://example.com")).toBe(true);
});
it("rejects http URLs (remote)", () => {
expect(validateGatewayUrl("http://example.com")).toBe(false);
});
it("accepts http URLs for localhost", () => {
expect(validateGatewayUrl("http://localhost:3000")).toBe(true);
expect(validateGatewayUrl("http://127.0.0.1:8080")).toBe(true);
});
});
describe("interpolateInstruction", () => {
it("interpolates variables correctly", () => {
const result = interpolateInstruction("Hello {{name}}!", { name: "World" });
expect(result).toBe("Hello World!");
});
it("handles missing variables", () => {
const result = interpolateInstruction("Hello {{name}}!", {});
expect(result).toBe("Hello !");
});
});
describe("shellEscapeArg", () => {
it("escapes simple string", () => {
expect(shellEscapeArg("foo")).toBe("'foo'");
});
it("escapes string with single quotes", () => {
expect(shellEscapeArg("it's")).toBe("'it'\\''s'");
});
});
describe("resolveCommandTimeoutMs", () => {
it("uses default timeout", () => {
expect(resolveCommandTimeoutMs(undefined, undefined)).toBe(5000);
});
it("uses provided timeout", () => {
expect(resolveCommandTimeoutMs(1000, undefined)).toBe(1000);
});
it("clamps timeout", () => {
expect(resolveCommandTimeoutMs(10, undefined)).toBe(100);
expect(resolveCommandTimeoutMs(1000000, undefined)).toBe(300000);
});
});
describe("wakeCommandGateway", () => {
it("rejects if disabled via env", async () => {
const oldEnv = process.env.OMX_OPENCLAW_COMMAND;
process.env.OMX_OPENCLAW_COMMAND = "0";
const config: OpenClawCommandGatewayConfig = {
type: "command",
command: "echo hi",
};
const result = await wakeCommandGateway("test", config, {});
expect(result.success).toBe(false);
expect(result.error).toContain("disabled");
process.env.OMX_OPENCLAW_COMMAND = oldEnv;
});
});
});