Revert "Merge pull request #2607 from code-yeongyu/feat/openclaw-integration"
This reverts commit8213534e87, reversing changes made to84fb1113f1.
This commit is contained in:
@@ -51,7 +51,6 @@ export const HookNameSchema = z.enum([
|
|||||||
"anthropic-effort",
|
"anthropic-effort",
|
||||||
"hashline-read-enhancer",
|
"hashline-read-enhancer",
|
||||||
"read-image-resizer",
|
"read-image-resizer",
|
||||||
"openclaw-sender",
|
|
||||||
])
|
])
|
||||||
|
|
||||||
export type HookName = z.infer<typeof HookNameSchema>
|
export type HookName = z.infer<typeof HookNameSchema>
|
||||||
|
|||||||
@@ -12,7 +12,6 @@ import { BuiltinCommandNameSchema } from "./commands"
|
|||||||
import { ExperimentalConfigSchema } from "./experimental"
|
import { ExperimentalConfigSchema } from "./experimental"
|
||||||
import { GitMasterConfigSchema } from "./git-master"
|
import { GitMasterConfigSchema } from "./git-master"
|
||||||
import { NotificationConfigSchema } from "./notification"
|
import { NotificationConfigSchema } from "./notification"
|
||||||
import { OpenClawConfigSchema } from "./openclaw"
|
|
||||||
import { RalphLoopConfigSchema } from "./ralph-loop"
|
import { RalphLoopConfigSchema } from "./ralph-loop"
|
||||||
import { RuntimeFallbackConfigSchema } from "./runtime-fallback"
|
import { RuntimeFallbackConfigSchema } from "./runtime-fallback"
|
||||||
import { SkillsConfigSchema } from "./skills"
|
import { SkillsConfigSchema } from "./skills"
|
||||||
@@ -56,7 +55,6 @@ export const OhMyOpenCodeConfigSchema = z.object({
|
|||||||
runtime_fallback: z.union([z.boolean(), RuntimeFallbackConfigSchema]).optional(),
|
runtime_fallback: z.union([z.boolean(), RuntimeFallbackConfigSchema]).optional(),
|
||||||
background_task: BackgroundTaskConfigSchema.optional(),
|
background_task: BackgroundTaskConfigSchema.optional(),
|
||||||
notification: NotificationConfigSchema.optional(),
|
notification: NotificationConfigSchema.optional(),
|
||||||
openclaw: OpenClawConfigSchema.optional(),
|
|
||||||
babysitting: BabysittingConfigSchema.optional(),
|
babysitting: BabysittingConfigSchema.optional(),
|
||||||
git_master: GitMasterConfigSchema.optional(),
|
git_master: GitMasterConfigSchema.optional(),
|
||||||
browser_automation_engine: BrowserAutomationConfigSchema.optional(),
|
browser_automation_engine: BrowserAutomationConfigSchema.optional(),
|
||||||
|
|||||||
@@ -1,51 +0,0 @@
|
|||||||
import { z } from "zod";
|
|
||||||
|
|
||||||
export const OpenClawHookEventSchema = z.enum([
|
|
||||||
"session-start",
|
|
||||||
"session-end",
|
|
||||||
"session-idle",
|
|
||||||
"ask-user-question",
|
|
||||||
"stop",
|
|
||||||
]);
|
|
||||||
|
|
||||||
export const OpenClawHttpGatewayConfigSchema = z.object({
|
|
||||||
type: z.literal("http").optional(),
|
|
||||||
url: z.string(), // Allow looser URL validation as it might contain placeholders
|
|
||||||
headers: z.record(z.string(), z.string()).optional(),
|
|
||||||
method: z.enum(["POST", "PUT"]).optional(),
|
|
||||||
timeout: z.number().optional(),
|
|
||||||
});
|
|
||||||
|
|
||||||
export const OpenClawCommandGatewayConfigSchema = z.object({
|
|
||||||
type: z.literal("command"),
|
|
||||||
command: z.string(),
|
|
||||||
timeout: z.number().optional(),
|
|
||||||
});
|
|
||||||
|
|
||||||
export const OpenClawGatewayConfigSchema = z.union([
|
|
||||||
OpenClawHttpGatewayConfigSchema,
|
|
||||||
OpenClawCommandGatewayConfigSchema,
|
|
||||||
]);
|
|
||||||
|
|
||||||
export const OpenClawHookMappingSchema = z.object({
|
|
||||||
gateway: z.string(),
|
|
||||||
instruction: z.string(),
|
|
||||||
enabled: z.boolean(),
|
|
||||||
});
|
|
||||||
|
|
||||||
export const OpenClawConfigSchema = z.object({
|
|
||||||
enabled: z.boolean(),
|
|
||||||
gateways: z.record(z.string(), OpenClawGatewayConfigSchema),
|
|
||||||
hooks: z
|
|
||||||
.object({
|
|
||||||
"session-start": OpenClawHookMappingSchema.optional(),
|
|
||||||
"session-end": OpenClawHookMappingSchema.optional(),
|
|
||||||
"session-idle": OpenClawHookMappingSchema.optional(),
|
|
||||||
"ask-user-question": OpenClawHookMappingSchema.optional(),
|
|
||||||
stop: OpenClawHookMappingSchema.optional(),
|
|
||||||
})
|
|
||||||
.strict()
|
|
||||||
.optional(),
|
|
||||||
});
|
|
||||||
|
|
||||||
export type OpenClawConfig = z.infer<typeof OpenClawConfigSchema>;
|
|
||||||
@@ -1,70 +0,0 @@
|
|||||||
import { wakeOpenClaw } from "../../openclaw/client";
|
|
||||||
import type { OpenClawConfig, OpenClawContext } from "../../openclaw/types";
|
|
||||||
import { getMainSessionID } from "../../features/claude-code-session-state";
|
|
||||||
import type { PluginContext } from "../../plugin/types";
|
|
||||||
|
|
||||||
export function createOpenClawSenderHook(
|
|
||||||
ctx: PluginContext,
|
|
||||||
config: OpenClawConfig
|
|
||||||
) {
|
|
||||||
return {
|
|
||||||
event: async (input: {
|
|
||||||
event: { type: string; properties?: Record<string, unknown> };
|
|
||||||
}) => {
|
|
||||||
const { type, properties } = input.event;
|
|
||||||
const info = properties?.info as Record<string, unknown> | undefined;
|
|
||||||
const context: OpenClawContext = {
|
|
||||||
sessionId:
|
|
||||||
(properties?.sessionID as string) ||
|
|
||||||
(info?.id as string) ||
|
|
||||||
getMainSessionID(),
|
|
||||||
projectPath: ctx.directory,
|
|
||||||
};
|
|
||||||
|
|
||||||
if (type === "session.created") {
|
|
||||||
await wakeOpenClaw("session-start", context, config);
|
|
||||||
} else if (type === "session.idle") {
|
|
||||||
await wakeOpenClaw("session-idle", context, config);
|
|
||||||
} else if (type === "session.deleted") {
|
|
||||||
await wakeOpenClaw("session-end", context, config);
|
|
||||||
}
|
|
||||||
},
|
|
||||||
|
|
||||||
"tool.execute.before": async (
|
|
||||||
input: { tool: string; sessionID: string },
|
|
||||||
output: { args: Record<string, unknown> }
|
|
||||||
) => {
|
|
||||||
const toolName = input.tool.toLowerCase();
|
|
||||||
const context: OpenClawContext = {
|
|
||||||
sessionId: input.sessionID,
|
|
||||||
projectPath: ctx.directory,
|
|
||||||
};
|
|
||||||
|
|
||||||
if (
|
|
||||||
toolName === "ask_user_question" ||
|
|
||||||
toolName === "askuserquestion" ||
|
|
||||||
toolName === "question"
|
|
||||||
) {
|
|
||||||
const question =
|
|
||||||
typeof output.args.question === "string"
|
|
||||||
? output.args.question
|
|
||||||
: undefined;
|
|
||||||
await wakeOpenClaw(
|
|
||||||
"ask-user-question",
|
|
||||||
{
|
|
||||||
...context,
|
|
||||||
question,
|
|
||||||
},
|
|
||||||
config
|
|
||||||
);
|
|
||||||
} else if (toolName === "skill") {
|
|
||||||
const rawName =
|
|
||||||
typeof output.args.name === "string" ? output.args.name : undefined;
|
|
||||||
const command = rawName?.replace(/^\//, "").toLowerCase();
|
|
||||||
if (command === "stop-continuation") {
|
|
||||||
await wakeOpenClaw("stop", context, config);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
},
|
|
||||||
};
|
|
||||||
}
|
|
||||||
@@ -1,41 +0,0 @@
|
|||||||
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();
|
|
||||||
});
|
|
||||||
});
|
|
||||||
});
|
|
||||||
@@ -1,40 +0,0 @@
|
|||||||
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);
|
|
||||||
});
|
|
||||||
});
|
|
||||||
@@ -1,78 +0,0 @@
|
|||||||
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;
|
|
||||||
});
|
|
||||||
});
|
|
||||||
});
|
|
||||||
@@ -1,256 +0,0 @@
|
|||||||
/**
|
|
||||||
* OpenClaw Integration - Client
|
|
||||||
*
|
|
||||||
* Wakes OpenClaw gateways on hook events. Non-blocking, fire-and-forget.
|
|
||||||
*
|
|
||||||
* Usage:
|
|
||||||
* wakeOpenClaw("session-start", { sessionId, projectPath: directory }, config);
|
|
||||||
*
|
|
||||||
* Activation requires OMX_OPENCLAW=1 env var and config in pluginConfig.openclaw.
|
|
||||||
*/
|
|
||||||
|
|
||||||
import {
|
|
||||||
type OpenClawConfig,
|
|
||||||
type OpenClawContext,
|
|
||||||
type OpenClawHookEvent,
|
|
||||||
type OpenClawResult,
|
|
||||||
type OpenClawGatewayConfig,
|
|
||||||
type OpenClawHttpGatewayConfig,
|
|
||||||
type OpenClawCommandGatewayConfig,
|
|
||||||
type OpenClawPayload,
|
|
||||||
} from "./types";
|
|
||||||
import {
|
|
||||||
interpolateInstruction,
|
|
||||||
isCommandGateway,
|
|
||||||
wakeCommandGateway,
|
|
||||||
wakeGateway,
|
|
||||||
} from "./dispatcher";
|
|
||||||
import { execSync } from "child_process";
|
|
||||||
import { basename } from "path";
|
|
||||||
|
|
||||||
/** Whether debug logging is enabled */
|
|
||||||
const DEBUG = process.env.OMX_OPENCLAW_DEBUG === "1";
|
|
||||||
|
|
||||||
// Helper for tmux session
|
|
||||||
function getCurrentTmuxSession(): string | undefined {
|
|
||||||
if (!process.env.TMUX) return undefined;
|
|
||||||
try {
|
|
||||||
// tmux display-message -p '#S'
|
|
||||||
const session = execSync("tmux display-message -p '#S'", {
|
|
||||||
encoding: "utf-8",
|
|
||||||
}).trim();
|
|
||||||
return session || undefined;
|
|
||||||
} catch {
|
|
||||||
return undefined;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Helper for tmux capture
|
|
||||||
function captureTmuxPane(paneId: string, lines: number): string | undefined {
|
|
||||||
try {
|
|
||||||
// tmux capture-pane -p -t {paneId} -S -{lines}
|
|
||||||
const output = execSync(
|
|
||||||
`tmux capture-pane -p -t "${paneId}" -S -${lines}`,
|
|
||||||
{ encoding: "utf-8" }
|
|
||||||
);
|
|
||||||
return output || undefined;
|
|
||||||
} catch {
|
|
||||||
return undefined;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Build a whitelisted context object from the input context.
|
|
||||||
* Only known fields are included to prevent accidental data leakage.
|
|
||||||
*/
|
|
||||||
function buildWhitelistedContext(context: OpenClawContext): OpenClawContext {
|
|
||||||
const result: OpenClawContext = {};
|
|
||||||
if (context.sessionId !== undefined) result.sessionId = context.sessionId;
|
|
||||||
if (context.projectPath !== undefined)
|
|
||||||
result.projectPath = context.projectPath;
|
|
||||||
if (context.tmuxSession !== undefined)
|
|
||||||
result.tmuxSession = context.tmuxSession;
|
|
||||||
if (context.prompt !== undefined) result.prompt = context.prompt;
|
|
||||||
if (context.contextSummary !== undefined)
|
|
||||||
result.contextSummary = context.contextSummary;
|
|
||||||
if (context.reason !== undefined) result.reason = context.reason;
|
|
||||||
if (context.question !== undefined) result.question = context.question;
|
|
||||||
if (context.tmuxTail !== undefined) result.tmuxTail = context.tmuxTail;
|
|
||||||
if (context.replyChannel !== undefined)
|
|
||||||
result.replyChannel = context.replyChannel;
|
|
||||||
if (context.replyTarget !== undefined)
|
|
||||||
result.replyTarget = context.replyTarget;
|
|
||||||
if (context.replyThread !== undefined)
|
|
||||||
result.replyThread = context.replyThread;
|
|
||||||
return result;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Resolve gateway config for a specific hook event.
|
|
||||||
* Returns null if the event is not mapped or disabled.
|
|
||||||
* Returns the gateway name alongside config to avoid O(n) reverse lookup.
|
|
||||||
*/
|
|
||||||
export function resolveGateway(
|
|
||||||
config: OpenClawConfig,
|
|
||||||
event: OpenClawHookEvent
|
|
||||||
): {
|
|
||||||
gatewayName: string;
|
|
||||||
gateway: OpenClawGatewayConfig;
|
|
||||||
instruction: string;
|
|
||||||
} | null {
|
|
||||||
const mapping = config.hooks?.[event];
|
|
||||||
if (!mapping || !mapping.enabled) {
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
const gateway = config.gateways?.[mapping.gateway];
|
|
||||||
if (!gateway) {
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
// Validate based on gateway type
|
|
||||||
if (gateway.type === "command") {
|
|
||||||
if (!gateway.command) return null;
|
|
||||||
} else {
|
|
||||||
// HTTP gateway (default when type is absent or "http")
|
|
||||||
if (!("url" in gateway) || !gateway.url) return null;
|
|
||||||
}
|
|
||||||
return {
|
|
||||||
gatewayName: mapping.gateway,
|
|
||||||
gateway,
|
|
||||||
instruction: mapping.instruction,
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Wake the OpenClaw gateway mapped to a hook event.
|
|
||||||
*
|
|
||||||
* This is the main entry point called from the notify hook.
|
|
||||||
* Non-blocking, swallows all errors. Returns null if OpenClaw
|
|
||||||
* is not configured or the event is not mapped.
|
|
||||||
*
|
|
||||||
* @param event - The hook event type
|
|
||||||
* @param context - Context data for template variable interpolation
|
|
||||||
* @param config - OpenClaw configuration
|
|
||||||
* @returns OpenClawResult or null if not configured/mapped
|
|
||||||
*/
|
|
||||||
export async function wakeOpenClaw(
|
|
||||||
event: OpenClawHookEvent,
|
|
||||||
context: OpenClawContext,
|
|
||||||
config?: OpenClawConfig
|
|
||||||
): Promise<OpenClawResult | null> {
|
|
||||||
try {
|
|
||||||
// Activation gate: only active when OMX_OPENCLAW=1
|
|
||||||
if (process.env.OMX_OPENCLAW !== "1") {
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!config || !config.enabled) return null;
|
|
||||||
|
|
||||||
const resolved = resolveGateway(config, event);
|
|
||||||
if (!resolved) return null;
|
|
||||||
|
|
||||||
const { gatewayName, gateway, instruction } = resolved;
|
|
||||||
const now = new Date().toISOString();
|
|
||||||
|
|
||||||
// Read originating channel context from env vars
|
|
||||||
const replyChannel =
|
|
||||||
context.replyChannel ?? process.env.OPENCLAW_REPLY_CHANNEL ?? undefined;
|
|
||||||
const replyTarget =
|
|
||||||
context.replyTarget ?? process.env.OPENCLAW_REPLY_TARGET ?? undefined;
|
|
||||||
const replyThread =
|
|
||||||
context.replyThread ?? process.env.OPENCLAW_REPLY_THREAD ?? undefined;
|
|
||||||
|
|
||||||
// Merge reply context
|
|
||||||
const enrichedContext: OpenClawContext = {
|
|
||||||
...context,
|
|
||||||
...(replyChannel !== undefined && { replyChannel }),
|
|
||||||
...(replyTarget !== undefined && { replyTarget }),
|
|
||||||
...(replyThread !== undefined && { replyThread }),
|
|
||||||
};
|
|
||||||
|
|
||||||
// Auto-detect tmux session
|
|
||||||
const tmuxSession =
|
|
||||||
enrichedContext.tmuxSession ?? getCurrentTmuxSession() ?? undefined;
|
|
||||||
|
|
||||||
// Auto-capture tmux pane content
|
|
||||||
let tmuxTail = enrichedContext.tmuxTail;
|
|
||||||
if (
|
|
||||||
!tmuxTail &&
|
|
||||||
(event === "stop" || event === "session-end") &&
|
|
||||||
process.env.TMUX
|
|
||||||
) {
|
|
||||||
const paneId = process.env.TMUX_PANE;
|
|
||||||
if (paneId) {
|
|
||||||
tmuxTail = captureTmuxPane(paneId, 15) ?? undefined;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Build template variables
|
|
||||||
const variables: Record<string, string | undefined> = {
|
|
||||||
sessionId: enrichedContext.sessionId,
|
|
||||||
projectPath: enrichedContext.projectPath,
|
|
||||||
projectName: enrichedContext.projectPath
|
|
||||||
? basename(enrichedContext.projectPath)
|
|
||||||
: undefined,
|
|
||||||
tmuxSession,
|
|
||||||
prompt: enrichedContext.prompt,
|
|
||||||
contextSummary: enrichedContext.contextSummary,
|
|
||||||
reason: enrichedContext.reason,
|
|
||||||
question: enrichedContext.question,
|
|
||||||
tmuxTail,
|
|
||||||
event,
|
|
||||||
timestamp: now,
|
|
||||||
replyChannel,
|
|
||||||
replyTarget,
|
|
||||||
replyThread,
|
|
||||||
};
|
|
||||||
|
|
||||||
// Interpolate instruction
|
|
||||||
const interpolatedInstruction = interpolateInstruction(
|
|
||||||
instruction,
|
|
||||||
variables
|
|
||||||
);
|
|
||||||
variables.instruction = interpolatedInstruction;
|
|
||||||
|
|
||||||
let result: OpenClawResult;
|
|
||||||
|
|
||||||
if (isCommandGateway(gateway)) {
|
|
||||||
result = await wakeCommandGateway(gatewayName, gateway, variables);
|
|
||||||
} else {
|
|
||||||
const payload: OpenClawPayload = {
|
|
||||||
event,
|
|
||||||
instruction: interpolatedInstruction,
|
|
||||||
text: interpolatedInstruction,
|
|
||||||
timestamp: now,
|
|
||||||
sessionId: enrichedContext.sessionId,
|
|
||||||
projectPath: enrichedContext.projectPath,
|
|
||||||
projectName: enrichedContext.projectPath
|
|
||||||
? basename(enrichedContext.projectPath)
|
|
||||||
: undefined,
|
|
||||||
tmuxSession,
|
|
||||||
tmuxTail,
|
|
||||||
...(replyChannel !== undefined && { channel: replyChannel }),
|
|
||||||
...(replyTarget !== undefined && { to: replyTarget }),
|
|
||||||
...(replyThread !== undefined && { threadId: replyThread }),
|
|
||||||
context: buildWhitelistedContext(enrichedContext),
|
|
||||||
};
|
|
||||||
result = await wakeGateway(gatewayName, gateway, payload);
|
|
||||||
}
|
|
||||||
|
|
||||||
if (DEBUG) {
|
|
||||||
console.error(
|
|
||||||
`[openclaw] wake ${event} -> ${gatewayName}: ${
|
|
||||||
result.success ? "ok" : result.error
|
|
||||||
}`
|
|
||||||
);
|
|
||||||
}
|
|
||||||
return result;
|
|
||||||
} catch (error) {
|
|
||||||
if (DEBUG) {
|
|
||||||
console.error(
|
|
||||||
`[openclaw] wakeOpenClaw error:`,
|
|
||||||
error instanceof Error ? error.message : error
|
|
||||||
);
|
|
||||||
}
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,317 +0,0 @@
|
|||||||
/**
|
|
||||||
* OpenClaw Gateway Dispatcher
|
|
||||||
*
|
|
||||||
* Sends instruction payloads to OpenClaw gateways via HTTP or CLI command.
|
|
||||||
* All calls are non-blocking with timeouts. Failures are swallowed
|
|
||||||
* to avoid blocking hooks.
|
|
||||||
*
|
|
||||||
* SECURITY: Command gateway requires OMX_OPENCLAW_COMMAND=1 opt-in.
|
|
||||||
* Command timeout is configurable with safe bounds.
|
|
||||||
* Prefers execFile for simple commands; falls back to sh -c only for shell metacharacters.
|
|
||||||
*/
|
|
||||||
|
|
||||||
import {
|
|
||||||
type OpenClawCommandGatewayConfig,
|
|
||||||
type OpenClawGatewayConfig,
|
|
||||||
type OpenClawHttpGatewayConfig,
|
|
||||||
type OpenClawPayload,
|
|
||||||
type OpenClawResult,
|
|
||||||
} from "./types";
|
|
||||||
import { exec, execFile } from "child_process";
|
|
||||||
|
|
||||||
/** Default per-request timeout for HTTP gateways */
|
|
||||||
const DEFAULT_HTTP_TIMEOUT_MS = 10_000;
|
|
||||||
/** Default command gateway timeout (backward-compatible default) */
|
|
||||||
const DEFAULT_COMMAND_TIMEOUT_MS = 5_000;
|
|
||||||
/**
|
|
||||||
* Command timeout safety bounds.
|
|
||||||
* - Minimum 100ms: avoids immediate/near-zero timeout misconfiguration.
|
|
||||||
* - Maximum 300000ms (5 minutes): prevents runaway long-lived command processes.
|
|
||||||
*/
|
|
||||||
const MIN_COMMAND_TIMEOUT_MS = 100;
|
|
||||||
const MAX_COMMAND_TIMEOUT_MS = 300_000;
|
|
||||||
|
|
||||||
/** Shell metacharacters that require sh -c instead of execFile */
|
|
||||||
const SHELL_METACHAR_RE = /[|&;><`$()]/;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Validate gateway URL. Must be HTTPS, except localhost/127.0.0.1/::1
|
|
||||||
* which allows HTTP for local development.
|
|
||||||
*/
|
|
||||||
export function validateGatewayUrl(url: string): boolean {
|
|
||||||
try {
|
|
||||||
const parsed = new URL(url);
|
|
||||||
if (parsed.protocol === "https:") return true;
|
|
||||||
if (
|
|
||||||
parsed.protocol === "http:" &&
|
|
||||||
(parsed.hostname === "localhost" ||
|
|
||||||
parsed.hostname === "127.0.0.1" ||
|
|
||||||
parsed.hostname === "::1" ||
|
|
||||||
parsed.hostname === "[::1]")
|
|
||||||
) {
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
return false;
|
|
||||||
} catch (err) {
|
|
||||||
process.stderr.write(`[openclaw-dispatcher] operation failed: ${err}\n`);
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Interpolate template variables in an instruction string.
|
|
||||||
*
|
|
||||||
* Supported variables (from hook context):
|
|
||||||
* - {{projectName}} - basename of project directory
|
|
||||||
* - {{projectPath}} - full project directory path
|
|
||||||
* - {{sessionId}} - session identifier
|
|
||||||
* - {{prompt}} - prompt text
|
|
||||||
* - {{contextSummary}} - context summary (session-end event)
|
|
||||||
* - {{question}} - question text (ask-user-question event)
|
|
||||||
* - {{timestamp}} - ISO timestamp
|
|
||||||
* - {{event}} - hook event name
|
|
||||||
* - {{instruction}} - interpolated instruction (for command gateway)
|
|
||||||
* - {{replyChannel}} - originating channel (from OPENCLAW_REPLY_CHANNEL env var)
|
|
||||||
* - {{replyTarget}} - reply target user/bot (from OPENCLAW_REPLY_TARGET env var)
|
|
||||||
* - {{replyThread}} - reply thread ID (from OPENCLAW_REPLY_THREAD env var)
|
|
||||||
*
|
|
||||||
* Unresolved variables are replaced with empty string.
|
|
||||||
*/
|
|
||||||
export function interpolateInstruction(
|
|
||||||
template: string,
|
|
||||||
variables: Record<string, string | undefined>
|
|
||||||
): string {
|
|
||||||
return template.replace(/\{\{(\w+)\}\}/g, (_match, key) => {
|
|
||||||
return variables[key] ?? "";
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Type guard: is this gateway config a command gateway?
|
|
||||||
*/
|
|
||||||
export function isCommandGateway(
|
|
||||||
config: OpenClawGatewayConfig
|
|
||||||
): config is OpenClawCommandGatewayConfig {
|
|
||||||
return config.type === "command";
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Shell-escape a string for safe embedding in a shell command.
|
|
||||||
* Uses single-quote wrapping with internal quote escaping.
|
|
||||||
*/
|
|
||||||
export function shellEscapeArg(value: string): string {
|
|
||||||
return "'" + value.replace(/'/g, "'\\''") + "'";
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Resolve command gateway timeout with precedence:
|
|
||||||
* gateway timeout > OMX_OPENCLAW_COMMAND_TIMEOUT_MS > default.
|
|
||||||
*/
|
|
||||||
export function resolveCommandTimeoutMs(
|
|
||||||
gatewayTimeout?: number,
|
|
||||||
envTimeoutRaw = process.env.OMX_OPENCLAW_COMMAND_TIMEOUT_MS
|
|
||||||
): number {
|
|
||||||
const parseFinite = (value: unknown): number | undefined => {
|
|
||||||
if (typeof value !== "number" || !Number.isFinite(value)) return undefined;
|
|
||||||
return value;
|
|
||||||
};
|
|
||||||
const parseEnv = (value: string | undefined): number | undefined => {
|
|
||||||
if (!value) return undefined;
|
|
||||||
const parsed = Number(value);
|
|
||||||
return Number.isFinite(parsed) ? parsed : undefined;
|
|
||||||
};
|
|
||||||
|
|
||||||
const rawTimeout =
|
|
||||||
parseFinite(gatewayTimeout) ??
|
|
||||||
parseEnv(envTimeoutRaw) ??
|
|
||||||
DEFAULT_COMMAND_TIMEOUT_MS;
|
|
||||||
|
|
||||||
return Math.min(
|
|
||||||
MAX_COMMAND_TIMEOUT_MS,
|
|
||||||
Math.max(MIN_COMMAND_TIMEOUT_MS, Math.trunc(rawTimeout))
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Wake an HTTP-type OpenClaw gateway with the given payload.
|
|
||||||
*/
|
|
||||||
export async function wakeGateway(
|
|
||||||
gatewayName: string,
|
|
||||||
gatewayConfig: OpenClawHttpGatewayConfig,
|
|
||||||
payload: OpenClawPayload
|
|
||||||
): Promise<OpenClawResult> {
|
|
||||||
if (!validateGatewayUrl(gatewayConfig.url)) {
|
|
||||||
return {
|
|
||||||
gateway: gatewayName,
|
|
||||||
success: false,
|
|
||||||
error: "Invalid URL (HTTPS required)",
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
try {
|
|
||||||
const headers = {
|
|
||||||
"Content-Type": "application/json",
|
|
||||||
...gatewayConfig.headers,
|
|
||||||
};
|
|
||||||
const timeout = gatewayConfig.timeout ?? DEFAULT_HTTP_TIMEOUT_MS;
|
|
||||||
|
|
||||||
const controller = new AbortController();
|
|
||||||
const timeoutId = setTimeout(() => controller.abort(), timeout);
|
|
||||||
|
|
||||||
const response = await fetch(gatewayConfig.url, {
|
|
||||||
method: gatewayConfig.method || "POST",
|
|
||||||
headers,
|
|
||||||
body: JSON.stringify(payload),
|
|
||||||
signal: controller.signal,
|
|
||||||
});
|
|
||||||
clearTimeout(timeoutId);
|
|
||||||
|
|
||||||
if (!response.ok) {
|
|
||||||
return {
|
|
||||||
gateway: gatewayName,
|
|
||||||
success: false,
|
|
||||||
error: `HTTP ${response.status}`,
|
|
||||||
statusCode: response.status,
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
return { gateway: gatewayName, success: true, statusCode: response.status };
|
|
||||||
} catch (error) {
|
|
||||||
return {
|
|
||||||
gateway: gatewayName,
|
|
||||||
success: false,
|
|
||||||
error: error instanceof Error ? error.message : "Unknown error",
|
|
||||||
};
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Wake a command-type OpenClaw gateway by executing a shell command.
|
|
||||||
*
|
|
||||||
* SECURITY REQUIREMENTS:
|
|
||||||
* - Requires OMX_OPENCLAW_COMMAND=1 opt-in (separate gate from OMX_OPENCLAW)
|
|
||||||
* - Timeout is configurable via gateway.timeout or OMX_OPENCLAW_COMMAND_TIMEOUT_MS
|
|
||||||
* with safe clamping bounds and backward-compatible default 5000ms
|
|
||||||
* - Prefers execFile for simple commands (no metacharacters)
|
|
||||||
* - Falls back to sh -c only when metacharacters detected
|
|
||||||
* - detached: false to prevent orphan processes
|
|
||||||
* - SIGTERM cleanup handler kills child on parent SIGTERM, 1s grace then SIGKILL
|
|
||||||
*
|
|
||||||
* The command template supports {{variable}} placeholders. All variable
|
|
||||||
* values are shell-escaped before interpolation to prevent injection.
|
|
||||||
*/
|
|
||||||
export async function wakeCommandGateway(
|
|
||||||
gatewayName: string,
|
|
||||||
gatewayConfig: OpenClawCommandGatewayConfig,
|
|
||||||
variables: Record<string, string | undefined>
|
|
||||||
): Promise<OpenClawResult> {
|
|
||||||
// Separate command gateway opt-in gate
|
|
||||||
if (process.env.OMX_OPENCLAW_COMMAND !== "1") {
|
|
||||||
return {
|
|
||||||
gateway: gatewayName,
|
|
||||||
success: false,
|
|
||||||
error: "Command gateway disabled (set OMX_OPENCLAW_COMMAND=1 to enable)",
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
let child: any = null;
|
|
||||||
let sigtermHandler: (() => void) | null = null;
|
|
||||||
|
|
||||||
try {
|
|
||||||
const timeout = resolveCommandTimeoutMs(gatewayConfig.timeout);
|
|
||||||
|
|
||||||
// Interpolate variables with shell escaping
|
|
||||||
const interpolated = gatewayConfig.command.replace(
|
|
||||||
/\{\{(\w+)\}\}/g,
|
|
||||||
(match, key) => {
|
|
||||||
const value = variables[key];
|
|
||||||
if (value === undefined) return match;
|
|
||||||
return shellEscapeArg(value);
|
|
||||||
}
|
|
||||||
);
|
|
||||||
|
|
||||||
// Detect whether the interpolated command contains shell metacharacters
|
|
||||||
const hasMetachars = SHELL_METACHAR_RE.test(interpolated);
|
|
||||||
|
|
||||||
await new Promise<void>((resolve, reject) => {
|
|
||||||
const cleanup = (signal: NodeJS.Signals) => {
|
|
||||||
if (child) {
|
|
||||||
child.kill(signal);
|
|
||||||
// 1s grace period then SIGKILL
|
|
||||||
setTimeout(() => {
|
|
||||||
try {
|
|
||||||
child?.kill("SIGKILL");
|
|
||||||
} catch (err) {
|
|
||||||
process.stderr.write(
|
|
||||||
`[openclaw-dispatcher] operation failed: ${err}\n`
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}, 1000);
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
sigtermHandler = () => cleanup("SIGTERM");
|
|
||||||
process.once("SIGTERM", sigtermHandler);
|
|
||||||
|
|
||||||
const onExit = (code: number | null, signal: NodeJS.Signals | null) => {
|
|
||||||
if (sigtermHandler) {
|
|
||||||
process.removeListener("SIGTERM", sigtermHandler);
|
|
||||||
sigtermHandler = null;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (signal) {
|
|
||||||
reject(new Error(`Command killed by signal ${signal}`));
|
|
||||||
} else if (code !== 0) {
|
|
||||||
reject(new Error(`Command exited with code ${code}`));
|
|
||||||
} else {
|
|
||||||
resolve();
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
const onError = (err: Error) => {
|
|
||||||
if (sigtermHandler) {
|
|
||||||
process.removeListener("SIGTERM", sigtermHandler);
|
|
||||||
sigtermHandler = null;
|
|
||||||
}
|
|
||||||
reject(err);
|
|
||||||
};
|
|
||||||
|
|
||||||
if (hasMetachars) {
|
|
||||||
// Fall back to sh -c for complex commands with metacharacters
|
|
||||||
child = exec(interpolated, {
|
|
||||||
timeout,
|
|
||||||
env: { ...process.env },
|
|
||||||
});
|
|
||||||
} else {
|
|
||||||
// Parse simple command: split on whitespace, use execFile
|
|
||||||
const parts = interpolated.split(/\s+/).filter(Boolean);
|
|
||||||
const cmd = parts[0];
|
|
||||||
const args = parts.slice(1);
|
|
||||||
child = execFile(cmd, args, {
|
|
||||||
timeout,
|
|
||||||
env: { ...process.env },
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
// Ensure detached is false (default, but explicit via options above)
|
|
||||||
if (child) {
|
|
||||||
child.on("exit", onExit);
|
|
||||||
child.on("error", onError);
|
|
||||||
} else {
|
|
||||||
reject(new Error("Failed to spawn process"));
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
return { gateway: gatewayName, success: true };
|
|
||||||
} catch (error) {
|
|
||||||
// Ensure SIGTERM handler is cleaned up on error
|
|
||||||
if (sigtermHandler) {
|
|
||||||
process.removeListener("SIGTERM", sigtermHandler as () => void);
|
|
||||||
}
|
|
||||||
return {
|
|
||||||
gateway: gatewayName,
|
|
||||||
success: false,
|
|
||||||
error: error instanceof Error ? error.message : "Unknown error",
|
|
||||||
};
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,10 +0,0 @@
|
|||||||
export { resolveGateway, wakeOpenClaw } from "./client";
|
|
||||||
export {
|
|
||||||
interpolateInstruction,
|
|
||||||
isCommandGateway,
|
|
||||||
shellEscapeArg,
|
|
||||||
validateGatewayUrl,
|
|
||||||
wakeCommandGateway,
|
|
||||||
wakeGateway,
|
|
||||||
} from "./dispatcher";
|
|
||||||
export * from "./types";
|
|
||||||
@@ -1,134 +0,0 @@
|
|||||||
/**
|
|
||||||
* OpenClaw Gateway Integration Types
|
|
||||||
*
|
|
||||||
* Defines types for the OpenClaw gateway waker system.
|
|
||||||
* Each hook event can be mapped to a gateway with a pre-defined instruction.
|
|
||||||
*/
|
|
||||||
|
|
||||||
/** Hook events that can trigger OpenClaw gateway calls */
|
|
||||||
export type OpenClawHookEvent =
|
|
||||||
| "session-start"
|
|
||||||
| "session-end"
|
|
||||||
| "session-idle"
|
|
||||||
| "ask-user-question"
|
|
||||||
| "stop";
|
|
||||||
|
|
||||||
/** HTTP gateway configuration (default when type is absent or "http") */
|
|
||||||
export interface OpenClawHttpGatewayConfig {
|
|
||||||
/** Gateway type discriminator (optional for backward compat) */
|
|
||||||
type?: "http";
|
|
||||||
/** Gateway endpoint URL (HTTPS required, HTTP allowed for localhost) */
|
|
||||||
url: string;
|
|
||||||
/** Optional custom headers (e.g., Authorization) */
|
|
||||||
headers?: Record<string, string>;
|
|
||||||
/** HTTP method (default: POST) */
|
|
||||||
method?: "POST" | "PUT";
|
|
||||||
/** Per-request timeout in ms (default: 10000) */
|
|
||||||
timeout?: number;
|
|
||||||
}
|
|
||||||
|
|
||||||
/** CLI command gateway configuration */
|
|
||||||
export interface OpenClawCommandGatewayConfig {
|
|
||||||
/** Gateway type discriminator */
|
|
||||||
type: "command";
|
|
||||||
/** Command template with {{variable}} placeholders.
|
|
||||||
* Variables are shell-escaped automatically before interpolation. */
|
|
||||||
command: string;
|
|
||||||
/**
|
|
||||||
* Per-command timeout in ms.
|
|
||||||
* Precedence: gateway timeout > OMX_OPENCLAW_COMMAND_TIMEOUT_MS > default (5000ms).
|
|
||||||
* Runtime clamps to safe bounds.
|
|
||||||
*/
|
|
||||||
timeout?: number;
|
|
||||||
}
|
|
||||||
|
|
||||||
/** Gateway configuration — HTTP or CLI command */
|
|
||||||
export type OpenClawGatewayConfig =
|
|
||||||
| OpenClawHttpGatewayConfig
|
|
||||||
| OpenClawCommandGatewayConfig;
|
|
||||||
|
|
||||||
/** Per-hook-event mapping to a gateway + instruction */
|
|
||||||
export interface OpenClawHookMapping {
|
|
||||||
/** Name of the gateway (key in gateways object) */
|
|
||||||
gateway: string;
|
|
||||||
/** Instruction template with {{variable}} placeholders */
|
|
||||||
instruction: string;
|
|
||||||
/** Whether this hook-event mapping is active */
|
|
||||||
enabled: boolean;
|
|
||||||
}
|
|
||||||
|
|
||||||
/** Top-level config schema for notifications.openclaw key in .omx-config.json */
|
|
||||||
export interface OpenClawConfig {
|
|
||||||
/** Global enable/disable */
|
|
||||||
enabled: boolean;
|
|
||||||
/** Named gateway endpoints */
|
|
||||||
gateways: Record<string, OpenClawGatewayConfig>;
|
|
||||||
/** Hook-event to gateway+instruction mappings */
|
|
||||||
hooks?: Partial<Record<OpenClawHookEvent, OpenClawHookMapping>>;
|
|
||||||
}
|
|
||||||
|
|
||||||
/** Payload sent to an OpenClaw gateway */
|
|
||||||
export interface OpenClawPayload {
|
|
||||||
/** The hook event that triggered this call */
|
|
||||||
event: OpenClawHookEvent;
|
|
||||||
/** Interpolated instruction text */
|
|
||||||
instruction: string;
|
|
||||||
/** Alias of instruction — allows OpenClaw /hooks/wake to consume the payload directly */
|
|
||||||
text: string;
|
|
||||||
/** ISO timestamp */
|
|
||||||
timestamp: string;
|
|
||||||
/** Session identifier (if available) */
|
|
||||||
sessionId?: string;
|
|
||||||
/** Project directory path */
|
|
||||||
projectPath?: string;
|
|
||||||
/** Project basename */
|
|
||||||
projectName?: string;
|
|
||||||
/** Tmux session name (if running inside tmux) */
|
|
||||||
tmuxSession?: string;
|
|
||||||
/** Recent tmux pane output (for stop/session-end events) */
|
|
||||||
tmuxTail?: string;
|
|
||||||
/** Originating channel for reply routing (if OPENCLAW_REPLY_CHANNEL is set) */
|
|
||||||
channel?: string;
|
|
||||||
/** Reply target user/bot (if OPENCLAW_REPLY_TARGET is set) */
|
|
||||||
to?: string;
|
|
||||||
/** Reply thread ID (if OPENCLAW_REPLY_THREAD is set) */
|
|
||||||
threadId?: string;
|
|
||||||
/** Context data from the hook (whitelisted fields only) */
|
|
||||||
context: OpenClawContext;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Context data passed from the hook to OpenClaw for template interpolation.
|
|
||||||
*
|
|
||||||
* All fields are explicitly enumerated (no index signature) to prevent
|
|
||||||
* accidental leakage of sensitive data into gateway payloads.
|
|
||||||
*/
|
|
||||||
export interface OpenClawContext {
|
|
||||||
sessionId?: string;
|
|
||||||
projectPath?: string;
|
|
||||||
tmuxSession?: string;
|
|
||||||
prompt?: string;
|
|
||||||
contextSummary?: string;
|
|
||||||
reason?: string;
|
|
||||||
question?: string;
|
|
||||||
/** Recent tmux pane output (captured automatically for stop/session-end events) */
|
|
||||||
tmuxTail?: string;
|
|
||||||
/** Originating channel for reply routing (from OPENCLAW_REPLY_CHANNEL env var) */
|
|
||||||
replyChannel?: string;
|
|
||||||
/** Reply target user/bot (from OPENCLAW_REPLY_TARGET env var) */
|
|
||||||
replyTarget?: string;
|
|
||||||
/** Reply thread ID for threaded conversations (from OPENCLAW_REPLY_THREAD env var) */
|
|
||||||
replyThread?: string;
|
|
||||||
}
|
|
||||||
|
|
||||||
/** Result of a gateway wake attempt */
|
|
||||||
export interface OpenClawResult {
|
|
||||||
/** Gateway name */
|
|
||||||
gateway: string;
|
|
||||||
/** Whether the call succeeded */
|
|
||||||
success: boolean;
|
|
||||||
/** Error message if failed */
|
|
||||||
error?: string;
|
|
||||||
/** HTTP status code if available */
|
|
||||||
statusCode?: number;
|
|
||||||
}
|
|
||||||
@@ -215,7 +215,6 @@ export function createEventHandler(args: {
|
|||||||
await Promise.resolve(hooks.compactionTodoPreserver?.event?.(input));
|
await Promise.resolve(hooks.compactionTodoPreserver?.event?.(input));
|
||||||
await Promise.resolve(hooks.writeExistingFileGuard?.event?.(input));
|
await Promise.resolve(hooks.writeExistingFileGuard?.event?.(input));
|
||||||
await Promise.resolve(hooks.atlasHook?.handler?.(input));
|
await Promise.resolve(hooks.atlasHook?.handler?.(input));
|
||||||
await Promise.resolve(hooks.openclawSender?.event?.(input));
|
|
||||||
await Promise.resolve(hooks.autoSlashCommand?.event?.(input));
|
await Promise.resolve(hooks.autoSlashCommand?.event?.(input));
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|||||||
@@ -26,7 +26,6 @@ import {
|
|||||||
createPreemptiveCompactionHook,
|
createPreemptiveCompactionHook,
|
||||||
createRuntimeFallbackHook,
|
createRuntimeFallbackHook,
|
||||||
} from "../../hooks"
|
} from "../../hooks"
|
||||||
import { createOpenClawSenderHook } from "../../hooks/openclaw-sender"
|
|
||||||
import { createAnthropicEffortHook } from "../../hooks/anthropic-effort"
|
import { createAnthropicEffortHook } from "../../hooks/anthropic-effort"
|
||||||
import {
|
import {
|
||||||
detectExternalNotificationPlugin,
|
detectExternalNotificationPlugin,
|
||||||
@@ -61,7 +60,6 @@ export type SessionHooks = {
|
|||||||
taskResumeInfo: ReturnType<typeof createTaskResumeInfoHook> | null
|
taskResumeInfo: ReturnType<typeof createTaskResumeInfoHook> | null
|
||||||
anthropicEffort: ReturnType<typeof createAnthropicEffortHook> | null
|
anthropicEffort: ReturnType<typeof createAnthropicEffortHook> | null
|
||||||
runtimeFallback: ReturnType<typeof createRuntimeFallbackHook> | null
|
runtimeFallback: ReturnType<typeof createRuntimeFallbackHook> | null
|
||||||
openclawSender: ReturnType<typeof createOpenClawSenderHook> | null
|
|
||||||
}
|
}
|
||||||
|
|
||||||
export function createSessionHooks(args: {
|
export function createSessionHooks(args: {
|
||||||
@@ -263,11 +261,6 @@ export function createSessionHooks(args: {
|
|||||||
pluginConfig,
|
pluginConfig,
|
||||||
}))
|
}))
|
||||||
: null
|
: null
|
||||||
|
|
||||||
const openclawSender = isHookEnabled("openclaw-sender") && pluginConfig.openclaw?.enabled
|
|
||||||
? safeHook("openclaw-sender", () => createOpenClawSenderHook(ctx, pluginConfig.openclaw!))
|
|
||||||
: null
|
|
||||||
|
|
||||||
return {
|
return {
|
||||||
contextWindowMonitor,
|
contextWindowMonitor,
|
||||||
preemptiveCompaction,
|
preemptiveCompaction,
|
||||||
@@ -292,6 +285,5 @@ export function createSessionHooks(args: {
|
|||||||
taskResumeInfo,
|
taskResumeInfo,
|
||||||
anthropicEffort,
|
anthropicEffort,
|
||||||
runtimeFallback,
|
runtimeFallback,
|
||||||
openclawSender,
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -33,7 +33,6 @@ export function createToolExecuteBeforeHandler(args: {
|
|||||||
await hooks.prometheusMdOnly?.["tool.execute.before"]?.(input, output)
|
await hooks.prometheusMdOnly?.["tool.execute.before"]?.(input, output)
|
||||||
await hooks.sisyphusJuniorNotepad?.["tool.execute.before"]?.(input, output)
|
await hooks.sisyphusJuniorNotepad?.["tool.execute.before"]?.(input, output)
|
||||||
await hooks.atlasHook?.["tool.execute.before"]?.(input, output)
|
await hooks.atlasHook?.["tool.execute.before"]?.(input, output)
|
||||||
await hooks.openclawSender?.["tool.execute.before"]?.(input, output)
|
|
||||||
|
|
||||||
const normalizedToolName = input.tool.toLowerCase()
|
const normalizedToolName = input.tool.toLowerCase()
|
||||||
if (
|
if (
|
||||||
|
|||||||
Reference in New Issue
Block a user