feat: port OpenClaw bidirectional integration from omx

Ports the complete OpenClaw integration system from oh-my-codex:

Outbound (opencode→OpenClaw):
- wakeOpenClaw() fire-and-forget gateway notifications
- HTTP and command gateway dispatchers
- Template variable interpolation
- Config from oh-my-opencode.jsonc (no env gate needed)

Inbound (OpenClaw→opencode):
- Reply listener daemon (Discord/Telegram polling)
- Session registry for message↔tmux pane correlation
- Tmux pane detection, content capture, and text injection
- Input sanitization and rate limiting
- Pane verification before injection

Files:
- src/openclaw/ (types, config, dispatcher, index, reply-listener, session-registry, tmux, daemon)
- src/config/schema/openclaw.ts (Zod v4 schema)
- src/hooks/openclaw.ts (session hook)
- Tests: 12 pass (config + dispatcher)
This commit is contained in:
YeonGyu-Kim
2026-03-16 21:55:10 +09:00
parent 427fa6d7a2
commit b79df5e018
13 changed files with 1804 additions and 0 deletions
+55
View File
@@ -0,0 +1,55 @@
import type { PluginContext } from "../plugin/types"
import type { OhMyOpenCodeConfig } from "../config"
import { wakeOpenClaw } from "../openclaw"
import type { OpenClawContext } from "../openclaw/types"
export function createOpenClawHook(
ctx: PluginContext,
pluginConfig: OhMyOpenCodeConfig,
) {
const config = pluginConfig.openclaw
if (!config?.enabled) return null
const handleWake = async (event: string, context: OpenClawContext) => {
await wakeOpenClaw(config, event, context)
}
return {
event: async (input: any) => {
const { event } = input
const props = event.properties || {}
const sessionID = props.sessionID || props.info?.id
const context: OpenClawContext = {
sessionId: sessionID,
projectPath: ctx.directory,
}
if (event.type === "session.created") {
await handleWake("session-start", context)
} else if (event.type === "session.deleted") {
await handleWake("session-end", context)
} else if (event.type === "session.idle") {
// Check if we are waiting for user input (ask-user-question)
// This is heuristic. If the last message was from assistant and ended with a question?
// Or if the system is idle.
await handleWake("session-idle", context)
} else if (event.type === "session.stopped") { // Assuming this event exists or map from error?
await handleWake("stop", context)
}
},
toolExecuteBefore: async (input: any) => {
const { toolName, toolInput, sessionID } = input
if (toolName === "ask_user" || toolName === "ask_followup_question") {
const context: OpenClawContext = {
sessionId: sessionID,
projectPath: ctx.directory,
question: toolInput.question,
}
await handleWake("ask-user-question", context)
}
}
}
}