openmausbot 0.1.84 → 0.1.85
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/assets/index-Ba9G44HI.js +310 -0
- package/dist/assets/{index-DNa2umw-.js → index-CTNNoqSJ.js} +1 -1
- package/dist/assets/index-CkRNp7wX.css +1 -0
- package/dist/index.html +2 -2
- package/dist-server/companion/src/routes.js +1 -0
- package/dist-server/container-mcp.js +212 -7
- package/dist-server/drivers/agents-proxy.js +135 -12
- package/dist-server/enterprise/server/index.js +1 -0
- package/dist-server/hooks/omb-hook.js +74 -0
- package/dist-server/index.js +8051 -6252
- package/dist-server/local-computer-proxy.js +83 -1
- package/dist-server/local-computer.js +14 -2
- package/dist-server/openmausbot.js +759 -301
- package/dist-server/pair-cli.js +759 -301
- package/dist-server/proxy-paths.js +1 -0
- package/dist-server/server/agent-tool-policy.js +1 -0
- package/dist-server/server/bot-package.js +2 -0
- package/dist-server/server/box.js +23 -4
- package/dist-server/server/browser-engine.js +87 -10
- package/dist-server/server/browser-live.js +7 -5
- package/dist-server/server/browser-runtime.js +65 -10
- package/dist-server/server/checkpoints.js +62 -18
- package/dist-server/server/cli-prompts.js +3 -1
- package/dist-server/server/commands.js +27 -0
- package/dist-server/server/compaction-summary.js +78 -0
- package/dist-server/server/config.js +59 -2
- package/dist-server/server/context-budget.js +29 -0
- package/dist-server/server/context-rebuild.js +72 -0
- package/dist-server/server/digest.js +130 -0
- package/dist-server/server/drivers/acp/core.js +540 -229
- package/dist-server/server/drivers/agents-proxy.js +51 -12
- package/dist-server/server/drivers/agents-result.js +23 -0
- package/dist-server/server/drivers/claude.js +62 -10
- package/dist-server/server/drivers/codex.js +6 -0
- package/dist-server/server/drivers/openai-chat.js +66 -45
- package/dist-server/server/drivers/openai-compat.js +1 -0
- package/dist-server/server/hooks/omb-hook.js +103 -0
- package/dist-server/server/index.js +1127 -365
- package/dist-server/server/local-computer.js +21 -2
- package/dist-server/server/mcp-bridge.js +10 -2
- package/dist-server/server/mcp-tool-images.js +37 -0
- package/dist-server/server/mcp-tool-schema.js +108 -0
- package/dist-server/server/message-db.js +80 -21
- package/dist-server/server/message-file.js +3 -2
- package/dist-server/server/package-export.js +1 -0
- package/dist-server/server/peer-roster.js +4 -2
- package/dist-server/server/provider-icon.js +20 -0
- package/dist-server/server/proxy-paths.js +1 -0
- package/dist-server/server/request-auth.js +1 -0
- package/dist-server/server/room-handoffs.js +16 -2
- package/dist-server/server/routine-requests.js +39 -1
- package/dist-server/server/routines.js +43 -9
- package/dist-server/server/shared-computer-control.js +31 -4
- package/dist-server/server/steer-queue.js +6 -0
- package/dist-server/server/store.js +94 -14
- package/dist-server/server/system-prompt.js +4 -4
- package/dist-server/server/team-backup.js +9 -1
- package/dist-server/server/tool-results.js +72 -0
- package/dist-server/server/tts/grok.js +74 -0
- package/dist-server/server/tts/index.js +19 -1
- package/dist-server/server/webhooks.js +28 -0
- package/dist-server/shared/approval-mode.js +7 -1
- package/dist-server/shared/digest.js +1 -0
- package/dist-server/shared/markdown-windows-paths.js +36 -0
- package/dist-server/shared/provider-icon.js +110 -0
- package/dist-server/shared/team-backup.js +3 -0
- package/dist-server/vps-container-mcp.js +217 -12
- package/enterprise/server/index.js +1 -0
- package/package.json +1 -1
- package/dist/assets/index-BbU5REzd.js +0 -310
- package/dist/assets/index-CLGfYlx_.css +0 -1
|
@@ -38,6 +38,7 @@ import { CREDENTIAL_TARGETS, isCredentialTargetId } from "../../shared/credentia
|
|
|
38
38
|
import { normalizeCronSchedule } from "../../shared/routine-schedule.js";
|
|
39
39
|
import { agentToolAnnotations } from "../agent-tool-policy.js";
|
|
40
40
|
import { peerName } from "../peer-roster.js";
|
|
41
|
+
import { boundedAgentResult } from "./agents-result.js";
|
|
41
42
|
const HARNESS = process.env.OMB_HARNESS_URL ?? "http://127.0.0.1:8799";
|
|
42
43
|
const BOT_ID = process.env.OMB_BOT_ID ?? "";
|
|
43
44
|
const THREAD_ID = process.env.OMB_THREAD_ID ?? "";
|
|
@@ -349,8 +350,8 @@ const ROUTINE_FIELDS_SCHEMA = {
|
|
|
349
350
|
schedule: ROUTINE_SCHEDULE_SCHEMA,
|
|
350
351
|
run_on: {
|
|
351
352
|
type: "string",
|
|
352
|
-
enum: ["maus", "
|
|
353
|
-
description: "
|
|
353
|
+
enum: ["maus", "box"],
|
|
354
|
+
description: "Default maus keeps the bot's selected model and configured computer, INCLUDING a self-hosted VPS. Omit this field for normal schedules. box explicitly switches the agent to the Box-hosted runner; it requires Box setup and is not the generic cloud/VPS option. Legacy cloud values from list_routines mean box, not VPS.",
|
|
354
355
|
},
|
|
355
356
|
timeout_minutes: {
|
|
356
357
|
type: "integer",
|
|
@@ -366,9 +367,26 @@ const ROUTINE_FIELDS_SCHEMA = {
|
|
|
366
367
|
type: "boolean",
|
|
367
368
|
description: "Opt in to using the latest completed run's bounded report as historical context. Defaults to false; set false in an update to start fresh again. Included in the applied result or pending confirmation.",
|
|
368
369
|
},
|
|
370
|
+
overlap: {
|
|
371
|
+
type: "string",
|
|
372
|
+
enum: ["skip", "queue"],
|
|
373
|
+
description: "While this routine is still working, skip scheduled occurrences (default) or queue at most one run. Queue skips further occurrences until the pending run starts; it never builds an unlimited backlog. Manual and webhook requests are separate.",
|
|
374
|
+
},
|
|
369
375
|
};
|
|
370
376
|
const PROPOSAL_OUTCOME = " Read the result: granted Full Access may apply the change immediately. If applied, continue the requested work without another confirmation. Only a pending result requires ending the turn and waiting for the in-app decision. Never claim success from the permission mode alone; report failed or cancelled results honestly. This does not elevate another bot's execution permissions.";
|
|
371
377
|
const TOOLS = [
|
|
378
|
+
{
|
|
379
|
+
name: "tool_result_read",
|
|
380
|
+
description: "Read a missing portion of an oversized agents-tool result using the saved id and next offset from its notice. Returns at most 16,000 characters, only from this bot in this conversation. Use only when the preview is insufficient; do not load every page by default. Results expire after one hour, on app restart, or under cache pressure. This never reruns the original action.",
|
|
381
|
+
inputSchema: {
|
|
382
|
+
type: "object", additionalProperties: false,
|
|
383
|
+
properties: {
|
|
384
|
+
id: { type: "string", description: "Saved result id copied from the truncation notice." },
|
|
385
|
+
offset: { type: "integer", minimum: 0, description: "Character offset copied from the previous result's notice. Defaults to 0." },
|
|
386
|
+
},
|
|
387
|
+
required: ["id"],
|
|
388
|
+
},
|
|
389
|
+
},
|
|
372
390
|
{
|
|
373
391
|
name: "list_shared_computers",
|
|
374
392
|
description: "List online desktop computers explicitly shared with this workspace, and their allowed folders/capabilities. These are the user's computers, not this server. An offline or unshared computer cannot be accessed. Folder paths use opaque folder IDs and relative paths.",
|
|
@@ -412,7 +430,7 @@ const TOOLS = [
|
|
|
412
430
|
},
|
|
413
431
|
{
|
|
414
432
|
name: "ask_bot",
|
|
415
|
-
description: "
|
|
433
|
+
description: "Brief synchronous consultation: send a short question to another bot. Quick replies return inline; slow replies become asynchronous delegations and return automatically after you finish your turn. Use only when that reply is required to write your current response. Do not use for assigning work, background tasks, or potentially long work; use delegate_bot for those. Returns promptly with a note if that bot is busy.",
|
|
416
434
|
inputSchema: {
|
|
417
435
|
type: "object",
|
|
418
436
|
properties: {
|
|
@@ -854,6 +872,8 @@ async function api(path, init) {
|
|
|
854
872
|
throw new Error(String(body.error ?? `HTTP ${status}`));
|
|
855
873
|
return body;
|
|
856
874
|
}
|
|
875
|
+
const capResult = (text) => boundedAgentResult(text, (retained, truncated) => api("/api/internal/tool-result", { method: "POST", signal: AbortSignal.timeout(3_000),
|
|
876
|
+
body: JSON.stringify({ text: retained, truncated }) }));
|
|
857
877
|
/** Like api, but a refusal comes back as its body instead of an Error —
|
|
858
878
|
* for the tools whose refusals carry more than a sentence. */
|
|
859
879
|
async function apiResponse(path, init) {
|
|
@@ -884,16 +904,17 @@ function routineFields(args) {
|
|
|
884
904
|
const fields = {};
|
|
885
905
|
// list_routines returns the harness names. Accept those when a model
|
|
886
906
|
// copies back a definition, as we already do for interval fields.
|
|
887
|
-
|
|
907
|
+
const destination = (value) => value === "box" ? "cloud" : value;
|
|
908
|
+
if (args.run_on != null && args.runOn != null && destination(args.run_on) !== destination(args.runOn)) {
|
|
888
909
|
return { fields, error: "Choose one run_on destination; run_on and runOn disagree." };
|
|
889
910
|
}
|
|
890
911
|
if (args.timeout_minutes != null && args.timeoutMinutes != null && args.timeout_minutes !== args.timeoutMinutes) {
|
|
891
912
|
return { fields, error: "Choose one timeout_minutes limit; timeout_minutes and timeoutMinutes disagree." };
|
|
892
913
|
}
|
|
893
|
-
const runOn = args.run_on ?? args.runOn;
|
|
914
|
+
const runOn = destination(args.run_on ?? args.runOn);
|
|
894
915
|
const timeoutMinutes = args.timeout_minutes ?? args.timeoutMinutes;
|
|
895
916
|
if (runOn != null && runOn !== "maus" && runOn !== "cloud") {
|
|
896
|
-
return { fields, error: 'run_on
|
|
917
|
+
return { fields, error: 'Use run_on="maus" for the bot’s current model and configured computer (including VPS), or run_on="box" only for the Box-hosted agent. Legacy "cloud" also means Box.' };
|
|
897
918
|
}
|
|
898
919
|
if (timeoutMinutes != null && (typeof timeoutMinutes !== "number" || !Number.isInteger(timeoutMinutes) || timeoutMinutes < 5 || timeoutMinutes > 240)) {
|
|
899
920
|
return { fields, error: "timeout_minutes must be a whole number from 5 to 240. Use clear_timeout to remove a limit." };
|
|
@@ -901,6 +922,9 @@ function routineFields(args) {
|
|
|
901
922
|
if (args.continuity != null && typeof args.continuity !== "boolean") {
|
|
902
923
|
return { fields, error: "continuity must be true or false." };
|
|
903
924
|
}
|
|
925
|
+
if (args.overlap !== undefined && args.overlap !== "skip" && args.overlap !== "queue") {
|
|
926
|
+
return { fields, error: "overlap must be skip or queue." };
|
|
927
|
+
}
|
|
904
928
|
if (args.clear_timeout != null && typeof args.clear_timeout !== "boolean") {
|
|
905
929
|
return { fields, error: "clear_timeout must be true or false." };
|
|
906
930
|
}
|
|
@@ -925,6 +949,8 @@ function routineFields(args) {
|
|
|
925
949
|
fields.timeoutMinutes = timeoutMinutes;
|
|
926
950
|
if (typeof args.continuity === "boolean")
|
|
927
951
|
fields.continuity = args.continuity;
|
|
952
|
+
if (args.overlap !== undefined)
|
|
953
|
+
fields.overlap = args.overlap;
|
|
928
954
|
return { fields };
|
|
929
955
|
}
|
|
930
956
|
/** Full Access is decided by the harness, not inferred from a model claim or
|
|
@@ -965,6 +991,17 @@ function recallSpeaker(hit) {
|
|
|
965
991
|
return hit.role === "user" ? "user" : "you";
|
|
966
992
|
}
|
|
967
993
|
async function callTool(name, args) {
|
|
994
|
+
if (name === "tool_result_read") {
|
|
995
|
+
if (typeof args.id !== "string" || !/^r-[0-9a-f-]{36}$/.test(args.id) ||
|
|
996
|
+
(args.offset !== undefined && (!Number.isSafeInteger(args.offset) || Number(args.offset) < 0))) {
|
|
997
|
+
return { text: "Use the saved result id and a non-negative integer offset from its notice.", isError: true };
|
|
998
|
+
}
|
|
999
|
+
const r = await api(`/api/internal/tool-result?id=${encodeURIComponent(args.id)}&offset=${args.offset ?? 0}`, { signal: AbortSignal.timeout(3_000) });
|
|
1000
|
+
const text = String(r.text ?? "");
|
|
1001
|
+
return { text: `${text}\n\n[${Number(r.nextOffset) < Number(r.length)
|
|
1002
|
+
? `Read more with tool_result_read id "${args.id}" and offset ${r.nextOffset}.`
|
|
1003
|
+
: `End of retained result.${r.truncated ? " The original tail exceeded the storage limit and was omitted." : ""}`}]` };
|
|
1004
|
+
}
|
|
968
1005
|
if (name === "list_room_targets") {
|
|
969
1006
|
const r = await api("/api/internal/room-targets");
|
|
970
1007
|
return { text: JSON.stringify(r), ...(r.error ? { isError: true } : {}) };
|
|
@@ -1081,9 +1118,11 @@ async function callTool(name, args) {
|
|
|
1081
1118
|
const taskId = String(r.taskId ?? "").trim();
|
|
1082
1119
|
if (taskId)
|
|
1083
1120
|
delegationTaskIdsThisTurn.add(taskId);
|
|
1084
|
-
const
|
|
1121
|
+
const waitedSeconds = Math.max(1, Math.round((Number(r.waitedMs) || 0) / 1000));
|
|
1122
|
+
const amount = waitedSeconds < 60 ? waitedSeconds : Math.round(waitedSeconds / 60);
|
|
1123
|
+
const unit = waitedSeconds < 60 ? "second" : "minute";
|
|
1085
1124
|
return {
|
|
1086
|
-
text: `${r.toBotName ?? "That bot"} is still working after ${
|
|
1125
|
+
text: `${r.toBotName ?? "That bot"} is still working after ${amount} ${unit}${amount === 1 ? "" : "s"} — the ask was converted to a delegation so the reply is not lost. Task id: ${taskId}. Finish your turn now; the result will be delivered to this conversation automatically. Use check_delegation in a later turn only if the user asks for status.`,
|
|
1087
1126
|
};
|
|
1088
1127
|
}
|
|
1089
1128
|
if (r.busy) {
|
|
@@ -1734,7 +1773,7 @@ async function handle(msg) {
|
|
|
1734
1773
|
return;
|
|
1735
1774
|
}
|
|
1736
1775
|
if (name === "list_shared_computers") {
|
|
1737
|
-
textResult(id, JSON.stringify(await api("/api/internal/shared-computers")));
|
|
1776
|
+
textResult(id, await capResult(JSON.stringify(await api("/api/internal/shared-computers"))));
|
|
1738
1777
|
return;
|
|
1739
1778
|
}
|
|
1740
1779
|
if (name === "shared_computer") {
|
|
@@ -1743,14 +1782,14 @@ async function handle(msg) {
|
|
|
1743
1782
|
if (Array.isArray(result?.content))
|
|
1744
1783
|
ok(id, result);
|
|
1745
1784
|
else
|
|
1746
|
-
textResult(id, JSON.stringify(result));
|
|
1785
|
+
textResult(id, await capResult(JSON.stringify(result)));
|
|
1747
1786
|
return;
|
|
1748
1787
|
}
|
|
1749
1788
|
const { text, isError } = await callTool(name, (params.arguments ?? {}));
|
|
1750
|
-
textResult(id, text, isError);
|
|
1789
|
+
textResult(id, name === "tool_result_read" ? text : await capResult(text), isError);
|
|
1751
1790
|
}
|
|
1752
1791
|
catch (e) {
|
|
1753
|
-
textResult(id, e.message, true);
|
|
1792
|
+
textResult(id, await capResult(e.message), true);
|
|
1754
1793
|
}
|
|
1755
1794
|
return;
|
|
1756
1795
|
}
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
import { redactSecretsInText } from "../../shared/redact.js";
|
|
2
|
+
import { TOOL_RESULT_MAX_CHARS, TOOL_RESULT_PREVIEW_CHARS, toolResultPrefix } from "../tool-results.js";
|
|
3
|
+
/** The operation already happened. Saving overflow must never retry it or
|
|
4
|
+
* turn a successful operation into a failed MCP call. Only cache I/O is timed. */
|
|
5
|
+
export async function boundedAgentResult(text, save) {
|
|
6
|
+
if (text.length <= 24_000)
|
|
7
|
+
return text;
|
|
8
|
+
const redacted = redactSecretsInText(text);
|
|
9
|
+
const prefix = toolResultPrefix(redacted, TOOL_RESULT_PREVIEW_CHARS);
|
|
10
|
+
const retained = toolResultPrefix(redacted, TOOL_RESULT_MAX_CHARS);
|
|
11
|
+
const truncated = retained.length < redacted.length;
|
|
12
|
+
try {
|
|
13
|
+
const saved = await save(retained, truncated);
|
|
14
|
+
if (!saved || typeof saved.id !== "string" || !/^r-[0-9a-f-]{36}$/.test(saved.id))
|
|
15
|
+
throw new Error("Invalid saved result");
|
|
16
|
+
return `${prefix}\n\n[Large tool result: showing the first ${prefix.length} characters. ${truncated || saved.truncated
|
|
17
|
+
? "Only a bounded portion was retained; the remaining tail was omitted."
|
|
18
|
+
: "The remaining redacted result is temporarily saved."} If a missing detail is needed, call tool_result_read with id "${saved.id}" and offset ${prefix.length}. Saved results expire after one hour, on app restart, or under cache pressure. Do not repeat an action just to retrieve its output.]`;
|
|
19
|
+
}
|
|
20
|
+
catch {
|
|
21
|
+
return `${prefix}\n\n[Large tool result: showing the first ${prefix.length} characters. The remaining output could not be saved. The original operation was not retried. Do not repeat an action just to retrieve its output.]`;
|
|
22
|
+
}
|
|
23
|
+
}
|
|
@@ -9,11 +9,12 @@
|
|
|
9
9
|
// - the bot's cloud computer (box.ascii.dev) via server/computer-proxy.ts
|
|
10
10
|
// — screenshot/exec/open_url, the CUA-on-the-box bridge
|
|
11
11
|
import { createHash, randomBytes } from "node:crypto";
|
|
12
|
-
import { chmodSync, mkdtempSync, readFileSync, rmSync, unlinkSync, writeFileSync } from "node:fs";
|
|
12
|
+
import { chmodSync, mkdirSync, mkdtempSync, readFileSync, rmSync, unlinkSync, writeFileSync } from "node:fs";
|
|
13
13
|
import { createServer as createNetServer } from "node:net";
|
|
14
14
|
import { homedir, tmpdir } from "node:os";
|
|
15
15
|
import { join, dirname, isAbsolute, normalize } from "node:path";
|
|
16
16
|
import { DATA_DIR, stripWorkspaceCredentialEnv } from "../config.js";
|
|
17
|
+
import { writeFileAtomic } from "../atomic.js";
|
|
17
18
|
import { augmentedPath } from "../env-path.js";
|
|
18
19
|
import { brokerSocketPath, describeSpawnFailure, execCli, killCliTree, spawnCli } from "../procs.js";
|
|
19
20
|
import { classifyResumeFailure, mayReplay, recoveryPromptFor } from "../resume-recovery.js";
|
|
@@ -25,6 +26,7 @@ import { classifyError, computeBackoff, interruptibleDelay, RETRY_MAX_ATTEMPTS }
|
|
|
25
26
|
import { applyClaudeInject, decodeInjectId, mergeLocalInject, probeLocalInjects, resolveInjectId, } from "./local-inject.js";
|
|
26
27
|
import { appendNative } from "./native.js";
|
|
27
28
|
import { SPAWNED_PROXIES } from "../proxy-paths.js";
|
|
29
|
+
import { extractMcpImages } from "../mcp-tool-images.js";
|
|
28
30
|
import { ASK_USER_QUESTION_TOOL, askQuestionSummary, parseAskQuestions, parseChoices, questionChoices, } from "../../shared/ask-question.js";
|
|
29
31
|
/** Whether `claude` has been signed in.
|
|
30
32
|
*
|
|
@@ -392,6 +394,7 @@ export function readClaudeModelCatalog(env = process.env) {
|
|
|
392
394
|
// far. See server/proxy-paths.ts.
|
|
393
395
|
const PERM_PROXY_PATH = SPAWNED_PROXIES.permission;
|
|
394
396
|
const DWEB_PROXY_PATH = SPAWNED_PROXIES.dweb;
|
|
397
|
+
const HOOK_HELPER_PATH = SPAWNED_PROXIES.hook;
|
|
395
398
|
// in the packaged app process.execPath is the Electron binary — this env
|
|
396
399
|
// makes it behave as plain node for the spawned MCP proxies (harmless in dev)
|
|
397
400
|
const NODE_ENV_FLAG = { ELECTRON_RUN_AS_NODE: "1" };
|
|
@@ -430,6 +433,26 @@ function askSummary(ask) {
|
|
|
430
433
|
return askQuestionSummary(questions).slice(0, 300);
|
|
431
434
|
return askInputSummary(ask.input) ?? ask.tool ?? "tool";
|
|
432
435
|
}
|
|
436
|
+
/** Where the hook helper reads this thread's current turn token. Stable per
|
|
437
|
+
* thread (so the CLI's environment can name it once) and private. */
|
|
438
|
+
export function hookTokenFile(threadId, botId) {
|
|
439
|
+
const digest = createHash("sha256").update(`${botId ?? ""}\0${threadId}`).digest("hex").slice(0, 24);
|
|
440
|
+
return join(DATA_DIR, "hook-tokens", `${digest}.token`);
|
|
441
|
+
}
|
|
442
|
+
/** The `hooks` block for the private --settings file: one command for each
|
|
443
|
+
* event the harness observes. Claude Code runs it with the event JSON on
|
|
444
|
+
* stdin and applies any hookSpecificOutput it prints. The command string is
|
|
445
|
+
* a shell line, so both paths are quoted (this repo's own path has a space). */
|
|
446
|
+
export function claudeHookSettings(helperPath) {
|
|
447
|
+
// JSON quoting is not shell quoting: $(), backticks and $names still
|
|
448
|
+
// expand inside double quotes on POSIX. Windows paths come through env
|
|
449
|
+
// variables so their backslashes are not JSON-escaped into the command.
|
|
450
|
+
const command = process.platform === "win32"
|
|
451
|
+
? '"%OMB_HOOK_NODE%" "%OMB_HOOK_HELPER%"'
|
|
452
|
+
: [process.execPath, helperPath].map(path => `'${path.replace(/'/g, "'\\''")}'`).join(" ");
|
|
453
|
+
const entry = [{ matcher: "", hooks: [{ type: "command", command, timeout: 5 }] }];
|
|
454
|
+
return { PostToolUse: entry, PreCompact: entry, SessionStart: entry, Stop: entry };
|
|
455
|
+
}
|
|
433
456
|
export function permissionSocketPath(threadId, botId) {
|
|
434
457
|
// A readable prefix alone is not unique: ids that agree on their first
|
|
435
458
|
// characters ("t-perm-dup-1", "t-perm-dup-2") would share a socket. POSIX
|
|
@@ -953,7 +976,7 @@ export const ClaudeDriver = {
|
|
|
953
976
|
// a retry relaunches the whole CLI; the backoff is scaled down in tests
|
|
954
977
|
// so a fake's transient failures don't stall real seconds
|
|
955
978
|
const retryScale = Number(process.env.FAKE_CLAUDE_RETRY_SCALE ?? "1");
|
|
956
|
-
const sessionId = typeof turn.resumeCursor === "string" ? turn.resumeCursor : null;
|
|
979
|
+
const sessionId = !turn.sessionReset && typeof turn.resumeCursor === "string" ? turn.resumeCursor : null;
|
|
957
980
|
const newSessionId = sessionId ? null : newId();
|
|
958
981
|
const args = [
|
|
959
982
|
"-p",
|
|
@@ -1140,7 +1163,28 @@ export const ClaudeDriver = {
|
|
|
1140
1163
|
const env = environment(turnModel);
|
|
1141
1164
|
const authSettings = isolated && !injected.injected
|
|
1142
1165
|
? readClaudeAuthSettings(env, input.environment) : {};
|
|
1143
|
-
|
|
1166
|
+
// Harness hooks (item 0.2): one helper command for the events the
|
|
1167
|
+
// harness observes. The helper reads its bearer from a per-thread file
|
|
1168
|
+
// the harness rewrites every turn, so a long-lived CLI process never
|
|
1169
|
+
// presents a stale token. Registered through the same private
|
|
1170
|
+
// --settings file as the auth override; both are 0600 and per launch.
|
|
1171
|
+
const hooks = turn.integrations?.hooks;
|
|
1172
|
+
const hookTokenPath = hooks ? hookTokenFile(threadId, botId) : null;
|
|
1173
|
+
if (hooks && hookTokenPath) {
|
|
1174
|
+
mkdirSync(dirname(hookTokenPath), { recursive: true, mode: 0o700 });
|
|
1175
|
+
writeFileAtomic(hookTokenPath, hooks.token, { mode: 0o600 });
|
|
1176
|
+
env.OMB_HOOK_URL = hooks.url;
|
|
1177
|
+
env.OMB_HOOK_TOKEN_FILE = hookTokenPath;
|
|
1178
|
+
env.OMB_HOOK_NODE = process.execPath;
|
|
1179
|
+
env.OMB_HOOK_HELPER = HOOK_HELPER_PATH;
|
|
1180
|
+
// in the packaged app process.execPath is Electron — run the helper as node
|
|
1181
|
+
if (process.versions.electron)
|
|
1182
|
+
env.ELECTRON_RUN_AS_NODE = "1";
|
|
1183
|
+
}
|
|
1184
|
+
const settings = { ...authSettings };
|
|
1185
|
+
if (hooks)
|
|
1186
|
+
settings.hooks = claudeHookSettings(HOOK_HELPER_PATH);
|
|
1187
|
+
const authSettingsPath = mcpConfigPath && Object.keys(settings).length
|
|
1144
1188
|
? join(dirname(mcpConfigPath), "auth-settings.json") : null;
|
|
1145
1189
|
if (authSettingsPath)
|
|
1146
1190
|
args.push("--settings", authSettingsPath);
|
|
@@ -1163,6 +1207,8 @@ export const ClaudeDriver = {
|
|
|
1163
1207
|
model: injected.model ?? null,
|
|
1164
1208
|
base: env.ANTHROPIC_BASE_URL ?? null,
|
|
1165
1209
|
configDir: env.CLAUDE_CONFIG_DIR ?? null,
|
|
1210
|
+
// hooks on/off changes the settings file the process was launched with
|
|
1211
|
+
hooks: Boolean(hooks),
|
|
1166
1212
|
// Rotating an account's key/helper must not reuse the old process.
|
|
1167
1213
|
auth: createHash("sha256").update(JSON.stringify({
|
|
1168
1214
|
settings: authSettings,
|
|
@@ -1170,10 +1216,10 @@ export const ClaudeDriver = {
|
|
|
1170
1216
|
})).digest("hex"),
|
|
1171
1217
|
});
|
|
1172
1218
|
// Reuse the live process when it is idle, unchanged, and is the session
|
|
1173
|
-
// the harness wants resumed.
|
|
1174
|
-
//
|
|
1219
|
+
// the harness wants resumed. Clearing a cursor alone does not opt out
|
|
1220
|
+
// of legacy reuse: an explicit rebuild must discard the idle context.
|
|
1175
1221
|
const live = sessions.get(threadId);
|
|
1176
|
-
if (live && !live.turn && !live.closing && live.child.exitCode === null && live.argsKey === argsKey && (!sessionId || sessionId === live.sessionId)) {
|
|
1222
|
+
if (!turn.sessionReset && live && !live.turn && !live.closing && live.child.exitCode === null && live.argsKey === argsKey && (!sessionId || sessionId === live.sessionId)) {
|
|
1177
1223
|
if (live.idleTimer)
|
|
1178
1224
|
clearTimeout(live.idleTimer);
|
|
1179
1225
|
live.turn = { turnId, input: turn, retryAbort, settled: false, sawStreamDelta: false };
|
|
@@ -1213,7 +1259,7 @@ export const ClaudeDriver = {
|
|
|
1213
1259
|
return { turnId };
|
|
1214
1260
|
}
|
|
1215
1261
|
if (live)
|
|
1216
|
-
closeSession(threadId, "spawn contract changed");
|
|
1262
|
+
closeSession(threadId, turn.sessionReset ? "context reset" : "spawn contract changed");
|
|
1217
1263
|
// Until sessions.set() below, this turn owns every launch resource.
|
|
1218
1264
|
// Any bind, private-config or synchronous spawn failure must release
|
|
1219
1265
|
// them here rather than leave a live listener or credential temp file.
|
|
@@ -1308,7 +1354,7 @@ export const ClaudeDriver = {
|
|
|
1308
1354
|
writeFileSync(mcpConfigPath, JSON.stringify({ mcpServers }), { mode: 0o600 });
|
|
1309
1355
|
}
|
|
1310
1356
|
if (authSettingsPath) {
|
|
1311
|
-
writeFileSync(authSettingsPath, JSON.stringify(
|
|
1357
|
+
writeFileSync(authSettingsPath, JSON.stringify(settings), { mode: 0o600 });
|
|
1312
1358
|
}
|
|
1313
1359
|
if (sessionId)
|
|
1314
1360
|
args.push("--resume", sessionId);
|
|
@@ -1493,6 +1539,9 @@ export const ClaudeDriver = {
|
|
|
1493
1539
|
for (const b of Array.isArray(o.message?.content) ? o.message.content : []) {
|
|
1494
1540
|
if (b.type === "tool_result") {
|
|
1495
1541
|
emit({ ...base(threadId, currentTurnId()), type: "item.completed", itemType: "tool", itemId: b.tool_use_id, ok: !b.is_error, output: toolDetailPreview(b.content) });
|
|
1542
|
+
for (const img of extractMcpImages(b.content)) {
|
|
1543
|
+
emit({ ...base(threadId, currentTurnId()), type: "item.completed", itemType: "assistant_image", data: img.data });
|
|
1544
|
+
}
|
|
1496
1545
|
}
|
|
1497
1546
|
}
|
|
1498
1547
|
break;
|
|
@@ -1627,7 +1676,9 @@ export const ClaudeDriver = {
|
|
|
1627
1676
|
active.set(threadId, { stop: () => { retry.cancelled = true; retryAbort.abort(); }, turnId });
|
|
1628
1677
|
try {
|
|
1629
1678
|
const cursor = session.sessionId ?? sessionId ?? undefined;
|
|
1630
|
-
|
|
1679
|
+
// The reset was consumed by the initial launch. Retry the
|
|
1680
|
+
// new session, never the context that launch replaced.
|
|
1681
|
+
await sendTurn({ ...turn, sessionReset: false, resumeCursor: cursor }, turnId);
|
|
1631
1682
|
}
|
|
1632
1683
|
catch (e) {
|
|
1633
1684
|
if (active.get(threadId)?.turnId === turnId)
|
|
@@ -1887,6 +1938,7 @@ export const ClaudeDriver = {
|
|
|
1887
1938
|
// Harness turns reassert a per-bot mode and restore the broker even
|
|
1888
1939
|
// when an old instance was configured with bypassPermissions.
|
|
1889
1940
|
localComputerMcp: true,
|
|
1941
|
+
hooks: true,
|
|
1890
1942
|
},
|
|
1891
1943
|
sendTurn,
|
|
1892
1944
|
steer,
|
|
@@ -1914,7 +1966,7 @@ export const ClaudeDriver = {
|
|
|
1914
1966
|
return () => listeners.delete(listener);
|
|
1915
1967
|
},
|
|
1916
1968
|
},
|
|
1917
|
-
generateText: (prompt) => generateReview(prompt),
|
|
1969
|
+
generateText: (prompt, options) => generateReview(prompt, options?.signal),
|
|
1918
1970
|
reviewPermission: generateReview,
|
|
1919
1971
|
dispose: async () => {
|
|
1920
1972
|
try {
|
|
@@ -25,6 +25,7 @@ import { codexDeveloperInstructions, syncCodexInstructions } from "./codex-instr
|
|
|
25
25
|
import { CodexDeviceAuthController } from "./codex-device-auth.js";
|
|
26
26
|
import { codexAccountEmail } from "./codex-identity.js";
|
|
27
27
|
import { classifyResumeFailure, mayReplay, recoveryPromptFor } from "../resume-recovery.js";
|
|
28
|
+
import { extractMcpImages } from "../mcp-tool-images.js";
|
|
28
29
|
export { decodeCodexSelection, readCodexModelCatalog, STATIC_CODEX_MODELS } from "./codex-catalog.js";
|
|
29
30
|
const DRIVER_KIND = "codex";
|
|
30
31
|
const ASTRA_MODEL_ID = "gpt-6-astra";
|
|
@@ -1039,6 +1040,11 @@ export const CodexDriver = {
|
|
|
1039
1040
|
ok: item.status !== "failed" && item.status !== "declined",
|
|
1040
1041
|
output: toolDetailPreview(item.type === "commandExecution" ? { output: item.aggregatedOutput, exitCode: item.exitCode } : item.type === "mcpToolCall" ? item.error ?? item.result : item.type === "fileChange" ? item.changes : item.action),
|
|
1041
1042
|
});
|
|
1043
|
+
if (item.type === "mcpToolCall") {
|
|
1044
|
+
for (const img of extractMcpImages(item.result)) {
|
|
1045
|
+
emit({ ...base(threadId, turnId), type: "item.completed", itemType: "assistant_image", data: img.data });
|
|
1046
|
+
}
|
|
1047
|
+
}
|
|
1042
1048
|
}
|
|
1043
1049
|
else if (item.type === "reasoning") {
|
|
1044
1050
|
emit({ ...base(threadId, turnId), type: "item.updated", itemType: "reasoning", tokens: null });
|
|
@@ -115,11 +115,67 @@ export function createOpenAIChatRuntime(options) {
|
|
|
115
115
|
const reader = response.body.getReader();
|
|
116
116
|
const decoder = new TextDecoder();
|
|
117
117
|
let buffer = "";
|
|
118
|
+
const consumeDataLine = (line, atEof = false) => {
|
|
119
|
+
if (!line.startsWith("data:"))
|
|
120
|
+
return false;
|
|
121
|
+
const data = line.slice(5).trim();
|
|
122
|
+
if (data === "[DONE]")
|
|
123
|
+
return true;
|
|
124
|
+
let chunk;
|
|
125
|
+
try {
|
|
126
|
+
chunk = JSON.parse(data);
|
|
127
|
+
}
|
|
128
|
+
catch {
|
|
129
|
+
// A tail left in the buffer when the socket closed is an INCOMPLETE
|
|
130
|
+
// frame, not a bad one: a stop, an abort or a dropped connection all
|
|
131
|
+
// end mid-frame. Only a properly newline-terminated frame that will
|
|
132
|
+
// not parse means the provider actually sent something malformed.
|
|
133
|
+
// Counting the tail here turned a stopped turn into a hard,
|
|
134
|
+
// non-retryable failure (`calls.finish(finishReason, malformedFrame)`).
|
|
135
|
+
if (!atEof)
|
|
136
|
+
malformedFrame = true;
|
|
137
|
+
return false;
|
|
138
|
+
}
|
|
139
|
+
const chunkError = providerError(chunk);
|
|
140
|
+
if (chunkError)
|
|
141
|
+
throw new ChatProtocolError(`provider returned a streaming completion error: ${chunkError.slice(0, 200)}`);
|
|
142
|
+
const choice = chunk.choices?.find((row) => row.index === undefined || row.index === 0);
|
|
143
|
+
const delta = choice?.delta;
|
|
144
|
+
if (object(delta))
|
|
145
|
+
sawChoice = true;
|
|
146
|
+
if (delta?.function_call)
|
|
147
|
+
throw new ChatProtocolError("legacy function_call is unsupported; use structured tool_calls");
|
|
148
|
+
calls.add(delta?.tool_calls, true);
|
|
149
|
+
details.add(delta?.reasoning_details);
|
|
150
|
+
if (choice?.finish_reason)
|
|
151
|
+
finishReason = choice.finish_reason;
|
|
152
|
+
const reasoningPart = delta?.reasoning_content ?? delta?.reasoning;
|
|
153
|
+
if (typeof reasoningPart === "string")
|
|
154
|
+
protocolReasoning += reasoningPart;
|
|
155
|
+
const reasoningDelta = options.reasoning && typeof reasoningPart === "string"
|
|
156
|
+
? reasoningPart
|
|
157
|
+
: "";
|
|
158
|
+
const contentDelta = typeof delta?.content === "string" ? delta.content : "";
|
|
159
|
+
if (reasoningDelta) {
|
|
160
|
+
reasoning += reasoningDelta;
|
|
161
|
+
onDelta?.(reasoningDelta, "reasoning_text");
|
|
162
|
+
}
|
|
163
|
+
if (contentDelta) {
|
|
164
|
+
text += contentDelta;
|
|
165
|
+
onDelta?.(contentDelta, "assistant_text");
|
|
166
|
+
}
|
|
167
|
+
if (chunk.usage)
|
|
168
|
+
usage = usageFrom(chunk.usage);
|
|
169
|
+
return false;
|
|
170
|
+
};
|
|
118
171
|
try {
|
|
119
172
|
readLoop: for (;;) {
|
|
120
173
|
const { done, value } = await reader.read();
|
|
121
174
|
if (done) {
|
|
122
175
|
buffer += decoder.decode();
|
|
176
|
+
const line = buffer.trim();
|
|
177
|
+
if (line && line !== "data: [DONE]")
|
|
178
|
+
consumeDataLine(line, true);
|
|
123
179
|
// MiniMax's api.minimax.io/v1 closes the connection after the
|
|
124
180
|
// finish_reason chunk and never sends `[DONE]`.
|
|
125
181
|
if (buffer.trim() === "data: [DONE]" || finishReason)
|
|
@@ -146,49 +202,8 @@ export function createOpenAIChatRuntime(options) {
|
|
|
146
202
|
while ((newline = buffer.indexOf("\n")) !== -1) {
|
|
147
203
|
const line = buffer.slice(0, newline).trim();
|
|
148
204
|
buffer = buffer.slice(newline + 1);
|
|
149
|
-
if (
|
|
150
|
-
continue;
|
|
151
|
-
const data = line.slice(5).trim();
|
|
152
|
-
if (data === "[DONE]")
|
|
205
|
+
if (consumeDataLine(line))
|
|
153
206
|
break readLoop;
|
|
154
|
-
let chunk;
|
|
155
|
-
try {
|
|
156
|
-
chunk = JSON.parse(data);
|
|
157
|
-
}
|
|
158
|
-
catch {
|
|
159
|
-
malformedFrame = true;
|
|
160
|
-
continue;
|
|
161
|
-
}
|
|
162
|
-
const chunkError = providerError(chunk);
|
|
163
|
-
if (chunkError)
|
|
164
|
-
throw new ChatProtocolError(`provider returned a streaming completion error: ${chunkError.slice(0, 200)}`);
|
|
165
|
-
const choice = chunk.choices?.find((row) => row.index === undefined || row.index === 0);
|
|
166
|
-
const delta = choice?.delta;
|
|
167
|
-
if (object(delta))
|
|
168
|
-
sawChoice = true;
|
|
169
|
-
if (delta?.function_call)
|
|
170
|
-
throw new ChatProtocolError("legacy function_call is unsupported; use structured tool_calls");
|
|
171
|
-
calls.add(delta?.tool_calls, true);
|
|
172
|
-
details.add(delta?.reasoning_details);
|
|
173
|
-
if (choice?.finish_reason)
|
|
174
|
-
finishReason = choice.finish_reason;
|
|
175
|
-
const reasoningPart = delta?.reasoning_content ?? delta?.reasoning;
|
|
176
|
-
if (typeof reasoningPart === "string")
|
|
177
|
-
protocolReasoning += reasoningPart;
|
|
178
|
-
const reasoningDelta = options.reasoning && typeof reasoningPart === "string"
|
|
179
|
-
? reasoningPart
|
|
180
|
-
: "";
|
|
181
|
-
const contentDelta = typeof delta?.content === "string" ? delta.content : "";
|
|
182
|
-
if (reasoningDelta) {
|
|
183
|
-
reasoning += reasoningDelta;
|
|
184
|
-
onDelta?.(reasoningDelta, "reasoning_text");
|
|
185
|
-
}
|
|
186
|
-
if (contentDelta) {
|
|
187
|
-
text += contentDelta;
|
|
188
|
-
onDelta?.(contentDelta, "assistant_text");
|
|
189
|
-
}
|
|
190
|
-
if (chunk.usage)
|
|
191
|
-
usage = usageFrom(chunk.usage);
|
|
192
207
|
}
|
|
193
208
|
}
|
|
194
209
|
}
|
|
@@ -383,7 +398,13 @@ export function createOpenAIChatRuntime(options) {
|
|
|
383
398
|
throw new ChatProtocolError("tool arguments must be a JSON object");
|
|
384
399
|
tools.validate(call.function.name, args);
|
|
385
400
|
const inputPreview = preview(args);
|
|
386
|
-
|
|
401
|
+
// Full access is the person's explicit grant to answer every
|
|
402
|
+
// prompt. This runtime has no provider reviewer to hand it to,
|
|
403
|
+
// so it is honoured here: without it every single tool call on
|
|
404
|
+
// an OpenAI-compatible engine stops for a card, and a Chief's
|
|
405
|
+
// delegated Full access cannot help either.
|
|
406
|
+
const allowed = turn.approvalMode === "full"
|
|
407
|
+
|| await approval.ask(call.function.name, inputPreview ?? "This tool has no arguments.");
|
|
387
408
|
abort.signal.throwIfAborted();
|
|
388
409
|
emit({ ...base(turn.threadId, turnId), type: "item.started", itemType: "tool", itemId: call.id,
|
|
389
410
|
title: call.function.name, ...(inputPreview ? { input: inputPreview } : {}),
|
|
@@ -489,9 +510,9 @@ export function createOpenAIChatRuntime(options) {
|
|
|
489
510
|
return () => listeners.delete(listener);
|
|
490
511
|
},
|
|
491
512
|
},
|
|
492
|
-
generateText: async (prompt) => {
|
|
513
|
+
generateText: async (prompt, { signal } = {}) => {
|
|
493
514
|
const model = options.generateModel?.() ?? options.models().default;
|
|
494
|
-
const { text, reasoning, toolCalls } = await complete([{ role: "user", content: prompt }], model, false);
|
|
515
|
+
const { text, reasoning, toolCalls } = await complete([{ role: "user", content: prompt }], model, false, signal);
|
|
495
516
|
if (toolCalls.length)
|
|
496
517
|
throw new ChatProtocolError("provider returned tool calls to a text-only helper");
|
|
497
518
|
return text.trim() ? text : reasoning;
|
|
@@ -132,6 +132,7 @@ export const OpenAICompatDriver = {
|
|
|
132
132
|
model,
|
|
133
133
|
messages,
|
|
134
134
|
stream,
|
|
135
|
+
stream_options: stream ? { include_usage: true } : undefined,
|
|
135
136
|
...(config.provider && isOpenRouterUrl(config.url)
|
|
136
137
|
? { provider: { order: [config.provider], allow_fallbacks: false } }
|
|
137
138
|
: {}),
|
|
@@ -0,0 +1,103 @@
|
|
|
1
|
+
// omb-hook — the one command OpenMausBot registers for Claude Code hooks
|
|
2
|
+
// (PostToolUse, PreCompact, SessionStart, Stop). Claude Code runs it with the
|
|
3
|
+
// hook's JSON on stdin and waits for it, so it obeys three rules learned
|
|
4
|
+
// the hard way by other harnesses:
|
|
5
|
+
//
|
|
6
|
+
// 1. It ALWAYS exits 0. A hook that fails or times out can block the
|
|
7
|
+
// agent's next step; the harness observing is never worth that.
|
|
8
|
+
// 2. It has a hard budget (OMB_HOOK_TIMEOUT_MS, default 4000) below Claude
|
|
9
|
+
// Code's own per-hook timeout, and speaks only to the loopback harness.
|
|
10
|
+
// 3. It is dependency-free and does no work itself: it forwards the event
|
|
11
|
+
// to POST /api/internal/hook with the turn's capability token (read
|
|
12
|
+
// from OMB_HOOK_TOKEN_FILE at run time — the token rotates per turn
|
|
13
|
+
// while the CLI process, and its environment, live on) and prints the
|
|
14
|
+
// harness's `hookSpecificOutput`, if any, for Claude Code to apply.
|
|
15
|
+
//
|
|
16
|
+
// stdout is the hook channel — never console.log anything else here.
|
|
17
|
+
import { readFileSync } from "node:fs";
|
|
18
|
+
const CONTEXT_EVENTS = new Set(["SessionStart", "UserPromptSubmit"]);
|
|
19
|
+
const requestedBudget = Number(process.env.OMB_HOOK_TIMEOUT_MS);
|
|
20
|
+
const budgetMs = Number.isFinite(requestedBudget) && requestedBudget > 0 ? Math.min(requestedBudget, 4_000) : 4_000;
|
|
21
|
+
const MAX_INPUT_BYTES = 1024 * 1024;
|
|
22
|
+
const url = process.env.OMB_HOOK_URL ?? "";
|
|
23
|
+
const tokenFile = process.env.OMB_HOOK_TOKEN_FILE ?? "";
|
|
24
|
+
const done = (out) => {
|
|
25
|
+
if (out === undefined)
|
|
26
|
+
process.exit(0);
|
|
27
|
+
process.stdout.write(out, () => process.exit(0));
|
|
28
|
+
};
|
|
29
|
+
process.stdout.on("error", () => process.exit(0));
|
|
30
|
+
// the outer fuse: whatever is still pending when this fires, we leave
|
|
31
|
+
const fuse = setTimeout(() => done(), budgetMs);
|
|
32
|
+
fuse.unref();
|
|
33
|
+
function readStdin() {
|
|
34
|
+
return new Promise((resolve) => {
|
|
35
|
+
let data = "";
|
|
36
|
+
let bytes = 0;
|
|
37
|
+
process.stdin.setEncoding("utf8");
|
|
38
|
+
process.stdin.on("data", (chunk) => {
|
|
39
|
+
bytes += Buffer.byteLength(chunk);
|
|
40
|
+
if (bytes > MAX_INPUT_BYTES)
|
|
41
|
+
return done();
|
|
42
|
+
data += chunk;
|
|
43
|
+
});
|
|
44
|
+
process.stdin.on("end", () => resolve(data));
|
|
45
|
+
process.stdin.on("error", () => resolve(data));
|
|
46
|
+
});
|
|
47
|
+
}
|
|
48
|
+
async function main() {
|
|
49
|
+
const raw = await readStdin();
|
|
50
|
+
let payload;
|
|
51
|
+
try {
|
|
52
|
+
const parsed = JSON.parse(raw);
|
|
53
|
+
if (!parsed || typeof parsed !== "object" || Array.isArray(parsed))
|
|
54
|
+
return done();
|
|
55
|
+
payload = parsed;
|
|
56
|
+
}
|
|
57
|
+
catch {
|
|
58
|
+
return done();
|
|
59
|
+
}
|
|
60
|
+
const event = typeof payload.hook_event_name === "string" ? payload.hook_event_name : "";
|
|
61
|
+
if (!event || !/^https?:\/\/127\.0\.0\.1(:\d+)?$/.test(url) || !tokenFile)
|
|
62
|
+
return done();
|
|
63
|
+
let token = "";
|
|
64
|
+
try {
|
|
65
|
+
token = readFileSync(tokenFile, "utf8").trim();
|
|
66
|
+
}
|
|
67
|
+
catch {
|
|
68
|
+
return done();
|
|
69
|
+
}
|
|
70
|
+
if (!token)
|
|
71
|
+
return done();
|
|
72
|
+
const controller = new AbortController();
|
|
73
|
+
const timer = setTimeout(() => controller.abort(), Math.max(200, budgetMs - 300));
|
|
74
|
+
try {
|
|
75
|
+
const response = await fetch(`${url}/api/internal/hook`, {
|
|
76
|
+
method: "POST",
|
|
77
|
+
headers: { "content-type": "application/json", authorization: `Bearer ${token}` },
|
|
78
|
+
body: JSON.stringify({ event, payload }),
|
|
79
|
+
redirect: "error",
|
|
80
|
+
signal: controller.signal,
|
|
81
|
+
});
|
|
82
|
+
if (!response.ok)
|
|
83
|
+
return done();
|
|
84
|
+
const body = (await response.json().catch(() => null));
|
|
85
|
+
if (body && typeof body === "object" && body.hookSpecificOutput && typeof body.hookSpecificOutput === "object") {
|
|
86
|
+
return done(JSON.stringify({ hookSpecificOutput: body.hookSpecificOutput }));
|
|
87
|
+
}
|
|
88
|
+
// On SessionStart (and UserPromptSubmit) Claude Code reads plain-text
|
|
89
|
+
// stdout as context the model sees; on every other event stdout is
|
|
90
|
+
// only a debug line, so a context body is printed nowhere else.
|
|
91
|
+
if (body && typeof body === "object" && typeof body.context === "string" && body.context && CONTEXT_EVENTS.has(event)) {
|
|
92
|
+
return done(body.context);
|
|
93
|
+
}
|
|
94
|
+
return done();
|
|
95
|
+
}
|
|
96
|
+
catch {
|
|
97
|
+
return done();
|
|
98
|
+
}
|
|
99
|
+
finally {
|
|
100
|
+
clearTimeout(timer);
|
|
101
|
+
}
|
|
102
|
+
}
|
|
103
|
+
void main();
|