openclaw
Version:
Multi-channel AI gateway with extensible messaging integrations
310 lines (309 loc) • 14.4 kB
JavaScript
import { d as toErrorObject } from "../../error-coercion-W9BtigRm.mjs";
import { c as isRecord } from "../../record-coerce-TIsquoVR.mjs";
import "../../error-runtime-oJAVOxQN.mjs";
import "../../string-coerce-runtime-BbIJhy7G.mjs";
import { t as hasClaudeRawToolInvocation } from "../../cli-output-DZHrAhcA.mjs";
import { t as prepareClaudeCliTransportArgs } from "../../cli-runtime-args-UumcGmHy.mjs";
import { n as readReplayedTaskId, r as recordTaskOwnership, t as isDescendantTask } from "../../cli-runtime-tasks-qGNxa-xC.mjs";
import { t as createClaudeCliTransport } from "../../cli-transport-BPep5hHt.mjs";
import { t as createClaudeCliUserInputAuthorizer } from "../../cli-user-input-CNvXwVKF.mjs";
import { randomUUID } from "node:crypto";
import { once } from "node:events";
import { PassThrough } from "node:stream";
//#region extensions/anthropic/cli.runtime.ts
const IDLE_TIMEOUT_MS = 6e5;
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, 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.runningDescendants.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;
let taskId;
if (message.type === "system") {
const startedTasks = message.subtype === "task_started" ? [message] : message.subtype === "background_tasks_changed" && Array.isArray(message.tasks) ? message.tasks.filter(isRecord) : [];
for (const task of startedTasks) if (typeof task.task_id === "string" && isDescendantTask(task)) session.runningDescendants.add(task.task_id);
if (message.subtype === "task_notification" && typeof message.task_id === "string") session.runningDescendants.delete(message.task_id);
const input = turn && (!session.hasInputLifecycle || turn.inputStarted) ? turn : void 0;
const inputUuid = input?.inputUuid ?? "";
if (message.subtype === "background_tasks_changed") {
if (Array.isArray(message.tasks)) for (const task of message.tasks) {
if (!isRecord(task)) continue;
const id = recordTaskOwnership(session, task, inputUuid);
if (id && input && session.taskOwners.get(id) === input.inputUuid && (task.task_type === "local_agent" || task.task_type === "local_workflow" || task.task_type === "local_bash" && input.foregroundTaskIds.has(id))) input.pendingBackgroundTaskIds.add(id);
}
} else taskId = recordTaskOwnership(session, message, inputUuid);
}
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" && taskId && session.taskOwners.get(taskId) === turn.inputUuid) {
if (message.is_backgrounded === false) turn.foregroundTaskIds.add(taskId);
if (typeof message.tool_use_id === "string" && turn.foregroundBashToolUseIds.delete(message.tool_use_id) && message.is_backgrounded === true) turn.pendingBackgroundTaskIds.add(taskId);
}
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" && taskId) {
if (message.status === "stopped") {
turn.pendingBackgroundTaskIds.delete(taskId);
turn.foregroundTaskIds.delete(taskId);
turn.taskNotifications.delete(taskId);
} else if (turn.foregroundTaskIds.has(taskId) && !turn.pendingBackgroundTaskIds.has(taskId)) turn.foregroundTaskIds.delete(taskId);
else turn.taskNotifications.set(taskId, "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 origin = isRecord(message.origin) ? message.origin : void 0;
const taskNotification = origin?.kind === "task-notification" && origin.subkind === void 0;
const runTaskId = taskNotification && typeof origin?.runId === "string" ? session.taskRuns.get(origin.runId) : void 0;
const notifiedTaskId = taskNotification ? runTaskId ?? turn.taskNotifications.keys().next().value : void 0;
const priorInputResult = notifiedTaskId !== void 0 && session.taskOwners.get(notifiedTaskId) !== turn.inputUuid && (!failed || runTaskId !== void 0);
if (taskNotification && message.subtype === "success" && !failed && notifiedTaskId) {
turn.taskNotifications.delete(notifiedTaskId);
turn.pendingBackgroundTaskIds.delete(notifiedTaskId);
turn.foregroundTaskIds.delete(notifiedTaskId);
}
const queuedContinuation = turn.sawTerminalResult && taskNotification && message.num_turns === 0 && message.result === "" && message.stop_reason === null && message.terminal_reason === void 0;
if (!priorInputResult) {
for (const [replayedId, state] of turn.taskNotifications) if (state === "replayed") turn.taskNotifications.delete(replayedId);
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,
taskOwners: /* @__PURE__ */ new Map(),
taskRuns: /* @__PURE__ */ new Map(),
runningDescendants: /* @__PURE__ */ new Set(),
handle: {
generation: randomUUID(),
fingerprint: capability?.fingerprint ?? randomUUID(),
isIdle: () => !session.closed && !session.currentTurn,
close: (_reason, error) => closeSession(session, 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(),
taskNotifications: /* @__PURE__ */ new Map()
};
session.currentTurn = turn;
const abort = () => session.handle.close("abort", context.abortSignal?.reason);
context.abortSignal?.addEventListener("abort", abort, { once: true });
let startup;
try {
startup = (async () => {
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 } : {}
});
})().catch((error) => session.handle.close("abort", error));
for await (const record of turn.events) yield record;
await startup;
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");
await startup;
if (session.closed) await session.handle.waitForExit();
}
}
//#endregion
export { executeClaudeCli };