@khalilgharbaoui/opencode-claude-code-plugin 0.29.2 → 0.31.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.js CHANGED
@@ -405,6 +405,17 @@ function titleizeAccount(account) {
405
405
  return normalizeAccountName(account).split("-").filter(Boolean).map((part) => part.charAt(0).toUpperCase() + part.slice(1)).join(" ");
406
406
  }
407
407
 
408
+ // src/types.ts
409
+ var READ_ONLY_PERMISSION_MODE = "read-only";
410
+ var DEFAULT_PROXY_TOOL_NAMES = [
411
+ "Bash",
412
+ "Edit",
413
+ "Write",
414
+ "WebFetch",
415
+ "Task"
416
+ ];
417
+ var PROXY_MCP_SERVER_NAME = "opencode_proxy";
418
+
408
419
  // src/cli-events.ts
409
420
  function isRecord(value) {
410
421
  return value !== null && typeof value === "object" && !Array.isArray(value);
@@ -529,6 +540,35 @@ function reportRateLimitEvent(msg) {
529
540
  log[report.level](report.message, data);
530
541
  return report.transcript;
531
542
  }
543
+ var MCP_SERVER_ERROR_TYPES = {
544
+ unknown_type: "its `type` is not one Claude Code knows",
545
+ url_missing_type: "it has a `url` but no `type`",
546
+ invalid_config: "its configuration did not validate",
547
+ reserved_name: "its name is reserved"
548
+ };
549
+ function parseMcpServerErrors(msg) {
550
+ const raw = msg.mcp_server_errors;
551
+ if (!Array.isArray(raw)) return [];
552
+ const errors = [];
553
+ for (const entry of raw) {
554
+ if (!isRecord(entry)) continue;
555
+ errors.push({
556
+ name: str(entry.name) ?? "unknown",
557
+ type: str(entry.type) ?? "unknown",
558
+ message: str(entry.message) ?? ""
559
+ });
560
+ }
561
+ return errors;
562
+ }
563
+ function describeMcpServerError(error) {
564
+ const why = MCP_SERVER_ERROR_TYPES[error.type];
565
+ const reason = why ? ` because ${why}` : "";
566
+ const detail = error.message ? ` Claude Code said: ${error.message}` : "";
567
+ if (error.name === PROXY_MCP_SERVER_NAME) {
568
+ return `Claude Code skipped the plugin's own MCP server "${error.name}" (${error.type})${reason}, so every proxied tool call this session will fail. This is a plugin bug or a corrupted scratch config rather than something in your own MCP settings: report it with this line.${detail}`;
569
+ }
570
+ return `Claude Code skipped MCP server "${error.name}" (${error.type})${reason}, so its tools are not available to the model this session. Fix the entry in your MCP config.${detail}`;
571
+ }
532
572
  var API_KEY_SOURCES = /* @__PURE__ */ new Set([
533
573
  "ANTHROPIC_API_KEY",
534
574
  "apiKeyHelper",
@@ -554,7 +594,8 @@ function parseSystemInit(msg) {
554
594
  model: str(raw.model),
555
595
  cliVersion: str(raw.claude_code_version),
556
596
  toolCount: Array.isArray(raw.tools) ? raw.tools.length : 0,
557
- mcpServers: servers
597
+ mcpServers: servers,
598
+ mcpServerErrors: parseMcpServerErrors(msg)
558
599
  };
559
600
  }
560
601
  function apiKeySourceWarning(apiKeySource, ignoreAnthropicApiKey) {
@@ -564,6 +605,11 @@ function apiKeySourceWarning(apiKeySource, ignoreAnthropicApiKey) {
564
605
  }
565
606
  var warnedApiKeySources = /* @__PURE__ */ new Set();
566
607
  var warnedMcpFailures = /* @__PURE__ */ new Set();
608
+ var warnedMcpSkips = /* @__PURE__ */ new Set();
609
+ var lastMcpServerErrors = /* @__PURE__ */ new Map();
610
+ function snapshotMcpServerErrors() {
611
+ return [...lastMcpServerErrors.values()];
612
+ }
567
613
  function reportSystemInit(msg, options = {}) {
568
614
  const info = parseSystemInit(msg);
569
615
  if (!info) return;
@@ -573,8 +619,20 @@ function reportSystemInit(msg, options = {}) {
573
619
  model: info.model ?? null,
574
620
  cliVersion: info.cliVersion ?? null,
575
621
  tools: info.toolCount,
576
- mcpServers: info.mcpServers
622
+ mcpServers: info.mcpServers,
623
+ mcpServerErrors: info.mcpServerErrors
577
624
  });
625
+ for (const error of info.mcpServerErrors) {
626
+ lastMcpServerErrors.set(error.name, error);
627
+ const key2 = `${error.name}:${error.type}`;
628
+ const message = describeMcpServerError(error);
629
+ if (warnedMcpSkips.has(key2)) {
630
+ log.debug(message, { server: error.name, type: error.type });
631
+ continue;
632
+ }
633
+ warnedMcpSkips.add(key2);
634
+ log.warn(message, { server: error.name, type: error.type });
635
+ }
578
636
  for (const server2 of info.mcpServers) {
579
637
  if (server2.status === "connected") continue;
580
638
  const key2 = `${server2.name}:${server2.status}`;
@@ -678,6 +736,38 @@ function formatSilentTurnNote(hadReasoning) {
678
736
  ${SILENT_TURN_MARKER} ${what}, so there is no reply above. Nothing failed and nothing is pending: send the message again, or rephrase it.
679
737
  `;
680
738
  }
739
+ var UNRECOGNIZED_MODEL_STDERR_MARKER = "[claude-code:unrecognized_model]";
740
+ var MODEL_CLI_FLOORS = {
741
+ // "Added Claude Sonnet 5.5 (`claude-sonnet-5-5`)" (CHANGELOG, 2.1.284).
742
+ "claude-sonnet-5-5": "2.1.284"
743
+ };
744
+ function parseUnrecognizedModel(stderr) {
745
+ const at = stderr.indexOf(UNRECOGNIZED_MODEL_STDERR_MARKER);
746
+ if (at === -1) return null;
747
+ const rest = stderr.slice(at + UNRECOGNIZED_MODEL_STDERR_MARKER.length);
748
+ const line = rest.split("\n", 1)[0].trim();
749
+ try {
750
+ const payload = JSON.parse(line);
751
+ return { model: isRecord(payload) ? str(payload.model) : void 0 };
752
+ } catch {
753
+ return { model: void 0 };
754
+ }
755
+ }
756
+ var warnedUnrecognizedModels = /* @__PURE__ */ new Set();
757
+ function reportUnrecognizedModel(stderr) {
758
+ const parsed = parseUnrecognizedModel(stderr);
759
+ if (!parsed) return;
760
+ const model = parsed.model ?? "unknown";
761
+ const floor = parsed.model ? MODEL_CLI_FLOORS[parsed.model] : void 0;
762
+ const fix = floor ? `It needs Claude Code ${floor} or newer: run \`claude update\`.` : "Update Claude Code (`claude update`) to a release that knows it.";
763
+ const message = `Claude Code does not recognise the model "${model}". It still runs it, but on fallback limits (measured on 2.1.280: a 200k context window instead of 1M, and an estimated cost), so its own compaction may start early and turnStats' cost is approximate. ${fix}`;
764
+ if (warnedUnrecognizedModels.has(model)) {
765
+ log.debug(message, { model });
766
+ return;
767
+ }
768
+ warnedUnrecognizedModels.add(model);
769
+ log.warn(message, { model, ...floor ? { cliFloor: floor } : {} });
770
+ }
681
771
 
682
772
  // src/plan-mode-question.ts
683
773
  var QUESTION_TOOL_NAME = "question";
@@ -1421,7 +1511,7 @@ function isExpectedCleanupError(message) {
1421
1511
  return message.includes("timed out after") && message.includes("waiting for opencode to resolve") || message.includes("rejecting as orphaned") || message.includes("was orphaned by a new user turn") || message.includes("stream was aborted") || message.includes(SERVER_CLOSED_MESSAGE);
1422
1512
  }
1423
1513
  var PROTOCOL_VERSION = "2024-11-05";
1424
- var SERVER_NAME = "opencode_proxy";
1514
+ var SERVER_NAME = PROXY_MCP_SERVER_NAME;
1425
1515
  var PROXY_TOOL_PREFIX = `mcp__${SERVER_NAME}__`;
1426
1516
  var PROXY_DEFAULT_TIMEOUT_MS = 10 * 60 * 1e3;
1427
1517
  var PROXY_NO_DEADLINE_MS = 0;
@@ -2707,16 +2797,6 @@ function clearCompression(sessionKey2) {
2707
2797
  compressions.delete(sessionKey2);
2708
2798
  }
2709
2799
 
2710
- // src/types.ts
2711
- var READ_ONLY_PERMISSION_MODE = "read-only";
2712
- var DEFAULT_PROXY_TOOL_NAMES = [
2713
- "Bash",
2714
- "Edit",
2715
- "Write",
2716
- "WebFetch",
2717
- "Task"
2718
- ];
2719
-
2720
2800
  // src/permission-presets.ts
2721
2801
  var READ_ONLY_DISALLOWED_CLI_TOOLS = [
2722
2802
  "Bash",
@@ -3083,6 +3163,9 @@ function claudeSpawnEnv(opts) {
3083
3163
  if (opts?.effort) {
3084
3164
  env.CLAUDE_CODE_EFFORT_LEVEL = cliEffortLevel(opts.effort);
3085
3165
  }
3166
+ if (opts?.promptCacheTtl) {
3167
+ env.CLAUDE_CODE_PROMPT_CACHE_TTL = opts.promptCacheTtl;
3168
+ }
3086
3169
  if (opts?.ignoreAnthropicApiKey) {
3087
3170
  delete env.ANTHROPIC_API_KEY;
3088
3171
  delete env.ANTHROPIC_AUTH_TOKEN;
@@ -3368,19 +3451,20 @@ function invalidateOtherEffortSessions(baseKey, effort) {
3368
3451
  clearCompression(key);
3369
3452
  }
3370
3453
  }
3371
- function spawnClaudeProcess(cliPath, cliArgs, cwd, sessionKey2, proxyServer, mcpHash, systemPromptFile, ignoreAnthropicApiKey, effort) {
3454
+ function spawnClaudeProcess(cliPath, cliArgs, cwd, sessionKey2, proxyServer, mcpHash, systemPromptFile, ignoreAnthropicApiKey, effort, promptCacheTtl) {
3372
3455
  evictIfNeeded();
3373
3456
  log.info("spawning new claude process", {
3374
3457
  cliPath,
3375
3458
  cliArgs,
3376
3459
  cwd,
3377
3460
  sessionKey: sessionKey2,
3378
- effort
3461
+ effort,
3462
+ promptCacheTtl
3379
3463
  });
3380
3464
  const proc = spawn(cliPath, cliArgs, {
3381
3465
  cwd,
3382
3466
  stdio: ["pipe", "pipe", "pipe"],
3383
- env: claudeSpawnEnv({ ignoreAnthropicApiKey, effort }),
3467
+ env: claudeSpawnEnv({ ignoreAnthropicApiKey, effort, promptCacheTtl }),
3384
3468
  shell: process.platform === "win32"
3385
3469
  });
3386
3470
  const lineEmitter = new EventEmitter3();
@@ -3391,6 +3475,7 @@ function spawnClaudeProcess(cliPath, cliArgs, cwd, sessionKey2, proxyServer, mcp
3391
3475
  mcpHash,
3392
3476
  systemPromptFile,
3393
3477
  effort,
3478
+ promptCacheTtl,
3394
3479
  startedAt: Date.now(),
3395
3480
  cliPath,
3396
3481
  cliArgs: [...cliArgs],
@@ -3454,6 +3539,7 @@ function spawnClaudeProcess(cliPath, cliArgs, cwd, sessionKey2, proxyServer, mcp
3454
3539
  const stderr = data.toString();
3455
3540
  log.debug("stderr", { data: stderr.slice(0, 200) });
3456
3541
  retainStderr(ap, stderr);
3542
+ reportUnrecognizedModel(stderr);
3457
3543
  if (stderr.includes("No conversation found") || stderr.includes("Session ID") && (stderr.includes("already in use") || stderr.includes("not found") || stderr.includes("invalid"))) {
3458
3544
  if (activeProcesses.get(sessionKey2) === ap) {
3459
3545
  log.warn("claude session ID error, clearing session", {
@@ -3497,7 +3583,8 @@ function respawnActiveProcess(sessionKey2, cliPath, cliArgs, cwd, ignoreAnthropi
3497
3583
  old.mcpHash,
3498
3584
  old.systemPromptFile,
3499
3585
  ignoreAnthropicApiKey,
3500
- old.effort
3586
+ old.effort,
3587
+ old.promptCacheTtl
3501
3588
  );
3502
3589
  replacement.pendingProxyCompletions = old.pendingProxyCompletions;
3503
3590
  delete old.pendingProxyCompletions;
@@ -3912,8 +3999,90 @@ async function handleBtwCommand(client, input, options = {}) {
3912
3999
  }
3913
4000
  }
3914
4001
 
3915
- // src/startup-diagnostics.ts
4002
+ // src/plan-usage.ts
3916
4003
  import { execFile as execFile2 } from "child_process";
4004
+ var PLAN_USAGE_ARGUMENTS = /* @__PURE__ */ new Set(["usage", "cost", "stats", "limits"]);
4005
+ function wantsPlanUsage(argument) {
4006
+ return PLAN_USAGE_ARGUMENTS.has(argument.trim().toLowerCase());
4007
+ }
4008
+ var PLAN_USAGE_MAX_CHARS = 4e3;
4009
+ function truncate(text) {
4010
+ return text.length <= PLAN_USAGE_MAX_CHARS ? text : `${text.slice(0, PLAN_USAGE_MAX_CHARS)}
4011
+ [truncated]`;
4012
+ }
4013
+ function parsePlanUsage(stdout) {
4014
+ const trimmed = stdout.trim();
4015
+ if (!trimmed) return { status: "failed", error: "the CLI printed nothing" };
4016
+ const candidates = trimmed.split("\n").reverse();
4017
+ for (const line of candidates) {
4018
+ const start = line.indexOf("{");
4019
+ if (start === -1) continue;
4020
+ let parsed;
4021
+ try {
4022
+ parsed = JSON.parse(line.slice(start));
4023
+ } catch {
4024
+ continue;
4025
+ }
4026
+ if (parsed === null || typeof parsed !== "object" || Array.isArray(parsed)) continue;
4027
+ const record = parsed;
4028
+ if (record.type !== void 0 && record.type !== "result") continue;
4029
+ const text = typeof record.result === "string" ? record.result.trim() : "";
4030
+ if (record.is_error === true) {
4031
+ return { status: "failed", error: text || "the CLI reported an error" };
4032
+ }
4033
+ if (!text) continue;
4034
+ return {
4035
+ status: "ok",
4036
+ text: truncate(text),
4037
+ costUsd: typeof record.total_cost_usd === "number" ? record.total_cost_usd : 0,
4038
+ numTurns: typeof record.num_turns === "number" ? record.num_turns : 0
4039
+ };
4040
+ }
4041
+ return { status: "failed", error: "no result object in the CLI's reply" };
4042
+ }
4043
+ function runCli(cliPath, args, timeoutMs, env) {
4044
+ return new Promise((resolve6, reject) => {
4045
+ execFile2(
4046
+ cliPath,
4047
+ args,
4048
+ // `killSignal` so a CLI wedged on a hook is actually gone, and a generous
4049
+ // buffer because the reply is prose of unbounded length.
4050
+ { timeout: timeoutMs, killSignal: "SIGKILL", maxBuffer: 4 * 1024 * 1024, env },
4051
+ (error, stdout) => {
4052
+ if (stdout && stdout.trim()) return resolve6(stdout);
4053
+ if (error) return reject(error);
4054
+ resolve6(stdout ?? "");
4055
+ }
4056
+ );
4057
+ });
4058
+ }
4059
+ async function fetchPlanUsage(cliPath, options = {}) {
4060
+ const timeoutMs = options.timeoutMs ?? 2e4;
4061
+ const run = options.runImpl ?? runCli;
4062
+ try {
4063
+ const env = claudeSpawnEnv({ ignoreAnthropicApiKey: options.ignoreAnthropicApiKey });
4064
+ const stdout = await run(
4065
+ cliPath,
4066
+ ["-p", "/cost", "--output-format", "json"],
4067
+ timeoutMs,
4068
+ env
4069
+ );
4070
+ const usage = parsePlanUsage(stdout);
4071
+ if (usage.status === "ok") {
4072
+ log.info("read claude plan usage", { costUsd: usage.costUsd, numTurns: usage.numTurns });
4073
+ } else if (usage.status === "failed") {
4074
+ log.debug("could not read claude plan usage", { error: usage.error });
4075
+ }
4076
+ return usage;
4077
+ } catch (error) {
4078
+ const message = error instanceof Error ? error.message : String(error);
4079
+ log.debug("could not read claude plan usage", { error: message });
4080
+ return { status: "failed", error: message };
4081
+ }
4082
+ }
4083
+
4084
+ // src/startup-diagnostics.ts
4085
+ import { execFile as execFile3 } from "child_process";
3917
4086
  import * as fs4 from "fs";
3918
4087
  import * as path5 from "path";
3919
4088
  import { promisify as promisify2 } from "util";
@@ -4448,7 +4617,7 @@ function pickOpencodeVersion(input) {
4448
4617
  if (typeof direct === "string" && direct.length > 0) return direct;
4449
4618
  return void 0;
4450
4619
  }
4451
- var execFileAsync2 = promisify2(execFile2);
4620
+ var execFileAsync2 = promisify2(execFile3);
4452
4621
  var opencodeVersionProbe;
4453
4622
  function detectOpencodeVersion(execPath = process.execPath) {
4454
4623
  if (opencodeVersionProbe) return opencodeVersionProbe;
@@ -4558,7 +4727,7 @@ function logStartupDiagnostics(providers, opencodeVersion) {
4558
4727
 
4559
4728
  // src/doctor.ts
4560
4729
  var DOCTOR_COMMAND = "claude-code-doctor";
4561
- var DOCTOR_COMMAND_DESCRIPTION = "Report what the Claude Code plugin sees: versions, cwd, live processes, pending proxy calls";
4730
+ var DOCTOR_COMMAND_DESCRIPTION = "Report what the Claude Code plugin sees: versions, cwd, live processes, pending proxy calls. Add `usage` for plan windows";
4562
4731
  var DOCTOR_MARKER = "\u258C **claude-code doctor**";
4563
4732
  function isRecord3(value) {
4564
4733
  return value !== null && typeof value === "object" && !Array.isArray(value);
@@ -4688,6 +4857,36 @@ function formatDoctorReport(report) {
4688
4857
  lines.push(`| ${server2.url} | ${describeAuth(server2.auth)} |`);
4689
4858
  }
4690
4859
  }
4860
+ if (report.mcpServerErrors.length > 0) {
4861
+ lines.push("");
4862
+ lines.push("**MCP config entries Claude Code skipped**");
4863
+ lines.push("");
4864
+ lines.push("| server | category | Claude Code said |");
4865
+ lines.push("|---|---|---|");
4866
+ for (const error of report.mcpServerErrors) {
4867
+ lines.push(`| ${error.name} | \`${error.type}\` | ${error.message || "no detail"} |`);
4868
+ }
4869
+ lines.push("");
4870
+ lines.push("A skipped server is missing from the model's tools with no other sign of it.");
4871
+ }
4872
+ lines.push("");
4873
+ lines.push("**Plan usage**");
4874
+ lines.push("");
4875
+ switch (report.planUsage.status) {
4876
+ case "ok":
4877
+ lines.push("```text");
4878
+ lines.push(report.planUsage.text);
4879
+ lines.push("```");
4880
+ break;
4881
+ case "failed":
4882
+ lines.push(`Could not read it from the CLI: ${report.planUsage.error}`);
4883
+ break;
4884
+ case "not-requested":
4885
+ lines.push(
4886
+ "Not checked. Run `/claude-code-doctor usage` for the account's plan windows and reset times, straight from the CLI. It costs no tokens, but it does start a `claude` process, so it runs your `SessionStart` hooks and takes a few seconds."
4887
+ );
4888
+ break;
4889
+ }
4691
4890
  const stderr = report.processes.filter((proc) => proc.lastStderr);
4692
4891
  if (stderr.length > 0) {
4693
4892
  lines.push("");
@@ -4747,6 +4946,9 @@ async function gatherDoctorReport(options) {
4747
4946
  auth: await checkProxyAuth(proc.proxyUrl, options.fetchImpl ?? fetch)
4748
4947
  });
4749
4948
  }
4949
+ const planUsage = wantsPlanUsage(options.argument ?? "") ? await (options.planUsageImpl ?? fetchPlanUsage)(cliPath, {
4950
+ ignoreAnthropicApiKey: options.ignoreAnthropicApiKey
4951
+ }) : { status: "not-requested" };
4750
4952
  return {
4751
4953
  plugin: base.plugin,
4752
4954
  opencode: base.opencode,
@@ -4763,7 +4965,9 @@ async function gatherDoctorReport(options) {
4763
4965
  anthropicApiKeyInEnv: base.anthropicApiKeyInEnv,
4764
4966
  processes,
4765
4967
  pendingCalls: snapshotPendingProxyCalls(),
4766
- proxyServers
4968
+ proxyServers,
4969
+ mcpServerErrors: snapshotMcpServerErrors(),
4970
+ planUsage
4767
4971
  };
4768
4972
  }
4769
4973
  async function buildDoctorReport(options) {
@@ -4775,7 +4979,9 @@ async function buildDoctorReport(options) {
4775
4979
  cwd: report.cwd,
4776
4980
  processes: report.processes.length,
4777
4981
  pendingCalls: report.pendingCalls.length,
4778
- proxyServers: report.proxyServers.map((server2) => server2.auth.status)
4982
+ proxyServers: report.proxyServers.map((server2) => server2.auth.status),
4983
+ mcpServerErrors: report.mcpServerErrors.length,
4984
+ planUsage: report.planUsage.status
4779
4985
  });
4780
4986
  return formatDoctorReport(report);
4781
4987
  } catch (error) {
@@ -4849,6 +5055,7 @@ function passthroughModel(id) {
4849
5055
  }
4850
5056
  var haikuCost = { input: 1, output: 5, cacheRead: 0.1, cacheWrite: 1.25 };
4851
5057
  var sonnetCost = { input: 3, output: 15, cacheRead: 0.3, cacheWrite: 3.75 };
5058
+ var sonnet5Cost = { input: 2, output: 10, cacheRead: 0.2, cacheWrite: 2.5 };
4852
5059
  var opusCost = { input: 5, output: 25, cacheRead: 0.5, cacheWrite: 6.25 };
4853
5060
  var fableCost = { input: 10, output: 50, cacheRead: 1, cacheWrite: 12.5 };
4854
5061
  var fable51Cost = { input: 10, output: 50, cacheRead: 0.25, cacheWrite: 12.5 };
@@ -4928,10 +5135,26 @@ var defaultModels = {
4928
5135
  reasoning: true,
4929
5136
  context: 1e6,
4930
5137
  output: 128e3,
4931
- cost: sonnetCost,
4932
- multiplier: 3,
5138
+ cost: sonnet5Cost,
5139
+ multiplier: 2,
4933
5140
  releaseDate: "2026-06-30"
4934
5141
  }),
5142
+ // Sonnet 5.5 runs on any recent Claude Code, but 2.1.284 is the first
5143
+ // release that knows it: an older CLI serves it on fallback limits (200k
5144
+ // context, an estimated cost) and says so on stderr, which
5145
+ // `reportUnrecognizedModel` turns into a warning. No `-fast` entry: the CLI
5146
+ // gates fast mode on Opus names.
5147
+ "claude-sonnet-5-5": defineModel({
5148
+ id: "claude-sonnet-5-5",
5149
+ name: "Claude Sonnet 5.5",
5150
+ family: "sonnet",
5151
+ reasoning: true,
5152
+ context: 1e6,
5153
+ output: 128e3,
5154
+ cost: sonnet5Cost,
5155
+ multiplier: 2,
5156
+ releaseDate: "2026-09-28"
5157
+ }),
4935
5158
  "claude-opus-4-5": defineModel({
4936
5159
  id: "claude-opus-4-5",
4937
5160
  name: "Claude Opus 4.5",
@@ -5108,6 +5331,7 @@ function parseModelId(modelId) {
5108
5331
 
5109
5332
  // src/agent-models.ts
5110
5333
  var AGENT_DIR_NAMES = ["agents", "agent"];
5334
+ var PROMPT_CACHE_TTLS = ["5m", "1h"];
5111
5335
  var REASONING_EFFORTS = [
5112
5336
  "minimal",
5113
5337
  "low",
@@ -5118,6 +5342,7 @@ var REASONING_EFFORTS = [
5118
5342
  ];
5119
5343
  var registry = {};
5120
5344
  var defaultSubagentModel;
5345
+ var defaultSubagentCacheTtl;
5121
5346
  var providerFallbackModels = [];
5122
5347
  function setAgentRegistry(records) {
5123
5348
  registry = records;
@@ -5131,6 +5356,9 @@ function setDefaultSubagentModel(model) {
5131
5356
  function getDefaultSubagentModel() {
5132
5357
  return defaultSubagentModel;
5133
5358
  }
5359
+ function setDefaultSubagentCacheTtl(ttl) {
5360
+ defaultSubagentCacheTtl = ttl?.trim() || void 0;
5361
+ }
5134
5362
  function setProviderFallbackModels(models) {
5135
5363
  providerFallbackModels = models ?? [];
5136
5364
  }
@@ -5195,6 +5423,25 @@ function resolveAgentEffort(agent, inherited, overrides) {
5195
5423
  }
5196
5424
  return declared;
5197
5425
  }
5426
+ function resolveAgentCacheTtl(agent, overrides) {
5427
+ if (!agent) return void 0;
5428
+ const record = (overrides?.records ?? registry)[agent];
5429
+ if (!record) return void 0;
5430
+ const fallback = overrides ? overrides.defaultSubagentCacheTtl : defaultSubagentCacheTtl;
5431
+ const declared = record.cacheTtl?.trim();
5432
+ const wanted = declared || (record.mode === "subagent" ? fallback?.trim() : void 0);
5433
+ if (!wanted) return void 0;
5434
+ if (!PROMPT_CACHE_TTLS.includes(wanted)) {
5435
+ log.warn("agent prompt cache ttl refused: unknown value", {
5436
+ agent,
5437
+ wanted,
5438
+ allowed: PROMPT_CACHE_TTLS.join(", ")
5439
+ });
5440
+ return void 0;
5441
+ }
5442
+ log.debug("agent prompt cache ttl", { agent, ttl: wanted });
5443
+ return wanted;
5444
+ }
5198
5445
  function parseAgentFrontmatter(text) {
5199
5446
  const record = {};
5200
5447
  if (!text.startsWith("---")) return record;
@@ -5226,6 +5473,7 @@ function parseAgentFrontmatter(text) {
5226
5473
  else if (key === "model") record.model = value;
5227
5474
  else if (key === "forceModel") record.forceModel = value;
5228
5475
  else if (key === "reasoningEffort") record.reasoningEffort = value;
5476
+ else if (key === "cacheTtl") record.cacheTtl = value;
5229
5477
  }
5230
5478
  return record;
5231
5479
  }
@@ -7566,6 +7814,17 @@ function toUsage(rawUsage) {
7566
7814
  raw: rawUsage
7567
7815
  };
7568
7816
  }
7817
+ function lastCallContextUsage(lastCallUsage, turnTotalUsage) {
7818
+ if (!lastCallUsage || !turnTotalUsage) return turnTotalUsage;
7819
+ const iterations = lastCallUsage.iterations;
7820
+ const context = iterations?.length ? iterations[iterations.length - 1] : lastCallUsage;
7821
+ return {
7822
+ input_tokens: context.input_tokens,
7823
+ cache_read_input_tokens: context.cache_read_input_tokens,
7824
+ cache_creation_input_tokens: context.cache_creation_input_tokens,
7825
+ output_tokens: turnTotalUsage.output_tokens
7826
+ };
7827
+ }
7569
7828
  function toFinishReason(reason = "stop") {
7570
7829
  return {
7571
7830
  unified: reason,
@@ -7657,6 +7916,7 @@ function createTurnState(init) {
7657
7916
  skipResultForIds: /* @__PURE__ */ new Set(),
7658
7917
  toolCallsById: /* @__PURE__ */ new Map(),
7659
7918
  resultMeta: {},
7919
+ lastCallUsage: void 0,
7660
7920
  resultFailure: void 0,
7661
7921
  accountLimitHit: null,
7662
7922
  accountBlock: null,
@@ -8066,7 +8326,8 @@ function finishWithToolCalls(state, calls) {
8066
8326
  state.controller.enqueue({
8067
8327
  type: "finish",
8068
8328
  finishReason: state.toFinishReason("tool-calls"),
8069
- usage: state.toUsage(state.resultMeta.usage),
8329
+ // No result yet (the usual mid-turn boundary) still reports nothing.
8330
+ usage: state.toUsage(lastCallContextUsage(state.lastCallUsage, state.resultMeta.usage)),
8070
8331
  providerMetadata: {
8071
8332
  "claude-code": state.resultMeta
8072
8333
  }
@@ -8097,7 +8358,7 @@ function finishWithQuestionCall(state, call) {
8097
8358
  state.controller.enqueue({
8098
8359
  type: "finish",
8099
8360
  finishReason: state.toFinishReason("tool-calls"),
8100
- usage: state.toUsage(state.resultMeta.usage),
8361
+ usage: state.toUsage(lastCallContextUsage(state.lastCallUsage, state.resultMeta.usage)),
8101
8362
  providerMetadata: {
8102
8363
  "claude-code": state.resultMeta
8103
8364
  }
@@ -8545,6 +8806,10 @@ ${plan}
8545
8806
  if (msg.type === "assistant" && msg.message && typeof msg.message.stop_reason === "string") {
8546
8807
  state.lastStopReason = msg.message.stop_reason;
8547
8808
  }
8809
+ const callUsage = msg.type === "assistant" ? msg.message?.usage : void 0;
8810
+ if (callUsage && (callUsage.input_tokens ?? 0) + (callUsage.cache_read_input_tokens ?? 0) + (callUsage.cache_creation_input_tokens ?? 0) > 0) {
8811
+ state.lastCallUsage = callUsage;
8812
+ }
8548
8813
  if (msg.type === "assistant" && msg.message?.content && state.gotPartialEvents) {
8549
8814
  const thinkingBlocks = msg.message.content.filter(
8550
8815
  (b) => b.type === "thinking"
@@ -9354,9 +9619,15 @@ var ClaudeCodeLanguageModel = class {
9354
9619
  this.getOpencodeAgent(options),
9355
9620
  this.getReasoningEffort(options.providerOptions)
9356
9621
  );
9622
+ const promptCacheTtl = compactionMode ? void 0 : resolveAgentCacheTtl(this.getOpencodeAgent(options));
9623
+ const context = [
9624
+ this.config.provider,
9625
+ this.getOpencodeAgent(options) ?? null
9626
+ ];
9627
+ if (promptCacheTtl) context.push(promptCacheTtl);
9357
9628
  const baseKey = sessionKey(
9358
9629
  cwd,
9359
- `${effectiveModelId}::${scope}::${affinity}::context=${JSON.stringify([this.config.provider, this.getOpencodeAgent(options) ?? null])}`
9630
+ `${effectiveModelId}::${scope}::${affinity}::context=${JSON.stringify(context)}`
9360
9631
  );
9361
9632
  const sk = compactionMode ? sessionKey(cwd, `${effectiveModelId}::compaction::${affinity}`) : effortSessionKey(baseKey, reasoningEffort);
9362
9633
  const toUsage2 = this.toUsage.bind(this);
@@ -9384,7 +9655,11 @@ var ClaudeCodeLanguageModel = class {
9384
9655
  const doctorOptions = {
9385
9656
  cliPath,
9386
9657
  interactive: !!useInteractive,
9387
- turnStats: this.config.turnStats === true
9658
+ turnStats: this.config.turnStats === true,
9659
+ ignoreAnthropicApiKey: this.config.ignoreAnthropicApiKey === true,
9660
+ // `/claude-code-doctor usage` opts into the CLI's own plan-usage
9661
+ // report; anything else here is ignored, as it always has been.
9662
+ argument: doctor.rest
9388
9663
  };
9389
9664
  const stream2 = new ReadableStream({
9390
9665
  async start(controller) {
@@ -9944,7 +10219,8 @@ var ClaudeCodeLanguageModel = class {
9944
10219
  spawnMcpHash,
9945
10220
  spawnSystemPromptFile,
9946
10221
  self.config.ignoreAnthropicApiKey,
9947
- reasoningEffort
10222
+ reasoningEffort,
10223
+ promptCacheTtl
9948
10224
  );
9949
10225
  state.proc = ap.proc;
9950
10226
  state.lineEmitter = ap.lineEmitter;
@@ -10017,7 +10293,7 @@ var ClaudeCodeLanguageModel = class {
10017
10293
  controller.enqueue({
10018
10294
  type: "finish",
10019
10295
  finishReason: { unified: "error", raw: refusal.kind },
10020
- usage: toUsage2(msg.usage),
10296
+ usage: toUsage2(lastCallContextUsage(state.lastCallUsage, msg.usage)),
10021
10297
  providerMetadata: {
10022
10298
  "claude-code": { ...state.resultMeta, path: "model-fallback" }
10023
10299
  }
@@ -10065,19 +10341,23 @@ var ClaudeCodeLanguageModel = class {
10065
10341
  });
10066
10342
  state.endTextBlock();
10067
10343
  }
10344
+ const usage = toUsage2(lastCallContextUsage(state.lastCallUsage, msg.usage));
10068
10345
  controller.enqueue({
10069
10346
  type: "finish",
10070
10347
  finishReason: state.resultFailure ? { unified: "error", raw: state.resultFailure } : toFinishReason2("stop"),
10071
- usage: toUsage2(msg.usage),
10348
+ usage,
10072
10349
  providerMetadata: {
10073
10350
  "claude-code": {
10074
10351
  ...state.resultMeta,
10075
10352
  ...state.resultFailure ? { resultSubtype: state.resultFailure } : {},
10076
10353
  ...compactionMode ? { compactionModel: effectiveModelId } : {}
10077
10354
  },
10355
+ // opencode falls back to this when the usage has no cache
10356
+ // write (0 is sent as none), so it must match the usage, never
10357
+ // the turn total.
10078
10358
  ...typeof msg.usage?.cache_creation_input_tokens === "number" ? {
10079
10359
  anthropic: {
10080
- cacheCreationInputTokens: msg.usage.cache_creation_input_tokens
10360
+ cacheCreationInputTokens: usage.inputTokens.cacheWrite ?? 0
10081
10361
  }
10082
10362
  } : {}
10083
10363
  }
@@ -11352,6 +11632,10 @@ async function buildAgentRegistry(config) {
11352
11632
  setDefaultSubagentModel(
11353
11633
  typeof configured === "string" ? configured : void 0
11354
11634
  );
11635
+ const configuredTtl = options?.defaultSubagentCacheTtl;
11636
+ setDefaultSubagentCacheTtl(
11637
+ typeof configuredTtl === "string" ? configuredTtl : void 0
11638
+ );
11355
11639
  setProviderFallbackModels(parseFallbackModelList(options?.fallbackModels));
11356
11640
  const records = await readAgentMarkdownRecords(
11357
11641
  agentDirectories(
@@ -11373,6 +11657,7 @@ async function buildAgentRegistry(config) {
11373
11657
  model: pick("model") ?? records[name]?.model,
11374
11658
  forceModel: pick("forceModel") ?? records[name]?.forceModel,
11375
11659
  reasoningEffort: pick("reasoningEffort") ?? records[name]?.reasoningEffort,
11660
+ cacheTtl: pick("cacheTtl") ?? records[name]?.cacheTtl,
11376
11661
  fallbackModels: declaredChain.length ? declaredChain : records[name]?.fallbackModels
11377
11662
  };
11378
11663
  }