@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/README.md +42 -7
- package/dist/index.d.ts +27 -0
- package/dist/index.js +317 -32
- package/dist/index.js.map +1 -1
- package/package.json +2 -2
- package/skills/claude-code-plugin/SKILL.md +59 -11
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 =
|
|
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/
|
|
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(
|
|
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:
|
|
4932
|
-
multiplier:
|
|
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
|
-
|
|
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(
|
|
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
|
|
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:
|
|
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
|
}
|