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

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