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:
@@ -0,0 +1,41 @@
|
||||
import { describe, it, expect } from "bun:test";
|
||||
import { resolveGateway } from "../client";
|
||||
import { type OpenClawConfig } from "../types";
|
||||
|
||||
describe("OpenClaw Client", () => {
|
||||
describe("resolveGateway", () => {
|
||||
const config: OpenClawConfig = {
|
||||
enabled: true,
|
||||
gateways: {
|
||||
foo: { type: "command", command: "echo foo" },
|
||||
bar: { type: "http", url: "https://example.com" },
|
||||
},
|
||||
hooks: {
|
||||
"session-start": {
|
||||
gateway: "foo",
|
||||
instruction: "start",
|
||||
enabled: true,
|
||||
},
|
||||
"session-end": { gateway: "bar", instruction: "end", enabled: true },
|
||||
stop: { gateway: "foo", instruction: "stop", enabled: false },
|
||||
},
|
||||
};
|
||||
|
||||
it("resolves valid mapping", () => {
|
||||
const result = resolveGateway(config, "session-start");
|
||||
expect(result).not.toBeNull();
|
||||
expect(result?.gatewayName).toBe("foo");
|
||||
expect(result?.instruction).toBe("start");
|
||||
});
|
||||
|
||||
it("returns null for disabled hook", () => {
|
||||
const result = resolveGateway(config, "stop");
|
||||
expect(result).toBeNull();
|
||||
});
|
||||
|
||||
it("returns null for unmapped event", () => {
|
||||
const result = resolveGateway(config, "ask-user-question");
|
||||
expect(result).toBeNull();
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,40 @@
|
||||
import { describe, it, expect } from "bun:test";
|
||||
import { OpenClawConfigSchema } from "../../config/schema/openclaw";
|
||||
|
||||
describe("OpenClaw Config Schema", () => {
|
||||
it("validates correct config", () => {
|
||||
const raw = {
|
||||
enabled: true,
|
||||
gateways: {
|
||||
foo: { type: "command", command: "echo foo" },
|
||||
bar: { type: "http", url: "https://example.com" },
|
||||
},
|
||||
hooks: {
|
||||
"session-start": {
|
||||
gateway: "foo",
|
||||
instruction: "start",
|
||||
enabled: true,
|
||||
},
|
||||
},
|
||||
};
|
||||
const parsed = OpenClawConfigSchema.safeParse(raw);
|
||||
if (!parsed.success) console.log(parsed.error);
|
||||
expect(parsed.success).toBe(true);
|
||||
});
|
||||
|
||||
it("fails on invalid event", () => {
|
||||
const raw = {
|
||||
enabled: true,
|
||||
gateways: {},
|
||||
hooks: {
|
||||
"invalid-event": {
|
||||
gateway: "foo",
|
||||
instruction: "start",
|
||||
enabled: true,
|
||||
},
|
||||
},
|
||||
};
|
||||
const parsed = OpenClawConfigSchema.safeParse(raw);
|
||||
expect(parsed.success).toBe(false);
|
||||
});
|
||||
});
|
||||
@@ -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;
|
||||
});
|
||||
});
|
||||
});
|
||||
Reference in New Issue
Block a user