UNPKG

openclaw

Version:

Multi-channel AI gateway with extensible messaging integrations

864 lines (863 loc) • 31.8 kB
import { t as hasClaudeRawToolInvocation } from "./cli-output-iGeO18JR.mjs"; import { isRecord } from "openclaw/plugin-sdk/string-coerce-runtime"; import { killProcessTree, prepareSecretInputStdio, signalProcessTree } from "openclaw/plugin-sdk/process-runtime"; import { randomUUID } from "node:crypto"; import { once } from "node:events"; import { PassThrough } from "node:stream"; import { attachErrorDiagnostic, toErrorObject } from "openclaw/plugin-sdk/error-runtime"; import { createDeferred } from "openclaw/plugin-sdk/extension-shared"; import { spawn } from "node:child_process"; import { redactSensitiveFieldValue, redactSensitiveText } from "openclaw/plugin-sdk/logging-core"; import { sliceUtf16Safe } from "openclaw/plugin-sdk/text-utility-runtime"; import { raceWithTimeout } from "openclaw/plugin-sdk/time-runtime"; //#region extensions/anthropic/cli-runtime-args.ts const PROTOCOL_FLAGS = /* @__PURE__ */ new Set([ "-p", "--print", "--verbose", "--include-partial-messages", "--replay-user-messages", "--dangerously-skip-permissions", "--allow-dangerously-skip-permissions" ]); const PROTOCOL_VALUE_FLAGS = /* @__PURE__ */ new Set([ "--output-format", "--input-format", "--permission-prompt-tool", "--permission-mode", "--model", "--session-id", "--resume", "-r", "--append-system-prompt-file", "--append-system-prompt", "--system-prompt-file", "--system-prompt" ]); const TOOL_FLAGS = /* @__PURE__ */ new Set([ "--tools", "--allowedTools", "--allowed-tools" ]); /** Keep prepared CLI arguments, replacing only transport and admission-owned policy. */ function prepareClaudeCliTransportArgs(context) { const args = []; const allowedTools = []; let tools; let settingSources = "user"; let excludeDynamicSections = false; for (let index = 0; index < context.args.length; index += 1) { const raw = context.args[index]; const equals = raw.indexOf("="); const flag = equals < 0 ? raw : raw.slice(0, equals); if (PROTOCOL_FLAGS.has(flag)) continue; if (flag === "--exclude-dynamic-system-prompt-sections") { excludeDynamicSections = true; continue; } if (!PROTOCOL_VALUE_FLAGS.has(flag) && !TOOL_FLAGS.has(flag) && flag !== "--setting-sources") { args.push(raw); continue; } const value = equals < 0 ? context.args[++index] : raw.slice(equals + 1); if (value === void 0) throw new Error(`Claude CLI cannot preserve ${flag} without its value.`); if (PROTOCOL_VALUE_FLAGS.has(flag)) continue; if (flag === "--setting-sources") { if (value !== "" && value !== "user") throw new Error("Claude CLI settings must be limited to user settings."); settingSources = value; continue; } const values = [value]; if (equals < 0) while (index + 1 < context.args.length && !context.args[index + 1]?.startsWith("-")) values.push(context.args[++index]); const names = values.flatMap((entry) => entry.split(",").map((name) => name.trim()).filter(Boolean)); if (flag === "--tools") tools = names; else allowedTools.push(...names.filter((name) => name.startsWith("mcp__openclaw__"))); } let approvedTools = [...new Set(allowedTools)]; if (context.toolAvailability) { tools = [...context.toolAvailability.native]; approvedTools = context.toolAvailability.openClaw.map((name) => `mcp__openclaw__${name}`).filter((name) => allowedTools.includes(name) || allowedTools.includes("mcp__openclaw__*")); } args.push("--print", "--input-format", "stream-json", "--output-format", "stream-json", "--verbose", "--include-partial-messages", "--replay-user-messages", "--permission-prompt-tool", "stdio", "--permission-mode", "default", "--setting-sources", settingSources, "--model", context.modelId); if (tools) args.push("--tools", tools.join(",")); if (approvedTools.length) args.push("--allowedTools", approvedTools.join(",")); if (context.sessionId) args.push(context.useResume ? "--resume" : "--session-id", context.sessionId); return { args, excludeDynamicSections }; } //#endregion //#region \0@oxc-project+runtime@0.150.0/helpers/esm/usingCtx.js function _usingCtx() { var r = "function" == typeof SuppressedError ? SuppressedError : function(r, e) { var n = Error(); return n.name = "SuppressedError", n.error = r, n.suppressed = e, n; }, e = {}, n = []; function using(r, e) { if (null != e) { if (Object(e) !== e) throw new TypeError("using declarations can only be used with objects, functions, null, or undefined."); if (r) var o = e[Symbol.asyncDispose || Symbol["for"]("Symbol.asyncDispose")]; if (void 0 === o && (o = e[Symbol.dispose || Symbol["for"]("Symbol.dispose")], r)) var t = o; if ("function" != typeof o) throw new TypeError("Object is not disposable."); t && (o = function o() { try { t.call(e); } catch (r) { return Promise.reject(r); } }), n.push({ v: e, d: o, a: r }); } else r && n.push({ d: e, a: r }); return e; } return { e, u: using.bind(null, !1), a: using.bind(null, !0), d: function d() { var o, t = this.e, s = 0; function next() { for (; o = n.pop();) try { if (!o.a && 1 === s) return s = 0, n.push(o), Promise.resolve().then(next); if (o.d) { var r = o.d.call(o.v); if (o.a) return s |= 2, Promise.resolve(r).then(next, err); } else s |= 1; } catch (r) { return err(r); } if (1 === s) return t !== e ? Promise.reject(t) : Promise.resolve(); if (t !== e) throw t; } function err(n) { return t = t !== e ? new r(n, t) : n, next(); } return next(); } }; } //#endregion //#region extensions/anthropic/cli-process.ts const STDERR_CAPTURE_CHARS = 8192; const STDERR_DRAIN_GRACE_MS = 200; function spawnClaudeCliProcess(options, secretInput, observeStderr) { try { var _usingCtx$1 = _usingCtx(); const stdio = [ "pipe", "pipe", "pipe" ]; const secretDelivery = _usingCtx$1.u(prepareSecretInputStdio(stdio, secretInput)); const child = spawn(options.command, options.args, { argv0: options.argv0, cwd: options.cwd, detached: process.platform !== "win32", env: options.env, signal: options.signal, stdio, windowsHide: true }); observeStderr(child); const killChild = child.kill.bind(child); child.kill = (signal) => { if (!child.pid || signal !== void 0 && signal !== "SIGTERM" && signal !== "SIGKILL") return killChild(signal); killProcessTree(child.pid, { detached: process.platform !== "win32", ...signal === "SIGKILL" ? { force: true } : {} }); return true; }; secretDelivery?.deliverTo(child).catch(() => child.kill()); return child; } catch (_) { _usingCtx$1.e = _; } finally { _usingCtx$1.d(); } } /** Owns process-wide diagnostics and the credentials needed to redact warm turns. */ function createClaudeCliProcessOwner(currentContext, secretInput) { const assertCurrent = () => { const context = currentContext(); if (!context) throw new Error("Claude CLI run is no longer active."); context.assertCurrent?.(); }; assertCurrent(); const credential = secretInput?.createData(); let environment = {}; let child; let drained; let disposed = false; let tail = ""; let dropPartialLine = false; const observeStderr = (process) => { child = process; drained = new Promise((resolve) => { process.stderr.once("close", resolve); }); process.stderr.setEncoding("utf8"); process.stderr.on("error", () => {}); process.stderr.on("data", (chunk) => { let text = chunk; if (disposed) return; if (dropPartialLine) { const newline = text.indexOf("\n"); if (newline < 0) return; text = text.slice(newline + 1); dropPartialLine = false; } tail += text; if (tail.length > STDERR_CAPTURE_CHARS) { const bounded = sliceUtf16Safe(tail, -8192); const newline = bounded.indexOf("\n"); tail = newline < 0 ? "" : bounded.slice(newline + 1); dropPartialLine = newline < 0; } }); }; return { [Symbol.dispose]() { disposed = true; credential?.fill(0); environment = {}; tail = ""; }, spawn: (options) => { assertCurrent(); environment = options.env; return spawnClaudeCliProcess(options, secretInput, observeStderr); }, async withDiagnostics(error) { const context = currentContext(); if (disposed || !context || context.abortSignal?.aborted) return error; if (child && (child.exitCode !== null || child.signalCode !== null)) await raceWithTimeout(Promise.resolve(drained), STDERR_DRAIN_GRACE_MS, () => void 0); if (disposed || currentContext() !== context || context.abortSignal?.aborted) return error; let diagnostic = tail; const secrets = [environment, context.env].flatMap((env) => Object.entries(env).flatMap(([name, value]) => value && redactSensitiveFieldValue(name, value, { mode: "tools" }) !== value ? [value] : [])); if (credential) secrets.push(credential.toString("utf8")); for (const secret of secrets.filter(Boolean)) for (const value of [ secret, encodeURIComponent(secret), JSON.stringify(secret).slice(1, -1) ]) diagnostic = diagnostic.replaceAll(value, "[REDACTED]"); diagnostic = sliceUtf16Safe(redactSensitiveText(diagnostic, { mode: "tools" }), -2e3).trim(); if (diagnostic && error instanceof Error) attachErrorDiagnostic(error, `stderr (process-wide; may include earlier turns): ${diagnostic}`); return error; } }; } //#endregion //#region extensions/anthropic/cli-transport.ts const MAX_LINE_CHARS = 8388608; const CLOSE_GRACE_MS = 500; const KILL_GRACE_MS = 1e3; /** One Claude Code subprocess and its bidirectional stream-json control channel. */ function createClaudeCliTransport(params) { const owner = createClaudeCliProcessOwner(params.currentContext, params.secretInput); let child; try { const env = { ...params.context.env }; delete env.NODE_OPTIONS; delete env.DEBUG; child = owner.spawn({ ...params.context, args: params.args, env }); } catch (error) { owner[Symbol.dispose](); throw error; } const requests = /* @__PURE__ */ new Map(); const initializeId = randomUUID(); const { promise: ready, resolve: resolveReady, reject: rejectReady } = createDeferred(); ready.catch(() => {}); let closed = false; let hasExited = false; let exitError; let terminateTimer; let killTimer; const { promise: exited, resolve: resolveExit } = createDeferred(); let treeExit = Promise.resolve(); const signalTree = (signal) => new Promise((resolve) => { if (!child.pid) { resolve(); return; } signalProcessTree(child.pid, signal, { detached: process.platform !== "win32", onComplete: resolve }); }); const terminate = () => { treeExit = signalTree("SIGTERM"); killTimer = setTimeout(() => { treeExit = signalTree("SIGKILL"); }, KILL_GRACE_MS); killTimer.unref(); }; const close = () => { if (closed) return; closed = true; rejectReady(/* @__PURE__ */ new Error("Claude CLI closed before initialization completed.")); for (const controller of requests.values()) controller.abort(); requests.clear(); owner[Symbol.dispose](); if (process.platform === "win32" && !hasExited) { treeExit = signalTree("SIGKILL"); treeExit.then(() => child.stdin.end()); } else child.stdin.end(); if (process.platform !== "win32" && !hasExited) { terminateTimer = setTimeout(terminate, CLOSE_GRACE_MS); terminateTimer.unref(); } }; const fail = async (error) => { if (closed) return; const diagnostic = await owner.withDiagnostics(error); if (!closed) { rejectReady(diagnostic); params.onError(diagnostic); close(); } }; const didExit = () => { if (hasExited) return; hasExited = true; clearTimeout(terminateTimer); clearTimeout(killTimer); if (process.platform !== "win32" || !closed) treeExit = signalTree("SIGKILL"); resolveExit(); }; child.once("error", (error) => { exitError = error; if (!child.pid) didExit(); fail(error); }); child.once("exit", (code, signal) => { if (code !== 0) exitError = /* @__PURE__ */ new Error(signal ? `Claude Code process exited with signal ${signal}` : `Claude Code process exited with code ${code}`); didExit(); }); child.stdin.on("error", (error) => { fail(error); }); const send = (message) => { if (closed) return Promise.reject(/* @__PURE__ */ new Error("Claude CLI control channel is closed.")); return new Promise((resolve, reject) => { child.stdin.write(`${JSON.stringify(message)}\n`, (error) => { if (error) reject(error); else resolve(); }); }); }; const dispatch = async (id, request) => { if (requests.has(id)) return; const controller = new AbortController(); requests.set(id, controller); try { let response; try { const reply = await params.onRequest(request, controller.signal); response = () => ({ subtype: "success", request_id: id, response: reply() }); } catch { response = () => ({ subtype: "error", request_id: id, error: "OpenClaw could not handle this Claude control request." }); } if (!closed) await send({ type: "control_response", response: response() }); } finally { if (requests.get(id) === controller) requests.delete(id); } }; const acceptLine = async (line) => { if (!line.trim()) return; let message; try { message = JSON.parse(line); } catch { return; } if (!isRecord(message)) return; if (message.type === "control_response") { const response = message.response; if (isRecord(response) && response.request_id === initializeId) { if (response.subtype === "success") resolveReady(); else throw new Error("Claude CLI initialization failed."); } } else if (message.type === "control_request") { if (typeof message.request_id !== "string" || !isRecord(message.request)) throw new Error("Claude CLI sent a malformed control request."); dispatch(message.request_id, message.request).catch(fail); } else if (message.type === "control_cancel_request") { if (typeof message.request_id === "string") requests.get(message.request_id)?.abort(); } else if (message.type !== "keep_alive") await params.onMessage(message); }; const read = async () => { let pending = ""; child.stdout.setEncoding("utf8"); for await (const chunk of child.stdout) { const text = String(chunk); for (let offset = 0; offset < text.length;) { const newline = text.indexOf("\n", offset); const end = newline < 0 ? text.length : newline; if (pending.length + end - offset > MAX_LINE_CHARS) throw new Error(`Claude CLI JSONL line exceeded ${MAX_LINE_CHARS} characters.`); pending += text.slice(offset, end); offset = end + 1; if (newline >= 0) { const line = pending; pending = ""; await acceptLine(line); } } } if (pending) await acceptLine(pending); await exited; if (!closed) throw exitError ?? /* @__PURE__ */ new Error("Claude CLI live session exited unexpectedly without a terminal result."); }; read().catch(fail); return { close, waitForExit: async () => { await exited; await treeExit; }, send, async initialize() { await send({ type: "control_request", request_id: initializeId, request: { subtype: "initialize", ...params.initialize } }); await ready; } }; } //#endregion //#region extensions/anthropic/cli-user-input.ts function createClaudeCliUserInputAuthorizer(context) { const requests = /* @__PURE__ */ new Map(); return { authorize(params) { const existing = params.toolUseId ? requests.get(params.toolUseId) : void 0; if (existing) return existing; const request = runClaudeUserInput(context, params); if (params.toolUseId) requests.set(params.toolUseId, request); return request; } }; } async function runClaudeUserInput(context, params) { const parsed = readClaudeUserInputQuestions(params.input); if (!parsed.ok) return { behavior: "deny", message: `OpenClaw rejected malformed Claude user questions: ${parsed.failure}. Correct the invalid field and retry AskUserQuestion.` }; const questions = parsed.questions; const result = await context.requestUserInput({ toolName: "AskUserQuestion", questions, intro: "Claude needs input:", ...params.toolUseId ? { toolCallId: params.toolUseId } : {}, abortSignal: params.signal }); if (result.status !== "answered") return { behavior: "deny", message: `${result.message} Continue with your best judgment.` }; const answers = {}; questions.forEach((question) => { answers[question.question] = (result.answers[question.id] ?? []).join(", "); }); return { behavior: "allow", updatedInput: { ...params.input, answers } }; } function readClaudeUserInputQuestions(input) { const rawQuestions = input.questions; if (!Array.isArray(rawQuestions) || rawQuestions.length < 1 || rawQuestions.length > 4) return { ok: false, failure: "questions must be an array of 1 to 4 questions" }; const questions = []; for (const [index, rawQuestion] of rawQuestions.entries()) { const questionPath = `questions[${index}]`; if (!isRecord(rawQuestion)) return { ok: false, failure: `${questionPath} must be an object` }; const question = readBoundedText(rawQuestion.question, 4096); if (!question.ok) return { ok: false, failure: `${questionPath}.question ${question.failure}` }; const header = readBoundedText(rawQuestion.header, 12); if (!header.ok) return { ok: false, failure: `${questionPath}.header ${header.failure}` }; const rawOptions = rawQuestion.options; if (!Array.isArray(rawOptions) || rawOptions.length < 2 || rawOptions.length > 4) return { ok: false, failure: `${questionPath}.options must be an array of 2 to 4 options` }; if (typeof rawQuestion.multiSelect !== "boolean") return { ok: false, failure: `${questionPath}.multiSelect must be a boolean` }; const options = []; for (const [optionIndex, rawOption] of rawOptions.entries()) { const optionPath = `${questionPath}.options[${optionIndex}]`; if (!isRecord(rawOption)) return { ok: false, failure: `${optionPath} must be an object` }; const label = readBoundedText(rawOption.label, 256); if (!label.ok) return { ok: false, failure: `${optionPath}.label ${label.failure}` }; const description = readBoundedText(rawOption.description, 1024); if (!description.ok) return { ok: false, failure: `${optionPath}.description ${description.failure}` }; options.push({ label: label.text, description: description.text }); } questions.push({ id: `question_${index + 1}`, header: header.text, question: question.text, multiSelect: rawQuestion.multiSelect, isOther: true, options }); } return { ok: true, questions }; } function readBoundedText(value, maxLength) { if (typeof value !== "string") return { ok: false, failure: "must be a string" }; if (value.length === 0) return { ok: false, failure: "must not be empty" }; if (value.length > maxLength) return { ok: false, failure: `must be at most ${maxLength} characters` }; return { ok: true, text: value }; } //#endregion //#region extensions/anthropic/cli.runtime.ts const IDLE_TIMEOUT_MS = 6e5; function readReplayedTaskId(message, inputUuid) { if (message.type !== "user" || message.isReplay !== true || message.parent_tool_use_id !== null || message.uuid === inputUuid || !isRecord(message.message)) return; if (message.origin !== void 0 && (!isRecord(message.origin) || message.origin.kind !== "task-notification" || message.origin.subkind !== void 0)) return; const content = message.message.content; const first = Array.isArray(content) ? content[0] : void 0; const text = typeof content === "string" ? content : isRecord(first) && first.type === "text" && typeof first.text === "string" ? first.text : ""; return /^<task-notification>\s*<task-id>([^<]+)<\/task-id>/u.exec(text)?.[1]; } const sessions = /* @__PURE__ */ new WeakMap(); function activeTurn(session) { const turn = session.currentTurn; if (!turn || turn.controller.signal.aborted || turn.context.abortSignal?.aborted) return; if (session.hasInputLifecycle && !turn.inputStarted) return; try { turn.context.assertCurrent?.(); return turn; } catch { return; } } async function authorizeTool(session, request, signal) { const turn = activeTurn(session); const { input, tool_name: toolName } = request; if (!turn || signal.aborted || typeof toolName !== "string" || !isRecord(input)) return { behavior: "deny", message: "The OpenClaw run or native tool input is no longer valid." }; const toolUseId = typeof request.tool_use_id === "string" ? request.tool_use_id : void 0; const abortSignal = AbortSignal.any([signal, turn.controller.signal]); try { const decision = toolName === "AskUserQuestion" ? await turn.userInput.authorize({ input, signal: abortSignal, toolUseId }) : await turn.context.requestToolPermission({ toolName, toolInput: input, cwd: typeof request.cwd === "string" ? request.cwd : void 0, toolCallId: toolUseId, abortSignal }); if (activeTurn(session) !== turn || abortSignal.aborted) return { behavior: "deny", message: "The OpenClaw run is no longer active." }; if (decision.behavior === "allow" && toolName === "Bash" && toolUseId && decision.updatedInput.run_in_background !== true) turn.foregroundBashToolUseIds.add(toolUseId); return decision; } catch { return { behavior: "deny", message: "OpenClaw could not authorize this tool call." }; } } async function handleRequest(session, request, signal) { if (request.subtype === "can_use_tool") return { ...await authorizeTool(session, request, signal), toolUseID: request.tool_use_id }; if (request.subtype !== "hook_callback" || !isRecord(request.input)) throw new Error("Unsupported Claude CLI control request."); const input = request.input; const turn = activeTurn(session); if (request.callback_id === "UserPromptSubmit" && input.hook_event_name === "UserPromptSubmit") { if (!turn || signal.aborted) return {}; const additionalContext = [turn.context.promptContext?.prependContext, turn.context.promptContext?.appendContext].filter(Boolean).join("\n\n"); return additionalContext ? { hookSpecificOutput: { hookEventName: "UserPromptSubmit", additionalContext } } : {}; } if (request.callback_id === "PreToolUse" && input.hook_event_name === "PreToolUse") { if (turn && !signal.aborted && typeof input.tool_name === "string" && input.tool_name.startsWith("mcp__openclaw__")) return { continue: true }; const decision = await authorizeTool(session, { tool_name: input.tool_name, input: input.tool_input, cwd: input.cwd, tool_use_id: request.tool_use_id ?? input.tool_use_id }, signal); return { hookSpecificOutput: { hookEventName: "PreToolUse", permissionDecision: decision.behavior, ...decision.behavior === "allow" ? { updatedInput: decision.updatedInput } : { permissionDecisionReason: decision.message } } }; } throw new Error("Unknown Claude CLI hook callback."); } function closeSession(session, _reason, error) { if (session.closed) return; session.closed = true; clearTimeout(session.idleTimer); session.capability?.remove(session.handle); const turn = session.currentTurn; session.currentTurn = void 0; if (turn) { turn.error = toErrorObject(error, "Claude CLI live session closed."); turn.controller.abort(); turn.events.end(); } session.transport?.close(); } function completeTurn(session, turn) { const holdsBackgroundTasks = turn.pendingBackgroundTaskIds.size > 0; session.currentTurn = void 0; turn.controller.abort(); turn.events.end(); if (!session.capability || holdsBackgroundTasks) session.handle.close(holdsBackgroundTasks ? "abort" : "idle"); else { session.idleTimer = setTimeout(() => session.handle.close("idle"), IDLE_TIMEOUT_MS); session.idleTimer.unref(); } } async function acceptMessage(session, message) { if (message.type === "system" && message.subtype === "init") session.hasInputLifecycle = Array.isArray(message.capabilities) && message.capabilities.includes("msg_lifecycle_v1"); const turn = session.currentTurn; if (!turn) return; if (message.type === "command_lifecycle") { if (message.state === "started" && message.command_uuid === turn.inputUuid) turn.inputStarted = true; return; } if (session.hasInputLifecycle && !turn.inputStarted && !(message.type === "system" && message.subtype === "init")) return; if (message.type === "system" && message.subtype === "task_started") { if (message.owned_by_subagent === true) { if (typeof message.task_id === "string" && message.task_id) turn.subagentTaskIds.add(message.task_id); } else if (typeof message.task_id === "string" && message.task_id) { if (message.is_backgrounded === false) turn.foregroundTaskIds.add(message.task_id); if (typeof message.tool_use_id === "string" && turn.foregroundBashToolUseIds.delete(message.tool_use_id) && message.is_backgrounded === true) turn.pendingBackgroundTaskIds.add(message.task_id); } } if (message.type === "system" && message.subtype === "background_tasks_changed") for (const task of Array.isArray(message.tasks) ? message.tasks : []) { if (!isRecord(task) || typeof task.task_id !== "string" || !task.task_id) continue; if (task.task_type === "local_agent" || task.task_type === "local_workflow" || task.task_type === "local_bash" && turn.foregroundTaskIds.has(task.task_id)) turn.pendingBackgroundTaskIds.add(task.task_id); } const replayedTaskId = readReplayedTaskId(message, turn.inputUuid); if (replayedTaskId) { turn.pendingBackgroundTaskIds.delete(replayedTaskId); turn.foregroundTaskIds.delete(replayedTaskId); turn.taskNotifications.set(replayedTaskId, "replayed"); } if (message.type === "system" && message.subtype === "task_notification" && typeof message.task_id === "string" && message.task_id && !turn.subagentTaskIds.delete(message.task_id)) turn.taskNotifications.set(message.task_id, "queued"); let completesTurn = false; if (message.type === "result") { const failed = message.is_error === true || typeof message.subtype === "string" && message.subtype.startsWith("error") || typeof message.result === "string" && hasClaudeRawToolInvocation(message.result); const taskNotification = isRecord(message.origin) && message.origin.kind === "task-notification" && message.origin.subkind === void 0; if (taskNotification && message.subtype === "success" && !failed) { const taskId = turn.taskNotifications.keys().next().value; if (taskId) { turn.taskNotifications.delete(taskId); turn.pendingBackgroundTaskIds.delete(taskId); turn.foregroundTaskIds.delete(taskId); } } for (const [taskId, state] of turn.taskNotifications) if (state === "replayed") turn.taskNotifications.delete(taskId); const queuedContinuation = turn.sawTerminalResult && taskNotification && message.num_turns === 0 && message.result === "" && message.stop_reason === null && message.terminal_reason === void 0; turn.sawTerminalResult = true; completesTurn = turn.pendingBackgroundTaskIds.size === 0 && !queuedContinuation || failed; } const outputMessage = message.type === "result" && !completesTurn ? { ...message, openclaw_interim_result: true } : message; if (!turn.events.write(outputMessage)) await once(turn.events, "drain", { signal: turn.controller.signal }); if (completesTurn) completeTurn(session, turn); } function createSession(capability) { const session = { capability, hasInputLifecycle: false, closed: false, handle: { generation: randomUUID(), fingerprint: capability?.fingerprint ?? randomUUID(), isIdle: () => !session.closed && !session.currentTurn, close: (reason, error) => closeSession(session, reason, error), waitForExit: () => session.transport?.waitForExit() ?? Promise.resolve() } }; sessions.set(session.handle, session); capability?.register(session.handle); return session; } async function* executeClaudeCli(context, secretInput) { context.assertCurrent?.(); context.abortSignal?.throwIfAborted(); const capability = context.liveSession; const existing = capability?.current(); const reusable = existing && existing.fingerprint === capability?.fingerprint ? sessions.get(existing) : void 0; if (!reusable && capability) await capability.restart(); context.assertCurrent?.(); const session = reusable ?? createSession(capability); session.capability = capability; if (session.closed || session.currentTurn) throw new Error("Claude CLI live session is closed or already handling another turn."); clearTimeout(session.idleTimer); const turn = { context, controller: new AbortController(), userInput: createClaudeCliUserInputAuthorizer(context), events: new PassThrough({ objectMode: true }), inputUuid: randomUUID(), inputStarted: false, sawTerminalResult: false, foregroundTaskIds: /* @__PURE__ */ new Set(), foregroundBashToolUseIds: /* @__PURE__ */ new Set(), pendingBackgroundTaskIds: /* @__PURE__ */ new Set(), subagentTaskIds: /* @__PURE__ */ new Set(), taskNotifications: /* @__PURE__ */ new Map() }; session.currentTurn = turn; const abort = () => session.handle.close("abort", context.abortSignal?.reason); context.abortSignal?.addEventListener("abort", abort, { once: true }); try { context.abortSignal?.throwIfAborted(); capability?.activate(session.handle); if (!session.transport) { const { args, excludeDynamicSections } = prepareClaudeCliTransportArgs(context); session.transport = createClaudeCliTransport({ context, args, secretInput, currentContext: () => session.currentTurn?.context, initialize: { appendSystemPrompt: context.systemPrompt, systemPromptSnapshot: false, excludeDynamicSections, hooks: { UserPromptSubmit: [{ hookCallbackIds: ["UserPromptSubmit"] }], PreToolUse: [{ hookCallbackIds: ["PreToolUse"] }] } }, onMessage: (message) => acceptMessage(session, message), onRequest: async (request, signal) => { if (request.subtype === "elicitation") return () => ({ action: "decline" }); const admittedTurn = activeTurn(session); const response = await handleRequest(session, request, signal); return () => { if (admittedTurn && activeTurn(session) === admittedTurn && !signal.aborted) return response; const message = "The OpenClaw run is no longer active."; if (request.subtype === "can_use_tool") return { behavior: "deny", message, toolUseID: request.tool_use_id }; return request.callback_id === "PreToolUse" ? { hookSpecificOutput: { hookEventName: "PreToolUse", permissionDecision: "deny", permissionDecisionReason: message } } : {}; }; }, onError: (error) => session.handle.close("abort", error) }); await session.transport.initialize(); } context.assertCurrent?.(); if (session.closed || session.currentTurn !== turn) throw turn.error ?? /* @__PURE__ */ new Error("Claude CLI closed before its prompt was accepted."); await session.transport.send({ type: "user", message: { role: "user", content: context.prompt }, parent_tool_use_id: null, uuid: turn.inputUuid, ...context.sessionId ? { session_id: context.sessionId } : {} }); for await (const record of turn.events) yield record; if (turn.error) throw turn.error; if (!turn.sawTerminalResult) throw new Error("Claude CLI exited without a terminal result."); } catch (error) { session.handle.close("abort", error); throw error; } finally { turn.controller.abort(); context.abortSignal?.removeEventListener("abort", abort); if (session.currentTurn === turn) session.handle.close("abort"); if (session.closed) await session.handle.waitForExit(); } } //#endregion export { executeClaudeCli };