@tangle-network/sdk-provider-claude-code 0.0.0-stage → 0.6.1

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/index.mjs ADDED
@@ -0,0 +1,1877 @@
1
+ import { randomBytes, randomUUID } from "node:crypto";
2
+ import { InteractionKind, ProxySecretBindingSchema, ProxySecretGrantSchema, ProxySecretReceiptSchema, ProxySecretUpdateControlsSchema, ProxySecretsSchema, isProxySecretScopeAttenuation, ownPropertyRecordSchema, proxySecretPresentationsEqual, resolveTangleRouterDefaults } from "@tangle-network/agent-interface";
3
+ import { CliProviderAdapterBase, EXECUTION_CORRELATION_HEADERS, InteractionBroker, InteractionMcpServer, TerminalPlanSupport, executionCorrelationHeaders, interactionMcpToolName, interactionToggleEnabled, jsonEventParser, localMcpArgv, localMcpEnv, nativeEffortArgs, partitionImageAttachments, reasoningEvents, recordCliTokenUsage, sessionObservedEvents, streamPartBase, textSnapshotEvents, toolResultEvents, toolStartEvents, trackFilePart, withTurnInteractions } from "@tangle-network/sdk-provider-cli-base";
4
+ import { z } from "zod";
5
+ import { HELD_SPARE_IDLE_MS, argAfter, createDeferredParser, createHeldSpareRegistry, launchDifferences, shellQuote } from "@tangle-network/sdk-cli-runner";
6
+ import { AsyncLocalStorage } from "node:async_hooks";
7
+ import { readFileSync } from "node:fs";
8
+ import { resolve } from "node:path";
9
+ import { unqualifyModelId } from "@tangle-network/agent-profile-materialize";
10
+ import { createServer } from "node:http";
11
+ //#region src/claude-event-state.ts
12
+ function assistantUsageEvents(event, state, backend) {
13
+ const message = event.message;
14
+ if (message?.usage === void 0) return [];
15
+ const id = message.id;
16
+ const usage = message.usage;
17
+ const keys = [
18
+ "input_tokens",
19
+ "output_tokens",
20
+ "cache_read_input_tokens",
21
+ "cache_creation_input_tokens"
22
+ ];
23
+ if (!(typeof id === "string" && id.length > 0 && keys.every((key) => Number.isSafeInteger(usage[key]) && usage[key] >= 0))) return [{
24
+ type: "raw",
25
+ backend,
26
+ event: {
27
+ type: "assistant.usage",
28
+ message_id: id,
29
+ usage
30
+ }
31
+ }];
32
+ const current = usage;
33
+ const previous = state.assistantUsage.get(id);
34
+ if (previous && keys.every((key) => current[key] === previous[key])) return [];
35
+ if (previous && keys.some((key) => current[key] < previous[key])) return [{
36
+ type: "raw",
37
+ backend,
38
+ event: {
39
+ type: "assistant.usage",
40
+ message_id: id,
41
+ usage: { error: "decreasing usage" }
42
+ }
43
+ }];
44
+ state.assistantUsage.set(id, { ...current });
45
+ return [{
46
+ type: "raw",
47
+ backend,
48
+ event: {
49
+ type: "assistant.usage",
50
+ message_id: id,
51
+ usage: Object.fromEntries(keys.map((key) => [key, current[key] - (previous?.[key] ?? 0)]))
52
+ }
53
+ }];
54
+ }
55
+ /**
56
+ * Creates a Part base with required fields.
57
+ */
58
+ /**
59
+ * Claude Code reports Anthropic's usage shape: `input_tokens` counts only the
60
+ * uncached prompt, and the cache read and cache creation counts are separate.
61
+ * The canonical `inputTokens` is the whole prompt with those two classes inside
62
+ * it, the convention the OpenCode and Codex adapters already report. Without
63
+ * this, a cached turn reported `inputTokens: 6` beside a cache read of 120k,
64
+ * and consumers that classify cache within the total discarded the breakdown.
65
+ * An absent input count stays absent rather than becoming a cache-only total.
66
+ */
67
+ function inclusiveClaudeInputTokens(usage) {
68
+ if (usage.inputTokens === void 0) return usage;
69
+ const inputTokens = usage.inputTokens + (usage.cacheReadInputTokens ?? 0) + (usage.cacheCreationInputTokens ?? 0);
70
+ return Number.isSafeInteger(inputTokens) ? {
71
+ ...usage,
72
+ inputTokens
73
+ } : usage;
74
+ }
75
+ /**
76
+ * Type guard for Claude Code events.
77
+ */
78
+ function isClaudeCodeEvent(value) {
79
+ if (typeof value !== "object" || value === null) return false;
80
+ return typeof value.type === "string";
81
+ }
82
+ //#endregion
83
+ //#region src/claude-shell-result.ts
84
+ /** The prefix the CLI puts on a failed shell command's result. */
85
+ const EXIT_CODE_PREFIX = /^(?:Error:\s*)?Exit code (-?\d+)\b/;
86
+ function exitCodeIn(value) {
87
+ const text = typeof value === "string" ? value : Array.isArray(value) ? value.map((part) => typeof part === "object" && part !== null && typeof part.text === "string" ? part.text : "").join("") : void 0;
88
+ const match = text ? EXIT_CODE_PREFIX.exec(text.trimStart()) : null;
89
+ return match ? Number(match[1]) : void 0;
90
+ }
91
+ /**
92
+ * A shell call's exit status and its two streams, for the part's metadata.
93
+ *
94
+ * The CLI sends a Bash result twice: as the tool_result text the model reads
95
+ * (stdout and stderr together), and as a structured `tool_use_result` beside it
96
+ * — `{ stdout, stderr, interrupted }` for a command that exited 0, or the text
97
+ * `"Error: Exit code N\n…"` for one that did not. The structured copy is the only
98
+ * place the streams are apart and the code is a number, so it is read here and
99
+ * carried as `metadata` the way OpenCode carries `metadata.exit`.
100
+ *
101
+ * Undefined for a tool that is not a shell, or a result that says neither.
102
+ */
103
+ function shellResultMetadata(toolName, content, isError, structured) {
104
+ if (!/^bash$/i.test(toolName)) return void 0;
105
+ const code = exitCodeIn(structured) ?? exitCodeIn(content);
106
+ if (typeof structured === "object" && structured !== null && !Array.isArray(structured)) {
107
+ const record = structured;
108
+ const stdout = typeof record.stdout === "string" ? record.stdout : void 0;
109
+ const stderr = typeof record.stderr === "string" ? record.stderr : void 0;
110
+ if (stdout !== void 0 || stderr !== void 0) {
111
+ const exitCode = code ?? (isError ? void 0 : 0);
112
+ return {
113
+ ...exitCode !== void 0 ? { exitCode } : {},
114
+ ...stdout !== void 0 ? { stdout } : {},
115
+ ...stderr ? { stderr } : {}
116
+ };
117
+ }
118
+ }
119
+ return code !== void 0 ? { exitCode: code } : void 0;
120
+ }
121
+ //#endregion
122
+ //#region ../runtime-contracts/dist/model-key-proxy.js
123
+ const MODEL_KEY_PROXY_ENV = "TANGLE_OPENCODE_MODEL_PROXY";
124
+ const TURN_KEY_PLACEHOLDER_PREFIX = "tangle-turn-key";
125
+ const MODEL_KEY_PROXY_PATH_PREFIX = "/v1/";
126
+ const MODEL_KEY_PROXY_SPARE_HEADER = "x-tangle-spare-id";
127
+ const LOOPBACK_BASE_URL = /^http:\/\/127\.0\.0\.1:[0-9]+$/;
128
+ function isTurnKeyPlaceholder(value) {
129
+ return value?.startsWith(`${TURN_KEY_PLACEHOLDER_PREFIX}.`) === true;
130
+ }
131
+ function parseModelKeyProxyBinding(raw) {
132
+ if (raw === void 0) return void 0;
133
+ const parsed = JSON.parse(raw);
134
+ const baseUrl = typeof parsed === "object" && parsed !== null && "baseUrl" in parsed ? parsed.baseUrl : void 0;
135
+ if (typeof baseUrl !== "string" || !LOOPBACK_BASE_URL.test(baseUrl)) throw new Error("Model key proxy binding is invalid");
136
+ return {
137
+ baseUrl,
138
+ passThrough: typeof parsed === "object" && parsed !== null && "passThrough" in parsed && parsed.passThrough === true
139
+ };
140
+ }
141
+ const MODEL_KEY_PROXY_HOME_PATH_PREFIX = "/v1h/";
142
+ function modelKeyProxyHomeTag(runtimeHome) {
143
+ return Buffer.from(runtimeHome.replace(/\/+$/, "")).toString("base64url");
144
+ }
145
+ function proxiedModelUrl(proxyBaseUrl, upstream, options) {
146
+ const encoded = Buffer.from(upstream).toString("base64url");
147
+ return options?.runtimeHome ? `${proxyBaseUrl}${MODEL_KEY_PROXY_HOME_PATH_PREFIX}${modelKeyProxyHomeTag(options.runtimeHome)}/${encoded}` : `${proxyBaseUrl}${MODEL_KEY_PROXY_PATH_PREFIX}${encoded}`;
148
+ }
149
+ function baseProxyToken(token) {
150
+ const i = token.indexOf(".");
151
+ return i === -1 ? token : token.slice(0, i);
152
+ }
153
+ const MODEL_PROXY_TOKENS = {
154
+ OPENCODE_MODEL_API_KEY: "proxy-model-token",
155
+ OPENAI_API_KEY: "proxy-openai-token",
156
+ ANTHROPIC_API_KEY: "proxy-anthropic-token",
157
+ OPENROUTER_API_KEY: "proxy-openrouter-token",
158
+ TANGLE_INFERENCE_KEY: "proxy-openai-token",
159
+ ZAI_API_KEY: "proxy-zai-token",
160
+ ZHIPU_API_KEY: "proxy-zai-token"
161
+ };
162
+ const MODEL_PROXY_TOKEN_VALUES = new Set(Object.values(MODEL_PROXY_TOKENS));
163
+ function isManagedModelToken(value) {
164
+ const key = value?.trim();
165
+ return !!key && MODEL_PROXY_TOKEN_VALUES.has(baseProxyToken(key));
166
+ }
167
+ function managedModelProxyUrl(input) {
168
+ if (!isManagedModelToken(input.apiKey) || isTurnKeyPlaceholder(input.apiKey)) return void 0;
169
+ const binding = parseModelKeyProxyBinding(input.binding);
170
+ if (binding?.passThrough !== true) return void 0;
171
+ try {
172
+ const url = new URL(input.upstream);
173
+ if (url.protocol !== "https:" && url.protocol !== "http:") return void 0;
174
+ if (url.username || url.password || url.search || url.hash) return void 0;
175
+ } catch {
176
+ return;
177
+ }
178
+ const runtimeHome = input.runtimeHome?.trim();
179
+ return proxiedModelUrl(binding.baseUrl, input.upstream, { ...runtimeHome ? { runtimeHome } : {} });
180
+ }
181
+ new Set([
182
+ ...Object.keys(MODEL_PROXY_TOKENS),
183
+ "TANGLE_API_KEY",
184
+ "GITHUB_TOKEN",
185
+ "GH_TOKEN"
186
+ ]);
187
+ /** True for a slot name, which exists at the proxy and never in a box env. */
188
+ function isDeclaredSecretSlotEnvVar(name) {
189
+ return /^TANGLE_DECLARED_SLOT_\d+$/.test(name);
190
+ }
191
+ //#endregion
192
+ //#region ../runtime-contracts/dist/harness-warm-transport.js
193
+ /** Set only by the Sandbox API after the OpenCode warm-server rollout decision. */
194
+ const AGENT_INTERNAL_OPENCODE_WARM_SERVER_ENV_VAR = "AGENT_INTERNAL_OPENCODE_WARM_SERVER";
195
+ /**
196
+ * Sidecar switch: after a warm claim, start the first session's OpenCode
197
+ * server before that session's first turn. Effective only with
198
+ * AGENT_INTERNAL_OPENCODE_WARM_SERVER=1. Any value other than "1" is off.
199
+ */
200
+ const AGENT_INTERNAL_OPENCODE_PREWARM_ENV_VAR = "AGENT_INTERNAL_OPENCODE_PREWARM";
201
+ /** Persistent Kimi wire process and shared create/resume prewarm gates. */
202
+ const AGENT_INTERNAL_KIMI_WARM_SERVER_ENV_VAR = "AGENT_INTERNAL_KIMI_WARM_SERVER";
203
+ const AGENT_INTERNAL_KIMI_PREWARM_ENV_VAR = "AGENT_INTERNAL_KIMI_PREWARM";
204
+ /**
205
+ * Claude Code and Codex run one process per turn. A harness's warm switch
206
+ * sends its managed-token turns' model requests through the sidecar's key
207
+ * proxy (which reports each turn's first model request) and lets a process
208
+ * started ahead of the turn take the turn: Claude Code launches a managed turn
209
+ * with its prompt on stdin (stream-json); Codex already reads its prompt from
210
+ * stdin. Its prewarm switch starts that process at claim, boot and recover.
211
+ * Both default off; off, every turn calls its upstream directly.
212
+ */
213
+ const AGENT_INTERNAL_CLAUDE_CODE_WARM_SERVER_ENV_VAR = "AGENT_INTERNAL_CLAUDE_CODE_WARM_SERVER";
214
+ /**
215
+ * Per harness, the two switches the Sandbox API sets from its warm-server
216
+ * rollout decision: the long-lived process transport, and prewarm, which
217
+ * starts that process before the turn that uses it and is only meaningful
218
+ * with it. A harness joins the rollout by adding one row; the Sandbox API,
219
+ * the orchestrator's provision guard and its recreate carry all read this
220
+ * table. Values are "1" or "0"; anything else is off.
221
+ */
222
+ const HARNESS_WARM_TRANSPORT_ENV = {
223
+ opencode: {
224
+ warmServer: AGENT_INTERNAL_OPENCODE_WARM_SERVER_ENV_VAR,
225
+ prewarm: AGENT_INTERNAL_OPENCODE_PREWARM_ENV_VAR
226
+ },
227
+ "kimi-code": {
228
+ warmServer: AGENT_INTERNAL_KIMI_WARM_SERVER_ENV_VAR,
229
+ prewarm: AGENT_INTERNAL_KIMI_PREWARM_ENV_VAR
230
+ },
231
+ "claude-code": {
232
+ warmServer: AGENT_INTERNAL_CLAUDE_CODE_WARM_SERVER_ENV_VAR,
233
+ prewarm: "AGENT_INTERNAL_CLAUDE_CODE_PREWARM"
234
+ },
235
+ codex: {
236
+ warmServer: "AGENT_INTERNAL_CODEX_WARM_SERVER",
237
+ prewarm: "AGENT_INTERNAL_CODEX_PREWARM"
238
+ }
239
+ };
240
+ //#endregion
241
+ //#region ../runtime-contracts/dist/proxy-secret-materialization.js
242
+ const syntheticHeader = {
243
+ alg: "HS256",
244
+ typ: "JWT"
245
+ };
246
+ const encode = (value) => btoa(value instanceof Uint8Array ? String.fromCharCode(...value) : JSON.stringify(value)).replaceAll("+", "-").replaceAll("/", "_").replaceAll("=", "");
247
+ function placeholderPrefix(slot, format, expiresAt) {
248
+ return format === "jwt" ? `${encode(syntheticHeader)}.${encode({
249
+ sub: "sandbox-placeholder",
250
+ aud: "proxy-secret-placeholder",
251
+ exp: Math.floor(Date.parse(expiresAt) / 1e3)
252
+ })}.` : `proxy-declared-${slot.slice(21)}-token-`;
253
+ }
254
+ function isSyntheticPlaceholder(material) {
255
+ const prefix = placeholderPrefix(material.slot, material.grant.placeholderFormat, material.issuerExpiresAt);
256
+ return material.placeholder.startsWith(prefix) && /^[A-Za-z0-9_-]{43}$/.test(material.placeholder.slice(prefix.length));
257
+ }
258
+ z.array(z.strictObject({
259
+ binding: ProxySecretBindingSchema,
260
+ grant: ProxySecretGrantSchema,
261
+ placeholder: z.string().min(1),
262
+ slot: z.string().refine(isDeclaredSecretSlotEnvVar),
263
+ issuerExpiresAt: z.iso.datetime()
264
+ })).superRefine((materials, ctx) => {
265
+ const bindings = ProxySecretsSchema.safeParse(materials.map((m) => m.binding));
266
+ const unique = (values) => new Set(values).size === values.length;
267
+ if (!bindings.success || !unique(materials.map((m) => m.slot)) || !unique(materials.map((m) => m.placeholder)) || !unique(materials.map((m) => m.grant.grantId)) || new Set(materials.map((m) => m.grant.epoch)).size > 1 || new Set(materials.map((m) => m.grant.sandboxId)).size > 1 || materials.some((material) => !isSyntheticPlaceholder(material)) || materials.some(({ binding, grant, issuerExpiresAt }) => !isProxySecretScopeAttenuation(binding, grant.scope) || !proxySecretPresentationsEqual(binding.presentation, grant.presentation) || (binding.placeholderFormat ?? "opaque") !== grant.placeholderFormat || Date.parse(grant.expiresAt) > Date.parse(issuerExpiresAt))) ctx.addIssue({
268
+ code: "custom",
269
+ message: "Invalid proxy secret materialization"
270
+ });
271
+ });
272
+ ProxySecretUpdateControlsSchema.extend({
273
+ resolvedSecrets: ownPropertyRecordSchema(z.string().min(1), { maxEntries: Number.MAX_SAFE_INTEGER }).default({}),
274
+ proxySecretGrants: ProxySecretReceiptSchema.options[0].shape.bindings,
275
+ expectedProxySecretGeneration: ProxySecretUpdateControlsSchema.shape.expectedProxySecretGeneration.unwrap()
276
+ }).strict().refine((body) => new Set(body.proxySecretGrants.map((b) => b.envName)).size === body.proxySecretGrants.length);
277
+ //#endregion
278
+ //#region ../runtime-contracts/dist/runtime-environment.js
279
+ const SAFE_ENV_VAR_NAME_RE = /^[A-Za-z_][A-Za-z0-9_]*$/;
280
+ function isSafeEnvVarName(name) {
281
+ if (typeof name !== "string") return false;
282
+ if (name.length === 0 || name.length > 256) return false;
283
+ return SAFE_ENV_VAR_NAME_RE.test(name);
284
+ }
285
+ /** Client proxy URLs first, then bypass names. */
286
+ const EGRESS_PROXY_ENV_NAMES = [
287
+ "HTTPS_PROXY",
288
+ "https_proxy",
289
+ "HTTP_PROXY",
290
+ "http_proxy",
291
+ "NO_PROXY",
292
+ "no_proxy"
293
+ ];
294
+ const CA_TRUST_ENV_NAMES = [
295
+ "NODE_EXTRA_CA_CERTS",
296
+ "SSL_CERT_FILE",
297
+ "CURL_CA_BUNDLE",
298
+ "GIT_SSL_CAINFO",
299
+ "NIX_SSL_CERT_FILE"
300
+ ];
301
+ [...EGRESS_PROXY_ENV_NAMES, ...CA_TRUST_ENV_NAMES];
302
+ const FORBIDDEN_ENV_VAR_NAMES = new Set([
303
+ "LD_PRELOAD",
304
+ "LD_LIBRARY_PATH",
305
+ "LD_AUDIT",
306
+ "DYLD_INSERT_LIBRARIES",
307
+ "DYLD_LIBRARY_PATH",
308
+ "DYLD_FALLBACK_LIBRARY_PATH",
309
+ "NODE_OPTIONS",
310
+ "NODE_PATH",
311
+ "NODE_TLS_REJECT_UNAUTHORIZED",
312
+ "PYTHONPATH",
313
+ "PYTHONSTARTUP",
314
+ "PERL5OPT",
315
+ "PERL5LIB",
316
+ "RUBYOPT",
317
+ "RUBYLIB",
318
+ "PATH",
319
+ "NIX_BIN_PATH",
320
+ "EGRESS_EDGE_POOL_ENABLED",
321
+ "EDGE_BIND_ID",
322
+ "PRIVACY_EGRESS",
323
+ "LOG_REDACT",
324
+ "IFS",
325
+ "BASH_ENV",
326
+ "ENV",
327
+ "SHELL",
328
+ "SHELLOPTS",
329
+ "PROMPT_COMMAND",
330
+ "HOME",
331
+ "XDG_CONFIG_HOME",
332
+ "XDG_DATA_HOME",
333
+ "XDG_CACHE_HOME",
334
+ "XDG_STATE_HOME",
335
+ "XDG_RUNTIME_DIR",
336
+ "HTTPS_PROXY",
337
+ "HTTP_PROXY",
338
+ "ALL_PROXY",
339
+ "NO_PROXY",
340
+ "https_proxy",
341
+ "http_proxy",
342
+ "all_proxy",
343
+ "no_proxy",
344
+ "SSL_CERT_FILE",
345
+ "SSL_CERT_DIR",
346
+ "REQUESTS_CA_BUNDLE",
347
+ "CURL_CA_BUNDLE",
348
+ "NODE_EXTRA_CA_CERTS",
349
+ "HOSTNAME",
350
+ "CONTAINER_ID",
351
+ "AGENT_WORKSPACE_ROOT",
352
+ "WORKSPACE_PATH",
353
+ "STORAGE_PATH",
354
+ "WORKSPACE_FILE_API_ROOT_ONLY",
355
+ "WORKSPACE_ALLOWED_ABSOLUTE_PATHS",
356
+ "TANGLE_PLAN_CALLBACK_ALLOWED_ORIGINS"
357
+ ]);
358
+ const FORBIDDEN_ENV_VAR_PREFIXES = [
359
+ "SIDECAR_",
360
+ "ORCHESTRATOR_",
361
+ "AGENT_INTERNAL_",
362
+ "SANDBOX_DEV_",
363
+ "AGENT_SUBPROCESS_",
364
+ "TEE_ATTESTATION_"
365
+ ];
366
+ const RUNTIME_CALLER_ALLOWED_ENV_NAMES = new Set(["SIDECAR_CAPABILITIES", "SIDECAR_PLATFORM_CONTROL_RUNTIME"]);
367
+ new Set(Object.values(HARNESS_WARM_TRANSPORT_ENV).flatMap(({ warmServer, prewarm }) => [warmServer, prewarm]));
368
+ function isForbiddenEnvVarName(name) {
369
+ if (RUNTIME_CALLER_ALLOWED_ENV_NAMES.has(name)) return false;
370
+ if (FORBIDDEN_ENV_VAR_NAMES.has(name)) return true;
371
+ for (const prefix of FORBIDDEN_ENV_VAR_PREFIXES) if (name.startsWith(prefix)) return true;
372
+ return false;
373
+ }
374
+ /**
375
+ * POSIX names for caller-owned container env. Runtime process.env merges must additionally enforce
376
+ * isForbiddenEnvVarName, including project provisioning and Sidecar config.
377
+ */
378
+ const safeEnvRecordSchema = ownPropertyRecordSchema(z.string(), { maxEntries: Number.MAX_SAFE_INTEGER }).refine((value) => Object.keys(value).every(isSafeEnvVarName), { message: "invalid env var name" });
379
+ /** Names only: the caller must keep a separate source and pass values to resume({ env }). */
380
+ const sandboxEnvRecoverySchema = z.object({ callerResupplies: z.array(z.string().max(256).refine(isSafeEnvVarName, { message: "invalid env var name" })).max(128).refine((keys) => new Set(keys).size === keys.length, { message: "callerResupplies must contain unique keys" }) }).strict();
381
+ z.object({
382
+ envRecovery: sandboxEnvRecoverySchema,
383
+ dryRun: z.boolean().optional()
384
+ }).strict();
385
+ z.object({
386
+ envRecovery: sandboxEnvRecoverySchema,
387
+ eligibleCallerResupplyKeys: z.array(z.string()),
388
+ requiredOnColdResumeKeys: z.array(z.string()),
389
+ automaticCredentialRecovery: z.literal(false),
390
+ dryRun: z.boolean()
391
+ }).strict();
392
+ safeEnvRecordSchema.refine((value) => Object.keys(value).every((key) => !isForbiddenEnvVarName(key) && !key.startsWith("SIDECAR_")), { message: "runtime-owned env var name cannot be set by callers" });
393
+ //#endregion
394
+ //#region src/claude-launch-settings.ts
395
+ const PROFILE_SETTINGS_PATH = ".tangle/claude-settings.json";
396
+ function isPlainObject(value) {
397
+ return typeof value === "object" && value !== null && !Array.isArray(value);
398
+ }
399
+ function mergeClaudeSettings(base, overlay) {
400
+ const merged = { ...base };
401
+ for (const [key, value] of Object.entries(overlay)) {
402
+ const previous = merged[key];
403
+ if (Array.isArray(previous) && Array.isArray(value)) merged[key] = [...previous, ...value];
404
+ else if (isPlainObject(previous) && isPlainObject(value)) merged[key] = mergeClaudeSettings(previous, value);
405
+ else merged[key] = value;
406
+ }
407
+ return merged;
408
+ }
409
+ const THINKING_DISPLAY_FLAG = "--thinking-display";
410
+ function setsThinkingDisplay(flags) {
411
+ return flags.some((flag) => flag === "--thinking-display" || flag.startsWith(`--thinking-display=`));
412
+ }
413
+ function extractProfileSettings(flags) {
414
+ const extraFlags = [];
415
+ let usesProfileSettings = false;
416
+ let usesOtherSettings = false;
417
+ for (let index = 0; index < (flags?.length ?? 0); index += 1) {
418
+ const flag = flags?.[index];
419
+ if (flag?.startsWith("--settings=")) {
420
+ const value = flag.slice(11);
421
+ if (!value) throw new Error("Claude --settings requires a value");
422
+ if (value === ".tangle/claude-settings.json") {
423
+ if (usesProfileSettings) throw new Error("Claude profile settings were supplied more than once");
424
+ usesProfileSettings = true;
425
+ } else {
426
+ usesOtherSettings = true;
427
+ extraFlags.push(flag);
428
+ }
429
+ continue;
430
+ }
431
+ if (flag !== "--settings") {
432
+ extraFlags.push(flag);
433
+ continue;
434
+ }
435
+ const value = flags?.[index + 1];
436
+ if (value === void 0) throw new Error("Claude --settings requires a value");
437
+ if (value !== ".tangle/claude-settings.json") {
438
+ usesOtherSettings = true;
439
+ extraFlags.push(flag, value);
440
+ index += 1;
441
+ continue;
442
+ }
443
+ if (usesProfileSettings) throw new Error("Claude profile settings were supplied more than once");
444
+ usesProfileSettings = true;
445
+ index += 1;
446
+ }
447
+ if (usesProfileSettings && usesOtherSettings) throw new Error("Claude profile settings cannot be combined with another --settings source");
448
+ return {
449
+ extraFlags,
450
+ usesProfileSettings,
451
+ usesOtherSettings
452
+ };
453
+ }
454
+ function readProfileSettings(workspaceRoot) {
455
+ const value = JSON.parse(readFileSync(resolve(workspaceRoot, PROFILE_SETTINGS_PATH), "utf8"));
456
+ if (!isPlainObject(value)) throw new Error("Claude profile settings must contain a JSON object");
457
+ return value;
458
+ }
459
+ /**
460
+ * Convert a runtime `McpConfig` into the entry shape claude-code's inline
461
+ * `--mcp-config` expects (`McpHttpServerConfig` / `McpStdioServerConfig`).
462
+ *
463
+ * Accepts both spellings the two config sources use — `remote`/`http` for URL
464
+ * servers, `command` + `environment`/`env` for stdio ones — the same tolerance
465
+ * `buildMcpAddArgs` applies, so a config that can be registered with the CLI can
466
+ * always also be inlined.
467
+ */
468
+ function toInlineMcpServer(config) {
469
+ if (config.type === "http" || config.type === "remote") return {
470
+ type: "http",
471
+ url: config.url,
472
+ ...config.headers ? { headers: config.headers } : {}
473
+ };
474
+ const [bin, ...args] = localMcpArgv(config);
475
+ const env = localMcpEnv(config);
476
+ return {
477
+ type: "stdio",
478
+ command: bin,
479
+ ...args.length > 0 ? { args } : {},
480
+ ...env ? { env } : {}
481
+ };
482
+ }
483
+ const PLAN_HOOK_RELAY_SOURCE = `
484
+ const endpoint = process.argv[1];
485
+ const deny = (reason) => ({ hookSpecificOutput: { hookEventName: "PreToolUse", permissionDecision: "deny", permissionDecisionReason: reason } });
486
+ if (!endpoint) {
487
+ process.stdout.write(JSON.stringify(deny("Plan hook endpoint is missing")));
488
+ process.exit(0);
489
+ }
490
+ try {
491
+ const chunks = [];
492
+ for await (const chunk of process.stdin) chunks.push(Buffer.from(chunk));
493
+ const response = await fetch(endpoint, { method: "POST", headers: { "content-type": "application/json" }, body: Buffer.concat(chunks) });
494
+ if (!response.ok) throw new Error(\`Plan hook returned HTTP \${response.status}\`);
495
+ process.stdout.write(await response.text());
496
+ } catch (error) {
497
+ process.stdout.write(JSON.stringify(deny(error instanceof Error ? error.message : String(error))));
498
+ }
499
+ `;
500
+ //#endregion
501
+ //#region src/claude-model-auth.ts
502
+ const ANTHROPIC_BASE_URL = "https://api.anthropic.com";
503
+ /**
504
+ * True when `url`'s host is the managed Tangle router. The router is an
505
+ * OpenAI/OpenRouter-compatible gateway that authenticates via
506
+ * `Authorization: Bearer`, so a Claude Code session pointed here must carry
507
+ * its key in ANTHROPIC_AUTH_TOKEN (Bearer), not ANTHROPIC_API_KEY (x-api-key).
508
+ * Any other host (direct api.anthropic.com or a custom Anthropic-native proxy)
509
+ * keeps the native x-api-key path.
510
+ */
511
+ function isTangleRouterBaseUrl(url) {
512
+ let host;
513
+ try {
514
+ host = new URL(url).host;
515
+ } catch {
516
+ return false;
517
+ }
518
+ const defaults = resolveTangleRouterDefaults();
519
+ for (const candidate of [defaults.rootUrl, defaults.baseUrl]) try {
520
+ if (new URL(candidate).host === host) return true;
521
+ } catch {}
522
+ return false;
523
+ }
524
+ /**
525
+ * The URL a Claude Code process with an API key calls, and the key proxy's
526
+ * upstream: the config's own minus any `/vN` (the CLI appends `/v1/messages`,
527
+ * so a box's `<router>/v1` posted `/v1/v1/messages`), else Anthropic.
528
+ */
529
+ function resolveClaudeCodeBaseUrl(baseUrl) {
530
+ return baseUrl?.replace(/\/(v\d+)?\/*$/, "") || ANTHROPIC_BASE_URL;
531
+ }
532
+ /** Native subscription model IDs exclude the profile's provider qualification. */
533
+ function normalizeExplicitClaudeModel(model, authMode) {
534
+ const normalized = authMode === "oauth" ? unqualifyModelId("anthropic", model) : model;
535
+ if (!normalized.trim()) throw new Error("Explicit Claude model is empty after provider qualification");
536
+ return normalized;
537
+ }
538
+ //#endregion
539
+ //#region src/permission-mcp-server.ts
540
+ /**
541
+ * Claude Code's wiring onto the shared {@link InteractionMcpServer}
542
+ * (`@tangle-network/sdk-provider-cli-base`). The HTTP/MCP host + tools are shared;
543
+ * only the `--permission-prompt-tool` reply contract (`PermissionResult`) and its
544
+ * grant mapping are claude-specific and stay here.
545
+ */
546
+ const PERMISSION_MCP_SERVER_NAME = "tangle-permission";
547
+ /** The `mcp__<server>__<tool>` name passed to `--permission-prompt-tool`. */
548
+ const PERMISSION_MCP_TOOL = interactionMcpToolName(PERMISSION_MCP_SERVER_NAME, "permission");
549
+ /**
550
+ * The `ask_user` MODEL tool name. Unlike the permission tool (the
551
+ * `--permission-prompt-tool`, auto-excluded from the toolset), this one the agent
552
+ * calls itself, so it MUST be added to `--allowedTools`.
553
+ */
554
+ const ASK_USER_MCP_TOOL = interactionMcpToolName(PERMISSION_MCP_SERVER_NAME, "ask_user");
555
+ /**
556
+ * Map a broker {@link PermissionGrant} to a Claude Code {@link PermissionResult}.
557
+ * `allow_session`/`allow_always` also emit `updatedPermissions` addRules so the
558
+ * CLI's own permission store records the grant; note each turn is a fresh
559
+ * `claude --print` process, so cross-turn persistence is best-effort and a
560
+ * re-prompt next turn is expected.
561
+ */
562
+ function grantToPermissionResult(grant, toolName, input) {
563
+ if (grant === "deny") return {
564
+ behavior: "deny",
565
+ message: "Denied by user"
566
+ };
567
+ if (grant === "allow_once") return {
568
+ behavior: "allow",
569
+ updatedInput: input
570
+ };
571
+ return {
572
+ behavior: "allow",
573
+ updatedInput: input,
574
+ updatedPermissions: [{
575
+ type: "addRules",
576
+ rules: [{ toolName }],
577
+ behavior: "allow",
578
+ destination: grant === "allow_always" ? "localSettings" : "session"
579
+ }]
580
+ };
581
+ }
582
+ //#endregion
583
+ //#region src/claude-exec-args.ts
584
+ const DEFAULT_MODEL = "claude-sonnet-4-5";
585
+ /** Output, model, session, prompt and permission-mode flags of one turn. */
586
+ function claudeSessionArgs(params, planMode, routesPermissions) {
587
+ const args = [];
588
+ args.push("--print");
589
+ args.push("--output-format", "stream-json");
590
+ args.push("--verbose");
591
+ const authMode = params.config.authMode ?? (params.config.apiKey ? "api-key" : "oauth");
592
+ const model = params.model ?? params.config.model ?? (authMode === "api-key" ? DEFAULT_MODEL : void 0);
593
+ if (model) args.push("--model", normalizeExplicitClaudeModel(model, authMode));
594
+ if (params.sessionId) args.push("--resume", params.sessionId);
595
+ else {
596
+ if (params.mintedSessionId) args.push("--session-id", params.mintedSessionId);
597
+ if (params.systemPrompt) args.push("--system-prompt", params.systemPrompt);
598
+ if (params.appendSystemPrompt) args.push("--append-system-prompt", params.appendSystemPrompt);
599
+ }
600
+ if (!routesPermissions && !planMode && params.config.bypassPermissions !== false) args.push("--dangerously-skip-permissions");
601
+ if (planMode) args.push("--permission-mode", "plan");
602
+ return args;
603
+ }
604
+ /**
605
+ * The inline `--settings` object of one turn: the plan hook relay and the
606
+ * managed proxy endpoint, layered over the profile's settings file.
607
+ */
608
+ function claudeTurnSettings(executionContext, profileSettings, workspaceRoot, resolveProxiedBaseUrl) {
609
+ const { usesProfileSettings, usesOtherSettings } = profileSettings;
610
+ let executionSettings;
611
+ if (executionContext?.planHookServer) {
612
+ if (usesOtherSettings) throw new Error("Claude plan hooks cannot be combined with another --settings source");
613
+ const planSettings = { hooks: { PreToolUse: [{
614
+ matcher: "ExitPlanMode",
615
+ hooks: [{
616
+ type: "command",
617
+ command: `${shellQuote(process.execPath)} -e ${shellQuote(PLAN_HOOK_RELAY_SOURCE)} ${shellQuote(executionContext.planHookServer.hookUrl)}`,
618
+ timeout: 60
619
+ }]
620
+ }] } };
621
+ executionSettings = mergeClaudeSettings(usesProfileSettings ? readProfileSettings(workspaceRoot) : executionSettings ?? {}, planSettings);
622
+ }
623
+ const proxiedBaseUrl = resolveProxiedBaseUrl();
624
+ if (proxiedBaseUrl) executionSettings = mergeClaudeSettings(executionSettings ?? (usesProfileSettings ? readProfileSettings(workspaceRoot) : {}), { env: { ANTHROPIC_BASE_URL: proxiedBaseUrl } });
625
+ return executionSettings;
626
+ }
627
+ /**
628
+ * MCP wiring. The interactive permission decider is the
629
+ * --permission-prompt-tool, which the CLI auto-EXCLUDES from the model
630
+ * toolset (it must NOT be added to --allowedTools). Everything rides a
631
+ * SINGLE --mcp-config object (the flag is passed once). Tool names follow
632
+ * the documented `mcp__<server>__<tool>`.
633
+ *
634
+ * Runtime servers (hub connections, sandbox capabilities) MUST be merged
635
+ * here, not left to `claude mcp add` alone: whenever this object is
636
+ * non-empty we also pass --strict-mcp-config, which makes the CLI use ONLY
637
+ * these entries and ignore its own config store. Registering a server
638
+ * solely through the store meant it silently vanished on exactly the turns
639
+ * that carry a permission or plan interaction.
640
+ *
641
+ * They need no --allowedTools entry: a normal turn runs with
642
+ * --dangerously-skip-permissions, and a permission-routing turn is
643
+ * deliberately supposed to surface each tool use to the human.
644
+ */
645
+ function claudeMcpArgs(runtimeMcpServers, executionContext, modes) {
646
+ const args = [];
647
+ const mcpServers = { ...runtimeMcpServers };
648
+ const allowedMcpTools = [];
649
+ if (modes.hostsServer && executionContext?.permissionServer) {
650
+ const server = executionContext.permissionServer;
651
+ mcpServers[PERMISSION_MCP_SERVER_NAME] = {
652
+ type: "http",
653
+ url: server.url,
654
+ headers: { Authorization: `Bearer ${server.token}` }
655
+ };
656
+ if (modes.exposesAskUser) allowedMcpTools.push(ASK_USER_MCP_TOOL);
657
+ }
658
+ if (Object.keys(mcpServers).length > 0) {
659
+ args.push("--mcp-config", JSON.stringify({ mcpServers }));
660
+ args.push("--strict-mcp-config");
661
+ }
662
+ if (allowedMcpTools.length > 0) args.push("--allowedTools", allowedMcpTools.join(","));
663
+ if (modes.routesPermissions && executionContext?.permissionServer) args.push("--permission-prompt-tool", PERMISSION_MCP_TOOL);
664
+ return args;
665
+ }
666
+ /**
667
+ * The turn as one stream-json user message instead of a positional arg —
668
+ * the only way the CLI accepts inline image content.
669
+ */
670
+ function claudeStreamJsonTurn(messageText, qualifyingImages) {
671
+ const content = messageText ? [{
672
+ type: "text",
673
+ text: messageText
674
+ }] : [];
675
+ for (const att of qualifyingImages) content.push({
676
+ type: "image",
677
+ source: att.base64 !== void 0 ? {
678
+ type: "base64",
679
+ media_type: att.mediaType ?? "image/png",
680
+ data: att.base64
681
+ } : {
682
+ type: "url",
683
+ url: att.url
684
+ }
685
+ });
686
+ return `${JSON.stringify({
687
+ type: "user",
688
+ message: {
689
+ role: "user",
690
+ content
691
+ }
692
+ })}\n`;
693
+ }
694
+ //#endregion
695
+ //#region src/claude-execution-context.ts
696
+ const executionContexts = /* @__PURE__ */ new WeakMap();
697
+ function registerClaudeExecutionContext(adapter, context) {
698
+ executionContexts.set(adapter, context);
699
+ return context;
700
+ }
701
+ function claudeExecutionContext(adapter) {
702
+ return executionContexts.get(adapter);
703
+ }
704
+ //#endregion
705
+ //#region src/claude-launch-routing.ts
706
+ function claudeRoutesPlans(config) {
707
+ if (typeof config.interactions === "boolean" || config.interactions !== void 0 && "plan" in config.interactions) return interactionToggleEnabled(config.interactions, "plan");
708
+ return config.planMode === true;
709
+ }
710
+ /**
711
+ * Derived interaction wiring for a turn. `routesPermissions` (interactive or
712
+ * permission mode) installs the permission decider tool and REQUIRES dropping the
713
+ * bypass flag; `exposesAskUser` (interactive or `interactions.question`)
714
+ * publishes the ask_user model tool. A question-only turn hosts the server
715
+ * for ask_user alone while permissions stay fully bypassed.
716
+ */
717
+ function interactionModes(config) {
718
+ const interactive = config.interactivePermissions === true;
719
+ const routesPermissions = interactive || interactionToggleEnabled(config.interactions, "permission");
720
+ const exposesAskUser = interactive || interactionToggleEnabled(config.interactions, "question");
721
+ return {
722
+ routesPermissions,
723
+ exposesAskUser,
724
+ hostsServer: routesPermissions || exposesAskUser
725
+ };
726
+ }
727
+ /**
728
+ * The box's claude-code warm switch, set from the cohort decision: it sends
729
+ * managed turns through the key proxy and lets a held process take them.
730
+ * Off, every turn keeps the direct path.
731
+ */
732
+ function claudeWarmSwitchOn() {
733
+ return process.env[AGENT_INTERNAL_CLAUDE_CODE_WARM_SERVER_ENV_VAR] === "1";
734
+ }
735
+ /**
736
+ * The key proxy's binding as the host or the process environment names it.
737
+ * An explicit host value, including a scrub, wins over inherited state.
738
+ */
739
+ function modelKeyProxyBindingValue(agentEnvironmentValues) {
740
+ const host = agentEnvironmentValues();
741
+ return Object.hasOwn(host, "TANGLE_OPENCODE_MODEL_PROXY") ? host[MODEL_KEY_PROXY_ENV] : process.env[MODEL_KEY_PROXY_ENV];
742
+ }
743
+ /**
744
+ * Where a managed-token turn sends its model requests: the sidecar's
745
+ * model-key proxy, which reports the turn's first request and relays every
746
+ * header unchanged. Undefined (the direct path) off the warm switch, for a
747
+ * caller's own key, a box whose proxy is not listening, or a turn that
748
+ * brings its own `--settings`, which this one's cannot be combined with.
749
+ */
750
+ function managedProxyBaseUrl(config, agentEnvironmentValues) {
751
+ if (!claudeWarmSwitchOn()) return void 0;
752
+ if ((config.authMode ?? (config.apiKey ? "api-key" : "oauth")) !== "api-key" || !config.apiKey) return void 0;
753
+ if (extractProfileSettings(config.extraFlags).usesOtherSettings) return void 0;
754
+ return managedModelProxyUrl({
755
+ binding: modelKeyProxyBindingValue(agentEnvironmentValues),
756
+ apiKey: config.apiKey,
757
+ upstream: resolveClaudeCodeBaseUrl(config.baseUrl),
758
+ runtimeHome: config.runtimeHome
759
+ });
760
+ }
761
+ //#endregion
762
+ //#region src/claude-launch-adapter.ts
763
+ /** Prompts above this many bytes go on stdin; the kernel caps one argv string at 128 KiB. */
764
+ const MAX_ARGV_PROMPT_BYTES = 120 * 1024;
765
+ /** Owns Claude's native launch, authentication, MCP registration, and request routing. */
766
+ var ClaudeLaunchAdapter = class extends CliProviderAdapterBase {
767
+ provider = "claude-code";
768
+ profileHarness = "claude-code";
769
+ defaultCliPath = "claude";
770
+ /** Per-turn state must not bleed between concurrent execute calls. */
771
+ executionContext = registerClaudeExecutionContext(this, new AsyncLocalStorage());
772
+ /**
773
+ * Servers installed at runtime via `addMcp` (hub connections, sandbox
774
+ * capabilities), kept so `buildArgs` can merge them into the single
775
+ * `--mcp-config` object. See the merge site for why the CLI's own config
776
+ * store is not sufficient on its own.
777
+ */
778
+ runtimeMcpServers = {};
779
+ /**
780
+ * Image attachments ride the native `--input-format stream-json` path
781
+ * (see `buildExecArgs`) instead of the `[Image: ...]` text marker — the
782
+ * base adapter pre-reads their base64/url into `attachments` and omits
783
+ * the marker for this kind.
784
+ */
785
+ nativeAttachmentKinds() {
786
+ return ["image"];
787
+ }
788
+ buildExecArgs(params) {
789
+ const { extraFlags, usesProfileSettings, usesOtherSettings } = extractProfileSettings(params.config.extraFlags);
790
+ const planMode = claudeRoutesPlans(params.config);
791
+ const modes = interactionModes(params.config);
792
+ const args = claudeSessionArgs(params, planMode, modes.routesPermissions);
793
+ const executionContext = this.executionContext.getStore();
794
+ const executionSettings = claudeTurnSettings(executionContext, {
795
+ usesProfileSettings,
796
+ usesOtherSettings
797
+ }, params.workspaceRoot, () => managedProxyBaseUrl(params.config, () => this.agentEnvironmentValues()));
798
+ if (executionSettings) args.push("--settings", JSON.stringify(executionSettings));
799
+ else if (usesProfileSettings) args.push("--settings", PROFILE_SETTINGS_PATH);
800
+ const mcpArgs = claudeMcpArgs(this.runtimeMcpServers, executionContext, modes);
801
+ args.push(...mcpArgs);
802
+ if (params.config.isolateHostSettings) {
803
+ args.push("--setting-sources", "project,local");
804
+ if (!mcpArgs.includes("--strict-mcp-config")) args.push("--strict-mcp-config");
805
+ }
806
+ if (extraFlags.length > 0) args.push(...extraFlags);
807
+ if (!setsThinkingDisplay(extraFlags)) args.push(THINKING_DISPLAY_FLAG, "summarized");
808
+ args.push(...nativeEffortArgs("claude-code", params.config.profile?.model?.reasoningEffort));
809
+ const { native: qualifyingImages, message: messageText } = partitionImageAttachments({
810
+ message: params.message,
811
+ attachments: params.attachments,
812
+ qualifies: (att) => att.base64 !== void 0 || att.url !== void 0
813
+ });
814
+ const held = this.launchesHeld(params.config, params.sessionId);
815
+ const promptTooLongForArgv = Buffer.byteLength(messageText) > MAX_ARGV_PROMPT_BYTES;
816
+ if (qualifyingImages.length > 0 || held || promptTooLongForArgv) {
817
+ args.push("--input-format", "stream-json");
818
+ return {
819
+ command: this.getCliPath(),
820
+ args,
821
+ useStdin: true,
822
+ stdin: claudeStreamJsonTurn(messageText, qualifyingImages)
823
+ };
824
+ }
825
+ args.push(messageText);
826
+ return {
827
+ command: this.getCliPath(),
828
+ args,
829
+ useStdin: false
830
+ };
831
+ }
832
+ buildExecEnv(config) {
833
+ const env = this.agentEnvironmentValues();
834
+ if (process.getuid?.() === 0) env.HOME = "/home/agent";
835
+ const nonessentialOff = env.CLAUDE_CODE_DISABLE_NONESSENTIAL_TRAFFIC;
836
+ env.CLAUDE_CODE_DISABLE_NONESSENTIAL_TRAFFIC = nonessentialOff === void 0 ? "1" : /^(?:0|false|no|off)?$/iu.test(nonessentialOff.trim()) ? void 0 : nonessentialOff;
837
+ if (config.isolateHostSettings) env.CLAUDE_CODE_DISABLE_AUTO_MEMORY = "1";
838
+ const authMode = config.authMode ?? (config.apiKey ? "api-key" : "oauth");
839
+ if (authMode === "api-key" || config.nativeAuthEnv !== void 0 || (config.authFiles?.length ?? 0) > 0) {
840
+ env.CLAUDE_CODE_OAUTH_TOKEN = void 0;
841
+ env.CLAUDE_CONFIG_DIR = void 0;
842
+ env.ANTHROPIC_CONFIG_DIR = void 0;
843
+ env.ANTHROPIC_PROFILE = void 0;
844
+ env.ANTHROPIC_FEDERATION_RULE_ID = void 0;
845
+ env.ANTHROPIC_ORGANIZATION_ID = void 0;
846
+ }
847
+ if (authMode === "api-key" && config.apiKey) {
848
+ const baseUrl = resolveClaudeCodeBaseUrl(config.baseUrl);
849
+ env.ANTHROPIC_BASE_URL = baseUrl;
850
+ if (baseUrl && isTangleRouterBaseUrl(baseUrl)) {
851
+ env.ANTHROPIC_AUTH_TOKEN = config.apiKey;
852
+ env.ANTHROPIC_API_KEY = void 0;
853
+ } else {
854
+ env.ANTHROPIC_API_KEY = config.apiKey;
855
+ env.ANTHROPIC_AUTH_TOKEN = void 0;
856
+ }
857
+ env.CLAUDE_CODE_USE_BEDROCK = void 0;
858
+ env.CLAUDE_CODE_USE_VERTEX = void 0;
859
+ env.CLAUDE_CODE_USE_FOUNDRY = void 0;
860
+ } else {
861
+ env.ANTHROPIC_API_KEY = void 0;
862
+ env.ANTHROPIC_AUTH_TOKEN = void 0;
863
+ env.ANTHROPIC_BASE_URL = void 0;
864
+ env.CLAUDE_CODE_USE_BEDROCK = void 0;
865
+ env.CLAUDE_CODE_USE_VERTEX = void 0;
866
+ env.CLAUDE_CODE_USE_FOUNDRY = void 0;
867
+ }
868
+ return env;
869
+ }
870
+ /**
871
+ * Whether a turn launches with its prompt on stdin (`--input-format
872
+ * stream-json`) instead of as an argument: a new session on the box's
873
+ * managed token, sent through the key proxy, with no interaction server to
874
+ * wire. Only such a turn can take a spare process, because the proxy, not
875
+ * the process environment, then carries its correlation headers: a launch
876
+ * the proxy would not route (its own `--settings`, OAuth, an upstream the
877
+ * proxy cannot name) is never held, so no spare starts for it.
878
+ */
879
+ launchesHeld(config, sessionId) {
880
+ if (!claudeWarmSwitchOn() || sessionId) return false;
881
+ if (!isManagedModelToken(config.apiKey)) return false;
882
+ const { routesPermissions, exposesAskUser } = interactionModes(config);
883
+ if (routesPermissions || exposesAskUser || claudeRoutesPlans(config)) return false;
884
+ const context = this.executionContext.getStore();
885
+ if (context?.permissionServer || context?.planHookServer) return false;
886
+ return managedProxyBaseUrl(config, () => this.agentEnvironmentValues()) !== void 0;
887
+ }
888
+ /**
889
+ * A turn on the box's managed token sends its requests through the sidecar's
890
+ * model-key proxy, which reports the turn's first model request and relays
891
+ * every header unchanged. The correlation headers are bound against the
892
+ * real base URL first (bindExecutionCorrelation, inside super), so the
893
+ * request carries the same headers on either path. A caller's own key, or a
894
+ * box whose proxy is not listening, keeps the direct path.
895
+ */
896
+ buildTurnEnvironment(executionConfig, profilePlan, workspaceRoot, correlationHeaders) {
897
+ const environment = super.buildTurnEnvironment(executionConfig, profilePlan, workspaceRoot, correlationHeaders);
898
+ const proxied = managedProxyBaseUrl(executionConfig, () => this.agentEnvironmentValues());
899
+ return proxied ? {
900
+ ...environment,
901
+ ANTHROPIC_BASE_URL: proxied
902
+ } : environment;
903
+ }
904
+ /**
905
+ * Claude Code adds `ANTHROPIC_CUSTOM_HEADERS` (one `Name: Value` per line) to
906
+ * every model request, so the router records this run's trace and execution
907
+ * binding on each dispatch. Only the Tangle router receives them.
908
+ */
909
+ bindExecutionCorrelation(environment, correlationHeaders) {
910
+ const baseUrl = environment.ANTHROPIC_BASE_URL;
911
+ if (correlationHeaders === void 0 || !baseUrl || !isTangleRouterBaseUrl(baseUrl)) return environment;
912
+ const lines = Object.entries(correlationHeaders).map(([name, value]) => `${name}: ${value}`);
913
+ const inherited = environment.ANTHROPIC_CUSTOM_HEADERS;
914
+ return {
915
+ ...environment,
916
+ ANTHROPIC_CUSTOM_HEADERS: [inherited, ...lines].filter((line) => Boolean(line)).join("\n")
917
+ };
918
+ }
919
+ buildMcpAddArgs(name, config) {
920
+ const args = ["mcp", "add"];
921
+ if (config.type === "http" || config.type === "remote") {
922
+ args.push("--transport", "http", name, config.url);
923
+ if (config.headers) for (const [key, value] of Object.entries(config.headers)) args.push("--header", `${key}: ${value}`);
924
+ } else {
925
+ args.push(name);
926
+ for (const [key, value] of Object.entries(localMcpEnv(config) ?? {})) args.push("-e", `${key}=${value}`);
927
+ args.push("--", ...localMcpArgv(config));
928
+ }
929
+ return args;
930
+ }
931
+ /**
932
+ * Record the server for inline `--mcp-config` merging, then register it with
933
+ * the CLI as before.
934
+ *
935
+ * Both halves are load-bearing and neither replaces the other. The inline
936
+ * merge is what actually reaches the model, because a turn carrying any MCP
937
+ * server also carries `--strict-mcp-config` and the CLI then ignores its own
938
+ * store. The store registration is what `getMcpStatus` / `claude mcp list`
939
+ * report, which the sidecar exposes over `/agents/config/mcp`.
940
+ *
941
+ * Recorded first so a CLI registration failure cannot leave the agent without
942
+ * a server that will work perfectly well inline.
943
+ */
944
+ async addMcp(name, config) {
945
+ this.runtimeMcpServers[name] = toInlineMcpServer(config);
946
+ await super.addMcp(name, config);
947
+ }
948
+ buildMcpListArgs() {
949
+ return ["mcp", "list"];
950
+ }
951
+ };
952
+ //#endregion
953
+ //#region src/held-spare.ts
954
+ /**
955
+ * The Claude Code process started before the turn that uses it (the shared
956
+ * mechanics are sdk-cli-runner held-spare.ts).
957
+ *
958
+ * A process started with `--input-format stream-json` and its stdin held open
959
+ * finishes its start ahead of the turn; the turn writes its prompt and the
960
+ * first model request leaves in tens of milliseconds.
961
+ *
962
+ * The one launch difference a turn may have is its correlation headers in
963
+ * ANTHROPIC_CUSTOM_HEADERS, which a process cannot receive after it starts.
964
+ * The spare carries a marker header instead and the sidecar's key proxy puts
965
+ * the turn's headers on the spare's requests (runtime-contracts
966
+ * MODEL_KEY_PROXY_SPARE_HEADER). Anything else that differs stops the spare.
967
+ */
968
+ let sink;
969
+ /**
970
+ * The sidecar registers the proxy's hook here. Without one, no turn can
971
+ * adopt a spare: nothing would put the turn's headers on its requests.
972
+ */
973
+ function setHeldSpareTurnSink(next) {
974
+ sink = next;
975
+ }
976
+ function heldSpareTurnSink() {
977
+ return sink;
978
+ }
979
+ const claudeHeldSpares = createHeldSpareRegistry();
980
+ //#endregion
981
+ //#region src/claude-warm-adapter.ts
982
+ /** How long a spare a turn reserved waits for that turn to spawn on it. */
983
+ const HELD_SPARE_RESERVATION_MS = 3e4;
984
+ /**
985
+ * ANTHROPIC_CUSTOM_HEADERS lines a spare and the turn that adopts it may
986
+ * differ in: the turn's correlation headers, which the key proxy puts on the
987
+ * spare's requests, and the marker that names the spare to the proxy.
988
+ */
989
+ const HELD_SPARE_IGNORED_HEADERS = new Set([...EXECUTION_CORRELATION_HEADERS, MODEL_KEY_PROXY_SPARE_HEADER]);
990
+ /**
991
+ * What must match between a spare's environment and its turn's: every value
992
+ * as it is, bar the ANTHROPIC_CUSTOM_HEADERS lines the turn may differ in.
993
+ */
994
+ function comparableLaunchValue(key, value) {
995
+ if (key !== "ANTHROPIC_CUSTOM_HEADERS" || value === void 0) return value;
996
+ const lines = value.split("\n").filter((line) => !HELD_SPARE_IGNORED_HEADERS.has((line.split(":")[0] ?? "").trim().toLowerCase()));
997
+ return lines.length > 0 ? lines.join("\n") : void 0;
998
+ }
999
+ /** Owns held-process preparation, reservation, and adoption for Claude turns. */
1000
+ var ClaudeWarmAdapter = class extends ClaudeLaunchAdapter {
1001
+ /**
1002
+ * Spares a turn reserved in `mintNativeSessionId`, by native session id.
1003
+ * The turn's spawn takes its entry, also when the spare stopped meanwhile.
1004
+ */
1005
+ reservedSpares = /* @__PURE__ */ new Map();
1006
+ /**
1007
+ * Start the process the box's first turn will adopt: the CLI launched as an
1008
+ * ordinary new-session managed turn would launch it, with its stdin held
1009
+ * open, in this adapter's runtime HOME. `systemPrompt` is the prompt that
1010
+ * turn will carry; a turn whose prompt differs stops the process and starts
1011
+ * its own. `ready` settles false when no spare could start.
1012
+ */
1013
+ async prewarmProcess(workspaceRootOrUndefined, launch) {
1014
+ this.ensureInitialized();
1015
+ const workspaceRoot = workspaceRootOrUndefined ?? this.storagePath;
1016
+ const refused = { ready: Promise.resolve(false) };
1017
+ const home = this.config.runtimeHome?.trim();
1018
+ if (!home || !claudeWarmSwitchOn()) return refused;
1019
+ const existing = claudeHeldSpares.spareFor(home);
1020
+ if (existing) return { ready: Promise.resolve(existing.state === "idle") };
1021
+ const input = {
1022
+ message: "",
1023
+ systemPrompt: launch.systemPrompt,
1024
+ workspaceRoot
1025
+ };
1026
+ if (this.resolveExecutionProfileForInput(input)) return refused;
1027
+ const profilePlan = await this.materializeProfileResources(void 0, workspaceRoot);
1028
+ if (profilePlan) return refused;
1029
+ const config = this.withProfilePlan(withTurnInteractions(this.config, input), profilePlan);
1030
+ const nativeSessionId = randomUUID();
1031
+ const execArgs = this.buildExecArgs({
1032
+ message: "",
1033
+ config,
1034
+ model: this.resolveProfileModel(void 0),
1035
+ mintedSessionId: nativeSessionId,
1036
+ workspaceRoot,
1037
+ systemPrompt: this.resolveProfileSystemPrompt(launch.systemPrompt, profilePlan)
1038
+ });
1039
+ if (!execArgs.useStdin) return refused;
1040
+ const env = this.buildTurnEnvironment(config, profilePlan, workspaceRoot, void 0);
1041
+ const id = randomUUID();
1042
+ const controller = new AbortController();
1043
+ const parser = createDeferredParser();
1044
+ const child = await super.spawnExecutionProcess({
1045
+ input,
1046
+ sessionId: nativeSessionId,
1047
+ config,
1048
+ execArgs,
1049
+ workspaceRoot,
1050
+ deferRawCapture: true,
1051
+ env: {
1052
+ ...env,
1053
+ ANTHROPIC_CUSTOM_HEADERS: [env.ANTHROPIC_CUSTOM_HEADERS, `${MODEL_KEY_PROXY_SPARE_HEADER}: ${id}`].filter((line) => Boolean(line)).join("\n")
1054
+ },
1055
+ signal: controller.signal,
1056
+ parser: parser.config
1057
+ });
1058
+ const spare = {
1059
+ id,
1060
+ home,
1061
+ workspaceRoot,
1062
+ nativeSessionId,
1063
+ upstream: resolveClaudeCodeBaseUrl(config.baseUrl),
1064
+ args: execArgs.args,
1065
+ env,
1066
+ process: child,
1067
+ parser,
1068
+ controller,
1069
+ startedAt: performance.now(),
1070
+ state: "idle"
1071
+ };
1072
+ spare.idleTimer = setTimeout(() => {
1073
+ this.logger.info("Held claude spare idle; stopping it", { home });
1074
+ claudeHeldSpares.stop(spare);
1075
+ }, HELD_SPARE_IDLE_MS);
1076
+ spare.idleTimer.unref();
1077
+ claudeHeldSpares.register(spare);
1078
+ const gone = () => {
1079
+ if (this.reservedSpares.get(nativeSessionId) === spare) this.reservedSpares.delete(nativeSessionId);
1080
+ claudeHeldSpares.stop(spare);
1081
+ };
1082
+ child.exitPromise.then(gone, gone);
1083
+ this.logger.info("Held claude spare started", { home });
1084
+ return { ready: Promise.resolve(child.pid !== void 0) };
1085
+ }
1086
+ /**
1087
+ * A new session adopts the spare in its HOME when the spare can serve the
1088
+ * turn: the same launch, and the key proxy holding the turn's headers for
1089
+ * the spare's requests. It then runs under the spare's native session id.
1090
+ * Every refusal is made here, before the turn records that id; a refused
1091
+ * spare is stopped and the turn runs under an id of its own once the
1092
+ * spare's process has exited, so no two processes ever hold one session or
1093
+ * share the HOME.
1094
+ */
1095
+ mintNativeSessionId(context) {
1096
+ const spare = claudeHeldSpares.spareFor(this.config.runtimeHome?.trim());
1097
+ if (!spare || spare.state !== "idle") return randomUUID();
1098
+ const refuse = (reason) => {
1099
+ this.logger.info("Held claude spare not adopted", { reason });
1100
+ claudeHeldSpares.stop(spare);
1101
+ const fresh = () => randomUUID();
1102
+ return spare.process.exitPromise.then(fresh, fresh);
1103
+ };
1104
+ if (spare.workspaceRoot !== context.workspaceRoot) return refuse("workspace");
1105
+ const turn = context.buildExecArgs(spare.nativeSessionId);
1106
+ if (!turn.useStdin) return refuse("prompt-not-on-stdin");
1107
+ const differences = launchDifferences(spare, {
1108
+ args: turn.args,
1109
+ env: context.env
1110
+ }, comparableLaunchValue);
1111
+ if (differences.length > 0) return refuse(`launch differs: ${differences.join(",")}`);
1112
+ if (context.timeoutMs !== void 0) return refuse("turn has a timeout");
1113
+ const { input } = context;
1114
+ const executionId = input.interactionBinding?.executionId;
1115
+ if (!executionId) return refuse("no execution binding");
1116
+ const headers = isTangleRouterBaseUrl(resolveClaudeCodeBaseUrl(context.config.baseUrl)) ? executionCorrelationHeaders(input.interactionBinding, input.traceId, input.managedQuoteVersion) ?? {} : {};
1117
+ if (!heldSpareTurnSink()?.({
1118
+ executionId,
1119
+ spareId: spare.id,
1120
+ upstream: spare.upstream,
1121
+ headers
1122
+ })) return refuse("no managed turn of this upstream holds the proxy");
1123
+ spare.state = "reserved";
1124
+ if (spare.idleTimer) clearTimeout(spare.idleTimer);
1125
+ spare.idleTimer = setTimeout(() => claudeHeldSpares.stop(spare), HELD_SPARE_RESERVATION_MS);
1126
+ spare.idleTimer.unref();
1127
+ this.reservedSpares.set(spare.nativeSessionId, spare);
1128
+ return spare.nativeSessionId;
1129
+ }
1130
+ /** A turn that reserved a spare runs on its process; any other starts one. */
1131
+ spawnExecutionProcess(request) {
1132
+ const nativeId = argAfter(request.execArgs.args, "--session-id");
1133
+ const spare = nativeId ? this.reservedSpares.get(nativeId) : void 0;
1134
+ if (!spare || !nativeId) return super.spawnExecutionProcess(request);
1135
+ this.reservedSpares.delete(nativeId);
1136
+ if (spare.state === "reserved") try {
1137
+ return this.adoptHeldSpare(spare, request);
1138
+ } catch (error) {
1139
+ this.logger.info("Held claude spare not adopted", { reason: error instanceof Error ? error.message : String(error) });
1140
+ }
1141
+ else this.logger.info("Held claude spare stopped before its turn spawned");
1142
+ const own = () => super.spawnExecutionProcess(request);
1143
+ return spare.process.exitPromise.then(own, own);
1144
+ }
1145
+ adoptHeldSpare(spare, request) {
1146
+ const adopted = claudeHeldSpares.adopt(spare, request);
1147
+ this.logger.observePhase?.("cli_ready_wait", 0);
1148
+ this.logger.info("Held claude spare adopted", { heldMs: Math.round(performance.now() - spare.startedAt) });
1149
+ return adopted;
1150
+ }
1151
+ };
1152
+ //#endregion
1153
+ //#region src/claude-event-adapter.ts
1154
+ /** Owns stream-json event translation and assistant/tool part state. */
1155
+ var ClaudeEventAdapter = class extends ClaudeWarmAdapter {
1156
+ createEventParser(state, onEvent) {
1157
+ const extendedState = state;
1158
+ extendedState.contentBlocks = /* @__PURE__ */ new Map();
1159
+ extendedState.assistantUsage = /* @__PURE__ */ new Map();
1160
+ return jsonEventParser(this.logger, onEvent, {
1161
+ label: "Claude Code",
1162
+ accept: isClaudeCodeEvent,
1163
+ map: (event) => this.mapEventToStreamEvents(event, extendedState),
1164
+ isReady: (event) => event.type === "init" || event.type === "system" && event.subtype === "init"
1165
+ });
1166
+ }
1167
+ mapEventToStreamEvents(event, state) {
1168
+ switch (event.type) {
1169
+ case "init": return this.mapInitEvent(event, state);
1170
+ case "system":
1171
+ if (event.subtype === "init" && event.session_id) return this.mapInitEvent(event, state);
1172
+ return this.mapSystemEvent(event);
1173
+ case "assistant": return this.mapAssistantEvent(event, state);
1174
+ case "user": return this.mapUserEvent(event, state);
1175
+ case "content_block_start": return this.mapContentBlockStart(event, state);
1176
+ case "content_block_delta": return this.mapContentBlockDelta(event, state);
1177
+ case "content_block_stop": return this.mapContentBlockStop(event, state);
1178
+ case "tool_use": return toolStartEvents(state, event);
1179
+ case "tool_result": return this.mapToolResultEvent(event, state);
1180
+ case "result": return this.mapResultEvent(event, state);
1181
+ case "error": return this.mapErrorEvent(event);
1182
+ case "rate_limit_event":
1183
+ observeQuotaWindow(event, state);
1184
+ return [{
1185
+ type: "raw",
1186
+ backend: this.provider,
1187
+ event
1188
+ }];
1189
+ default: return [{
1190
+ type: "raw",
1191
+ backend: this.provider,
1192
+ event
1193
+ }];
1194
+ }
1195
+ }
1196
+ mapInitEvent(event, state) {
1197
+ const events = event.session_id ? sessionObservedEvents(state, "claude-code", event.session_id) : [];
1198
+ events.push({
1199
+ type: "status",
1200
+ status: "processing",
1201
+ detail: "Initializing session"
1202
+ });
1203
+ return events;
1204
+ }
1205
+ mapSystemEvent(event) {
1206
+ return [{
1207
+ type: "status",
1208
+ status: "processing",
1209
+ detail: event.message
1210
+ }];
1211
+ }
1212
+ /**
1213
+ * Maps Claude Code stream-json "assistant" events to stream events.
1214
+ * These contain the full message content (thinking, text, tool_use blocks).
1215
+ */
1216
+ mapAssistantEvent(event, state) {
1217
+ if (event.is_api_error_message || event.error === "rate_limit") {
1218
+ const content = event.message?.content;
1219
+ const message = (Array.isArray(content) ? content.filter((block) => block?.type === "text" && typeof block.text === "string").map((block) => block.text).join("\n") : "") || "Claude Code reported an API error";
1220
+ const failure = quotaFailure(state, {
1221
+ message,
1222
+ code: event.error === "rate_limit" ? "rate_limit_error" : "native_api_error"
1223
+ });
1224
+ state.failure = failure;
1225
+ return [{
1226
+ type: "warning",
1227
+ code: failure.code,
1228
+ message
1229
+ }];
1230
+ }
1231
+ const message = event.message;
1232
+ const usageEvents = assistantUsageEvents(event, state, this.provider);
1233
+ if (!message?.content || !Array.isArray(message.content)) return [...usageEvents, {
1234
+ type: "raw",
1235
+ backend: this.provider,
1236
+ event
1237
+ }];
1238
+ if (message.id && !state.messageId) state.messageId = message.id;
1239
+ const events = [];
1240
+ for (const block of message.content) if (block.type === "text" && block.text) events.push(...textSnapshotEvents(state, block.text));
1241
+ else if (block.type === "thinking" && block.thinking) events.push(...reasoningEvents(state, block.thinking, { signalPhase: false }));
1242
+ else if (block.type === "tool_use" && block.name && block.id) {
1243
+ if (!state.toolCalls.has(block.id)) events.push(...toolStartEvents(state, {
1244
+ id: block.id,
1245
+ name: block.name,
1246
+ input: block.input
1247
+ }));
1248
+ }
1249
+ return [...events, ...usageEvents];
1250
+ }
1251
+ mapContentBlockStart(event, state) {
1252
+ const block = {
1253
+ index: event.index,
1254
+ type: event.content_block.type,
1255
+ id: event.content_block.id,
1256
+ name: event.content_block.name,
1257
+ buffer: "",
1258
+ partId: randomUUID()
1259
+ };
1260
+ state.contentBlocks.set(event.index, block);
1261
+ const events = [];
1262
+ if (block.type === "thinking") events.push({
1263
+ type: "model-processing",
1264
+ phase: "thinking"
1265
+ });
1266
+ else if (block.type === "text") events.push({
1267
+ type: "model-processing",
1268
+ phase: "generating"
1269
+ });
1270
+ return events;
1271
+ }
1272
+ mapContentBlockDelta(event, state) {
1273
+ const block = state.contentBlocks.get(event.index);
1274
+ if (!block) return [];
1275
+ let delta = "";
1276
+ if (event.delta.type === "text_delta" && event.delta.text) {
1277
+ delta = event.delta.text;
1278
+ block.buffer += delta;
1279
+ if (block.type === "text") {
1280
+ state.textBuffer += delta;
1281
+ if (!state.textPartId) state.textPartId = block.partId;
1282
+ return [{
1283
+ type: "message.part.updated",
1284
+ part: {
1285
+ ...streamPartBase(state, state.textPartId),
1286
+ type: "text",
1287
+ text: state.textBuffer
1288
+ },
1289
+ delta
1290
+ }];
1291
+ }
1292
+ } else if (event.delta.type === "thinking_delta" && event.delta.thinking) {
1293
+ delta = event.delta.thinking;
1294
+ const isFirstDelta = block.buffer === "";
1295
+ block.buffer += delta;
1296
+ if (!block.partId) block.partId = randomUUID();
1297
+ const partId = block.partId;
1298
+ state.reasoningParts ??= [];
1299
+ let entry = state.reasoningParts.find((p) => p.id === partId);
1300
+ if (!entry) {
1301
+ entry = {
1302
+ id: partId,
1303
+ text: ""
1304
+ };
1305
+ state.reasoningParts.push(entry);
1306
+ }
1307
+ entry.text = block.buffer;
1308
+ if (isFirstDelta) state.reasoningBuffer.push(block.buffer);
1309
+ else state.reasoningBuffer[state.reasoningBuffer.length - 1] = block.buffer;
1310
+ return [{
1311
+ type: "message.part.updated",
1312
+ part: {
1313
+ ...streamPartBase(state, block.partId),
1314
+ type: "reasoning",
1315
+ text: block.buffer
1316
+ },
1317
+ delta
1318
+ }];
1319
+ } else if (event.delta.type === "input_json_delta" && event.delta.partial_json) block.buffer += event.delta.partial_json;
1320
+ return [];
1321
+ }
1322
+ mapContentBlockStop(event, state) {
1323
+ const block = state.contentBlocks.get(event.index);
1324
+ if (!block) return [];
1325
+ state.contentBlocks.delete(event.index);
1326
+ if (block.type === "thinking" && block.buffer) {
1327
+ if (state.reasoningBuffer.length === 0 || state.reasoningBuffer[state.reasoningBuffer.length - 1] !== block.buffer) state.reasoningBuffer.push(block.buffer);
1328
+ }
1329
+ return [];
1330
+ }
1331
+ /**
1332
+ * The real claude CLI (2.x stream-json, verified on 2.1.209) delivers every
1333
+ * tool result inside a top-level user event:
1334
+ * `{type:"user", message:{content:[{type:"tool_result", tool_use_id,
1335
+ * content, is_error?}]}}` — there is NO top-level `{type:"tool_result"}`
1336
+ * event on that wire. Each tool_result block routes through the same
1337
+ * completion logic as the top-level shape; other block types in the user
1338
+ * message (echoed text, etc.) carry no completion signal and are skipped.
1339
+ */
1340
+ mapUserEvent(event, state) {
1341
+ const content = event.message?.content;
1342
+ if (!Array.isArray(content)) return [];
1343
+ const events = [];
1344
+ const results = content.filter((block) => typeof block === "object" && block !== null && block.type === "tool_result" && typeof block.tool_use_id === "string");
1345
+ const structured = results.length === 1 ? event.tool_use_result : void 0;
1346
+ for (const block of results) events.push(...this.completeToolResult({
1347
+ toolUseId: block.tool_use_id,
1348
+ content: block.content,
1349
+ isError: block.is_error === true,
1350
+ structured
1351
+ }, state));
1352
+ return events;
1353
+ }
1354
+ /**
1355
+ * Top-level `{type:"tool_result"}` shape — not emitted by the current CLI
1356
+ * (results arrive via user events, see mapUserEvent) but kept for replay
1357
+ * fixtures and synthetic feeds.
1358
+ */
1359
+ mapToolResultEvent(event, state) {
1360
+ return this.completeToolResult({
1361
+ toolUseId: event.tool_use_id,
1362
+ content: event.content,
1363
+ isError: event.is_error === true
1364
+ }, state);
1365
+ }
1366
+ /** Shared completion path for a tool result, however the wire delivered it. */
1367
+ completeToolResult(result, state) {
1368
+ const toolCall = state.toolCalls.get(result.toolUseId);
1369
+ const metadata = toolCall ? shellResultMetadata(toolCall.name, result.content, result.isError, result.structured) : void 0;
1370
+ return toolResultEvents(state, {
1371
+ id: result.toolUseId,
1372
+ content: result.content,
1373
+ isError: result.isError,
1374
+ ...metadata ? { metadata } : {},
1375
+ afterSuccess: (call) => this.detectFileParts(call, result.content, state)
1376
+ });
1377
+ }
1378
+ /**
1379
+ * Surfaces files an agent produced this turn from a successful tool result.
1380
+ * Built-in write/edit tools carry the path directly on their call input, so
1381
+ * detection needs no result content; MCP tools (`mcp__*`) instead report
1382
+ * output as content blocks, so an `{type:"image"}` block there becomes a
1383
+ * data-URI file part. Every other tool (Bash, Read, Grep, …) has no file
1384
+ * signal Claude Code exposes — a shell command's side effects on disk are
1385
+ * undetectable from the CLI's tool-result event, so those yield nothing.
1386
+ */
1387
+ detectFileParts(toolCall, content, state) {
1388
+ const events = [];
1389
+ const input = toolCall.input ?? {};
1390
+ const emit = (args) => {
1391
+ const tracked = trackFilePart(state, args);
1392
+ if (!tracked) return;
1393
+ const filePart = {
1394
+ ...streamPartBase(state, tracked.id),
1395
+ type: "file",
1396
+ ...tracked.filename ? { filename: tracked.filename } : {},
1397
+ ...tracked.mediaType ? { mediaType: tracked.mediaType } : {},
1398
+ url: tracked.url
1399
+ };
1400
+ events.push({
1401
+ type: "message.part.updated",
1402
+ part: filePart
1403
+ });
1404
+ };
1405
+ switch (toolCall.name) {
1406
+ case "Write":
1407
+ if (typeof input.file_path === "string") emit({
1408
+ path: input.file_path,
1409
+ changeKind: "add"
1410
+ });
1411
+ return events;
1412
+ case "Edit":
1413
+ case "MultiEdit":
1414
+ if (typeof input.file_path === "string") emit({
1415
+ path: input.file_path,
1416
+ changeKind: "update"
1417
+ });
1418
+ return events;
1419
+ case "NotebookEdit":
1420
+ if (typeof input.notebook_path === "string") emit({
1421
+ path: input.notebook_path,
1422
+ changeKind: "update"
1423
+ });
1424
+ return events;
1425
+ default: break;
1426
+ }
1427
+ if (toolCall.name.startsWith("mcp__") && Array.isArray(content)) for (const block of content) {
1428
+ if (typeof block !== "object" || block === null || block.type !== "image") continue;
1429
+ const { data, mimeType } = block;
1430
+ if (typeof data !== "string" || typeof mimeType !== "string") {
1431
+ this.logger.debug("malformed image block in MCP tool result", { tool: toolCall.name });
1432
+ continue;
1433
+ }
1434
+ emit({
1435
+ dataUri: `data:${mimeType};base64,${data}`,
1436
+ mediaType: mimeType
1437
+ });
1438
+ }
1439
+ return events;
1440
+ }
1441
+ mapResultEvent(event, state) {
1442
+ if (event.session_id) state.sessionId = event.session_id;
1443
+ state.resultEvent = event;
1444
+ if (recordCliTokenUsage(state, event) && state.tokenUsage) state.tokenUsage = inclusiveClaudeInputTokens(state.tokenUsage);
1445
+ const events = [];
1446
+ if (event.session_id) events.push({
1447
+ type: "session.updated",
1448
+ sessionId: event.session_id
1449
+ }, {
1450
+ type: "native.session.observed",
1451
+ provider: "claude-code",
1452
+ nativeSessionId: event.session_id
1453
+ });
1454
+ if (event.is_error) {
1455
+ const observedStatus = event.api_error_status;
1456
+ const statusCode = typeof observedStatus === "number" && Number.isInteger(observedStatus) && observedStatus >= 400 && observedStatus <= 599 ? observedStatus : void 0;
1457
+ const code = statusCode !== void 0 ? "native_api_error" : state.failure?.code ?? (event.terminal_reason === "api_error" ? "native_api_error" : "native_execution_failed");
1458
+ const priorMessage = state.failure?.message;
1459
+ state.terminalStatus = "failed";
1460
+ const failure = quotaFailure(state, {
1461
+ message: priorMessage || "Claude Code reported a failed result",
1462
+ code,
1463
+ ...statusCode === void 0 ? {} : { statusCode }
1464
+ });
1465
+ state.failure = failure;
1466
+ const message = (typeof event.result === "string" ? event.result : "") || (Array.isArray(event.errors) ? event.errors.filter((error) => typeof error === "string").join("; ") : "") || failure.message;
1467
+ failure.message = message;
1468
+ events.push({
1469
+ type: "warning",
1470
+ code: failure.code,
1471
+ message
1472
+ });
1473
+ events.push({
1474
+ type: "status",
1475
+ status: "failed",
1476
+ detail: message
1477
+ });
1478
+ return events;
1479
+ }
1480
+ if (event.result && !state.textBuffer) {
1481
+ state.textBuffer = event.result;
1482
+ if (!state.textPartId) state.textPartId = randomUUID();
1483
+ const textPart = {
1484
+ ...streamPartBase(state, state.textPartId),
1485
+ type: "text",
1486
+ text: event.result
1487
+ };
1488
+ events.push({
1489
+ type: "message.part.updated",
1490
+ part: textPart,
1491
+ delta: event.result
1492
+ });
1493
+ }
1494
+ events.push({
1495
+ type: "status",
1496
+ status: "completed"
1497
+ });
1498
+ return events;
1499
+ }
1500
+ mapErrorEvent(event) {
1501
+ const message = event.error?.message ?? event.message ?? "Unknown error";
1502
+ return [{
1503
+ type: "warning",
1504
+ code: event.error?.type ?? "CLAUDE_CODE_ERROR",
1505
+ message
1506
+ }];
1507
+ }
1508
+ };
1509
+ /** A rejected subscription window (stream-json rate_limit_event) names an exhausted quota and when it resets. */
1510
+ function observeQuotaWindow(event, state) {
1511
+ const { status, resetsAt } = event.rate_limit_info ?? {};
1512
+ state.quotaResetAt = status === "rejected" && Number.isFinite(resetsAt) ? (/* @__PURE__ */ new Date(resetsAt * 1e3)).toISOString() : void 0;
1513
+ if (state.failure?.code) state.failure = quotaFailure(state, {
1514
+ ...state.failure,
1515
+ code: state.failure.code
1516
+ });
1517
+ }
1518
+ /** A rate-limit failure inside a rejected window is the seat's spent quota (runtime-contracts seat-selection), not a transient 429. */
1519
+ function quotaFailure(state, failure) {
1520
+ return state.quotaResetAt && (failure.code === "rate_limit_error" || failure.code === "provider_quota_exhausted" || failure.statusCode === 429) ? {
1521
+ ...failure,
1522
+ code: "provider_quota_exhausted",
1523
+ statusCode: 429,
1524
+ resetAt: state.quotaResetAt
1525
+ } : failure;
1526
+ }
1527
+ //#endregion
1528
+ //#region src/plan-hook-server.ts
1529
+ var ClaudePlanHookServer = class {
1530
+ secret = randomBytes(24).toString("hex");
1531
+ terminalPlan;
1532
+ server;
1533
+ port = 0;
1534
+ rejectionDelivered = false;
1535
+ nativeSessionId;
1536
+ pendingSubmissions = /* @__PURE__ */ new Set();
1537
+ constructor(options) {
1538
+ this.options = options;
1539
+ this.terminalPlan = new TerminalPlanSupport({
1540
+ planHost: options.planHost,
1541
+ emit: options.emit,
1542
+ providerState: () => {
1543
+ if (!this.nativeSessionId) throw new Error("Claude session identity is unavailable");
1544
+ return {
1545
+ kind: "claude-code",
1546
+ version: 1,
1547
+ nativeSessionId: this.nativeSessionId
1548
+ };
1549
+ }
1550
+ });
1551
+ }
1552
+ get hookUrl() {
1553
+ return `http://127.0.0.1:${this.port}/${this.secret}`;
1554
+ }
1555
+ async start() {
1556
+ const MAX_BODY = 4 * 1024 * 1024;
1557
+ const server = createServer((request, response) => {
1558
+ if (request.method !== "POST" || request.url !== `/${this.secret}`) {
1559
+ response.writeHead(404).end();
1560
+ return;
1561
+ }
1562
+ const chunks = [];
1563
+ let size = 0;
1564
+ request.on("data", (chunk) => {
1565
+ size += chunk.length;
1566
+ if (size > MAX_BODY) {
1567
+ response.writeHead(413).end();
1568
+ request.destroy();
1569
+ return;
1570
+ }
1571
+ chunks.push(chunk);
1572
+ });
1573
+ request.on("error", () => {
1574
+ if (!response.writableEnded) response.writeHead(400).end();
1575
+ });
1576
+ request.on("end", () => {
1577
+ if (response.writableEnded) return;
1578
+ this.handle(Buffer.concat(chunks).toString("utf8")).then((decision) => {
1579
+ response.writeHead(200, { "content-type": "application/json" }).end(JSON.stringify(decision));
1580
+ }, (error) => {
1581
+ response.writeHead(200, { "content-type": "application/json" }).end(JSON.stringify(deny(error instanceof Error ? error.message : String(error))));
1582
+ });
1583
+ });
1584
+ });
1585
+ await new Promise((resolve, reject) => {
1586
+ server.once("error", reject);
1587
+ server.listen(0, "127.0.0.1", resolve);
1588
+ });
1589
+ const address = server.address();
1590
+ if (!address || typeof address === "string") {
1591
+ await new Promise((resolve) => server.close(() => resolve()));
1592
+ throw new Error("Claude plan hook server failed to bind a TCP port");
1593
+ }
1594
+ this.port = address.port;
1595
+ this.server = server;
1596
+ }
1597
+ async stop() {
1598
+ const server = this.server;
1599
+ this.server = void 0;
1600
+ if (!server) return;
1601
+ await new Promise((resolve) => server.close(() => resolve()));
1602
+ }
1603
+ async waitForPendingSubmissions() {
1604
+ while (this.pendingSubmissions.size > 0) await Promise.allSettled([...this.pendingSubmissions]);
1605
+ }
1606
+ async handle(raw) {
1607
+ let hook;
1608
+ try {
1609
+ hook = JSON.parse(raw);
1610
+ } catch {
1611
+ throw new Error("Claude plan hook received invalid JSON");
1612
+ }
1613
+ if (hook.hook_event_name !== "PreToolUse" || hook.tool_name !== "ExitPlanMode" || typeof hook.session_id !== "string" || !hook.session_id || typeof hook.tool_use_id !== "string" || !hook.tool_use_id || !hook.tool_input || typeof hook.tool_input !== "object") throw new Error("Claude plan hook received an invalid ExitPlanMode call");
1614
+ this.nativeSessionId = hook.session_id;
1615
+ const fail = (message) => {
1616
+ const error = new Error(message);
1617
+ this.options.onFailure(hook.session_id, error);
1618
+ throw error;
1619
+ };
1620
+ const continuation = this.options.continuation;
1621
+ if (continuation && !this.rejectionDelivered) {
1622
+ if (continuation.providerState.kind !== "claude-code") return fail(`Claude cannot resume ${continuation.providerState.kind} plan state`);
1623
+ if (hook.session_id !== continuation.providerState.nativeSessionId) return fail("Claude resumed a different session than the durable plan");
1624
+ if (hook.tool_use_id !== continuation.sourceToolCallId) return fail("Claude resumed a different deferred plan tool call");
1625
+ if (continuation.decision.outcome === "approved") return allow(hook.tool_input);
1626
+ this.rejectionDelivered = true;
1627
+ return deny(`Plan rejected: ${continuation.decision.feedback}`);
1628
+ }
1629
+ const body = hook.tool_input.plan;
1630
+ if (typeof body !== "string" || !body.trim()) return fail("ExitPlanMode did not provide a complete plan body");
1631
+ try {
1632
+ const submitted = this.terminalPlan.submit(body, hook.tool_use_id);
1633
+ const pending = submitted.then(() => void 0, () => void 0);
1634
+ this.pendingSubmissions.add(pending);
1635
+ pending.finally(() => this.pendingSubmissions.delete(pending));
1636
+ const plan = await submitted;
1637
+ this.options.onSubmitted({
1638
+ nativeSessionId: hook.session_id,
1639
+ sourceToolCallId: hook.tool_use_id,
1640
+ plan,
1641
+ confirm: () => this.terminalPlan.confirm(),
1642
+ withdraw: (reason) => this.terminalPlan.withdraw(reason)
1643
+ });
1644
+ return defer();
1645
+ } catch (error) {
1646
+ const failure = error instanceof Error ? error : new Error(String(error));
1647
+ this.options.onFailure(hook.session_id, failure);
1648
+ throw failure;
1649
+ }
1650
+ }
1651
+ };
1652
+ function allow(updatedInput) {
1653
+ return { hookSpecificOutput: {
1654
+ hookEventName: "PreToolUse",
1655
+ permissionDecision: "allow",
1656
+ updatedInput
1657
+ } };
1658
+ }
1659
+ function deny(reason) {
1660
+ return { hookSpecificOutput: {
1661
+ hookEventName: "PreToolUse",
1662
+ permissionDecision: "deny",
1663
+ permissionDecisionReason: reason
1664
+ } };
1665
+ }
1666
+ function defer() {
1667
+ return { hookSpecificOutput: {
1668
+ hookEventName: "PreToolUse",
1669
+ permissionDecision: "defer"
1670
+ } };
1671
+ }
1672
+ //#endregion
1673
+ //#region src/adapter.ts
1674
+ /**
1675
+ * A plan continuation's turn opens with the user's decision: execute the
1676
+ * approved plan, or revise the rejected one and submit it again.
1677
+ */
1678
+ function withContinuationInstruction(effectiveInput) {
1679
+ const continuationInstruction = effectiveInput.planContinuation ? effectiveInput.planContinuation.decision.outcome === "approved" ? `The user approved durable plan ${effectiveInput.planContinuation.plan.id}. Continue and execute that exact plan.` : `The user rejected durable plan ${effectiveInput.planContinuation.plan.id}. Revise it using this feedback:\n\n${effectiveInput.planContinuation.decision.feedback}\n\nWhen the revision is complete, call ExitPlanMode again. That submission ends this turn.` : void 0;
1680
+ return continuationInstruction ? {
1681
+ ...effectiveInput,
1682
+ message: [continuationInstruction, effectiveInput.message].filter((part) => typeof part === "string" && part.length > 0).join("\n\n")
1683
+ } : effectiveInput;
1684
+ }
1685
+ /** Withdraw every plan a tracked native session submitted; returns each withdrawal failure. */
1686
+ async function withdrawTrackedPlans(reason, trackedNativeSessions, planSubmissions) {
1687
+ return (await Promise.allSettled([...trackedNativeSessions].map((nativeSessionId) => planSubmissions.get(nativeSessionId)).filter((submission) => submission !== void 0).map((submission) => submission.withdraw(reason)))).flatMap((result) => result.status === "rejected" ? [result.reason] : []);
1688
+ }
1689
+ /**
1690
+ * Claude Code provider adapter.
1691
+ *
1692
+ * Implements the SdkProviderAdapter interface for the Claude Code CLI tool.
1693
+ * Extends CliProviderAdapterBase with Claude Code-specific event parsing and CLI arguments.
1694
+ */
1695
+ var ClaudeCodeProviderAdapter = class extends ClaudeEventAdapter {
1696
+ /** Shared `kind:"permission"` round-trip — see InteractionBroker. */
1697
+ permissions = new InteractionBroker();
1698
+ getCapabilities() {
1699
+ return {
1700
+ ...super.getCapabilities(),
1701
+ multimodal: true,
1702
+ interactions: [
1703
+ ...this.config.interactivePermissions === true || interactionToggleEnabled(this.config.interactions, "permission") ? [InteractionKind.Permission] : [],
1704
+ ...this.config.interactivePermissions === true || interactionToggleEnabled(this.config.interactions, "question") ? [InteractionKind.Question] : [],
1705
+ ...claudeRoutesPlans(this.config) ? [InteractionKind.Plan] : []
1706
+ ]
1707
+ };
1708
+ }
1709
+ continuationInteractions(base, outcome) {
1710
+ if (base === true) return outcome === "rejected" ? true : {
1711
+ permission: true,
1712
+ question: true,
1713
+ plan: false
1714
+ };
1715
+ return {
1716
+ ...typeof base === "object" ? base : {},
1717
+ ...outcome === "approved" ? { permission: true } : {},
1718
+ plan: outcome === "rejected"
1719
+ };
1720
+ }
1721
+ /**
1722
+ * Host a per-execution interaction MCP server when interactive permissions,
1723
+ * permission or question interactions are on.
1724
+ * Started BEFORE buildExecArgs runs (so its loopback port + token reach
1725
+ * `--mcp-config`) and torn down in finally, denying any decision still pending
1726
+ * when the turn unwinds (fail-closed). Plain turns use the base path.
1727
+ */
1728
+ async execute(input, services, onEvent) {
1729
+ const nativeResume = input.nativeResume;
1730
+ if (nativeResume) {
1731
+ if (nativeResume.harness !== "claude-code" || input.planContinuation) throw new Error("Claude native resume requires Claude without plan continuation");
1732
+ await this.seedNativeSessionForResume(input, nativeResume.nativeSessionId);
1733
+ }
1734
+ await this.reseedNativeSessionForContinuation(input);
1735
+ const requestedInteractions = input.interactions;
1736
+ const configuredInteractions = this.config.interactions;
1737
+ const baseInteractions = requestedInteractions ?? configuredInteractions;
1738
+ const continuationInteractions = input.planContinuation ? this.continuationInteractions(baseInteractions, input.planContinuation.decision.outcome) : void 0;
1739
+ const effectiveInput = input.planContinuation ? {
1740
+ ...input,
1741
+ interactions: continuationInteractions
1742
+ } : input;
1743
+ const turnConfig = withTurnInteractions(this.config, effectiveInput);
1744
+ const hostsPermissionServer = interactionModes(turnConfig).hostsServer;
1745
+ const hostsPlanHook = claudeRoutesPlans(turnConfig) || !!effectiveInput.planContinuation;
1746
+ if (!hostsPermissionServer && !hostsPlanHook) return super.execute(input, services, onEvent);
1747
+ const permScopeId = input.sessionId ?? input.interactionBinding?.sessionId ?? randomUUID();
1748
+ const permissionServer = hostsPermissionServer ? this.createPermissionServer(permScopeId, this.interactionExecutionBinding(input, permScopeId), onEvent, turnConfig) : void 0;
1749
+ const trackedNativeSessions = /* @__PURE__ */ new Set();
1750
+ const planSubmissions = /* @__PURE__ */ new Map();
1751
+ const planFailures = /* @__PURE__ */ new Map();
1752
+ const planHookServer = hostsPlanHook ? new ClaudePlanHookServer({
1753
+ planHost: services.planHost,
1754
+ continuation: effectiveInput.planContinuation,
1755
+ emit: onEvent,
1756
+ onSubmitted: (submission) => {
1757
+ trackedNativeSessions.add(submission.nativeSessionId);
1758
+ planSubmissions.set(submission.nativeSessionId, submission);
1759
+ },
1760
+ onFailure: (nativeSessionId, error) => {
1761
+ trackedNativeSessions.add(nativeSessionId);
1762
+ planFailures.set(nativeSessionId, error);
1763
+ }
1764
+ }) : void 0;
1765
+ await permissionServer?.start();
1766
+ try {
1767
+ await planHookServer?.start();
1768
+ } catch (error) {
1769
+ await permissionServer?.stop();
1770
+ throw error;
1771
+ }
1772
+ const executionContext = {
1773
+ permissionServer,
1774
+ planHookServer,
1775
+ planSubmissions,
1776
+ planFailures
1777
+ };
1778
+ try {
1779
+ return await claudeExecutionContext(this).run(executionContext, () => super.execute(withContinuationInstruction(effectiveInput), services, onEvent));
1780
+ } catch (error) {
1781
+ await planHookServer?.stop();
1782
+ await planHookServer?.waitForPendingSubmissions();
1783
+ const withdrawals = await withdrawTrackedPlans(`Claude failed to terminalize its planning turn: ${error instanceof Error ? error.message : String(error)}`, trackedNativeSessions, planSubmissions);
1784
+ if (withdrawals.length > 0) throw new AggregateError([error, ...withdrawals], "Claude planning failed and its durable plan could not be withdrawn");
1785
+ throw error;
1786
+ } finally {
1787
+ this.permissions.failSession(permScopeId);
1788
+ await Promise.all([permissionServer?.stop(), planHookServer?.stop()]);
1789
+ }
1790
+ }
1791
+ /**
1792
+ * Make the plan continuation's recorded native Claude session authoritative
1793
+ * on the session store so super.execute resumes that conversation. Seed it
1794
+ * when the local mapping is missing, and overwrite a mapping that points at a
1795
+ * different conversation — an intervening turn can recreate the mapping with a
1796
+ * fresh providerSessionId after the plan was submitted (e.g. the box was
1797
+ * recycled and the store lost its entry). Leaving a stale, different id in
1798
+ * place would resume the wrong conversation, which the plan hook then rejects
1799
+ * as a different session than the durable plan. A mapping that already points
1800
+ * at the recorded session is left untouched.
1801
+ */
1802
+ async reseedNativeSessionForContinuation(input) {
1803
+ const continuation = input.planContinuation;
1804
+ if (!input.sessionId || !continuation || continuation.providerState.kind !== "claude-code") return;
1805
+ const nativeSessionId = continuation.providerState.nativeSessionId;
1806
+ const store = this.getSessionStore();
1807
+ const existing = await store.get(input.sessionId);
1808
+ if (!existing) await store.create({
1809
+ id: input.sessionId,
1810
+ workspaceRoot: input.workspaceRoot ?? this.storagePath,
1811
+ providerSessionId: nativeSessionId
1812
+ });
1813
+ else if (existing.providerSessionId !== nativeSessionId) await store.update(input.sessionId, { providerSessionId: nativeSessionId });
1814
+ }
1815
+ async respondToInteraction(response) {
1816
+ if (this.permissions.respond(response)) return;
1817
+ this.permissions.respondQuestion(response);
1818
+ }
1819
+ /**
1820
+ * Build the per-execution interaction MCP server. The `permission` bridge
1821
+ * (the `--permission-prompt-tool` decider) is registered only when the turn
1822
+ * routes permissions; the `ask_user` model tool only when the question
1823
+ * channel is exposed — a question-only turn therefore hosts a server
1824
+ * where the permission tool does not even exist.
1825
+ */
1826
+ createPermissionServer(permScopeId, binding, onEvent, config = this.config) {
1827
+ const modes = interactionModes(config);
1828
+ const permission = async ({ tool_name, input: toolInput, tool_use_id }) => {
1829
+ return grantToPermissionResult(await this.permissions.request({
1830
+ id: tool_use_id,
1831
+ sessionId: permScopeId,
1832
+ binding,
1833
+ toolName: tool_name,
1834
+ input: toolInput,
1835
+ allowlistGrant: "deny",
1836
+ emit: onEvent
1837
+ }), tool_name, toolInput);
1838
+ };
1839
+ const askUser = async ({ questions }) => this.permissions.requestQuestion({
1840
+ id: randomUUID(),
1841
+ sessionId: permScopeId,
1842
+ binding,
1843
+ questions,
1844
+ emit: onEvent
1845
+ });
1846
+ return new InteractionMcpServer({
1847
+ serverName: PERMISSION_MCP_SERVER_NAME,
1848
+ ...modes.routesPermissions ? { permission } : {},
1849
+ ...modes.exposesAskUser ? { askUser } : {}
1850
+ });
1851
+ }
1852
+ async executionOutcome(state, _input) {
1853
+ const result = state.resultEvent;
1854
+ const nativeSessionId = state.sessionId;
1855
+ const executionContext = claudeExecutionContext(this).getStore();
1856
+ const failure = nativeSessionId ? executionContext?.planFailures.get(nativeSessionId) : void 0;
1857
+ if (failure) throw failure;
1858
+ const submission = nativeSessionId ? executionContext?.planSubmissions.get(nativeSessionId) : void 0;
1859
+ if (result?.stop_reason === "tool_deferred") {
1860
+ const deferred = result.deferred_tool_use;
1861
+ if (!submission || !deferred) throw new Error("Claude deferred a tool without a committed durable plan");
1862
+ if (deferred.name !== "ExitPlanMode" || deferred.id !== submission.sourceToolCallId) throw new Error("Claude deferred a different tool than the committed durable plan");
1863
+ await submission.confirm();
1864
+ return {
1865
+ type: "awaiting_plan_decision",
1866
+ plan: submission.plan
1867
+ };
1868
+ }
1869
+ if (result?.stop_reason === "tool_deferred_unavailable") throw new Error("Claude could not resume the deferred plan tool");
1870
+ if (submission) throw new Error("Claude committed a durable plan without ending at its deferred tool");
1871
+ return { type: "completed" };
1872
+ }
1873
+ };
1874
+ //#endregion
1875
+ export { ClaudeCodeProviderAdapter, claudeHeldSpares, normalizeExplicitClaudeModel, resolveClaudeCodeBaseUrl, setHeldSpareTurnSink };
1876
+
1877
+ //# sourceMappingURL=index.mjs.map