Fix OpenClaw review issues
This commit is contained in:
@@ -9,7 +9,7 @@ export const OpenClawGatewaySchema = z.object({
|
|||||||
// Command specific
|
// Command specific
|
||||||
command: z.string().optional(),
|
command: z.string().optional(),
|
||||||
// Shared
|
// Shared
|
||||||
timeout: z.number().default(10000),
|
timeout: z.number().optional(),
|
||||||
})
|
})
|
||||||
|
|
||||||
export const OpenClawHookSchema = z.object({
|
export const OpenClawHookSchema = z.object({
|
||||||
@@ -18,14 +18,7 @@ export const OpenClawHookSchema = z.object({
|
|||||||
instruction: z.string(),
|
instruction: z.string(),
|
||||||
})
|
})
|
||||||
|
|
||||||
export const OpenClawConfigSchema = z.object({
|
export const OpenClawReplyListenerConfigSchema = z.object({
|
||||||
enabled: z.boolean().default(false),
|
|
||||||
|
|
||||||
// Outbound Configuration
|
|
||||||
gateways: z.record(z.string(), OpenClawGatewaySchema).default({}),
|
|
||||||
hooks: z.record(z.string(), OpenClawHookSchema).default({}),
|
|
||||||
|
|
||||||
// Inbound Configuration (Reply Listener)
|
|
||||||
discordBotToken: z.string().optional(),
|
discordBotToken: z.string().optional(),
|
||||||
discordChannelId: z.string().optional(),
|
discordChannelId: z.string().optional(),
|
||||||
discordMention: z.string().optional(), // For allowed_mentions
|
discordMention: z.string().optional(), // For allowed_mentions
|
||||||
@@ -40,6 +33,18 @@ export const OpenClawConfigSchema = z.object({
|
|||||||
includePrefix: z.boolean().default(true),
|
includePrefix: z.boolean().default(true),
|
||||||
})
|
})
|
||||||
|
|
||||||
|
export const OpenClawConfigSchema = z.object({
|
||||||
|
enabled: z.boolean().default(false),
|
||||||
|
|
||||||
|
// Outbound Configuration
|
||||||
|
gateways: z.record(z.string(), OpenClawGatewaySchema).default({}),
|
||||||
|
hooks: z.record(z.string(), OpenClawHookSchema).default({}),
|
||||||
|
|
||||||
|
// Inbound Configuration (Reply Listener)
|
||||||
|
replyListener: OpenClawReplyListenerConfigSchema.optional(),
|
||||||
|
})
|
||||||
|
|
||||||
export type OpenClawConfig = z.infer<typeof OpenClawConfigSchema>
|
export type OpenClawConfig = z.infer<typeof OpenClawConfigSchema>
|
||||||
export type OpenClawGateway = z.infer<typeof OpenClawGatewaySchema>
|
export type OpenClawGateway = z.infer<typeof OpenClawGatewaySchema>
|
||||||
export type OpenClawHook = z.infer<typeof OpenClawHookSchema>
|
export type OpenClawHook = z.infer<typeof OpenClawHookSchema>
|
||||||
|
export type OpenClawReplyListenerConfig = z.infer<typeof OpenClawReplyListenerConfigSchema>
|
||||||
|
|||||||
@@ -0,0 +1,60 @@
|
|||||||
|
import { beforeEach, describe, expect, mock, test } from "bun:test"
|
||||||
|
|
||||||
|
const wakeOpenClawMock = mock(async () => null)
|
||||||
|
|
||||||
|
mock.module("../openclaw", () => ({
|
||||||
|
wakeOpenClaw: wakeOpenClawMock,
|
||||||
|
}))
|
||||||
|
|
||||||
|
describe("createOpenClawHook", () => {
|
||||||
|
beforeEach(() => {
|
||||||
|
wakeOpenClawMock.mockClear()
|
||||||
|
})
|
||||||
|
|
||||||
|
test("maps session.stop events to stop", async () => {
|
||||||
|
const { createOpenClawHook } = await import("./openclaw")
|
||||||
|
const hook = createOpenClawHook(
|
||||||
|
{ directory: "/tmp/project" } as any,
|
||||||
|
{ openclaw: { enabled: true } } as any,
|
||||||
|
)
|
||||||
|
|
||||||
|
await hook?.event?.({
|
||||||
|
event: {
|
||||||
|
type: "session.stop",
|
||||||
|
properties: { sessionID: "session-1" },
|
||||||
|
},
|
||||||
|
})
|
||||||
|
|
||||||
|
expect(wakeOpenClawMock).toHaveBeenCalledWith(
|
||||||
|
expect.anything(),
|
||||||
|
"stop",
|
||||||
|
expect.objectContaining({
|
||||||
|
projectPath: "/tmp/project",
|
||||||
|
sessionId: "session-1",
|
||||||
|
}),
|
||||||
|
)
|
||||||
|
})
|
||||||
|
|
||||||
|
test("uses tool.execute.before for question tools", async () => {
|
||||||
|
const { createOpenClawHook } = await import("./openclaw")
|
||||||
|
const hook = createOpenClawHook(
|
||||||
|
{ directory: "/tmp/project" } as any,
|
||||||
|
{ openclaw: { enabled: true } } as any,
|
||||||
|
)
|
||||||
|
|
||||||
|
await hook?.["tool.execute.before"]?.(
|
||||||
|
{ tool: "ask_user_question", sessionID: "session-2" },
|
||||||
|
{ args: { question: "Need approval?" } },
|
||||||
|
)
|
||||||
|
|
||||||
|
expect(wakeOpenClawMock).toHaveBeenCalledWith(
|
||||||
|
expect.anything(),
|
||||||
|
"ask-user-question",
|
||||||
|
expect.objectContaining({
|
||||||
|
projectPath: "/tmp/project",
|
||||||
|
question: "Need approval?",
|
||||||
|
sessionId: "session-2",
|
||||||
|
}),
|
||||||
|
)
|
||||||
|
})
|
||||||
|
})
|
||||||
+22
-13
@@ -3,7 +3,6 @@ import type { OhMyOpenCodeConfig } from "../config"
|
|||||||
import { wakeOpenClaw } from "../openclaw"
|
import { wakeOpenClaw } from "../openclaw"
|
||||||
import type { OpenClawContext } from "../openclaw/types"
|
import type { OpenClawContext } from "../openclaw/types"
|
||||||
|
|
||||||
|
|
||||||
export function createOpenClawHook(
|
export function createOpenClawHook(
|
||||||
ctx: PluginContext,
|
ctx: PluginContext,
|
||||||
pluginConfig: OhMyOpenCodeConfig,
|
pluginConfig: OhMyOpenCodeConfig,
|
||||||
@@ -35,21 +34,31 @@ export function createOpenClawHook(
|
|||||||
// This is heuristic. If the last message was from assistant and ended with a question?
|
// This is heuristic. If the last message was from assistant and ended with a question?
|
||||||
// Or if the system is idle.
|
// Or if the system is idle.
|
||||||
await handleWake("session-idle", context)
|
await handleWake("session-idle", context)
|
||||||
} else if (event.type === "session.stopped") { // Assuming this event exists or map from error?
|
} else if (event.type === "session.stop") {
|
||||||
await handleWake("stop", context)
|
await handleWake("stop", context)
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
|
||||||
toolExecuteBefore: async (input: any) => {
|
"tool.execute.before": async (
|
||||||
const { toolName, toolInput, sessionID } = input
|
input: { tool: string; sessionID: string },
|
||||||
if (toolName === "ask_user" || toolName === "ask_followup_question") {
|
output: { args: Record<string, unknown> },
|
||||||
const context: OpenClawContext = {
|
) => {
|
||||||
sessionId: sessionID,
|
const normalizedToolName = input.tool.toLowerCase()
|
||||||
projectPath: ctx.directory,
|
if (
|
||||||
question: toolInput.question,
|
normalizedToolName !== "question"
|
||||||
}
|
&& normalizedToolName !== "ask_user_question"
|
||||||
await handleWake("ask-user-question", context)
|
&& normalizedToolName !== "askuserquestion"
|
||||||
}
|
) {
|
||||||
}
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
const question = typeof output.args.question === "string" ? output.args.question : undefined
|
||||||
|
const context: OpenClawContext = {
|
||||||
|
sessionId: input.sessionID,
|
||||||
|
projectPath: ctx.directory,
|
||||||
|
question,
|
||||||
|
}
|
||||||
|
await handleWake("ask-user-question", context)
|
||||||
|
},
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,6 +1,7 @@
|
|||||||
import { describe, expect, test } from "bun:test"
|
import { describe, expect, test } from "bun:test"
|
||||||
import { resolveGateway, validateGatewayUrl, normalizeReplyListenerConfig } from "../config"
|
import { resolveGateway, validateGatewayUrl, normalizeReplyListenerConfig } from "../config"
|
||||||
import type { OpenClawConfig } from "../types"
|
import type { OpenClawConfig } from "../types"
|
||||||
|
import { OpenClawConfigSchema } from "../../config/schema/openclaw"
|
||||||
|
|
||||||
describe("OpenClaw Config", () => {
|
describe("OpenClaw Config", () => {
|
||||||
test("resolveGateway resolves HTTP gateway", () => {
|
test("resolveGateway resolves HTTP gateway", () => {
|
||||||
@@ -49,7 +50,7 @@ describe("OpenClaw Config", () => {
|
|||||||
test("resolveGateway returns null for disabled hook", () => {
|
test("resolveGateway returns null for disabled hook", () => {
|
||||||
const config: OpenClawConfig = {
|
const config: OpenClawConfig = {
|
||||||
enabled: true,
|
enabled: true,
|
||||||
gateways: { g: { url: "https://example.com" } },
|
gateways: { g: { type: "http", url: "https://example.com" } },
|
||||||
hooks: {
|
hooks: {
|
||||||
event: { enabled: false, gateway: "g", instruction: "i" },
|
event: { enabled: false, gateway: "g", instruction: "i" },
|
||||||
},
|
},
|
||||||
@@ -69,4 +70,46 @@ describe("OpenClaw Config", () => {
|
|||||||
expect(validateGatewayUrl("http://localhost:3000")).toBe(true)
|
expect(validateGatewayUrl("http://localhost:3000")).toBe(true)
|
||||||
expect(validateGatewayUrl("http://127.0.0.1:3000")).toBe(true)
|
expect(validateGatewayUrl("http://127.0.0.1:3000")).toBe(true)
|
||||||
})
|
})
|
||||||
|
|
||||||
|
test("normalizeReplyListenerConfig normalizes nested reply listener fields", () => {
|
||||||
|
const config = normalizeReplyListenerConfig({
|
||||||
|
enabled: true,
|
||||||
|
gateways: {},
|
||||||
|
hooks: {},
|
||||||
|
replyListener: {
|
||||||
|
discordBotToken: "discord-token",
|
||||||
|
discordChannelId: "channel-id",
|
||||||
|
authorizedDiscordUserIds: ["user-1", "", "user-2"],
|
||||||
|
pollIntervalMs: 100,
|
||||||
|
rateLimitPerMinute: 0,
|
||||||
|
maxMessageLength: 9000,
|
||||||
|
includePrefix: false,
|
||||||
|
},
|
||||||
|
} as OpenClawConfig)
|
||||||
|
|
||||||
|
expect(config.replyListener).toEqual({
|
||||||
|
discordBotToken: "discord-token",
|
||||||
|
discordChannelId: "channel-id",
|
||||||
|
authorizedDiscordUserIds: ["user-1", "user-2"],
|
||||||
|
pollIntervalMs: 500,
|
||||||
|
rateLimitPerMinute: 1,
|
||||||
|
maxMessageLength: 4000,
|
||||||
|
includePrefix: false,
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
test("gateway timeout remains optional so env fallback can apply", () => {
|
||||||
|
const parsed = OpenClawConfigSchema.parse({
|
||||||
|
enabled: true,
|
||||||
|
gateways: {
|
||||||
|
command: {
|
||||||
|
type: "command",
|
||||||
|
command: "echo hi",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
hooks: {},
|
||||||
|
})
|
||||||
|
|
||||||
|
expect(parsed.gateways.command.timeout).toBeUndefined()
|
||||||
|
})
|
||||||
})
|
})
|
||||||
|
|||||||
@@ -1,6 +1,7 @@
|
|||||||
import { describe, expect, test, mock, spyOn } from "bun:test"
|
import { describe, expect, test, mock, spyOn } from "bun:test"
|
||||||
import {
|
import {
|
||||||
interpolateInstruction,
|
interpolateInstruction,
|
||||||
|
resolveCommandTimeoutMs,
|
||||||
shellEscapeArg,
|
shellEscapeArg,
|
||||||
wakeGateway,
|
wakeGateway,
|
||||||
wakeCommandGateway,
|
wakeCommandGateway,
|
||||||
@@ -30,21 +31,22 @@ describe("OpenClaw Dispatcher", () => {
|
|||||||
const fetchSpy = spyOn(global, "fetch").mockResolvedValue(
|
const fetchSpy = spyOn(global, "fetch").mockResolvedValue(
|
||||||
new Response(JSON.stringify({ ok: true }), { status: 200 }),
|
new Response(JSON.stringify({ ok: true }), { status: 200 }),
|
||||||
)
|
)
|
||||||
|
try {
|
||||||
|
const result = await wakeGateway(
|
||||||
|
"test",
|
||||||
|
{ url: "https://example.com", method: "POST", timeout: 1000, type: "http" },
|
||||||
|
{ foo: "bar" },
|
||||||
|
)
|
||||||
|
|
||||||
const result = await wakeGateway(
|
expect(result.success).toBe(true)
|
||||||
"test",
|
expect(fetchSpy).toHaveBeenCalled()
|
||||||
{ url: "https://example.com", method: "POST", timeout: 1000, type: "http" },
|
const call = fetchSpy.mock.calls[0]
|
||||||
{ foo: "bar" },
|
expect(call[0]).toBe("https://example.com")
|
||||||
)
|
expect(call[1]?.method).toBe("POST")
|
||||||
|
expect(call[1]?.body).toBe('{"foo":"bar"}')
|
||||||
expect(result.success).toBe(true)
|
} finally {
|
||||||
expect(fetchSpy).toHaveBeenCalled()
|
fetchSpy.mockRestore()
|
||||||
const call = fetchSpy.mock.calls[0]
|
}
|
||||||
expect(call[0]).toBe("https://example.com")
|
|
||||||
expect(call[1]?.method).toBe("POST")
|
|
||||||
expect(call[1]?.body).toBe('{"foo":"bar"}')
|
|
||||||
|
|
||||||
fetchSpy.mockRestore()
|
|
||||||
})
|
})
|
||||||
|
|
||||||
test("wakeGateway fails on invalid URL", async () => {
|
test("wakeGateway fails on invalid URL", async () => {
|
||||||
@@ -52,4 +54,16 @@ describe("OpenClaw Dispatcher", () => {
|
|||||||
expect(result.success).toBe(false)
|
expect(result.success).toBe(false)
|
||||||
expect(result.error).toContain("Invalid URL")
|
expect(result.error).toContain("Invalid URL")
|
||||||
})
|
})
|
||||||
|
|
||||||
|
test("resolveCommandTimeoutMs reads OMO env fallback", () => {
|
||||||
|
const original = process.env.OMO_OPENCLAW_COMMAND_TIMEOUT_MS
|
||||||
|
process.env.OMO_OPENCLAW_COMMAND_TIMEOUT_MS = "4321"
|
||||||
|
|
||||||
|
try {
|
||||||
|
expect(resolveCommandTimeoutMs(undefined, process.env.OMO_OPENCLAW_COMMAND_TIMEOUT_MS)).toBe(4321)
|
||||||
|
} finally {
|
||||||
|
if (original === undefined) delete process.env.OMO_OPENCLAW_COMMAND_TIMEOUT_MS
|
||||||
|
else process.env.OMO_OPENCLAW_COMMAND_TIMEOUT_MS = original
|
||||||
|
}
|
||||||
|
})
|
||||||
})
|
})
|
||||||
|
|||||||
@@ -0,0 +1,13 @@
|
|||||||
|
import { describe, expect, test } from "bun:test"
|
||||||
|
import { analyzePaneContent } from "../tmux"
|
||||||
|
|
||||||
|
describe("openclaw tmux helpers", () => {
|
||||||
|
test("analyzePaneContent recognizes the opencode welcome prompt", () => {
|
||||||
|
const content = "opencode\nAsk anything...\nRun /help"
|
||||||
|
expect(analyzePaneContent(content).confidence).toBeGreaterThanOrEqual(1)
|
||||||
|
})
|
||||||
|
|
||||||
|
test("analyzePaneContent returns zero confidence for empty content", () => {
|
||||||
|
expect(analyzePaneContent(null).confidence).toBe(0)
|
||||||
|
})
|
||||||
|
})
|
||||||
+24
-17
@@ -1,4 +1,8 @@
|
|||||||
import type { OpenClawConfig, OpenClawGateway } from "./types"
|
import type {
|
||||||
|
OpenClawConfig,
|
||||||
|
OpenClawGateway,
|
||||||
|
OpenClawReplyListenerConfig,
|
||||||
|
} from "./types"
|
||||||
|
|
||||||
const DEFAULT_REPLY_POLL_INTERVAL_MS = 3000
|
const DEFAULT_REPLY_POLL_INTERVAL_MS = 3000
|
||||||
const MIN_REPLY_POLL_INTERVAL_MS = 500
|
const MIN_REPLY_POLL_INTERVAL_MS = 500
|
||||||
@@ -29,41 +33,44 @@ function normalizeInteger(
|
|||||||
}
|
}
|
||||||
|
|
||||||
export function normalizeReplyListenerConfig(config: OpenClawConfig): OpenClawConfig {
|
export function normalizeReplyListenerConfig(config: OpenClawConfig): OpenClawConfig {
|
||||||
const discordEnabled =
|
const replyListener = config.replyListener
|
||||||
config.discordBotToken && config.discordChannelId ? true : false
|
if (!replyListener) return config
|
||||||
const telegramEnabled =
|
|
||||||
config.telegramBotToken && config.telegramChatId ? true : false
|
|
||||||
|
|
||||||
return {
|
const normalizedReplyListener: OpenClawReplyListenerConfig = {
|
||||||
...config,
|
...replyListener,
|
||||||
discordBotToken: config.discordBotToken,
|
discordBotToken: replyListener.discordBotToken,
|
||||||
discordChannelId: config.discordChannelId,
|
discordChannelId: replyListener.discordChannelId,
|
||||||
telegramBotToken: config.telegramBotToken,
|
telegramBotToken: replyListener.telegramBotToken,
|
||||||
telegramChatId: config.telegramChatId,
|
telegramChatId: replyListener.telegramChatId,
|
||||||
pollIntervalMs: normalizeInteger(
|
pollIntervalMs: normalizeInteger(
|
||||||
config.pollIntervalMs,
|
replyListener.pollIntervalMs,
|
||||||
DEFAULT_REPLY_POLL_INTERVAL_MS,
|
DEFAULT_REPLY_POLL_INTERVAL_MS,
|
||||||
MIN_REPLY_POLL_INTERVAL_MS,
|
MIN_REPLY_POLL_INTERVAL_MS,
|
||||||
MAX_REPLY_POLL_INTERVAL_MS,
|
MAX_REPLY_POLL_INTERVAL_MS,
|
||||||
),
|
),
|
||||||
rateLimitPerMinute: normalizeInteger(
|
rateLimitPerMinute: normalizeInteger(
|
||||||
config.rateLimitPerMinute,
|
replyListener.rateLimitPerMinute,
|
||||||
DEFAULT_REPLY_RATE_LIMIT_PER_MINUTE,
|
DEFAULT_REPLY_RATE_LIMIT_PER_MINUTE,
|
||||||
MIN_REPLY_RATE_LIMIT_PER_MINUTE,
|
MIN_REPLY_RATE_LIMIT_PER_MINUTE,
|
||||||
),
|
),
|
||||||
maxMessageLength: normalizeInteger(
|
maxMessageLength: normalizeInteger(
|
||||||
config.maxMessageLength,
|
replyListener.maxMessageLength,
|
||||||
DEFAULT_REPLY_MAX_MESSAGE_LENGTH,
|
DEFAULT_REPLY_MAX_MESSAGE_LENGTH,
|
||||||
MIN_REPLY_MAX_MESSAGE_LENGTH,
|
MIN_REPLY_MAX_MESSAGE_LENGTH,
|
||||||
MAX_REPLY_MAX_MESSAGE_LENGTH,
|
MAX_REPLY_MAX_MESSAGE_LENGTH,
|
||||||
),
|
),
|
||||||
includePrefix: config.includePrefix !== false,
|
includePrefix: replyListener.includePrefix !== false,
|
||||||
authorizedDiscordUserIds: Array.isArray(config.authorizedDiscordUserIds)
|
authorizedDiscordUserIds: Array.isArray(replyListener.authorizedDiscordUserIds)
|
||||||
? config.authorizedDiscordUserIds.filter(
|
? replyListener.authorizedDiscordUserIds.filter(
|
||||||
(id) => typeof id === "string" && id.trim() !== "",
|
(id) => typeof id === "string" && id.trim() !== "",
|
||||||
)
|
)
|
||||||
: [],
|
: [],
|
||||||
}
|
}
|
||||||
|
|
||||||
|
return {
|
||||||
|
...config,
|
||||||
|
replyListener: normalizedReplyListener,
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
export function resolveGateway(
|
export function resolveGateway(
|
||||||
|
|||||||
@@ -1,6 +1,9 @@
|
|||||||
import { pollLoop } from "./reply-listener"
|
import { pollLoop, logReplyListenerMessage } from "./reply-listener"
|
||||||
|
|
||||||
pollLoop().catch((err) => {
|
pollLoop().catch((err) => {
|
||||||
|
logReplyListenerMessage(
|
||||||
|
`FATAL: reply listener daemon crashed: ${err instanceof Error ? err.stack ?? err.message : String(err)}`,
|
||||||
|
)
|
||||||
console.error(err)
|
console.error(err)
|
||||||
process.exit(1)
|
process.exit(1)
|
||||||
})
|
})
|
||||||
|
|||||||
@@ -41,7 +41,9 @@ export function shellEscapeArg(value: string): string {
|
|||||||
|
|
||||||
export function resolveCommandTimeoutMs(
|
export function resolveCommandTimeoutMs(
|
||||||
gatewayTimeout?: number,
|
gatewayTimeout?: number,
|
||||||
envTimeoutRaw = process.env.OMX_OPENCLAW_COMMAND_TIMEOUT_MS,
|
envTimeoutRaw =
|
||||||
|
process.env.OMO_OPENCLAW_COMMAND_TIMEOUT_MS
|
||||||
|
?? process.env.OMX_OPENCLAW_COMMAND_TIMEOUT_MS,
|
||||||
): number {
|
): number {
|
||||||
const parseFinite = (value: unknown): number | undefined => {
|
const parseFinite = (value: unknown): number | undefined => {
|
||||||
if (typeof value !== "number" || !Number.isFinite(value)) return undefined
|
if (typeof value !== "number" || !Number.isFinite(value)) return undefined
|
||||||
@@ -93,10 +95,10 @@ export async function wakeGateway(
|
|||||||
headers,
|
headers,
|
||||||
body: JSON.stringify(payload),
|
body: JSON.stringify(payload),
|
||||||
signal: controller.signal,
|
signal: controller.signal,
|
||||||
|
}).finally(() => {
|
||||||
|
clearTimeout(timeoutId)
|
||||||
})
|
})
|
||||||
|
|
||||||
clearTimeout(timeoutId)
|
|
||||||
|
|
||||||
if (!response.ok) {
|
if (!response.ok) {
|
||||||
return {
|
return {
|
||||||
gateway: gatewayName,
|
gateway: gatewayName,
|
||||||
@@ -133,7 +135,7 @@ export async function wakeCommandGateway(
|
|||||||
const timeout = resolveCommandTimeoutMs(gatewayConfig.timeout)
|
const timeout = resolveCommandTimeoutMs(gatewayConfig.timeout)
|
||||||
|
|
||||||
// Interpolate variables with shell escaping
|
// Interpolate variables with shell escaping
|
||||||
let interpolated = gatewayConfig.command.replace(/\{\{(\w+)\}\}/g, (_match, key) => {
|
const interpolated = gatewayConfig.command.replace(/\{\{(\w+)\}\}/g, (_match, key) => {
|
||||||
const value = variables[key]
|
const value = variables[key]
|
||||||
if (value === undefined) return _match
|
if (value === undefined) return _match
|
||||||
return shellEscapeArg(value)
|
return shellEscapeArg(value)
|
||||||
@@ -147,14 +149,21 @@ export async function wakeCommandGateway(
|
|||||||
})
|
})
|
||||||
|
|
||||||
// Handle timeout manually
|
// Handle timeout manually
|
||||||
const timeoutPromise = new Promise<number>((_, reject) => {
|
let timeoutId: ReturnType<typeof setTimeout> | undefined
|
||||||
setTimeout(() => {
|
const timeoutPromise = new Promise<never>((_, reject) => {
|
||||||
|
timeoutId = setTimeout(() => {
|
||||||
proc.kill()
|
proc.kill()
|
||||||
reject(new Error("Command timed out"))
|
reject(new Error("Command timed out"))
|
||||||
}, timeout)
|
}, timeout)
|
||||||
})
|
})
|
||||||
|
|
||||||
await Promise.race([proc.exited, timeoutPromise])
|
try {
|
||||||
|
await Promise.race([proc.exited, timeoutPromise])
|
||||||
|
} finally {
|
||||||
|
if (timeoutId !== undefined) {
|
||||||
|
clearTimeout(timeoutId)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
if (proc.exitCode !== 0) {
|
if (proc.exitCode !== 0) {
|
||||||
throw new Error(`Command exited with code ${proc.exitCode}`)
|
throw new Error(`Command exited with code ${proc.exitCode}`)
|
||||||
@@ -169,4 +178,3 @@ export async function wakeCommandGateway(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
+14
-6
@@ -9,7 +9,9 @@ import { getCurrentTmuxSession, captureTmuxPane } from "./tmux"
|
|||||||
import { startReplyListener, stopReplyListener } from "./reply-listener"
|
import { startReplyListener, stopReplyListener } from "./reply-listener"
|
||||||
import type { OpenClawConfig, OpenClawContext, OpenClawPayload, WakeResult } from "./types"
|
import type { OpenClawConfig, OpenClawContext, OpenClawPayload, WakeResult } from "./types"
|
||||||
|
|
||||||
const DEBUG = process.env.OMX_OPENCLAW_DEBUG === "1"
|
const DEBUG =
|
||||||
|
process.env.OMO_OPENCLAW_DEBUG === "1"
|
||||||
|
|| process.env.OMX_OPENCLAW_DEBUG === "1"
|
||||||
|
|
||||||
function buildWhitelistedContext(context: OpenClawContext): OpenClawContext {
|
function buildWhitelistedContext(context: OpenClawContext): OpenClawContext {
|
||||||
const result: OpenClawContext = {}
|
const result: OpenClawContext = {}
|
||||||
@@ -18,7 +20,7 @@ function buildWhitelistedContext(context: OpenClawContext): OpenClawContext {
|
|||||||
if (context.tmuxSession !== undefined) result.tmuxSession = context.tmuxSession
|
if (context.tmuxSession !== undefined) result.tmuxSession = context.tmuxSession
|
||||||
if (context.prompt !== undefined) result.prompt = context.prompt
|
if (context.prompt !== undefined) result.prompt = context.prompt
|
||||||
if (context.contextSummary !== undefined) result.contextSummary = context.contextSummary
|
if (context.contextSummary !== undefined) result.contextSummary = context.contextSummary
|
||||||
if (context.reason !== undefined) result.reason = context.reason
|
if (context.reasoning !== undefined) result.reasoning = context.reasoning
|
||||||
if (context.question !== undefined) result.question = context.question
|
if (context.question !== undefined) result.question = context.question
|
||||||
if (context.tmuxTail !== undefined) result.tmuxTail = context.tmuxTail
|
if (context.tmuxTail !== undefined) result.tmuxTail = context.tmuxTail
|
||||||
if (context.replyChannel !== undefined) result.replyChannel = context.replyChannel
|
if (context.replyChannel !== undefined) result.replyChannel = context.replyChannel
|
||||||
@@ -62,8 +64,13 @@ export async function wakeOpenClaw(
|
|||||||
if (paneId) {
|
if (paneId) {
|
||||||
tmuxTail = (await captureTmuxPane(paneId, 15)) ?? undefined
|
tmuxTail = (await captureTmuxPane(paneId, 15)) ?? undefined
|
||||||
}
|
}
|
||||||
} catch {
|
} catch (error) {
|
||||||
// Ignore
|
if (DEBUG) {
|
||||||
|
console.error(
|
||||||
|
"[openclaw] failed to capture tmux tail:",
|
||||||
|
error instanceof Error ? error.message : error,
|
||||||
|
)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -74,7 +81,7 @@ export async function wakeOpenClaw(
|
|||||||
tmuxSession,
|
tmuxSession,
|
||||||
prompt: enrichedContext.prompt,
|
prompt: enrichedContext.prompt,
|
||||||
contextSummary: enrichedContext.contextSummary,
|
contextSummary: enrichedContext.contextSummary,
|
||||||
reason: enrichedContext.reason,
|
reasoning: enrichedContext.reasoning,
|
||||||
question: enrichedContext.question,
|
question: enrichedContext.question,
|
||||||
tmuxTail,
|
tmuxTail,
|
||||||
event,
|
event,
|
||||||
@@ -125,7 +132,8 @@ export async function wakeOpenClaw(
|
|||||||
}
|
}
|
||||||
|
|
||||||
export async function initializeOpenClaw(config: OpenClawConfig): Promise<void> {
|
export async function initializeOpenClaw(config: OpenClawConfig): Promise<void> {
|
||||||
if (config.enabled && (config.discordBotToken || config.telegramBotToken)) {
|
const replyListener = config.replyListener
|
||||||
|
if (config.enabled && (replyListener?.discordBotToken || replyListener?.telegramBotToken)) {
|
||||||
await startReplyListener(config)
|
await startReplyListener(config)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -58,7 +58,7 @@ const STATE_FILE_PATH = join(DEFAULT_STATE_DIR, "reply-listener-state.json")
|
|||||||
const CONFIG_FILE_PATH = join(DEFAULT_STATE_DIR, "reply-listener-config.json")
|
const CONFIG_FILE_PATH = join(DEFAULT_STATE_DIR, "reply-listener-config.json")
|
||||||
const LOG_FILE_PATH = join(DEFAULT_STATE_DIR, "reply-listener.log")
|
const LOG_FILE_PATH = join(DEFAULT_STATE_DIR, "reply-listener.log")
|
||||||
|
|
||||||
const DAEMON_IDENTITY_MARKER = "pollLoop"
|
export const DAEMON_IDENTITY_MARKER = "--openclaw-reply-listener-daemon"
|
||||||
|
|
||||||
function createMinimalDaemonEnv(): Record<string, string> {
|
function createMinimalDaemonEnv(): Record<string, string> {
|
||||||
const env: Record<string, string> = {}
|
const env: Record<string, string> = {}
|
||||||
@@ -114,6 +114,10 @@ function log(message: string): void {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export function logReplyListenerMessage(message: string): void {
|
||||||
|
log(message)
|
||||||
|
}
|
||||||
|
|
||||||
interface DaemonState {
|
interface DaemonState {
|
||||||
isRunning: boolean
|
isRunning: boolean
|
||||||
pid: number | null
|
pid: number | null
|
||||||
@@ -255,20 +259,21 @@ async function injectReply(
|
|||||||
platform: string,
|
platform: string,
|
||||||
config: OpenClawConfig,
|
config: OpenClawConfig,
|
||||||
): Promise<boolean> {
|
): Promise<boolean> {
|
||||||
|
const replyListener = config.replyListener
|
||||||
const content = await captureTmuxPane(paneId, 15)
|
const content = await captureTmuxPane(paneId, 15)
|
||||||
const analysis = analyzePaneContent(content)
|
const analysis = analyzePaneContent(content)
|
||||||
|
|
||||||
if (analysis.confidence < 0.3) { // Lower threshold for simple check
|
if (analysis.confidence < 0.3) { // Lower threshold for simple check
|
||||||
log(
|
log(
|
||||||
`WARN: Pane ${paneId} does not appear to be running Codex CLI (confidence: ${analysis.confidence}). Skipping injection, removing stale mapping.`,
|
`WARN: Pane ${paneId} does not appear to be running OpenCode CLI (confidence: ${analysis.confidence}). Skipping injection, removing stale mapping.`,
|
||||||
)
|
)
|
||||||
removeMessagesByPane(paneId)
|
removeMessagesByPane(paneId)
|
||||||
return false
|
return false
|
||||||
}
|
}
|
||||||
|
|
||||||
const prefix = config.includePrefix ? `[reply:${platform}] ` : ""
|
const prefix = replyListener?.includePrefix === false ? "" : `[reply:${platform}] `
|
||||||
const sanitized = sanitizeReplyInput(prefix + text)
|
const sanitized = sanitizeReplyInput(prefix + text)
|
||||||
const truncated = sanitized.slice(0, config.maxMessageLength)
|
const truncated = sanitized.slice(0, replyListener?.maxMessageLength ?? 500)
|
||||||
const success = await sendToPane(paneId, truncated, true)
|
const success = await sendToPane(paneId, truncated, true)
|
||||||
|
|
||||||
if (success) {
|
if (success) {
|
||||||
@@ -288,22 +293,28 @@ async function pollDiscord(
|
|||||||
state: DaemonState,
|
state: DaemonState,
|
||||||
rateLimiter: RateLimiter,
|
rateLimiter: RateLimiter,
|
||||||
): Promise<void> {
|
): Promise<void> {
|
||||||
if (!config.discordBotToken || !config.discordChannelId) return
|
const replyListener = config.replyListener
|
||||||
if (!config.authorizedDiscordUserIds || config.authorizedDiscordUserIds.length === 0) return
|
if (!replyListener?.discordBotToken || !replyListener.discordChannelId) return
|
||||||
|
if (
|
||||||
|
!replyListener.authorizedDiscordUserIds
|
||||||
|
|| replyListener.authorizedDiscordUserIds.length === 0
|
||||||
|
) {
|
||||||
|
return
|
||||||
|
}
|
||||||
if (Date.now() < discordBackoffUntil) return
|
if (Date.now() < discordBackoffUntil) return
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const after = state.discordLastMessageId
|
const after = state.discordLastMessageId
|
||||||
? `?after=${state.discordLastMessageId}&limit=10`
|
? `?after=${state.discordLastMessageId}&limit=10`
|
||||||
: "?limit=10"
|
: "?limit=10"
|
||||||
const url = `https://discord.com/api/v10/channels/${config.discordChannelId}/messages${after}`
|
const url = `https://discord.com/api/v10/channels/${replyListener.discordChannelId}/messages${after}`
|
||||||
|
|
||||||
const controller = new AbortController()
|
const controller = new AbortController()
|
||||||
const timeout = setTimeout(() => controller.abort(), 10000)
|
const timeout = setTimeout(() => controller.abort(), 10000)
|
||||||
|
|
||||||
const response = await fetch(url, {
|
const response = await fetch(url, {
|
||||||
method: "GET",
|
method: "GET",
|
||||||
headers: { Authorization: `Bot ${config.discordBotToken}` },
|
headers: { Authorization: `Bot ${replyListener.discordBotToken}` },
|
||||||
signal: controller.signal,
|
signal: controller.signal,
|
||||||
})
|
})
|
||||||
|
|
||||||
@@ -338,7 +349,7 @@ async function pollDiscord(
|
|||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!config.authorizedDiscordUserIds.includes(msg.author.id)) {
|
if (!replyListener.authorizedDiscordUserIds.includes(msg.author.id)) {
|
||||||
state.discordLastMessageId = msg.id
|
state.discordLastMessageId = msg.id
|
||||||
writeDaemonState(state)
|
writeDaemonState(state)
|
||||||
continue
|
continue
|
||||||
@@ -369,10 +380,10 @@ async function pollDiscord(
|
|||||||
// Add reaction
|
// Add reaction
|
||||||
try {
|
try {
|
||||||
await fetch(
|
await fetch(
|
||||||
`https://discord.com/api/v10/channels/${config.discordChannelId}/messages/${msg.id}/reactions/%E2%9C%85/@me`,
|
`https://discord.com/api/v10/channels/${replyListener.discordChannelId}/messages/${msg.id}/reactions/%E2%9C%85/@me`,
|
||||||
{
|
{
|
||||||
method: "PUT",
|
method: "PUT",
|
||||||
headers: { Authorization: `Bot ${config.discordBotToken}` },
|
headers: { Authorization: `Bot ${replyListener.discordBotToken}` },
|
||||||
},
|
},
|
||||||
)
|
)
|
||||||
} catch {
|
} catch {
|
||||||
@@ -394,11 +405,12 @@ async function pollTelegram(
|
|||||||
state: DaemonState,
|
state: DaemonState,
|
||||||
rateLimiter: RateLimiter,
|
rateLimiter: RateLimiter,
|
||||||
): Promise<void> {
|
): Promise<void> {
|
||||||
if (!config.telegramBotToken || !config.telegramChatId) return
|
const replyListener = config.replyListener
|
||||||
|
if (!replyListener?.telegramBotToken || !replyListener.telegramChatId) return
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const offset = state.telegramLastUpdateId ? state.telegramLastUpdateId + 1 : 0
|
const offset = state.telegramLastUpdateId ? state.telegramLastUpdateId + 1 : 0
|
||||||
const url = `https://api.telegram.org/bot${config.telegramBotToken}/getUpdates?offset=${offset}&timeout=0`
|
const url = `https://api.telegram.org/bot${replyListener.telegramBotToken}/getUpdates?offset=${offset}&timeout=0`
|
||||||
|
|
||||||
const controller = new AbortController()
|
const controller = new AbortController()
|
||||||
const timeout = setTimeout(() => controller.abort(), 10000)
|
const timeout = setTimeout(() => controller.abort(), 10000)
|
||||||
@@ -432,7 +444,7 @@ async function pollTelegram(
|
|||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
|
|
||||||
if (String(msg.chat.id) !== config.telegramChatId) {
|
if (String(msg.chat.id) !== replyListener.telegramChatId) {
|
||||||
state.telegramLastUpdateId = update.update_id
|
state.telegramLastUpdateId = update.update_id
|
||||||
writeDaemonState(state)
|
writeDaemonState(state)
|
||||||
continue
|
continue
|
||||||
@@ -469,12 +481,12 @@ async function pollTelegram(
|
|||||||
state.messagesInjected++
|
state.messagesInjected++
|
||||||
try {
|
try {
|
||||||
await fetch(
|
await fetch(
|
||||||
`https://api.telegram.org/bot${config.telegramBotToken}/sendMessage`,
|
`https://api.telegram.org/bot${replyListener.telegramBotToken}/sendMessage`,
|
||||||
{
|
{
|
||||||
method: "POST",
|
method: "POST",
|
||||||
headers: { "Content-Type": "application/json" },
|
headers: { "Content-Type": "application/json" },
|
||||||
body: JSON.stringify({
|
body: JSON.stringify({
|
||||||
chat_id: config.telegramChatId,
|
chat_id: replyListener.telegramChatId,
|
||||||
text: "Injected into Codex CLI session.",
|
text: "Injected into Codex CLI session.",
|
||||||
reply_to_message_id: msg.message_id,
|
reply_to_message_id: msg.message_id,
|
||||||
}),
|
}),
|
||||||
@@ -518,7 +530,7 @@ export async function pollLoop(): Promise<void> {
|
|||||||
state.isRunning = true
|
state.isRunning = true
|
||||||
state.pid = process.pid
|
state.pid = process.pid
|
||||||
|
|
||||||
const rateLimiter = new RateLimiter(config.rateLimitPerMinute || 10)
|
const rateLimiter = new RateLimiter(config.replyListener?.rateLimitPerMinute || 10)
|
||||||
let lastPruneAt = Date.now()
|
let lastPruneAt = Date.now()
|
||||||
|
|
||||||
const shutdown = (): void => {
|
const shutdown = (): void => {
|
||||||
@@ -556,13 +568,17 @@ export async function pollLoop(): Promise<void> {
|
|||||||
}
|
}
|
||||||
|
|
||||||
writeDaemonState(state)
|
writeDaemonState(state)
|
||||||
await new Promise((resolve) => setTimeout(resolve, config.pollIntervalMs || 3000))
|
await new Promise((resolve) =>
|
||||||
|
setTimeout(resolve, config.replyListener?.pollIntervalMs || 3000),
|
||||||
|
)
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
state.errors++
|
state.errors++
|
||||||
state.lastError = error instanceof Error ? error.message : String(error)
|
state.lastError = error instanceof Error ? error.message : String(error)
|
||||||
log(`Poll error: ${state.lastError}`)
|
log(`Poll error: ${state.lastError}`)
|
||||||
writeDaemonState(state)
|
writeDaemonState(state)
|
||||||
await new Promise((resolve) => setTimeout(resolve, (config.pollIntervalMs || 3000) * 2))
|
await new Promise((resolve) =>
|
||||||
|
setTimeout(resolve, (config.replyListener?.pollIntervalMs || 3000) * 2),
|
||||||
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
log("Poll loop ended")
|
log("Poll loop ended")
|
||||||
@@ -586,7 +602,8 @@ export async function startReplyListener(config: OpenClawConfig): Promise<{ succ
|
|||||||
}
|
}
|
||||||
|
|
||||||
const normalizedConfig = normalizeReplyListenerConfig(config)
|
const normalizedConfig = normalizeReplyListenerConfig(config)
|
||||||
if (!normalizedConfig.discordBotToken && !normalizedConfig.telegramBotToken) {
|
const replyListener = normalizedConfig.replyListener
|
||||||
|
if (!replyListener?.discordBotToken && !replyListener?.telegramBotToken) {
|
||||||
// Only warn if no platforms enabled, but user might just want outbound
|
// Only warn if no platforms enabled, but user might just want outbound
|
||||||
// Actually, instructions say: "Fire-and-forget for outbound, daemon process for inbound"
|
// Actually, instructions say: "Fire-and-forget for outbound, daemon process for inbound"
|
||||||
// So if no inbound config, we shouldn't start daemon.
|
// So if no inbound config, we shouldn't start daemon.
|
||||||
@@ -606,7 +623,7 @@ export async function startReplyListener(config: OpenClawConfig): Promise<{ succ
|
|||||||
: join(dirname(new URL(currentFile).pathname), "daemon.js")
|
: join(dirname(new URL(currentFile).pathname), "daemon.js")
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const proc = spawn(["bun", "run", daemonScript], {
|
const proc = spawn(["bun", "run", daemonScript, DAEMON_IDENTITY_MARKER], {
|
||||||
detached: true,
|
detached: true,
|
||||||
stdio: ["ignore", "ignore", "ignore"],
|
stdio: ["ignore", "ignore", "ignore"],
|
||||||
cwd: process.cwd(),
|
cwd: process.cwd(),
|
||||||
|
|||||||
@@ -11,11 +11,12 @@ import {
|
|||||||
constants,
|
constants,
|
||||||
} from "fs"
|
} from "fs"
|
||||||
import { join, dirname } from "path"
|
import { join, dirname } from "path"
|
||||||
import { homedir } from "os"
|
|
||||||
import { randomUUID } from "crypto"
|
import { randomUUID } from "crypto"
|
||||||
|
import { getOpenCodeStorageDir } from "../shared/data-path"
|
||||||
|
|
||||||
const REGISTRY_PATH = join(homedir(), ".omx", "state", "reply-session-registry.jsonl")
|
const OPENCLAW_STORAGE_DIR = join(getOpenCodeStorageDir(), "openclaw")
|
||||||
const REGISTRY_LOCK_PATH = join(homedir(), ".omx", "state", "reply-session-registry.lock")
|
const REGISTRY_PATH = join(OPENCLAW_STORAGE_DIR, "reply-session-registry.jsonl")
|
||||||
|
const REGISTRY_LOCK_PATH = join(OPENCLAW_STORAGE_DIR, "reply-session-registry.lock")
|
||||||
const SECURE_FILE_MODE = 0o600
|
const SECURE_FILE_MODE = 0o600
|
||||||
const MAX_AGE_MS = 24 * 60 * 60 * 1000
|
const MAX_AGE_MS = 24 * 60 * 60 * 1000
|
||||||
const LOCK_TIMEOUT_MS = 2000
|
const LOCK_TIMEOUT_MS = 2000
|
||||||
@@ -120,12 +121,26 @@ function acquireRegistryLock(): LockHandle | null {
|
|||||||
constants.O_CREAT | constants.O_EXCL | constants.O_WRONLY,
|
constants.O_CREAT | constants.O_EXCL | constants.O_WRONLY,
|
||||||
SECURE_FILE_MODE,
|
SECURE_FILE_MODE,
|
||||||
)
|
)
|
||||||
const lockPayload = JSON.stringify({
|
try {
|
||||||
pid: process.pid,
|
const lockPayload = JSON.stringify({
|
||||||
acquiredAt: Date.now(),
|
pid: process.pid,
|
||||||
token,
|
acquiredAt: Date.now(),
|
||||||
})
|
token,
|
||||||
writeSync(fd, lockPayload)
|
})
|
||||||
|
writeSync(fd, lockPayload)
|
||||||
|
} catch (writeError) {
|
||||||
|
try {
|
||||||
|
closeSync(fd)
|
||||||
|
} catch {
|
||||||
|
// Ignore
|
||||||
|
}
|
||||||
|
try {
|
||||||
|
unlinkSync(REGISTRY_LOCK_PATH)
|
||||||
|
} catch {
|
||||||
|
// Ignore
|
||||||
|
}
|
||||||
|
throw writeError
|
||||||
|
}
|
||||||
return { fd, token }
|
return { fd, token }
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
const err = error as NodeJS.ErrnoException
|
const err = error as NodeJS.ErrnoException
|
||||||
|
|||||||
+20
-14
@@ -14,7 +14,9 @@ export async function getTmuxSessionName(): Promise<string | null> {
|
|||||||
stdout: "pipe",
|
stdout: "pipe",
|
||||||
stderr: "ignore",
|
stderr: "ignore",
|
||||||
})
|
})
|
||||||
const output = await new Response(proc.stdout).text()
|
const outputPromise = new Response(proc.stdout).text()
|
||||||
|
await proc.exited
|
||||||
|
const output = await outputPromise
|
||||||
if (proc.exitCode !== 0) return null
|
if (proc.exitCode !== 0) return null
|
||||||
return output.trim() || null
|
return output.trim() || null
|
||||||
} catch {
|
} catch {
|
||||||
@@ -31,7 +33,9 @@ export async function captureTmuxPane(paneId: string, lines = 15): Promise<strin
|
|||||||
stderr: "ignore",
|
stderr: "ignore",
|
||||||
},
|
},
|
||||||
)
|
)
|
||||||
const output = await new Response(proc.stdout).text()
|
const outputPromise = new Response(proc.stdout).text()
|
||||||
|
await proc.exited
|
||||||
|
const output = await outputPromise
|
||||||
if (proc.exitCode !== 0) return null
|
if (proc.exitCode !== 0) return null
|
||||||
return output.trim() || null
|
return output.trim() || null
|
||||||
} catch {
|
} catch {
|
||||||
@@ -41,12 +45,21 @@ export async function captureTmuxPane(paneId: string, lines = 15): Promise<strin
|
|||||||
|
|
||||||
export async function sendToPane(paneId: string, text: string, confirm = true): Promise<boolean> {
|
export async function sendToPane(paneId: string, text: string, confirm = true): Promise<boolean> {
|
||||||
try {
|
try {
|
||||||
const proc = spawn(["tmux", "send-keys", "-t", paneId, text, ...(confirm ? ["Enter"] : [])], {
|
const literalProc = spawn(["tmux", "send-keys", "-t", paneId, "-l", "--", text], {
|
||||||
stdout: "ignore",
|
stdout: "ignore",
|
||||||
stderr: "ignore",
|
stderr: "ignore",
|
||||||
})
|
})
|
||||||
await proc.exited
|
await literalProc.exited
|
||||||
return proc.exitCode === 0
|
if (literalProc.exitCode !== 0) return false
|
||||||
|
|
||||||
|
if (!confirm) return true
|
||||||
|
|
||||||
|
const enterProc = spawn(["tmux", "send-keys", "-t", paneId, "Enter"], {
|
||||||
|
stdout: "ignore",
|
||||||
|
stderr: "ignore",
|
||||||
|
})
|
||||||
|
await enterProc.exited
|
||||||
|
return enterProc.exitCode === 0
|
||||||
} catch {
|
} catch {
|
||||||
return false
|
return false
|
||||||
}
|
}
|
||||||
@@ -67,18 +80,11 @@ export async function isTmuxAvailable(): Promise<boolean> {
|
|||||||
|
|
||||||
export function analyzePaneContent(content: string | null): { confidence: number } {
|
export function analyzePaneContent(content: string | null): { confidence: number } {
|
||||||
if (!content) return { confidence: 0 }
|
if (!content) return { confidence: 0 }
|
||||||
// Simple heuristic: check for common CLI prompts or output
|
|
||||||
// Reference implementation had more logic, but for now simple check is okay
|
|
||||||
// Ideally, I should port the reference logic.
|
|
||||||
// Reference logic:
|
|
||||||
// if (content.includes("opencode")) confidence += 0.5
|
|
||||||
// if (content.includes("How can I help you?")) confidence += 0.8
|
|
||||||
// etc.
|
|
||||||
|
|
||||||
let confidence = 0
|
let confidence = 0
|
||||||
if (content.includes("opencode")) confidence += 0.3
|
if (content.includes("opencode")) confidence += 0.3
|
||||||
if (content.includes("How can I help you?")) confidence += 0.5
|
if (content.includes("Ask anything...")) confidence += 0.5
|
||||||
if (content.includes("Type /help")) confidence += 0.2
|
if (content.includes("Run /help")) confidence += 0.2
|
||||||
|
|
||||||
return { confidence: Math.min(1, confidence) }
|
return { confidence: Math.min(1, confidence) }
|
||||||
}
|
}
|
||||||
|
|||||||
+12
-2
@@ -1,6 +1,16 @@
|
|||||||
import type { OpenClawConfig, OpenClawGateway, OpenClawHook } from "../config/schema/openclaw"
|
import type {
|
||||||
|
OpenClawConfig,
|
||||||
|
OpenClawGateway,
|
||||||
|
OpenClawHook,
|
||||||
|
OpenClawReplyListenerConfig,
|
||||||
|
} from "../config/schema/openclaw"
|
||||||
|
|
||||||
export type { OpenClawConfig, OpenClawGateway, OpenClawHook }
|
export type {
|
||||||
|
OpenClawConfig,
|
||||||
|
OpenClawGateway,
|
||||||
|
OpenClawHook,
|
||||||
|
OpenClawReplyListenerConfig,
|
||||||
|
}
|
||||||
|
|
||||||
export interface OpenClawContext {
|
export interface OpenClawContext {
|
||||||
sessionId?: string
|
sessionId?: string
|
||||||
|
|||||||
Reference in New Issue
Block a user