@khalilgharbaoui/opencode-claude-code-plugin 0.18.2 → 0.19.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 +167 -45
- package/dist/index.d.ts +72 -9
- package/dist/index.js +2186 -1405
- package/dist/index.js.map +1 -1
- package/package.json +2 -2
- package/skills/claude-code-plugin/SKILL.md +27 -0
package/dist/index.js
CHANGED
|
@@ -324,8 +324,8 @@ function mapTool(name, input, opts) {
|
|
|
324
324
|
}
|
|
325
325
|
if (name === "TaskUpdate") {
|
|
326
326
|
if (opts?.sessionId) {
|
|
327
|
-
const
|
|
328
|
-
if (
|
|
327
|
+
const list2 = applyTaskUpdate(opts.sessionId, input);
|
|
328
|
+
if (list2 !== null) return emitTodoWrite(list2);
|
|
329
329
|
}
|
|
330
330
|
return { name, input, executed: true, skip: true };
|
|
331
331
|
}
|
|
@@ -1366,6 +1366,7 @@ function queuePendingProxyCall(sessionKey2, call, timeoutOverrides) {
|
|
|
1366
1366
|
input: call.input,
|
|
1367
1367
|
channel: call.channel,
|
|
1368
1368
|
createdAt: Date.now(),
|
|
1369
|
+
deadlineMs,
|
|
1369
1370
|
timer,
|
|
1370
1371
|
resolve: call.resolve,
|
|
1371
1372
|
reject: call.reject
|
|
@@ -1397,6 +1398,21 @@ function getPendingProxyCalls(sessionKey2) {
|
|
|
1397
1398
|
}
|
|
1398
1399
|
return out;
|
|
1399
1400
|
}
|
|
1401
|
+
function snapshotPendingProxyCalls(now = Date.now()) {
|
|
1402
|
+
const out = [];
|
|
1403
|
+
for (const pending of pendingByCallId.values()) {
|
|
1404
|
+
out.push({
|
|
1405
|
+
sessionKey: pending.sessionKey,
|
|
1406
|
+
toolCallId: pending.toolCallId,
|
|
1407
|
+
toolName: pending.toolName,
|
|
1408
|
+
ageMs: Math.max(0, now - pending.createdAt),
|
|
1409
|
+
deadlineMs: pending.deadlineMs,
|
|
1410
|
+
emitted: pending.emitted === true,
|
|
1411
|
+
channelClosed: pending.channel?.closed === true
|
|
1412
|
+
});
|
|
1413
|
+
}
|
|
1414
|
+
return out;
|
|
1415
|
+
}
|
|
1400
1416
|
function resolvePendingProxyCallById(toolCallId, result) {
|
|
1401
1417
|
const pending = pendingByCallId.get(toolCallId);
|
|
1402
1418
|
if (!pending) return false;
|
|
@@ -1832,6 +1848,18 @@ function takeUnattendedLines(ap) {
|
|
|
1832
1848
|
ap.unattendedDropped = 0;
|
|
1833
1849
|
return { lines, dropped };
|
|
1834
1850
|
}
|
|
1851
|
+
var STDERR_RETAIN_BYTES = 2 * 1024;
|
|
1852
|
+
function retainStderr(ap, chunk) {
|
|
1853
|
+
ap.lastStderr = ((ap.lastStderr ?? "") + chunk).slice(-STDERR_RETAIN_BYTES);
|
|
1854
|
+
}
|
|
1855
|
+
function describeChildCrash(exitCode, signal, lastStderr) {
|
|
1856
|
+
const how = signal ? `was killed by ${signal}` : typeof exitCode === "number" ? `exited with code ${exitCode}` : "closed its output";
|
|
1857
|
+
const tail = lastStderr?.trim();
|
|
1858
|
+
return `The Claude Code CLI ${how} before finishing this turn (no result was emitted), so the answer above may be incomplete.` + (tail ? `
|
|
1859
|
+
|
|
1860
|
+
Last stderr from the CLI:
|
|
1861
|
+
${tail}` : "");
|
|
1862
|
+
}
|
|
1835
1863
|
var activeProcesses = /* @__PURE__ */ new Map();
|
|
1836
1864
|
var claudeSessions = /* @__PURE__ */ new Map();
|
|
1837
1865
|
var idleEvictionTimers = /* @__PURE__ */ new Map();
|
|
@@ -1877,10 +1905,22 @@ function touch(key) {
|
|
|
1877
1905
|
}
|
|
1878
1906
|
function evictIfNeeded() {
|
|
1879
1907
|
while (activeProcesses.size >= MAX_ACTIVE_PROCESSES) {
|
|
1880
|
-
|
|
1881
|
-
|
|
1882
|
-
|
|
1883
|
-
|
|
1908
|
+
let victimKey;
|
|
1909
|
+
for (const [key, ap] of activeProcesses) {
|
|
1910
|
+
if (!isTurnInFlight(ap)) {
|
|
1911
|
+
victimKey = key;
|
|
1912
|
+
break;
|
|
1913
|
+
}
|
|
1914
|
+
}
|
|
1915
|
+
if (!victimKey) {
|
|
1916
|
+
log.warn("every claude process is mid-turn; skipping LRU eviction", {
|
|
1917
|
+
active: activeProcesses.size,
|
|
1918
|
+
cap: MAX_ACTIVE_PROCESSES
|
|
1919
|
+
});
|
|
1920
|
+
return;
|
|
1921
|
+
}
|
|
1922
|
+
log.info("evicting LRU claude process", { sessionKey: victimKey });
|
|
1923
|
+
deleteActiveProcess(victimKey);
|
|
1884
1924
|
}
|
|
1885
1925
|
}
|
|
1886
1926
|
var TURN_INTERRUPT_TIMEOUT_MS = 5e3;
|
|
@@ -2095,6 +2135,7 @@ function spawnClaudeProcess(cliPath, cliArgs, cwd, sessionKey2, proxyServer, mcp
|
|
|
2095
2135
|
mcpHash,
|
|
2096
2136
|
systemPromptFile,
|
|
2097
2137
|
effort,
|
|
2138
|
+
startedAt: Date.now(),
|
|
2098
2139
|
cliArgs: [...cliArgs],
|
|
2099
2140
|
unattendedLines: [],
|
|
2100
2141
|
unattendedDropped: 0
|
|
@@ -2118,6 +2159,14 @@ function spawnClaudeProcess(cliPath, cliArgs, cwd, sessionKey2, proxyServer, mcp
|
|
|
2118
2159
|
proc.on("error", (err) => {
|
|
2119
2160
|
log.error("claude process error", { sessionKey: sessionKey2, error: err.message });
|
|
2120
2161
|
});
|
|
2162
|
+
proc.stdin?.on("error", (err) => {
|
|
2163
|
+
log.warn("claude process stdin error", {
|
|
2164
|
+
sessionKey: sessionKey2,
|
|
2165
|
+
code: err.code,
|
|
2166
|
+
error: err.message
|
|
2167
|
+
});
|
|
2168
|
+
settleTurn(ap);
|
|
2169
|
+
});
|
|
2121
2170
|
proc.on("exit", (code, signal) => {
|
|
2122
2171
|
log.info("claude process exited", { code, signal, sessionKey: sessionKey2 });
|
|
2123
2172
|
void proxyServer?.close();
|
|
@@ -2141,6 +2190,7 @@ function spawnClaudeProcess(cliPath, cliArgs, cwd, sessionKey2, proxyServer, mcp
|
|
|
2141
2190
|
proc.stderr?.on("data", (data) => {
|
|
2142
2191
|
const stderr = data.toString();
|
|
2143
2192
|
log.debug("stderr", { data: stderr.slice(0, 200) });
|
|
2193
|
+
retainStderr(ap, stderr);
|
|
2144
2194
|
if (stderr.includes("No conversation found") || stderr.includes("Session ID") && (stderr.includes("already in use") || stderr.includes("not found") || stderr.includes("invalid"))) {
|
|
2145
2195
|
if (activeProcesses.get(sessionKey2) === ap) {
|
|
2146
2196
|
log.warn("claude session ID error, clearing session", {
|
|
@@ -2263,6 +2313,36 @@ function buildCliArgs(opts) {
|
|
|
2263
2313
|
function sessionKey(cwd, modelId) {
|
|
2264
2314
|
return `${cwd}::${modelId}`;
|
|
2265
2315
|
}
|
|
2316
|
+
function describeSessionKey(key) {
|
|
2317
|
+
const parts = key.split("::");
|
|
2318
|
+
return {
|
|
2319
|
+
cwd: parts[0] ?? "unknown",
|
|
2320
|
+
model: parts[1] ?? "unknown",
|
|
2321
|
+
session: parts[3] ?? "unknown",
|
|
2322
|
+
compaction: parts[2] === "compaction"
|
|
2323
|
+
};
|
|
2324
|
+
}
|
|
2325
|
+
function snapshotActiveProcesses(now = Date.now()) {
|
|
2326
|
+
const out = [];
|
|
2327
|
+
for (const [key, ap] of activeProcesses) {
|
|
2328
|
+
const described = describeSessionKey(key);
|
|
2329
|
+
const lastStderr = ap.lastStderr;
|
|
2330
|
+
out.push({
|
|
2331
|
+
sessionKey: key,
|
|
2332
|
+
session: ap.opencodeSessionID ?? described.session,
|
|
2333
|
+
model: described.model,
|
|
2334
|
+
compaction: described.compaction,
|
|
2335
|
+
pid: ap.proc.pid,
|
|
2336
|
+
inFlight: ap.turnInFlight === true,
|
|
2337
|
+
ageMs: ap.startedAt === void 0 ? void 0 : Math.max(0, now - ap.startedAt),
|
|
2338
|
+
effort: ap.effort,
|
|
2339
|
+
attached: ap.lineEmitter.listenerCount("line") > 0,
|
|
2340
|
+
proxyUrl: ap.proxyServer?.url,
|
|
2341
|
+
lastStderr: typeof lastStderr === "string" ? lastStderr : void 0
|
|
2342
|
+
});
|
|
2343
|
+
}
|
|
2344
|
+
return out;
|
|
2345
|
+
}
|
|
2266
2346
|
|
|
2267
2347
|
// src/btw-command.ts
|
|
2268
2348
|
var BtwHandledError = class extends Error {
|
|
@@ -2548,1359 +2628,2024 @@ async function handleBtwCommand(client, input, options = {}) {
|
|
|
2548
2628
|
}
|
|
2549
2629
|
}
|
|
2550
2630
|
|
|
2551
|
-
// src/
|
|
2552
|
-
|
|
2553
|
-
|
|
2554
|
-
if (!part || part.type !== "text" || typeof part.text !== "string") return false;
|
|
2555
|
-
const text = part.text.trimStart();
|
|
2556
|
-
return ASIDE_MARKERS.some((marker) => text.startsWith(marker));
|
|
2631
|
+
// src/cli-events.ts
|
|
2632
|
+
function isRecord2(value) {
|
|
2633
|
+
return value !== null && typeof value === "object" && !Array.isArray(value);
|
|
2557
2634
|
}
|
|
2558
|
-
function
|
|
2559
|
-
|
|
2560
|
-
|
|
2561
|
-
|
|
2635
|
+
function str(value) {
|
|
2636
|
+
return typeof value === "string" && value.length > 0 ? value : void 0;
|
|
2637
|
+
}
|
|
2638
|
+
function num(value) {
|
|
2639
|
+
return typeof value === "number" && Number.isFinite(value) ? value : void 0;
|
|
2640
|
+
}
|
|
2641
|
+
var RATE_LIMIT_MARKER = "\u258C **rate limit:**";
|
|
2642
|
+
var RATE_LIMIT_ACTION = "Enable extra usage or add credits on the account, wait for the window to reset, switch account, org or plan, or authenticate with an API key.";
|
|
2643
|
+
var OVERAGE_DISABLED_REASONS = {
|
|
2644
|
+
overage_not_provisioned: "extra usage is not set up on this account",
|
|
2645
|
+
org_level_disabled: "extra usage is disabled for your organization",
|
|
2646
|
+
org_level_disabled_until: "extra usage is disabled for your organization for now",
|
|
2647
|
+
out_of_credits: "the account's usage credits are spent",
|
|
2648
|
+
seat_tier_level_disabled: "your seat tier does not allow extra usage",
|
|
2649
|
+
member_level_disabled: "extra usage is disabled for your member account",
|
|
2650
|
+
seat_tier_zero_credit_limit: "your seat tier has a zero credit limit",
|
|
2651
|
+
group_zero_credit_limit: "your group has a zero credit limit",
|
|
2652
|
+
member_zero_credit_limit: "your member account has a zero credit limit",
|
|
2653
|
+
org_service_level_disabled: "your organization's service level does not include extra usage",
|
|
2654
|
+
no_limits_configured: "no usage limits are configured for this account",
|
|
2655
|
+
fetch_error: "the CLI could not read the account's usage limits"
|
|
2656
|
+
};
|
|
2657
|
+
var RATE_LIMIT_WINDOWS = {
|
|
2658
|
+
five_hour: "the 5-hour window",
|
|
2659
|
+
seven_day: "the 7-day window",
|
|
2660
|
+
seven_day_opus: "the 7-day Opus window",
|
|
2661
|
+
seven_day_sonnet: "the 7-day Sonnet window",
|
|
2662
|
+
seven_day_overage_included: "the 7-day extra-usage window",
|
|
2663
|
+
overage: "extra usage"
|
|
2664
|
+
};
|
|
2665
|
+
function parseRateLimitEvent(msg) {
|
|
2666
|
+
if (msg.type !== "rate_limit_event") return null;
|
|
2667
|
+
const info = msg.rate_limit_info;
|
|
2668
|
+
if (!isRecord2(info)) return null;
|
|
2669
|
+
return {
|
|
2670
|
+
status: str(info.status),
|
|
2671
|
+
rateLimitType: str(info.rateLimitType),
|
|
2672
|
+
resetsAt: num(info.resetsAt),
|
|
2673
|
+
utilization: num(info.utilization),
|
|
2674
|
+
isUsingOverage: info.isUsingOverage === true,
|
|
2675
|
+
overageStatus: str(info.overageStatus),
|
|
2676
|
+
overageResetsAt: num(info.overageResetsAt),
|
|
2677
|
+
overageDisabledReason: str(info.overageDisabledReason)
|
|
2678
|
+
};
|
|
2562
2679
|
}
|
|
2563
|
-
function
|
|
2564
|
-
|
|
2565
|
-
const
|
|
2566
|
-
|
|
2567
|
-
|
|
2568
|
-
return !aside;
|
|
2569
|
-
}
|
|
2570
|
-
return message.role !== "assistant" || !aside;
|
|
2571
|
-
});
|
|
2572
|
-
return kept.map(
|
|
2573
|
-
(message) => message.role === "assistant" ? { ...message, content: stripInlineAsides(message.content) } : message
|
|
2574
|
-
);
|
|
2680
|
+
function formatResetsAt(resetsAt) {
|
|
2681
|
+
if (resetsAt === void 0) return void 0;
|
|
2682
|
+
const ms = resetsAt < 1e12 ? resetsAt * 1e3 : resetsAt;
|
|
2683
|
+
const date = new Date(ms);
|
|
2684
|
+
return Number.isNaN(date.getTime()) ? void 0 : date.toISOString();
|
|
2575
2685
|
}
|
|
2576
|
-
|
|
2577
|
-
|
|
2578
|
-
|
|
2579
|
-
|
|
2580
|
-
|
|
2581
|
-
|
|
2582
|
-
|
|
2583
|
-
|
|
2584
|
-
|
|
2585
|
-
|
|
2586
|
-
|
|
2686
|
+
function rateLimitKey(info) {
|
|
2687
|
+
return [
|
|
2688
|
+
info.status ?? "?",
|
|
2689
|
+
info.rateLimitType ?? "?",
|
|
2690
|
+
info.overageStatus ?? "?",
|
|
2691
|
+
info.overageDisabledReason ?? "?"
|
|
2692
|
+
].join("|");
|
|
2693
|
+
}
|
|
2694
|
+
function isRateLimitRejected(info) {
|
|
2695
|
+
return info.status === "rejected" || info.overageStatus === "rejected";
|
|
2696
|
+
}
|
|
2697
|
+
function describeRateLimit(info) {
|
|
2698
|
+
if (!info.status && !info.overageStatus) return null;
|
|
2699
|
+
const window = info.rateLimitType ? RATE_LIMIT_WINDOWS[info.rateLimitType] ?? info.rateLimitType : void 0;
|
|
2700
|
+
const resets = formatResetsAt(info.resetsAt);
|
|
2701
|
+
const overageResets = formatResetsAt(info.overageResetsAt);
|
|
2702
|
+
const key = rateLimitKey(info);
|
|
2703
|
+
if (isRateLimitRejected(info)) {
|
|
2704
|
+
const parts = [];
|
|
2705
|
+
parts.push(
|
|
2706
|
+
info.status === "rejected" ? `Claude Code rejected this request: you are out of usage${window ? ` in ${window}` : ""}.` : "Claude Code rejected this request: paid extra usage is not available on this account."
|
|
2707
|
+
);
|
|
2708
|
+
const reason = info.overageDisabledReason ? OVERAGE_DISABLED_REASONS[info.overageDisabledReason] ?? info.overageDisabledReason : void 0;
|
|
2709
|
+
if (reason) parts.push(`Extra usage is unavailable because ${reason}.`);
|
|
2710
|
+
const resetAt = resets ?? overageResets;
|
|
2711
|
+
if (resetAt) parts.push(`Resets at ${resetAt}.`);
|
|
2712
|
+
parts.push(RATE_LIMIT_ACTION);
|
|
2713
|
+
const message = parts.join(" ");
|
|
2714
|
+
return { key, level: "warn", message, transcript: `
|
|
2715
|
+
${RATE_LIMIT_MARKER} ${message}
|
|
2716
|
+
` };
|
|
2717
|
+
}
|
|
2718
|
+
if (info.status === "allowed_warning" || info.overageStatus === "allowed_warning") {
|
|
2719
|
+
const used = info.utilization === void 0 ? "" : ` (${Math.round(info.utilization * 100)}% used)`;
|
|
2720
|
+
return {
|
|
2721
|
+
key,
|
|
2722
|
+
level: "notice",
|
|
2723
|
+
message: `Approaching the usage limit${window ? ` for ${window}` : ""}${used}${resets ? `, resets at ${resets}` : ""}.`,
|
|
2724
|
+
transcript: null
|
|
2725
|
+
};
|
|
2587
2726
|
}
|
|
2588
|
-
|
|
2589
|
-
|
|
2590
|
-
|
|
2591
|
-
|
|
2592
|
-
|
|
2593
|
-
|
|
2594
|
-
|
|
2595
|
-
|
|
2596
|
-
|
|
2597
|
-
|
|
2598
|
-
|
|
2599
|
-
|
|
2600
|
-
|
|
2601
|
-
|
|
2602
|
-
|
|
2603
|
-
|
|
2604
|
-
}
|
|
2605
|
-
} else if (raw instanceof URL) {
|
|
2606
|
-
log.warn("remote URL images are not supported by Claude CLI, skipping");
|
|
2607
|
-
return null;
|
|
2608
|
-
} else if (raw instanceof Uint8Array || Buffer.isBuffer(raw)) {
|
|
2609
|
-
base64 = Buffer.from(raw).toString("base64");
|
|
2610
|
-
} else {
|
|
2611
|
-
log.warn("unsupported file part data type", { dataType: typeof raw });
|
|
2727
|
+
return {
|
|
2728
|
+
key,
|
|
2729
|
+
level: "info",
|
|
2730
|
+
message: `usage limits updated${window ? ` for ${window}` : ""}`,
|
|
2731
|
+
transcript: null
|
|
2732
|
+
};
|
|
2733
|
+
}
|
|
2734
|
+
var reportedRateLimits = /* @__PURE__ */ new Set();
|
|
2735
|
+
function reportRateLimitEvent(msg) {
|
|
2736
|
+
const info = parseRateLimitEvent(msg);
|
|
2737
|
+
if (!info) return null;
|
|
2738
|
+
const report = describeRateLimit(info);
|
|
2739
|
+
if (!report) return null;
|
|
2740
|
+
const data = { ...info };
|
|
2741
|
+
if (reportedRateLimits.has(report.key)) {
|
|
2742
|
+
log.debug(report.message, data);
|
|
2612
2743
|
return null;
|
|
2613
2744
|
}
|
|
2614
|
-
|
|
2615
|
-
|
|
2616
|
-
|
|
2617
|
-
|
|
2618
|
-
|
|
2745
|
+
reportedRateLimits.add(report.key);
|
|
2746
|
+
log[report.level](report.message, data);
|
|
2747
|
+
return report.transcript;
|
|
2748
|
+
}
|
|
2749
|
+
var API_KEY_SOURCES = /* @__PURE__ */ new Set([
|
|
2750
|
+
"ANTHROPIC_API_KEY",
|
|
2751
|
+
"apiKeyHelper",
|
|
2752
|
+
"/login managed key",
|
|
2753
|
+
"user",
|
|
2754
|
+
"project",
|
|
2755
|
+
"org",
|
|
2756
|
+
"temporary"
|
|
2757
|
+
]);
|
|
2758
|
+
function parseSystemInit(msg) {
|
|
2759
|
+
if (msg.type !== "system" || msg.subtype !== "init") return null;
|
|
2760
|
+
const raw = msg;
|
|
2761
|
+
const servers = [];
|
|
2762
|
+
if (Array.isArray(raw.mcp_servers)) {
|
|
2763
|
+
for (const entry of raw.mcp_servers) {
|
|
2764
|
+
if (!isRecord2(entry)) continue;
|
|
2765
|
+
servers.push({ name: str(entry.name) ?? "unknown", status: str(entry.status) ?? "unknown" });
|
|
2766
|
+
}
|
|
2619
2767
|
}
|
|
2620
2768
|
return {
|
|
2621
|
-
|
|
2622
|
-
|
|
2769
|
+
apiKeySource: str(raw.apiKeySource),
|
|
2770
|
+
permissionMode: str(raw.permissionMode),
|
|
2771
|
+
model: str(raw.model),
|
|
2772
|
+
cliVersion: str(raw.claude_code_version),
|
|
2773
|
+
toolCount: Array.isArray(raw.tools) ? raw.tools.length : 0,
|
|
2774
|
+
mcpServers: servers
|
|
2623
2775
|
};
|
|
2624
2776
|
}
|
|
2625
|
-
function
|
|
2626
|
-
|
|
2627
|
-
|
|
2628
|
-
|
|
2629
|
-
|
|
2630
|
-
|
|
2631
|
-
|
|
2777
|
+
function apiKeySourceWarning(apiKeySource, ignoreAnthropicApiKey) {
|
|
2778
|
+
if (!apiKeySource || !API_KEY_SOURCES.has(apiKeySource)) return null;
|
|
2779
|
+
const base = `Claude Code authenticated with an API key (apiKeySource: ${apiKeySource}), so these turns bill as pay-as-you-go API usage instead of your subscription's Agent SDK credit.`;
|
|
2780
|
+
return ignoreAnthropicApiKey ? `${base} \`ignoreAnthropicApiKey\` is already on, so the key is not coming from the environment: check the CLI's own settings (\`claude config\`) or an \`apiKeyHelper\`.` : `${base} Set the provider option \`ignoreAnthropicApiKey: true\` to strip the key from spawns and fall back to the stored subscription auth.`;
|
|
2781
|
+
}
|
|
2782
|
+
var warnedApiKeySources = /* @__PURE__ */ new Set();
|
|
2783
|
+
var warnedMcpFailures = /* @__PURE__ */ new Set();
|
|
2784
|
+
function reportSystemInit(msg, options = {}) {
|
|
2785
|
+
const info = parseSystemInit(msg);
|
|
2786
|
+
if (!info) return;
|
|
2787
|
+
log.info("claude session init", {
|
|
2788
|
+
apiKeySource: info.apiKeySource ?? null,
|
|
2789
|
+
permissionMode: info.permissionMode ?? null,
|
|
2790
|
+
model: info.model ?? null,
|
|
2791
|
+
cliVersion: info.cliVersion ?? null,
|
|
2792
|
+
tools: info.toolCount,
|
|
2793
|
+
mcpServers: info.mcpServers
|
|
2794
|
+
});
|
|
2795
|
+
for (const server2 of info.mcpServers) {
|
|
2796
|
+
if (server2.status === "connected") continue;
|
|
2797
|
+
const key2 = `${server2.name}:${server2.status}`;
|
|
2798
|
+
const message = `MCP server "${server2.name}" is ${server2.status} in Claude Code; its tools are not available to the model this session.`;
|
|
2799
|
+
if (warnedMcpFailures.has(key2)) {
|
|
2800
|
+
log.debug(message, { server: server2.name, status: server2.status });
|
|
2801
|
+
continue;
|
|
2802
|
+
}
|
|
2803
|
+
warnedMcpFailures.add(key2);
|
|
2804
|
+
log.warn(message, { server: server2.name, status: server2.status });
|
|
2632
2805
|
}
|
|
2633
|
-
|
|
2634
|
-
|
|
2635
|
-
|
|
2636
|
-
|
|
2637
|
-
|
|
2638
|
-
|
|
2639
|
-
return JSON.stringify(value.value);
|
|
2640
|
-
case "execution-denied":
|
|
2641
|
-
return value.reason ? `Execution denied: ${value.reason}` : "Execution denied";
|
|
2642
|
-
case "content":
|
|
2643
|
-
return Array.isArray(value.value) ? value.value.map((item) => {
|
|
2644
|
-
if (item?.type === "text") return item.text;
|
|
2645
|
-
return JSON.stringify(item);
|
|
2646
|
-
}).join("\n") : JSON.stringify(value.value);
|
|
2647
|
-
default:
|
|
2648
|
-
return JSON.stringify(value);
|
|
2806
|
+
const apiKeyMessage = apiKeySourceWarning(info.apiKeySource, options.ignoreAnthropicApiKey);
|
|
2807
|
+
if (!apiKeyMessage) return;
|
|
2808
|
+
const key = `${info.apiKeySource}:${options.ignoreAnthropicApiKey ? "ignored" : "passed"}`;
|
|
2809
|
+
if (warnedApiKeySources.has(key)) {
|
|
2810
|
+
log.debug(apiKeyMessage, { apiKeySource: info.apiKeySource });
|
|
2811
|
+
return;
|
|
2649
2812
|
}
|
|
2813
|
+
warnedApiKeySources.add(key);
|
|
2814
|
+
log.warn(apiKeyMessage, { apiKeySource: info.apiKeySource });
|
|
2650
2815
|
}
|
|
2651
|
-
var
|
|
2652
|
-
|
|
2653
|
-
|
|
2654
|
-
|
|
2655
|
-
|
|
2656
|
-
return
|
|
2657
|
-
|
|
2816
|
+
var COMPACT_BOUNDARY_MARKER = "\u258C **context compacted:**";
|
|
2817
|
+
function parseCompactBoundary(msg) {
|
|
2818
|
+
if (msg.type !== "system" || msg.subtype !== "compact_boundary") return null;
|
|
2819
|
+
const raw = msg;
|
|
2820
|
+
const meta = isRecord2(raw.compact_metadata) ? raw.compact_metadata : isRecord2(raw.compactMetadata) ? raw.compactMetadata : void 0;
|
|
2821
|
+
return {
|
|
2822
|
+
trigger: str(meta?.trigger) ?? "unknown",
|
|
2823
|
+
preTokens: num(meta?.pre_tokens) ?? num(meta?.preTokens),
|
|
2824
|
+
postTokens: num(meta?.post_tokens) ?? num(meta?.postTokens)
|
|
2825
|
+
};
|
|
2658
2826
|
}
|
|
2659
|
-
function
|
|
2660
|
-
|
|
2827
|
+
function formatCompactBoundaryNote(boundary) {
|
|
2828
|
+
const how = boundary.trigger === "auto" ? "on its own" : `on a ${boundary.trigger} request`;
|
|
2829
|
+
const sizes = boundary.preTokens !== void 0 && boundary.postTokens !== void 0 ? ` (${boundary.preTokens.toLocaleString("en-US")} tokens to ${boundary.postTokens.toLocaleString("en-US")})` : "";
|
|
2830
|
+
return `
|
|
2831
|
+
${COMPACT_BOUNDARY_MARKER} Claude Code compacted its own context ${how}${sizes}. Earlier detail in this conversation is now a summary.
|
|
2832
|
+
`;
|
|
2833
|
+
}
|
|
2834
|
+
function reportCompactBoundary(msg) {
|
|
2835
|
+
const boundary = parseCompactBoundary(msg);
|
|
2836
|
+
if (!boundary) return null;
|
|
2837
|
+
log.notice("claude code compacted its own context", {
|
|
2838
|
+
trigger: boundary.trigger,
|
|
2839
|
+
preTokens: boundary.preTokens ?? null,
|
|
2840
|
+
postTokens: boundary.postTokens ?? null
|
|
2841
|
+
});
|
|
2842
|
+
return formatCompactBoundaryNote(boundary);
|
|
2843
|
+
}
|
|
2844
|
+
var RESULT_ERROR_MARKER = "\u258C **claude code error:**";
|
|
2845
|
+
var RESULT_SUBTYPES = {
|
|
2846
|
+
error_max_turns: "it hit its internal turn limit before finishing",
|
|
2847
|
+
error_during_execution: "it failed while running the turn",
|
|
2848
|
+
error_max_budget_usd: "it hit the configured spend limit for the turn",
|
|
2849
|
+
error_max_structured_output_retries: "it could not produce valid structured output"
|
|
2850
|
+
};
|
|
2851
|
+
function describeResultFailure(msg) {
|
|
2852
|
+
if (msg.type !== "result") return null;
|
|
2853
|
+
const subtype = msg.subtype;
|
|
2854
|
+
if (!subtype || subtype === "success") return null;
|
|
2855
|
+
const explanation = RESULT_SUBTYPES[subtype];
|
|
2856
|
+
return explanation ? `Claude Code ended the turn with \`${subtype}\`: ${explanation}.` : `Claude Code ended the turn with \`${subtype}\`.`;
|
|
2857
|
+
}
|
|
2858
|
+
function formatResultFailureNote(message) {
|
|
2859
|
+
return `
|
|
2860
|
+
${RESULT_ERROR_MARKER} ${message}
|
|
2861
|
+
`;
|
|
2862
|
+
}
|
|
2863
|
+
|
|
2864
|
+
// src/startup-diagnostics.ts
|
|
2865
|
+
import { execFile as execFile2 } from "child_process";
|
|
2866
|
+
import * as fs4 from "fs";
|
|
2867
|
+
import * as path4 from "path";
|
|
2868
|
+
import { promisify as promisify2 } from "util";
|
|
2869
|
+
import { fileURLToPath } from "url";
|
|
2870
|
+
|
|
2871
|
+
// src/mcp-bridge.ts
|
|
2872
|
+
import * as fs3 from "fs";
|
|
2873
|
+
import * as path3 from "path";
|
|
2874
|
+
import * as os2 from "os";
|
|
2875
|
+
import * as crypto2 from "crypto";
|
|
2876
|
+
import {
|
|
2877
|
+
parse as parseJsonc,
|
|
2878
|
+
printParseErrorCode
|
|
2879
|
+
} from "jsonc-parser";
|
|
2880
|
+
var FILE_NAMES = ["opencode.jsonc", "opencode.json", "config.json"];
|
|
2881
|
+
var PROJECT_FILE_NAMES = ["opencode.json", "opencode.jsonc"];
|
|
2882
|
+
function fileExists(p) {
|
|
2661
2883
|
try {
|
|
2662
|
-
|
|
2884
|
+
return fs3.statSync(p).isFile();
|
|
2663
2885
|
} catch {
|
|
2664
|
-
|
|
2886
|
+
return false;
|
|
2665
2887
|
}
|
|
2666
|
-
return clipWithMarker(raw, MAX_TOOL_INPUT_CHARS);
|
|
2667
2888
|
}
|
|
2668
|
-
function
|
|
2669
|
-
|
|
2670
|
-
|
|
2671
|
-
|
|
2672
|
-
return
|
|
2673
|
-
}
|
|
2674
|
-
if (!Array.isArray(msg.content)) {
|
|
2675
|
-
return { text: "", toolResultCount: 0 };
|
|
2676
|
-
}
|
|
2677
|
-
for (const part of msg.content) {
|
|
2678
|
-
if (!part) continue;
|
|
2679
|
-
switch (part.type) {
|
|
2680
|
-
case "text":
|
|
2681
|
-
if (part.text) lines.push(part.text);
|
|
2682
|
-
break;
|
|
2683
|
-
case "tool-call":
|
|
2684
|
-
lines.push(
|
|
2685
|
-
`[tool_use:${part.toolName ?? "unknown"}(${renderToolInput(part.input)})]`
|
|
2686
|
-
);
|
|
2687
|
-
break;
|
|
2688
|
-
case "tool-result":
|
|
2689
|
-
toolResultCount++;
|
|
2690
|
-
lines.push(
|
|
2691
|
-
`[tool_result:${part.toolName ?? part.toolCallId ?? "unknown"}]
|
|
2692
|
-
${clipWithMarker(
|
|
2693
|
-
getToolResultText(part),
|
|
2694
|
-
MAX_TOOL_RESULT_CHARS
|
|
2695
|
-
)}`
|
|
2696
|
-
);
|
|
2697
|
-
break;
|
|
2698
|
-
case "image":
|
|
2699
|
-
lines.push(
|
|
2700
|
-
`[image: ${part.mediaType ?? part.mimeType ?? "unknown"}]`
|
|
2701
|
-
);
|
|
2702
|
-
break;
|
|
2703
|
-
case "file":
|
|
2704
|
-
lines.push(
|
|
2705
|
-
`[file: ${part.mediaType ?? part.mimeType ?? "unknown"}]`
|
|
2706
|
-
);
|
|
2707
|
-
break;
|
|
2708
|
-
case "reasoning":
|
|
2709
|
-
break;
|
|
2710
|
-
}
|
|
2889
|
+
function dirExists(p) {
|
|
2890
|
+
try {
|
|
2891
|
+
return fs3.statSync(p).isDirectory();
|
|
2892
|
+
} catch {
|
|
2893
|
+
return false;
|
|
2711
2894
|
}
|
|
2712
|
-
return { text: lines.join("\n"), toolResultCount };
|
|
2713
2895
|
}
|
|
2714
|
-
function
|
|
2715
|
-
|
|
2716
|
-
|
|
2717
|
-
|
|
2718
|
-
|
|
2719
|
-
|
|
2720
|
-
|
|
2721
|
-
|
|
2722
|
-
|
|
2723
|
-
|
|
2724
|
-
return null;
|
|
2725
|
-
}
|
|
2726
|
-
const historyParts = [];
|
|
2727
|
-
for (let i = 0; i < conversationMessages.length - 1; i++) {
|
|
2728
|
-
const msg = conversationMessages[i];
|
|
2729
|
-
const role = msg.role === "user" ? "User" : msg.role === "assistant" ? "Assistant" : "Tool";
|
|
2730
|
-
const { text } = renderMessageContentForCompaction(msg);
|
|
2731
|
-
if (text.trim()) {
|
|
2732
|
-
const truncated = text.length > 2e3 ? text.slice(0, 2e3) + "..." : text;
|
|
2733
|
-
historyParts.push(`${role}: ${truncated}`);
|
|
2896
|
+
function readAndParse(file) {
|
|
2897
|
+
try {
|
|
2898
|
+
const raw = fs3.readFileSync(file, "utf8");
|
|
2899
|
+
const errors = [];
|
|
2900
|
+
const parsed = parseJsonc(raw, errors, { allowTrailingComma: true });
|
|
2901
|
+
if (errors.length > 0) {
|
|
2902
|
+
const first = errors[0];
|
|
2903
|
+
throw new Error(
|
|
2904
|
+
`${printParseErrorCode(first.error)} at offset ${first.offset}`
|
|
2905
|
+
);
|
|
2734
2906
|
}
|
|
2735
|
-
|
|
2736
|
-
|
|
2907
|
+
return parsed;
|
|
2908
|
+
} catch (e) {
|
|
2909
|
+
log.warn("failed to parse opencode config", {
|
|
2910
|
+
file,
|
|
2911
|
+
error: e instanceof Error ? e.message : String(e)
|
|
2912
|
+
});
|
|
2737
2913
|
return null;
|
|
2738
2914
|
}
|
|
2739
|
-
return historyParts.join("\n\n");
|
|
2740
2915
|
}
|
|
2741
|
-
function
|
|
2742
|
-
|
|
2743
|
-
|
|
2744
|
-
|
|
2745
|
-
|
|
2746
|
-
const
|
|
2747
|
-
|
|
2748
|
-
const
|
|
2749
|
-
|
|
2750
|
-
|
|
2751
|
-
|
|
2752
|
-
|
|
2753
|
-
if (total + entry.length > MAX_HISTORY_CHARS) {
|
|
2754
|
-
droppedOldest = i + 1;
|
|
2755
|
-
break;
|
|
2916
|
+
function isPlainObject(x) {
|
|
2917
|
+
return typeof x === "object" && x !== null && !Array.isArray(x);
|
|
2918
|
+
}
|
|
2919
|
+
function deepMerge(target, source) {
|
|
2920
|
+
const out = { ...target };
|
|
2921
|
+
for (const [k, v] of Object.entries(source)) {
|
|
2922
|
+
if (v === void 0) continue;
|
|
2923
|
+
const existing = out[k];
|
|
2924
|
+
if (isPlainObject(existing) && isPlainObject(v)) {
|
|
2925
|
+
out[k] = deepMerge(existing, v);
|
|
2926
|
+
} else {
|
|
2927
|
+
out[k] = v;
|
|
2756
2928
|
}
|
|
2757
|
-
entries.push(entry);
|
|
2758
|
-
total += entry.length + 2;
|
|
2759
|
-
totalToolResults += toolResultCount;
|
|
2760
2929
|
}
|
|
2761
|
-
|
|
2762
|
-
entries.reverse();
|
|
2763
|
-
log.info("built compaction history", {
|
|
2764
|
-
entries: entries.length,
|
|
2765
|
-
chars: total,
|
|
2766
|
-
toolResults: totalToolResults,
|
|
2767
|
-
droppedOldestBefore: droppedOldest
|
|
2768
|
-
});
|
|
2769
|
-
return entries.join("\n\n");
|
|
2930
|
+
return out;
|
|
2770
2931
|
}
|
|
2771
|
-
function
|
|
2772
|
-
const
|
|
2773
|
-
|
|
2774
|
-
|
|
2775
|
-
|
|
2776
|
-
|
|
2777
|
-
|
|
2778
|
-
if (!cliToolCallIds || cliToolCallIds.has(id)) {
|
|
2779
|
-
content.push({ type: "tool_result", tool_use_id: id, content: text });
|
|
2780
|
-
return;
|
|
2781
|
-
}
|
|
2782
|
-
log.info("rendering opencode-side tool result as text", {
|
|
2783
|
-
toolCallId: id,
|
|
2784
|
-
toolName: part.toolName,
|
|
2785
|
-
chars: text.length
|
|
2786
|
-
});
|
|
2787
|
-
content.push({
|
|
2788
|
-
type: "text",
|
|
2789
|
-
text: `<opencode_tool_result tool="${part.toolName ?? "unknown"}">
|
|
2790
|
-
${text}
|
|
2791
|
-
</opencode_tool_result>`
|
|
2792
|
-
});
|
|
2793
|
-
};
|
|
2794
|
-
if (compactionMode) {
|
|
2795
|
-
const transcript = compactConversationHistory(prompt, {
|
|
2796
|
-
mode: "compaction"
|
|
2797
|
-
});
|
|
2798
|
-
if (transcript) {
|
|
2799
|
-
log.info("including compaction transcript", {
|
|
2800
|
-
historyLength: transcript.length
|
|
2801
|
-
});
|
|
2802
|
-
content.push({
|
|
2803
|
-
type: "text",
|
|
2804
|
-
text: `<conversation_transcript>
|
|
2805
|
-
${transcript}
|
|
2806
|
-
</conversation_transcript>
|
|
2807
|
-
|
|
2808
|
-
The complete prior conversation appears above. The synthesis instructions follow below.
|
|
2809
|
-
|
|
2810
|
-
`
|
|
2811
|
-
});
|
|
2932
|
+
function walkUp(opts) {
|
|
2933
|
+
const out = [];
|
|
2934
|
+
let current = path3.resolve(opts.start);
|
|
2935
|
+
while (true) {
|
|
2936
|
+
for (const target of opts.targets) {
|
|
2937
|
+
const candidate = path3.join(current, target);
|
|
2938
|
+
if (opts.predicate(candidate)) out.push(candidate);
|
|
2812
2939
|
}
|
|
2813
|
-
|
|
2814
|
-
const
|
|
2815
|
-
if (
|
|
2816
|
-
|
|
2817
|
-
|
|
2818
|
-
|
|
2819
|
-
|
|
2820
|
-
|
|
2821
|
-
|
|
2822
|
-
|
|
2823
|
-
|
|
2824
|
-
|
|
2825
|
-
|
|
2826
|
-
|
|
2827
|
-
|
|
2828
|
-
|
|
2829
|
-
|
|
2830
|
-
`
|
|
2831
|
-
});
|
|
2940
|
+
if (opts.stop && current === path3.resolve(opts.stop)) break;
|
|
2941
|
+
const parent = path3.dirname(current);
|
|
2942
|
+
if (parent === current) break;
|
|
2943
|
+
current = parent;
|
|
2944
|
+
}
|
|
2945
|
+
return out;
|
|
2946
|
+
}
|
|
2947
|
+
function detectWorktree(cwd) {
|
|
2948
|
+
const override = process.env.OPENCODE_WORKTREE;
|
|
2949
|
+
if (override) return path3.resolve(override);
|
|
2950
|
+
let current = path3.resolve(cwd);
|
|
2951
|
+
while (true) {
|
|
2952
|
+
const gitPath = path3.join(current, ".git");
|
|
2953
|
+
try {
|
|
2954
|
+
if (fs3.existsSync(gitPath)) return current;
|
|
2955
|
+
} catch {
|
|
2832
2956
|
}
|
|
2957
|
+
const parent = path3.dirname(current);
|
|
2958
|
+
if (parent === current) return void 0;
|
|
2959
|
+
current = parent;
|
|
2833
2960
|
}
|
|
2834
|
-
|
|
2835
|
-
|
|
2836
|
-
|
|
2837
|
-
|
|
2961
|
+
}
|
|
2962
|
+
function globalConfigDir() {
|
|
2963
|
+
const xdg = process.env.XDG_CONFIG_HOME ?? path3.join(os2.homedir(), ".config");
|
|
2964
|
+
return path3.join(xdg, "opencode");
|
|
2965
|
+
}
|
|
2966
|
+
function loadGlobalConfig() {
|
|
2967
|
+
const dir = globalConfigDir();
|
|
2968
|
+
let merged = {};
|
|
2969
|
+
for (const name of FILE_NAMES.slice().reverse()) {
|
|
2970
|
+
const file = path3.join(dir, name);
|
|
2971
|
+
if (!fileExists(file)) continue;
|
|
2972
|
+
const parsed = readAndParse(file);
|
|
2973
|
+
if (parsed) merged = deepMerge(merged, parsed);
|
|
2838
2974
|
}
|
|
2839
|
-
|
|
2840
|
-
|
|
2841
|
-
|
|
2842
|
-
|
|
2843
|
-
|
|
2844
|
-
|
|
2845
|
-
|
|
2846
|
-
|
|
2847
|
-
|
|
2848
|
-
|
|
2849
|
-
|
|
2850
|
-
|
|
2851
|
-
|
|
2852
|
-
|
|
2853
|
-
|
|
2854
|
-
|
|
2855
|
-
|
|
2856
|
-
|
|
2857
|
-
|
|
2858
|
-
|
|
2859
|
-
mediaType: part.mediaType
|
|
2860
|
-
});
|
|
2861
|
-
}
|
|
2862
|
-
} else if (part.type === "tool-result") {
|
|
2863
|
-
pushToolResult(part);
|
|
2864
|
-
}
|
|
2865
|
-
}
|
|
2866
|
-
}
|
|
2867
|
-
} else if (msg.role === "tool") {
|
|
2868
|
-
if (Array.isArray(msg.content)) {
|
|
2869
|
-
for (const part of msg.content) {
|
|
2870
|
-
if (part?.type === "tool-result") {
|
|
2871
|
-
pushToolResult(part);
|
|
2872
|
-
}
|
|
2873
|
-
}
|
|
2874
|
-
}
|
|
2975
|
+
return merged;
|
|
2976
|
+
}
|
|
2977
|
+
function loadProjectFilesInDir(dir) {
|
|
2978
|
+
let merged = {};
|
|
2979
|
+
for (const name of PROJECT_FILE_NAMES) {
|
|
2980
|
+
const file = path3.join(dir, name);
|
|
2981
|
+
if (!fileExists(file)) continue;
|
|
2982
|
+
const parsed = readAndParse(file);
|
|
2983
|
+
if (parsed) merged = deepMerge(merged, parsed);
|
|
2984
|
+
}
|
|
2985
|
+
return merged;
|
|
2986
|
+
}
|
|
2987
|
+
function dotOpencodeDirs(cwd, worktree) {
|
|
2988
|
+
const dirs = [];
|
|
2989
|
+
const seen = /* @__PURE__ */ new Set();
|
|
2990
|
+
const push = (p) => {
|
|
2991
|
+
const abs = path3.resolve(p);
|
|
2992
|
+
if (!seen.has(abs) && dirExists(abs)) {
|
|
2993
|
+
seen.add(abs);
|
|
2994
|
+
dirs.push(abs);
|
|
2875
2995
|
}
|
|
2996
|
+
};
|
|
2997
|
+
for (const dir of walkUp({
|
|
2998
|
+
start: cwd,
|
|
2999
|
+
stop: worktree,
|
|
3000
|
+
targets: [".opencode"],
|
|
3001
|
+
predicate: dirExists
|
|
3002
|
+
})) {
|
|
3003
|
+
push(dir);
|
|
2876
3004
|
}
|
|
2877
|
-
|
|
2878
|
-
|
|
2879
|
-
|
|
2880
|
-
|
|
2881
|
-
|
|
2882
|
-
|
|
2883
|
-
|
|
2884
|
-
|
|
3005
|
+
const home = os2.homedir();
|
|
3006
|
+
if (home) {
|
|
3007
|
+
const homeDot = path3.join(home, ".opencode");
|
|
3008
|
+
if (dirExists(homeDot)) push(homeDot);
|
|
3009
|
+
}
|
|
3010
|
+
const envDir = process.env.OPENCODE_CONFIG_DIR;
|
|
3011
|
+
if (envDir && dirExists(envDir)) push(envDir);
|
|
3012
|
+
return dirs;
|
|
3013
|
+
}
|
|
3014
|
+
function substituteEnvPlaceholders(source) {
|
|
3015
|
+
const out = {};
|
|
3016
|
+
for (const [k, v] of Object.entries(source)) {
|
|
3017
|
+
if (typeof v !== "string") continue;
|
|
3018
|
+
out[k] = v.replace(/\{env:([A-Za-z_][A-Za-z0-9_]*)\}/g, (_match, name) => {
|
|
3019
|
+
const resolved = process.env[name];
|
|
3020
|
+
return typeof resolved === "string" ? resolved : "";
|
|
2885
3021
|
});
|
|
2886
3022
|
}
|
|
2887
|
-
return
|
|
2888
|
-
|
|
2889
|
-
|
|
2890
|
-
|
|
2891
|
-
|
|
3023
|
+
return out;
|
|
3024
|
+
}
|
|
3025
|
+
function translateServer(name, spec) {
|
|
3026
|
+
if (spec.enabled === false) return null;
|
|
3027
|
+
const type = spec.type;
|
|
3028
|
+
if (type === "local") {
|
|
3029
|
+
const cmd = spec.command;
|
|
3030
|
+
if (!Array.isArray(cmd) || cmd.length === 0) {
|
|
3031
|
+
log.warn("skipping local MCP server with no command", { name });
|
|
3032
|
+
return null;
|
|
3033
|
+
}
|
|
3034
|
+
const out = {
|
|
3035
|
+
type: "stdio",
|
|
3036
|
+
command: String(cmd[0])
|
|
3037
|
+
};
|
|
3038
|
+
if (cmd.length > 1) out.args = cmd.slice(1).map((s) => String(s));
|
|
3039
|
+
if (spec.environment && typeof spec.environment === "object") {
|
|
3040
|
+
out.env = substituteEnvPlaceholders(
|
|
3041
|
+
spec.environment
|
|
3042
|
+
);
|
|
3043
|
+
}
|
|
3044
|
+
return out;
|
|
3045
|
+
}
|
|
3046
|
+
if (type === "remote") {
|
|
3047
|
+
if (typeof spec.url !== "string" || !spec.url) {
|
|
3048
|
+
log.warn("skipping remote MCP server with no url", { name });
|
|
3049
|
+
return null;
|
|
3050
|
+
}
|
|
3051
|
+
const out = {
|
|
3052
|
+
type: "http",
|
|
3053
|
+
url: spec.url
|
|
3054
|
+
};
|
|
3055
|
+
if (spec.headers && typeof spec.headers === "object") {
|
|
3056
|
+
out.headers = substituteEnvPlaceholders(
|
|
3057
|
+
spec.headers
|
|
3058
|
+
);
|
|
2892
3059
|
}
|
|
3060
|
+
return out;
|
|
3061
|
+
}
|
|
3062
|
+
log.warn("skipping MCP server with unknown type", {
|
|
3063
|
+
name,
|
|
3064
|
+
type: type ?? null
|
|
2893
3065
|
});
|
|
3066
|
+
return null;
|
|
2894
3067
|
}
|
|
2895
|
-
|
|
2896
|
-
|
|
2897
|
-
|
|
2898
|
-
|
|
2899
|
-
|
|
2900
|
-
// src/models.ts
|
|
2901
|
-
var PROVIDER_ID = "claude-code";
|
|
2902
|
-
var NPM = "@khalilgharbaoui/opencode-claude-code-plugin";
|
|
2903
|
-
var reasoningVariants = {
|
|
2904
|
-
low: { reasoningEffort: "low" },
|
|
2905
|
-
medium: { reasoningEffort: "medium" },
|
|
2906
|
-
high: { reasoningEffort: "high" },
|
|
2907
|
-
xhigh: { reasoningEffort: "xhigh" },
|
|
2908
|
-
max: { reasoningEffort: "max" }
|
|
2909
|
-
};
|
|
2910
|
-
var baseCapabilities = {
|
|
2911
|
-
temperature: false,
|
|
2912
|
-
attachment: true,
|
|
2913
|
-
toolcall: true,
|
|
2914
|
-
input: { text: true, audio: false, image: true, video: false, pdf: false },
|
|
2915
|
-
output: { text: true, audio: false, image: false, video: false, pdf: false },
|
|
2916
|
-
interleaved: false
|
|
2917
|
-
};
|
|
2918
|
-
function defineModel(opts) {
|
|
2919
|
-
return {
|
|
2920
|
-
id: opts.id,
|
|
2921
|
-
providerID: PROVIDER_ID,
|
|
2922
|
-
api: { id: opts.id, url: "", npm: NPM },
|
|
2923
|
-
name: `${opts.name} (${opts.multiplier}\xD7)`,
|
|
2924
|
-
family: opts.family,
|
|
2925
|
-
capabilities: { ...baseCapabilities, reasoning: opts.reasoning },
|
|
2926
|
-
cost: {
|
|
2927
|
-
input: opts.cost.input,
|
|
2928
|
-
output: opts.cost.output,
|
|
2929
|
-
cache: { read: opts.cost.cacheRead, write: opts.cost.cacheWrite }
|
|
2930
|
-
},
|
|
2931
|
-
limit: { context: opts.context, output: opts.output },
|
|
2932
|
-
status: opts.status ?? "active",
|
|
2933
|
-
options: {},
|
|
2934
|
-
headers: {},
|
|
2935
|
-
release_date: opts.releaseDate,
|
|
2936
|
-
variants: opts.reasoning ? reasoningVariants : void 0
|
|
2937
|
-
};
|
|
3068
|
+
function extractMcpBlock(config) {
|
|
3069
|
+
const mcp = config.mcp;
|
|
3070
|
+
if (!mcp || typeof mcp !== "object" || Array.isArray(mcp)) return {};
|
|
3071
|
+
return mcp;
|
|
2938
3072
|
}
|
|
2939
|
-
|
|
2940
|
-
|
|
2941
|
-
|
|
2942
|
-
|
|
2943
|
-
|
|
2944
|
-
|
|
2945
|
-
|
|
2946
|
-
|
|
2947
|
-
|
|
2948
|
-
|
|
2949
|
-
|
|
3073
|
+
function mergeMcp(target, source) {
|
|
3074
|
+
const out = { ...target };
|
|
3075
|
+
for (const [name, spec] of Object.entries(source)) {
|
|
3076
|
+
if (!spec || typeof spec !== "object") continue;
|
|
3077
|
+
const existing = out[name];
|
|
3078
|
+
if (existing && typeof existing === "object") {
|
|
3079
|
+
out[name] = deepMerge(
|
|
3080
|
+
existing,
|
|
3081
|
+
spec
|
|
3082
|
+
);
|
|
3083
|
+
} else {
|
|
3084
|
+
out[name] = spec;
|
|
3085
|
+
}
|
|
2950
3086
|
}
|
|
2951
|
-
|
|
2952
|
-
|
|
3087
|
+
return out;
|
|
3088
|
+
}
|
|
3089
|
+
function bridgeOpencodeMcp(cwd, runtimeStatus, excludeServers) {
|
|
3090
|
+
const {
|
|
3091
|
+
servers: merged,
|
|
3092
|
+
enabledServerNames: allEnabledServerNames,
|
|
3093
|
+
hash
|
|
3094
|
+
} = mergeOpencodeMcp(cwd, runtimeStatus);
|
|
3095
|
+
const servers = {};
|
|
3096
|
+
const bridgedServerNames = [];
|
|
3097
|
+
for (const [name, spec] of Object.entries(merged)) {
|
|
3098
|
+
if (!spec || typeof spec !== "object") continue;
|
|
3099
|
+
if (excludeServers?.has(name)) continue;
|
|
3100
|
+
const translated = translateServer(name, spec);
|
|
3101
|
+
if (translated) {
|
|
3102
|
+
servers[name] = translated;
|
|
3103
|
+
bridgedServerNames.push(name);
|
|
3104
|
+
}
|
|
3105
|
+
}
|
|
3106
|
+
return finishBridge({
|
|
3107
|
+
servers,
|
|
3108
|
+
bridgedServerNames,
|
|
3109
|
+
allEnabledServerNames,
|
|
3110
|
+
hash,
|
|
3111
|
+
excludeServers
|
|
3112
|
+
});
|
|
3113
|
+
}
|
|
3114
|
+
function mergeOpencodeMcp(cwd, runtimeStatus) {
|
|
3115
|
+
const worktree = detectWorktree(cwd);
|
|
3116
|
+
let merged = {};
|
|
3117
|
+
merged = mergeMcp(merged, extractMcpBlock(loadGlobalConfig()));
|
|
3118
|
+
const explicitConfig = process.env.OPENCODE_CONFIG;
|
|
3119
|
+
if (explicitConfig && fileExists(explicitConfig)) {
|
|
3120
|
+
const parsed = readAndParse(explicitConfig);
|
|
3121
|
+
if (parsed) merged = mergeMcp(merged, extractMcpBlock(parsed));
|
|
3122
|
+
}
|
|
3123
|
+
const projectFiles = walkUp({
|
|
3124
|
+
start: cwd,
|
|
3125
|
+
stop: worktree,
|
|
3126
|
+
targets: PROJECT_FILE_NAMES,
|
|
3127
|
+
predicate: fileExists
|
|
3128
|
+
});
|
|
3129
|
+
const projectDirs = [];
|
|
3130
|
+
const seenProjectDirs = /* @__PURE__ */ new Set();
|
|
3131
|
+
for (const f of projectFiles) {
|
|
3132
|
+
const d = path3.dirname(f);
|
|
3133
|
+
if (!seenProjectDirs.has(d)) {
|
|
3134
|
+
seenProjectDirs.add(d);
|
|
3135
|
+
projectDirs.push(d);
|
|
3136
|
+
}
|
|
3137
|
+
}
|
|
3138
|
+
for (const dir of projectDirs.slice().reverse()) {
|
|
3139
|
+
merged = mergeMcp(merged, extractMcpBlock(loadProjectFilesInDir(dir)));
|
|
3140
|
+
}
|
|
3141
|
+
for (const dir of dotOpencodeDirs(cwd, worktree)) {
|
|
3142
|
+
merged = mergeMcp(merged, extractMcpBlock(loadProjectFilesInDir(dir)));
|
|
3143
|
+
}
|
|
3144
|
+
if (runtimeStatus) {
|
|
3145
|
+
for (const name of Object.keys(merged)) {
|
|
3146
|
+
const status = runtimeStatus[name];
|
|
3147
|
+
if (status === void 0) continue;
|
|
3148
|
+
const existing = merged[name];
|
|
3149
|
+
const base = existing && typeof existing === "object" ? existing : {};
|
|
3150
|
+
merged[name] = { ...base, enabled: status === "connected" };
|
|
3151
|
+
}
|
|
2953
3152
|
}
|
|
3153
|
+
const enabledServerNames = [];
|
|
3154
|
+
for (const [name, spec] of Object.entries(merged)) {
|
|
3155
|
+
if (!spec || typeof spec !== "object") continue;
|
|
3156
|
+
const enabled = spec.enabled;
|
|
3157
|
+
if (enabled === false) continue;
|
|
3158
|
+
enabledServerNames.push(name);
|
|
3159
|
+
}
|
|
3160
|
+
const mergedBody = JSON.stringify({ mcpServers: merged }, null, 2);
|
|
3161
|
+
const hash = crypto2.createHash("sha256").update(mergedBody).digest("hex").slice(0, 12);
|
|
3162
|
+
return { servers: merged, enabledServerNames, hash };
|
|
3163
|
+
}
|
|
3164
|
+
function finishBridge(input) {
|
|
3165
|
+
const { servers, bridgedServerNames, allEnabledServerNames, hash, excludeServers } = input;
|
|
3166
|
+
if (Object.keys(servers).length === 0) {
|
|
3167
|
+
const allEnabledServersExcluded = excludeServers && allEnabledServerNames.length > 0 && allEnabledServerNames.every((name) => excludeServers.has(name));
|
|
3168
|
+
if (!allEnabledServersExcluded) return null;
|
|
3169
|
+
return {
|
|
3170
|
+
path: "",
|
|
3171
|
+
hash,
|
|
3172
|
+
serverNames: [],
|
|
3173
|
+
allEnabledServerNames
|
|
3174
|
+
};
|
|
3175
|
+
}
|
|
3176
|
+
const body = JSON.stringify({ mcpServers: servers }, null, 2);
|
|
3177
|
+
const outPath = path3.join(
|
|
3178
|
+
pluginTmpDir(),
|
|
3179
|
+
`mcp-${hash}.json`
|
|
3180
|
+
);
|
|
3181
|
+
try {
|
|
3182
|
+
if (!fileExists(outPath)) {
|
|
3183
|
+
fs3.writeFileSync(outPath, body, { encoding: "utf8", mode: 384 });
|
|
3184
|
+
}
|
|
3185
|
+
} catch (e) {
|
|
3186
|
+
log.warn("failed to write bridged MCP config", {
|
|
3187
|
+
error: e instanceof Error ? e.message : String(e)
|
|
3188
|
+
});
|
|
3189
|
+
return null;
|
|
3190
|
+
}
|
|
3191
|
+
log.info("bridged opencode MCP config", {
|
|
3192
|
+
target: outPath,
|
|
3193
|
+
hash,
|
|
3194
|
+
servers: bridgedServerNames,
|
|
3195
|
+
excluded: excludeServers ? Array.from(excludeServers) : []
|
|
3196
|
+
});
|
|
2954
3197
|
return {
|
|
2955
|
-
|
|
2956
|
-
|
|
2957
|
-
|
|
2958
|
-
|
|
2959
|
-
release_date: model.release_date,
|
|
2960
|
-
temperature: model.capabilities.temperature,
|
|
2961
|
-
reasoning: model.capabilities.reasoning,
|
|
2962
|
-
attachment: model.capabilities.attachment,
|
|
2963
|
-
tool_call: model.capabilities.toolcall,
|
|
2964
|
-
modalities: { input: inputMods, output: outputMods },
|
|
2965
|
-
cost: {
|
|
2966
|
-
input: model.cost.input,
|
|
2967
|
-
output: model.cost.output,
|
|
2968
|
-
cache_read: model.cost.cache.read,
|
|
2969
|
-
cache_write: model.cost.cache.write
|
|
2970
|
-
},
|
|
2971
|
-
limit: model.limit,
|
|
2972
|
-
options: model.options,
|
|
2973
|
-
headers: model.headers,
|
|
2974
|
-
variants: model.variants
|
|
3198
|
+
path: outPath,
|
|
3199
|
+
hash,
|
|
3200
|
+
serverNames: bridgedServerNames,
|
|
3201
|
+
allEnabledServerNames
|
|
2975
3202
|
};
|
|
2976
3203
|
}
|
|
2977
|
-
var defaultModels = {
|
|
2978
|
-
"claude-haiku-4-5": defineModel({
|
|
2979
|
-
id: "claude-haiku-4-5",
|
|
2980
|
-
name: "Claude Haiku 4.5",
|
|
2981
|
-
family: "haiku",
|
|
2982
|
-
reasoning: false,
|
|
2983
|
-
context: 2e5,
|
|
2984
|
-
output: 64e3,
|
|
2985
|
-
cost: haikuCost,
|
|
2986
|
-
multiplier: 1,
|
|
2987
|
-
releaseDate: "2025-10-01"
|
|
2988
|
-
}),
|
|
2989
|
-
"claude-sonnet-4-5": defineModel({
|
|
2990
|
-
id: "claude-sonnet-4-5",
|
|
2991
|
-
name: "Claude Sonnet 4.5",
|
|
2992
|
-
family: "sonnet",
|
|
2993
|
-
reasoning: true,
|
|
2994
|
-
context: 2e5,
|
|
2995
|
-
output: 64e3,
|
|
2996
|
-
cost: sonnetCost,
|
|
2997
|
-
multiplier: 3,
|
|
2998
|
-
releaseDate: "2025-09-29"
|
|
2999
|
-
}),
|
|
3000
|
-
"claude-sonnet-4-6": defineModel({
|
|
3001
|
-
id: "claude-sonnet-4-6",
|
|
3002
|
-
name: "Claude Sonnet 4.6",
|
|
3003
|
-
family: "sonnet",
|
|
3004
|
-
reasoning: true,
|
|
3005
|
-
context: 1e6,
|
|
3006
|
-
output: 128e3,
|
|
3007
|
-
cost: sonnetCost,
|
|
3008
|
-
multiplier: 3,
|
|
3009
|
-
releaseDate: "2025-06-19"
|
|
3010
|
-
}),
|
|
3011
|
-
"claude-sonnet-5": defineModel({
|
|
3012
|
-
id: "claude-sonnet-5",
|
|
3013
|
-
name: "Claude Sonnet 5",
|
|
3014
|
-
family: "sonnet",
|
|
3015
|
-
reasoning: true,
|
|
3016
|
-
context: 1e6,
|
|
3017
|
-
output: 128e3,
|
|
3018
|
-
cost: sonnetCost,
|
|
3019
|
-
multiplier: 3,
|
|
3020
|
-
releaseDate: "2026-06-30"
|
|
3021
|
-
}),
|
|
3022
|
-
"claude-opus-4-5": defineModel({
|
|
3023
|
-
id: "claude-opus-4-5",
|
|
3024
|
-
name: "Claude Opus 4.5",
|
|
3025
|
-
family: "opus",
|
|
3026
|
-
reasoning: true,
|
|
3027
|
-
context: 2e5,
|
|
3028
|
-
output: 64e3,
|
|
3029
|
-
cost: opusCost,
|
|
3030
|
-
multiplier: 5,
|
|
3031
|
-
releaseDate: "2025-11-01"
|
|
3032
|
-
}),
|
|
3033
|
-
"claude-opus-4-6": defineModel({
|
|
3034
|
-
id: "claude-opus-4-6",
|
|
3035
|
-
name: "Claude Opus 4.6",
|
|
3036
|
-
family: "opus",
|
|
3037
|
-
reasoning: true,
|
|
3038
|
-
context: 1e6,
|
|
3039
|
-
output: 128e3,
|
|
3040
|
-
cost: opusCost,
|
|
3041
|
-
multiplier: 5,
|
|
3042
|
-
releaseDate: "2025-06-19"
|
|
3043
|
-
}),
|
|
3044
|
-
"claude-opus-4-7": defineModel({
|
|
3045
|
-
id: "claude-opus-4-7",
|
|
3046
|
-
name: "Claude Opus 4.7",
|
|
3047
|
-
family: "opus",
|
|
3048
|
-
reasoning: true,
|
|
3049
|
-
context: 1e6,
|
|
3050
|
-
output: 128e3,
|
|
3051
|
-
cost: opusCost,
|
|
3052
|
-
multiplier: 5,
|
|
3053
|
-
releaseDate: "2025-07-16"
|
|
3054
|
-
}),
|
|
3055
|
-
"claude-opus-4-8": defineModel({
|
|
3056
|
-
id: "claude-opus-4-8",
|
|
3057
|
-
name: "Claude Opus 4.8",
|
|
3058
|
-
family: "opus",
|
|
3059
|
-
reasoning: true,
|
|
3060
|
-
context: 1e6,
|
|
3061
|
-
output: 128e3,
|
|
3062
|
-
cost: opusCost,
|
|
3063
|
-
multiplier: 5,
|
|
3064
|
-
releaseDate: "2026-05-28"
|
|
3065
|
-
}),
|
|
3066
|
-
// Fast mode. The `-fast` suffix is OUR marker, not a model name Anthropic
|
|
3067
|
-
// serves: `parseModelId` strips it before `--model` and turns it into
|
|
3068
|
-
// `--settings {"fastMode":true}` on the spawn. Retired `-fast` model strings
|
|
3069
|
-
// (`claude-opus-4-6-fast`) are a different thing and are not registered here.
|
|
3070
|
-
//
|
|
3071
|
-
// Only Opus 4.8 and Opus 5 qualify: the CLI gates fast mode on the resolved
|
|
3072
|
-
// model name containing `opus-4-8` or `opus-5`, so registering a fast entry
|
|
3073
|
-
// for any other model would produce a picker option that silently runs at
|
|
3074
|
-
// standard speed while displaying the 10x price.
|
|
3075
|
-
"claude-opus-4-8-fast": defineModel({
|
|
3076
|
-
id: "claude-opus-4-8-fast",
|
|
3077
|
-
name: "Claude Opus 4.8 Fast",
|
|
3078
|
-
family: "opus",
|
|
3079
|
-
reasoning: true,
|
|
3080
|
-
context: 1e6,
|
|
3081
|
-
output: 128e3,
|
|
3082
|
-
cost: opusFastCost,
|
|
3083
|
-
multiplier: 10,
|
|
3084
|
-
releaseDate: "2026-05-28"
|
|
3085
|
-
}),
|
|
3086
|
-
"claude-opus-5": defineModel({
|
|
3087
|
-
id: "claude-opus-5",
|
|
3088
|
-
name: "Claude Opus 5",
|
|
3089
|
-
family: "opus",
|
|
3090
|
-
reasoning: true,
|
|
3091
|
-
context: 1e6,
|
|
3092
|
-
output: 128e3,
|
|
3093
|
-
cost: opusCost,
|
|
3094
|
-
multiplier: 5,
|
|
3095
|
-
releaseDate: "2026-07-24"
|
|
3096
|
-
}),
|
|
3097
|
-
"claude-opus-5-fast": defineModel({
|
|
3098
|
-
id: "claude-opus-5-fast",
|
|
3099
|
-
name: "Claude Opus 5 Fast",
|
|
3100
|
-
family: "opus",
|
|
3101
|
-
reasoning: true,
|
|
3102
|
-
context: 1e6,
|
|
3103
|
-
output: 128e3,
|
|
3104
|
-
cost: opusFastCost,
|
|
3105
|
-
multiplier: 10,
|
|
3106
|
-
releaseDate: "2026-07-24"
|
|
3107
|
-
}),
|
|
3108
|
-
"claude-fable-5": defineModel({
|
|
3109
|
-
id: "claude-fable-5",
|
|
3110
|
-
name: "Claude Fable 5",
|
|
3111
|
-
family: "fable",
|
|
3112
|
-
reasoning: true,
|
|
3113
|
-
context: 1e6,
|
|
3114
|
-
output: 128e3,
|
|
3115
|
-
cost: fableCost,
|
|
3116
|
-
multiplier: 10,
|
|
3117
|
-
releaseDate: "2026-06-09"
|
|
3118
|
-
}),
|
|
3119
|
-
"claude-fable-5-1": defineModel({
|
|
3120
|
-
id: "claude-fable-5-1",
|
|
3121
|
-
name: "Claude Fable 5.1",
|
|
3122
|
-
family: "fable",
|
|
3123
|
-
reasoning: true,
|
|
3124
|
-
context: 1e6,
|
|
3125
|
-
output: 128e3,
|
|
3126
|
-
cost: fable51Cost,
|
|
3127
|
-
multiplier: 10,
|
|
3128
|
-
releaseDate: "2026-09-01"
|
|
3129
|
-
}),
|
|
3130
|
-
// Mythos 5 and 5.1 share the corresponding Fable models' capabilities and
|
|
3131
|
-
// pricing without the safety classifiers; limited availability via Project
|
|
3132
|
-
// Glasswing. `claude --model` simply errors for accounts without access, so
|
|
3133
|
-
// they are safe to register unconditionally.
|
|
3134
|
-
"claude-mythos-5": defineModel({
|
|
3135
|
-
id: "claude-mythos-5",
|
|
3136
|
-
name: "Claude Mythos 5",
|
|
3137
|
-
family: "mythos",
|
|
3138
|
-
reasoning: true,
|
|
3139
|
-
context: 1e6,
|
|
3140
|
-
output: 128e3,
|
|
3141
|
-
cost: fableCost,
|
|
3142
|
-
multiplier: 10,
|
|
3143
|
-
releaseDate: "2026-06-09"
|
|
3144
|
-
}),
|
|
3145
|
-
"claude-mythos-5-1": defineModel({
|
|
3146
|
-
id: "claude-mythos-5-1",
|
|
3147
|
-
name: "Claude Mythos 5.1",
|
|
3148
|
-
family: "mythos",
|
|
3149
|
-
reasoning: true,
|
|
3150
|
-
context: 1e6,
|
|
3151
|
-
output: 128e3,
|
|
3152
|
-
cost: fable51Cost,
|
|
3153
|
-
multiplier: 10,
|
|
3154
|
-
releaseDate: "2026-09-01"
|
|
3155
|
-
})
|
|
3156
|
-
};
|
|
3157
|
-
var FAST_SUFFIX = "-fast";
|
|
3158
|
-
function parseModelId(modelId) {
|
|
3159
|
-
const at = modelId.indexOf("@");
|
|
3160
|
-
const base = at === -1 ? modelId : modelId.slice(0, at);
|
|
3161
|
-
const account = at === -1 ? "" : modelId.slice(at);
|
|
3162
|
-
if (!base.endsWith(FAST_SUFFIX)) return { model: modelId, fast: false };
|
|
3163
|
-
if (!Object.hasOwn(defaultModels, base)) return { model: modelId, fast: false };
|
|
3164
|
-
return { model: base.slice(0, -FAST_SUFFIX.length) + account, fast: true };
|
|
3165
|
-
}
|
|
3166
3204
|
|
|
3167
|
-
// src/
|
|
3168
|
-
var
|
|
3169
|
-
|
|
3170
|
-
"
|
|
3171
|
-
|
|
3172
|
-
|
|
3173
|
-
"high",
|
|
3174
|
-
"xhigh",
|
|
3175
|
-
"max"
|
|
3176
|
-
];
|
|
3177
|
-
var registry = {};
|
|
3178
|
-
var defaultSubagentModel;
|
|
3179
|
-
function setAgentRegistry(records) {
|
|
3180
|
-
registry = records;
|
|
3205
|
+
// src/runtime-status.ts
|
|
3206
|
+
var opencodeClient = null;
|
|
3207
|
+
function setOpencodeClient(client) {
|
|
3208
|
+
if (client && typeof client === "object") {
|
|
3209
|
+
opencodeClient = client;
|
|
3210
|
+
}
|
|
3181
3211
|
}
|
|
3182
|
-
function
|
|
3183
|
-
return
|
|
3212
|
+
function getOpencodeClient() {
|
|
3213
|
+
return opencodeClient;
|
|
3184
3214
|
}
|
|
3185
|
-
|
|
3186
|
-
|
|
3215
|
+
var opencodeProjectDirectory;
|
|
3216
|
+
function setOpencodeProjectDirectory(dir) {
|
|
3217
|
+
opencodeProjectDirectory = dir;
|
|
3187
3218
|
}
|
|
3188
|
-
function
|
|
3189
|
-
return
|
|
3219
|
+
function getOpencodeProjectDirectory() {
|
|
3220
|
+
return opencodeProjectDirectory;
|
|
3190
3221
|
}
|
|
3191
|
-
function
|
|
3192
|
-
|
|
3193
|
-
return at === -1 ? "" : modelId.slice(at);
|
|
3222
|
+
function isUsableDirectory(d) {
|
|
3223
|
+
return typeof d === "string" && d.length > 1 && d !== "/";
|
|
3194
3224
|
}
|
|
3195
|
-
function
|
|
3196
|
-
|
|
3197
|
-
|
|
3225
|
+
function resolveSpawnCwdFrom(configured, live, captured, sessionDir) {
|
|
3226
|
+
if (configured) return configured;
|
|
3227
|
+
if (isUsableDirectory(sessionDir)) return sessionDir;
|
|
3228
|
+
if (isUsableDirectory(live)) return live;
|
|
3229
|
+
return captured ?? live;
|
|
3198
3230
|
}
|
|
3199
|
-
function
|
|
3200
|
-
if (
|
|
3201
|
-
const
|
|
3202
|
-
|
|
3203
|
-
|
|
3204
|
-
|
|
3205
|
-
|
|
3206
|
-
|
|
3207
|
-
|
|
3208
|
-
const base = withoutAccountMarker(wanted);
|
|
3209
|
-
if (!Object.hasOwn(defaultModels, base)) {
|
|
3210
|
-
log.warn("agent model override refused: unknown model", {
|
|
3211
|
-
agent,
|
|
3212
|
-
wanted: base,
|
|
3213
|
-
keeping: modelId
|
|
3214
|
-
});
|
|
3215
|
-
return modelId;
|
|
3216
|
-
}
|
|
3217
|
-
const resolved = `${base}${accountMarker(modelId)}`;
|
|
3218
|
-
if (resolved !== modelId) {
|
|
3219
|
-
log.debug("agent model override", { agent, from: modelId, to: resolved });
|
|
3220
|
-
}
|
|
3221
|
-
return resolved;
|
|
3231
|
+
async function resolveSpawnCwdForSession(configured, sessionID) {
|
|
3232
|
+
if (configured) return configured;
|
|
3233
|
+
const sessionDir = sessionID ? await fetchSessionDirectory(sessionID) : void 0;
|
|
3234
|
+
return resolveSpawnCwdFrom(
|
|
3235
|
+
configured,
|
|
3236
|
+
process.cwd(),
|
|
3237
|
+
opencodeProjectDirectory,
|
|
3238
|
+
sessionDir
|
|
3239
|
+
);
|
|
3222
3240
|
}
|
|
3223
|
-
function
|
|
3224
|
-
if (!
|
|
3225
|
-
const
|
|
3226
|
-
|
|
3227
|
-
|
|
3228
|
-
|
|
3229
|
-
|
|
3230
|
-
|
|
3231
|
-
|
|
3232
|
-
|
|
3241
|
+
async function fetchSessionDirectory(sessionID) {
|
|
3242
|
+
if (!sessionID || sessionID === "default") return void 0;
|
|
3243
|
+
const client = opencodeClient;
|
|
3244
|
+
if (!client?.session?.get) return void 0;
|
|
3245
|
+
try {
|
|
3246
|
+
const res = await client.session.get({ path: { id: sessionID } });
|
|
3247
|
+
const data = res.data;
|
|
3248
|
+
if (!data || typeof data !== "object") return void 0;
|
|
3249
|
+
const dir = data.directory;
|
|
3250
|
+
return isUsableDirectory(dir) ? dir : void 0;
|
|
3251
|
+
} catch (err) {
|
|
3252
|
+
log.warn("failed to fetch opencode session directory", {
|
|
3253
|
+
sessionID,
|
|
3254
|
+
error: err instanceof Error ? err.message : String(err)
|
|
3233
3255
|
});
|
|
3234
|
-
return
|
|
3256
|
+
return void 0;
|
|
3235
3257
|
}
|
|
3236
|
-
|
|
3237
|
-
|
|
3238
|
-
|
|
3239
|
-
|
|
3240
|
-
|
|
3258
|
+
}
|
|
3259
|
+
async function getRuntimeMcpStatus() {
|
|
3260
|
+
const client = opencodeClient;
|
|
3261
|
+
if (!client?.mcp?.status) return void 0;
|
|
3262
|
+
try {
|
|
3263
|
+
const res = await client.mcp.status();
|
|
3264
|
+
const data = res.data;
|
|
3265
|
+
if (!data || typeof data !== "object") return void 0;
|
|
3266
|
+
const out = {};
|
|
3267
|
+
for (const [name, entry] of Object.entries(data)) {
|
|
3268
|
+
if (entry && typeof entry === "object") {
|
|
3269
|
+
const status = entry.status;
|
|
3270
|
+
if (typeof status === "string") out[name] = status;
|
|
3271
|
+
}
|
|
3272
|
+
}
|
|
3273
|
+
return out;
|
|
3274
|
+
} catch (err) {
|
|
3275
|
+
log.warn("failed to fetch opencode MCP runtime status", {
|
|
3276
|
+
error: err instanceof Error ? err.message : String(err)
|
|
3241
3277
|
});
|
|
3278
|
+
return void 0;
|
|
3242
3279
|
}
|
|
3243
|
-
return declared;
|
|
3244
3280
|
}
|
|
3245
|
-
function
|
|
3246
|
-
const
|
|
3247
|
-
if (!
|
|
3248
|
-
|
|
3249
|
-
|
|
3250
|
-
|
|
3251
|
-
|
|
3252
|
-
const
|
|
3253
|
-
if (!
|
|
3254
|
-
const
|
|
3255
|
-
|
|
3256
|
-
continue;
|
|
3257
|
-
|
|
3258
|
-
|
|
3281
|
+
async function fetchOpencodeToolList(provider, model, directory) {
|
|
3282
|
+
const client = opencodeClient;
|
|
3283
|
+
if (!client?.tool?.list) return void 0;
|
|
3284
|
+
try {
|
|
3285
|
+
const res = await client.tool.list({
|
|
3286
|
+
query: { provider, model, ...directory ? { directory } : {} }
|
|
3287
|
+
});
|
|
3288
|
+
const data = res.data;
|
|
3289
|
+
if (!Array.isArray(data)) return void 0;
|
|
3290
|
+
const out = [];
|
|
3291
|
+
for (const entry of data) {
|
|
3292
|
+
if (!entry || typeof entry !== "object") continue;
|
|
3293
|
+
const e = entry;
|
|
3294
|
+
const id = typeof e.id === "string" ? e.id : null;
|
|
3295
|
+
const description = typeof e.description === "string" ? e.description : "";
|
|
3296
|
+
const parameters = e.parameters && typeof e.parameters === "object" ? e.parameters : {};
|
|
3297
|
+
if (!id) continue;
|
|
3298
|
+
out.push({ id, description, parameters });
|
|
3299
|
+
}
|
|
3300
|
+
return out;
|
|
3301
|
+
} catch (err) {
|
|
3302
|
+
log.warn("failed to fetch opencode tool list", {
|
|
3303
|
+
provider,
|
|
3304
|
+
model,
|
|
3305
|
+
error: err instanceof Error ? err.message : String(err)
|
|
3306
|
+
});
|
|
3307
|
+
return void 0;
|
|
3259
3308
|
}
|
|
3260
|
-
return record;
|
|
3261
3309
|
}
|
|
3262
|
-
|
|
3263
|
-
|
|
3264
|
-
|
|
3265
|
-
|
|
3266
|
-
|
|
3267
|
-
|
|
3268
|
-
|
|
3269
|
-
|
|
3310
|
+
|
|
3311
|
+
// src/startup-diagnostics.ts
|
|
3312
|
+
var cachedPluginVersion;
|
|
3313
|
+
function pluginVersion() {
|
|
3314
|
+
if (cachedPluginVersion) return cachedPluginVersion;
|
|
3315
|
+
try {
|
|
3316
|
+
const here = path4.dirname(fileURLToPath(import.meta.url));
|
|
3317
|
+
const raw = fs4.readFileSync(path4.join(here, "..", "package.json"), "utf8");
|
|
3318
|
+
const version = JSON.parse(raw).version;
|
|
3319
|
+
cachedPluginVersion = typeof version === "string" ? version : "unknown";
|
|
3320
|
+
} catch {
|
|
3321
|
+
cachedPluginVersion = "unknown";
|
|
3322
|
+
}
|
|
3323
|
+
return cachedPluginVersion;
|
|
3324
|
+
}
|
|
3325
|
+
function pickOpencodeVersion(input) {
|
|
3326
|
+
if (!input || typeof input !== "object") return void 0;
|
|
3327
|
+
const app = input.app;
|
|
3328
|
+
if (app && typeof app === "object") {
|
|
3329
|
+
const version = app.version;
|
|
3330
|
+
if (typeof version === "string" && version.length > 0) return version;
|
|
3331
|
+
}
|
|
3332
|
+
const direct = input.version;
|
|
3333
|
+
if (typeof direct === "string" && direct.length > 0) return direct;
|
|
3334
|
+
return void 0;
|
|
3335
|
+
}
|
|
3336
|
+
var execFileAsync2 = promisify2(execFile2);
|
|
3337
|
+
var opencodeVersionProbe;
|
|
3338
|
+
function detectOpencodeVersion(execPath = process.execPath) {
|
|
3339
|
+
if (opencodeVersionProbe) return opencodeVersionProbe;
|
|
3340
|
+
opencodeVersionProbe = (async () => {
|
|
3341
|
+
if (!path4.basename(execPath).toLowerCase().includes("opencode")) {
|
|
3342
|
+
log.debug("skipping opencode version probe: execPath is not opencode", { execPath });
|
|
3343
|
+
return void 0;
|
|
3270
3344
|
}
|
|
3271
|
-
|
|
3272
|
-
|
|
3273
|
-
const
|
|
3274
|
-
|
|
3275
|
-
|
|
3276
|
-
|
|
3277
|
-
|
|
3278
|
-
|
|
3279
|
-
|
|
3280
|
-
|
|
3281
|
-
error: String(err)
|
|
3282
|
-
});
|
|
3283
|
-
}
|
|
3345
|
+
try {
|
|
3346
|
+
const { stdout } = await execFileAsync2(execPath, ["--version"], { timeout: 5e3 });
|
|
3347
|
+
const match = /\d+\.\d+\.\d+\S*/.exec(stdout.trim());
|
|
3348
|
+
return match ? match[0] : void 0;
|
|
3349
|
+
} catch (err) {
|
|
3350
|
+
log.debug("opencode version probe failed", {
|
|
3351
|
+
execPath,
|
|
3352
|
+
error: err instanceof Error ? err.message : String(err)
|
|
3353
|
+
});
|
|
3354
|
+
return void 0;
|
|
3284
3355
|
}
|
|
3356
|
+
})();
|
|
3357
|
+
return opencodeVersionProbe;
|
|
3358
|
+
}
|
|
3359
|
+
function describeSpawnCwd(configured, live = process.cwd(), captured = getOpencodeProjectDirectory()) {
|
|
3360
|
+
if (typeof configured === "string" && configured.length > 0) {
|
|
3361
|
+
return { resolved: configured, source: "configured" };
|
|
3285
3362
|
}
|
|
3286
|
-
return
|
|
3363
|
+
if (isUsableDirectory(live)) return { resolved: live, source: "process" };
|
|
3364
|
+
if (isUsableDirectory(captured)) return { resolved: captured, source: "captured" };
|
|
3365
|
+
return { resolved: live, source: "unresolved" };
|
|
3287
3366
|
}
|
|
3288
|
-
function
|
|
3289
|
-
|
|
3290
|
-
|
|
3291
|
-
|
|
3292
|
-
|
|
3293
|
-
|
|
3367
|
+
function stringList(value) {
|
|
3368
|
+
if (!Array.isArray(value)) return [];
|
|
3369
|
+
return value.filter((entry) => typeof entry === "string");
|
|
3370
|
+
}
|
|
3371
|
+
function firstOption(providers, key) {
|
|
3372
|
+
for (const entry of Object.values(providers)) {
|
|
3373
|
+
const value = entry?.options?.[key];
|
|
3374
|
+
if (value !== void 0) return value;
|
|
3294
3375
|
}
|
|
3295
|
-
|
|
3296
|
-
|
|
3297
|
-
|
|
3298
|
-
|
|
3376
|
+
return void 0;
|
|
3377
|
+
}
|
|
3378
|
+
function collectStartupDiagnostics(providers, opencodeVersion) {
|
|
3379
|
+
const accounts = [];
|
|
3380
|
+
for (const entry of Object.values(providers)) {
|
|
3381
|
+
const account = entry?.options?.account;
|
|
3382
|
+
if (typeof account === "string" && account.length > 0) accounts.push(account);
|
|
3299
3383
|
}
|
|
3300
|
-
|
|
3384
|
+
const cwd = describeSpawnCwd(firstOption(providers, "cwd"));
|
|
3385
|
+
let mcpServers = [];
|
|
3386
|
+
try {
|
|
3387
|
+
mcpServers = mergeOpencodeMcp(cwd.resolved).enabledServerNames;
|
|
3388
|
+
} catch (err) {
|
|
3389
|
+
log.debug("startup diagnostics could not read MCP config", {
|
|
3390
|
+
error: err instanceof Error ? err.message : String(err)
|
|
3391
|
+
});
|
|
3392
|
+
}
|
|
3393
|
+
return {
|
|
3394
|
+
plugin: pluginVersion(),
|
|
3395
|
+
opencode: opencodeVersion ?? process.env.OPENCODE_VERSION ?? "unknown",
|
|
3396
|
+
claudeCliPath: String(firstOption(providers, "cliPath") ?? "claude"),
|
|
3397
|
+
cwd,
|
|
3398
|
+
providers: Object.keys(providers),
|
|
3399
|
+
accounts,
|
|
3400
|
+
proxyTools: stringList(firstOption(providers, "proxyTools")),
|
|
3401
|
+
mcpServers,
|
|
3402
|
+
interactiveTransport: firstOption(providers, "interactive") === true || process.env.CLAUDE_CODE_INTERACTIVE_TRANSPORT === "1",
|
|
3403
|
+
planModeQuestion: firstOption(providers, "planModeQuestion") === true,
|
|
3404
|
+
anthropicApiKeyInEnv: Boolean(
|
|
3405
|
+
process.env.ANTHROPIC_API_KEY || process.env.ANTHROPIC_AUTH_TOKEN
|
|
3406
|
+
)
|
|
3407
|
+
};
|
|
3408
|
+
}
|
|
3409
|
+
var logged = false;
|
|
3410
|
+
var lastProviders = {};
|
|
3411
|
+
var lastOpencodeVersion;
|
|
3412
|
+
function lastDiagnosticsProviders() {
|
|
3413
|
+
return lastProviders;
|
|
3414
|
+
}
|
|
3415
|
+
function lastKnownOpencodeVersion() {
|
|
3416
|
+
return lastOpencodeVersion;
|
|
3417
|
+
}
|
|
3418
|
+
function logStartupDiagnostics(providers, opencodeVersion) {
|
|
3419
|
+
lastProviders = providers;
|
|
3420
|
+
if (opencodeVersion) lastOpencodeVersion = opencodeVersion;
|
|
3421
|
+
if (logged) return;
|
|
3422
|
+
logged = true;
|
|
3423
|
+
void (async () => {
|
|
3424
|
+
try {
|
|
3425
|
+
const version = opencodeVersion ?? process.env.OPENCODE_VERSION ?? await detectOpencodeVersion();
|
|
3426
|
+
const { claudeCliPath, ...rest } = collectStartupDiagnostics(providers, version);
|
|
3427
|
+
const cli = await detectCliVersion(claudeCliPath);
|
|
3428
|
+
const diagnostics = {
|
|
3429
|
+
...rest,
|
|
3430
|
+
claudeCli: { path: claudeCliPath, version: cli?.raw ?? "not detected" }
|
|
3431
|
+
};
|
|
3432
|
+
log.notice("claude-code plugin ready", { ...diagnostics });
|
|
3433
|
+
} catch (err) {
|
|
3434
|
+
log.debug("startup diagnostics failed", {
|
|
3435
|
+
error: err instanceof Error ? err.message : String(err)
|
|
3436
|
+
});
|
|
3437
|
+
}
|
|
3438
|
+
})();
|
|
3301
3439
|
}
|
|
3302
3440
|
|
|
3303
|
-
// src/
|
|
3304
|
-
|
|
3305
|
-
|
|
3306
|
-
|
|
3307
|
-
|
|
3308
|
-
|
|
3309
|
-
var
|
|
3310
|
-
function
|
|
3311
|
-
|
|
3312
|
-
|
|
3313
|
-
|
|
3314
|
-
|
|
3315
|
-
|
|
3441
|
+
// src/doctor.ts
|
|
3442
|
+
var DOCTOR_COMMAND = "claude-code-doctor";
|
|
3443
|
+
var DOCTOR_MARKER = "\u258C **claude-code doctor**";
|
|
3444
|
+
function isRecord3(value) {
|
|
3445
|
+
return value !== null && typeof value === "object" && !Array.isArray(value);
|
|
3446
|
+
}
|
|
3447
|
+
var SYSTEM_REMINDER_BLOCK2 = /<system-reminder>[\s\S]*?<\/system-reminder>/g;
|
|
3448
|
+
function parseDoctorCommandContent(content) {
|
|
3449
|
+
let text;
|
|
3450
|
+
if (typeof content === "string") {
|
|
3451
|
+
text = content;
|
|
3452
|
+
} else if (Array.isArray(content)) {
|
|
3453
|
+
const parts = [];
|
|
3454
|
+
for (const part of content) {
|
|
3455
|
+
if (!isRecord3(part) || part.type !== "text" || typeof part.text !== "string") return null;
|
|
3456
|
+
parts.push(part.text);
|
|
3457
|
+
}
|
|
3458
|
+
text = parts.join("\n");
|
|
3459
|
+
} else {
|
|
3316
3460
|
return null;
|
|
3317
3461
|
}
|
|
3462
|
+
const match = new RegExp(`^/${DOCTOR_COMMAND}(?:\\s+([\\s\\S]*))?$`).exec(
|
|
3463
|
+
text.replace(SYSTEM_REMINDER_BLOCK2, "").trim()
|
|
3464
|
+
);
|
|
3465
|
+
return match ? { rest: (match[1] ?? "").trim() } : null;
|
|
3318
3466
|
}
|
|
3319
|
-
function
|
|
3320
|
-
|
|
3321
|
-
|
|
3322
|
-
|
|
3323
|
-
|
|
3467
|
+
function parseDoctorCommand(prompt) {
|
|
3468
|
+
const latest = prompt.at(-1);
|
|
3469
|
+
return latest?.role === "user" ? parseDoctorCommandContent(latest.content) : null;
|
|
3470
|
+
}
|
|
3471
|
+
function formatAge(ms) {
|
|
3472
|
+
if (ms === void 0) return "unknown";
|
|
3473
|
+
if (ms < 1e3) return `${Math.round(ms)}ms`;
|
|
3474
|
+
if (ms < 6e4) return `${(ms / 1e3).toFixed(1)}s`;
|
|
3475
|
+
const minutes = Math.floor(ms / 6e4);
|
|
3476
|
+
if (minutes < 60) return `${minutes}m`;
|
|
3477
|
+
return `${Math.floor(minutes / 60)}h ${minutes % 60}m`;
|
|
3478
|
+
}
|
|
3479
|
+
function list(values) {
|
|
3480
|
+
return values.length ? values.join(", ") : "none";
|
|
3481
|
+
}
|
|
3482
|
+
function describeAuth(auth) {
|
|
3483
|
+
switch (auth.status) {
|
|
3484
|
+
case "ok":
|
|
3485
|
+
return `${auth.code}, good`;
|
|
3486
|
+
case "unsafe":
|
|
3487
|
+
return `${auth.code}, UNSAFE: an unauthenticated caller was accepted. Restart every opencode window; a window opened before 0.13.2 keeps serving an open port.`;
|
|
3488
|
+
case "unreachable":
|
|
3489
|
+
return `could not be checked (${auth.error})`;
|
|
3490
|
+
case "skipped":
|
|
3491
|
+
return "not checked";
|
|
3492
|
+
}
|
|
3493
|
+
}
|
|
3494
|
+
function formatDoctorReport(report) {
|
|
3495
|
+
const lines = [];
|
|
3496
|
+
lines.push(DOCTOR_MARKER);
|
|
3497
|
+
lines.push("");
|
|
3498
|
+
lines.push("| Field | Value |");
|
|
3499
|
+
lines.push("|---|---|");
|
|
3500
|
+
lines.push(`| plugin | ${report.plugin} |`);
|
|
3501
|
+
lines.push(`| opencode | ${report.opencode} |`);
|
|
3502
|
+
lines.push(`| claude CLI | \`${report.claudeCli.path}\` (${report.claudeCli.version}) |`);
|
|
3503
|
+
lines.push(`| cwd | \`${report.cwd.resolved}\` (${report.cwd.source}) |`);
|
|
3504
|
+
lines.push(`| providers | ${list(report.providers)} |`);
|
|
3505
|
+
lines.push(`| accounts | ${list(report.accounts)} |`);
|
|
3506
|
+
lines.push(`| proxyTools | ${list(report.proxyTools)} |`);
|
|
3507
|
+
lines.push(`| MCP servers (on disk) | ${list(report.mcpServers)} |`);
|
|
3508
|
+
lines.push(`| transport | ${report.transport} |`);
|
|
3509
|
+
lines.push(`| planModeQuestion | ${report.planModeQuestion} |`);
|
|
3510
|
+
lines.push(`| turnStats | ${report.turnStats} |`);
|
|
3511
|
+
lines.push(`| ANTHROPIC_API_KEY in env | ${report.anthropicApiKeyInEnv ? "yes" : "no"} |`);
|
|
3512
|
+
lines.push("");
|
|
3513
|
+
lines.push("**Live `claude` processes**");
|
|
3514
|
+
lines.push("");
|
|
3515
|
+
if (report.processes.length === 0) {
|
|
3516
|
+
lines.push("None. The next message in a Claude Code session spawns one.");
|
|
3517
|
+
} else {
|
|
3518
|
+
lines.push("| session | model | pid | in flight | age | effort |");
|
|
3519
|
+
lines.push("|---|---|---|---|---|---|");
|
|
3520
|
+
for (const proc of report.processes) {
|
|
3521
|
+
lines.push(
|
|
3522
|
+
`| ${proc.session}${proc.compaction ? " (compaction)" : ""} | ${proc.model} | ${proc.pid ?? "unknown"} | ${proc.inFlight ? "yes" : "no"} | ${formatAge(proc.ageMs)} | ${proc.effort ?? "inherited"} |`
|
|
3523
|
+
);
|
|
3524
|
+
}
|
|
3525
|
+
}
|
|
3526
|
+
lines.push("");
|
|
3527
|
+
lines.push("**Pending proxy calls**");
|
|
3528
|
+
lines.push("");
|
|
3529
|
+
if (report.pendingCalls.length === 0) {
|
|
3530
|
+
lines.push("None.");
|
|
3531
|
+
} else {
|
|
3532
|
+
lines.push("| tool | call id | age | deadline |");
|
|
3533
|
+
lines.push("|---|---|---|---|");
|
|
3534
|
+
for (const call of report.pendingCalls) {
|
|
3535
|
+
lines.push(
|
|
3536
|
+
`| ${call.toolName} | \`${call.toolCallId}\` | ${formatAge(call.ageMs)} | ${formatAge(
|
|
3537
|
+
call.deadlineMs
|
|
3538
|
+
)} |`
|
|
3539
|
+
);
|
|
3540
|
+
}
|
|
3541
|
+
}
|
|
3542
|
+
lines.push("");
|
|
3543
|
+
lines.push("**Proxy servers**");
|
|
3544
|
+
lines.push("");
|
|
3545
|
+
if (report.proxyServers.length === 0) {
|
|
3546
|
+
lines.push("None running.");
|
|
3547
|
+
} else {
|
|
3548
|
+
lines.push("| url | unauthenticated `initialize` |");
|
|
3549
|
+
lines.push("|---|---|");
|
|
3550
|
+
for (const server2 of report.proxyServers) {
|
|
3551
|
+
lines.push(`| ${server2.url} | ${describeAuth(server2.auth)} |`);
|
|
3552
|
+
}
|
|
3553
|
+
}
|
|
3554
|
+
const stderr = report.processes.filter((proc) => proc.lastStderr);
|
|
3555
|
+
if (stderr.length > 0) {
|
|
3556
|
+
lines.push("");
|
|
3557
|
+
lines.push("**Last stderr**");
|
|
3558
|
+
lines.push("");
|
|
3559
|
+
for (const proc of stderr) {
|
|
3560
|
+
lines.push(`\`${proc.session}\`:`);
|
|
3561
|
+
lines.push("");
|
|
3562
|
+
lines.push("```text");
|
|
3563
|
+
lines.push(proc.lastStderr.trimEnd());
|
|
3564
|
+
lines.push("```");
|
|
3565
|
+
}
|
|
3324
3566
|
}
|
|
3567
|
+
return lines.join("\n");
|
|
3325
3568
|
}
|
|
3326
|
-
function
|
|
3569
|
+
async function checkProxyAuth(url, fetchImpl = fetch, timeoutMs = 3e3) {
|
|
3570
|
+
let authority;
|
|
3327
3571
|
try {
|
|
3328
|
-
|
|
3572
|
+
authority = new URL(url).host;
|
|
3329
3573
|
} catch {
|
|
3330
|
-
return
|
|
3574
|
+
return { status: "skipped" };
|
|
3575
|
+
}
|
|
3576
|
+
const controller = new AbortController();
|
|
3577
|
+
const timer = setTimeout(() => controller.abort(), timeoutMs);
|
|
3578
|
+
try {
|
|
3579
|
+
const response = await fetchImpl(url, {
|
|
3580
|
+
method: "POST",
|
|
3581
|
+
headers: { "content-type": "application/json", host: authority },
|
|
3582
|
+
body: JSON.stringify({ jsonrpc: "2.0", id: 0, method: "initialize", params: {} }),
|
|
3583
|
+
signal: controller.signal
|
|
3584
|
+
});
|
|
3585
|
+
await response.text().catch(() => "");
|
|
3586
|
+
return response.status === 401 ? { status: "ok", code: response.status } : { status: "unsafe", code: response.status };
|
|
3587
|
+
} catch (error) {
|
|
3588
|
+
return {
|
|
3589
|
+
status: "unreachable",
|
|
3590
|
+
error: error instanceof Error ? error.message : String(error)
|
|
3591
|
+
};
|
|
3592
|
+
} finally {
|
|
3593
|
+
clearTimeout(timer);
|
|
3331
3594
|
}
|
|
3332
3595
|
}
|
|
3333
|
-
function
|
|
3334
|
-
const
|
|
3596
|
+
async function gatherDoctorReport(options) {
|
|
3597
|
+
const providers = lastDiagnosticsProviders();
|
|
3598
|
+
const opencodeVersion = lastKnownOpencodeVersion() ?? process.env.OPENCODE_VERSION ?? await detectOpencodeVersion().catch(() => void 0);
|
|
3599
|
+
const { claudeCliPath, ...base } = collectStartupDiagnostics(providers, opencodeVersion);
|
|
3600
|
+
const cliPath = options.cliPath || claudeCliPath;
|
|
3601
|
+
const cli = await detectCliVersion(cliPath).catch(() => null);
|
|
3602
|
+
const processes = snapshotActiveProcesses();
|
|
3335
3603
|
const seen = /* @__PURE__ */ new Set();
|
|
3336
|
-
const
|
|
3337
|
-
|
|
3338
|
-
if (seen.has(
|
|
3339
|
-
seen.add(
|
|
3340
|
-
|
|
3341
|
-
|
|
3342
|
-
|
|
3343
|
-
|
|
3344
|
-
push(path4.join(current, ".opencode", "skills"));
|
|
3345
|
-
const parent = path4.dirname(current);
|
|
3346
|
-
if (parent === current) break;
|
|
3347
|
-
current = parent;
|
|
3604
|
+
const proxyServers = [];
|
|
3605
|
+
for (const proc of processes) {
|
|
3606
|
+
if (!proc.proxyUrl || seen.has(proc.proxyUrl)) continue;
|
|
3607
|
+
seen.add(proc.proxyUrl);
|
|
3608
|
+
proxyServers.push({
|
|
3609
|
+
url: proc.proxyUrl,
|
|
3610
|
+
auth: await checkProxyAuth(proc.proxyUrl, options.fetchImpl ?? fetch)
|
|
3611
|
+
});
|
|
3348
3612
|
}
|
|
3349
|
-
|
|
3350
|
-
|
|
3351
|
-
|
|
3352
|
-
|
|
3353
|
-
|
|
3354
|
-
|
|
3355
|
-
|
|
3613
|
+
return {
|
|
3614
|
+
plugin: base.plugin,
|
|
3615
|
+
opencode: base.opencode,
|
|
3616
|
+
claudeCli: { path: cliPath, version: cli?.raw ?? "not detected" },
|
|
3617
|
+
cwd: base.cwd,
|
|
3618
|
+
providers: base.providers,
|
|
3619
|
+
accounts: base.accounts,
|
|
3620
|
+
proxyTools: base.proxyTools,
|
|
3621
|
+
mcpServers: base.mcpServers,
|
|
3622
|
+
transport: options.interactive || base.interactiveTransport ? "interactive" : "headless",
|
|
3623
|
+
planModeQuestion: base.planModeQuestion,
|
|
3624
|
+
turnStats: options.turnStats,
|
|
3625
|
+
anthropicApiKeyInEnv: base.anthropicApiKeyInEnv,
|
|
3626
|
+
processes,
|
|
3627
|
+
pendingCalls: snapshotPendingProxyCalls(),
|
|
3628
|
+
proxyServers
|
|
3629
|
+
};
|
|
3356
3630
|
}
|
|
3357
|
-
function
|
|
3358
|
-
let entries;
|
|
3631
|
+
async function buildDoctorReport(options) {
|
|
3359
3632
|
try {
|
|
3360
|
-
|
|
3361
|
-
|
|
3362
|
-
|
|
3363
|
-
|
|
3364
|
-
|
|
3365
|
-
|
|
3366
|
-
|
|
3367
|
-
|
|
3368
|
-
|
|
3369
|
-
|
|
3370
|
-
|
|
3371
|
-
|
|
3372
|
-
|
|
3633
|
+
const report = await gatherDoctorReport(options);
|
|
3634
|
+
log.info("claude-code doctor report", {
|
|
3635
|
+
plugin: report.plugin,
|
|
3636
|
+
opencode: report.opencode,
|
|
3637
|
+
cwd: report.cwd,
|
|
3638
|
+
processes: report.processes.length,
|
|
3639
|
+
pendingCalls: report.pendingCalls.length,
|
|
3640
|
+
proxyServers: report.proxyServers.map((server2) => server2.auth.status)
|
|
3641
|
+
});
|
|
3642
|
+
return formatDoctorReport(report);
|
|
3643
|
+
} catch (error) {
|
|
3644
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
3645
|
+
log.warn("claude-code doctor failed to build its report", { error: message });
|
|
3646
|
+
return `${DOCTOR_MARKER}
|
|
3647
|
+
|
|
3648
|
+
Could not build the report: ${message}`;
|
|
3373
3649
|
}
|
|
3374
3650
|
}
|
|
3375
|
-
|
|
3376
|
-
|
|
3377
|
-
|
|
3378
|
-
|
|
3379
|
-
|
|
3380
|
-
|
|
3651
|
+
|
|
3652
|
+
// src/turn-stats.ts
|
|
3653
|
+
var TURN_STATS_MARKER = "\u258C **stats:**";
|
|
3654
|
+
function extractTurnStats(msg) {
|
|
3655
|
+
const usage = msg.usage;
|
|
3656
|
+
const stats = {};
|
|
3657
|
+
if (typeof msg.total_cost_usd === "number") stats.costUsd = msg.total_cost_usd;
|
|
3658
|
+
if (typeof msg.duration_ms === "number") stats.durationMs = msg.duration_ms;
|
|
3659
|
+
if (typeof msg.duration_api_ms === "number") stats.durationApiMs = msg.duration_api_ms;
|
|
3660
|
+
if (typeof msg.num_turns === "number") stats.numTurns = msg.num_turns;
|
|
3661
|
+
if (typeof usage?.input_tokens === "number") stats.inputTokens = usage.input_tokens;
|
|
3662
|
+
if (typeof usage?.output_tokens === "number") stats.outputTokens = usage.output_tokens;
|
|
3663
|
+
if (typeof usage?.cache_read_input_tokens === "number") {
|
|
3664
|
+
stats.cacheReadTokens = usage.cache_read_input_tokens;
|
|
3665
|
+
}
|
|
3666
|
+
if (typeof usage?.cache_creation_input_tokens === "number") {
|
|
3667
|
+
stats.cacheWriteTokens = usage.cache_creation_input_tokens;
|
|
3668
|
+
}
|
|
3669
|
+
if (msg.modelUsage && typeof msg.modelUsage === "object") stats.modelUsage = msg.modelUsage;
|
|
3670
|
+
if (Array.isArray(msg.permission_denials)) stats.permissionDenials = msg.permission_denials;
|
|
3671
|
+
return stats;
|
|
3672
|
+
}
|
|
3673
|
+
function formatCost(costUsd) {
|
|
3674
|
+
if (!Number.isFinite(costUsd) || costUsd < 0) return "$0.00";
|
|
3675
|
+
return costUsd >= 1 ? `$${costUsd.toFixed(2)}` : `$${costUsd.toFixed(4)}`;
|
|
3676
|
+
}
|
|
3677
|
+
function formatDuration(durationMs) {
|
|
3678
|
+
if (!Number.isFinite(durationMs) || durationMs < 0) return "0.0 s";
|
|
3679
|
+
if (durationMs < 6e4) return `${(durationMs / 1e3).toFixed(1)} s`;
|
|
3680
|
+
const totalSeconds = Math.round(durationMs / 1e3);
|
|
3681
|
+
return `${Math.floor(totalSeconds / 60)}m ${totalSeconds % 60}s`;
|
|
3682
|
+
}
|
|
3683
|
+
function formatTokens(tokens) {
|
|
3684
|
+
if (!Number.isFinite(tokens) || tokens < 0) return "0";
|
|
3685
|
+
if (tokens < 1e3) return String(Math.round(tokens));
|
|
3686
|
+
if (tokens < 1e6) return `${(tokens / 1e3).toFixed(1)}k`;
|
|
3687
|
+
return `${(tokens / 1e6).toFixed(1)}M`;
|
|
3688
|
+
}
|
|
3689
|
+
function formatTurnStatsLine(stats) {
|
|
3690
|
+
const parts = [];
|
|
3691
|
+
if (stats.costUsd !== void 0) parts.push(formatCost(stats.costUsd));
|
|
3692
|
+
if (stats.durationMs !== void 0) parts.push(formatDuration(stats.durationMs));
|
|
3693
|
+
if (stats.numTurns !== void 0) {
|
|
3694
|
+
parts.push(`${stats.numTurns} CLI ${stats.numTurns === 1 ? "turn" : "turns"}`);
|
|
3695
|
+
}
|
|
3696
|
+
if (stats.inputTokens !== void 0) parts.push(`in ${formatTokens(stats.inputTokens)}`);
|
|
3697
|
+
if (stats.outputTokens !== void 0) parts.push(`out ${formatTokens(stats.outputTokens)}`);
|
|
3698
|
+
if (stats.cacheReadTokens) parts.push(`cache read ${formatTokens(stats.cacheReadTokens)}`);
|
|
3699
|
+
if (stats.cacheWriteTokens) parts.push(`cache write ${formatTokens(stats.cacheWriteTokens)}`);
|
|
3700
|
+
if (stats.permissionDenials?.length) {
|
|
3701
|
+
const count = stats.permissionDenials.length;
|
|
3702
|
+
parts.push(`${count} permission ${count === 1 ? "denial" : "denials"}`);
|
|
3703
|
+
}
|
|
3704
|
+
if (parts.length === 0) return null;
|
|
3705
|
+
return `${TURN_STATS_MARKER} ${parts.join(" \xB7 ")}`;
|
|
3706
|
+
}
|
|
3707
|
+
function formatTurnStatsBlock(stats) {
|
|
3708
|
+
const line = formatTurnStatsLine(stats);
|
|
3709
|
+
return line === null ? null : `
|
|
3710
|
+
${line}
|
|
3711
|
+
`;
|
|
3381
3712
|
}
|
|
3382
|
-
function
|
|
3383
|
-
|
|
3384
|
-
|
|
3385
|
-
|
|
3386
|
-
|
|
3387
|
-
|
|
3713
|
+
function turnStatsLogPayload(stats) {
|
|
3714
|
+
return {
|
|
3715
|
+
costUsd: stats.costUsd ?? null,
|
|
3716
|
+
durationMs: stats.durationMs ?? null,
|
|
3717
|
+
durationApiMs: stats.durationApiMs ?? null,
|
|
3718
|
+
numTurns: stats.numTurns ?? null,
|
|
3719
|
+
inputTokens: stats.inputTokens ?? null,
|
|
3720
|
+
outputTokens: stats.outputTokens ?? null,
|
|
3721
|
+
cacheReadTokens: stats.cacheReadTokens ?? null,
|
|
3722
|
+
cacheWriteTokens: stats.cacheWriteTokens ?? null,
|
|
3723
|
+
modelUsage: stats.modelUsage ?? null,
|
|
3724
|
+
permissionDenials: stats.permissionDenials?.length ?? 0
|
|
3725
|
+
};
|
|
3388
3726
|
}
|
|
3389
|
-
|
|
3390
|
-
|
|
3391
|
-
|
|
3392
|
-
|
|
3393
|
-
|
|
3394
|
-
|
|
3395
|
-
|
|
3396
|
-
|
|
3727
|
+
|
|
3728
|
+
// src/message-builder.ts
|
|
3729
|
+
var PLUGIN_NOTE_MARKERS = [
|
|
3730
|
+
INLINE_ASIDE_MARKER,
|
|
3731
|
+
...LEGACY_INLINE_ASIDE_MARKERS,
|
|
3732
|
+
TURN_STATS_MARKER,
|
|
3733
|
+
COMPACT_BOUNDARY_MARKER,
|
|
3734
|
+
RATE_LIMIT_MARKER,
|
|
3735
|
+
RESULT_ERROR_MARKER,
|
|
3736
|
+
DOCTOR_MARKER
|
|
3737
|
+
];
|
|
3738
|
+
function isPluginNote(part) {
|
|
3739
|
+
if (!part || part.type !== "text" || typeof part.text !== "string") return false;
|
|
3740
|
+
const text = part.text.trimStart();
|
|
3741
|
+
return PLUGIN_NOTE_MARKERS.some((marker) => text.startsWith(marker));
|
|
3397
3742
|
}
|
|
3398
|
-
function
|
|
3399
|
-
|
|
3400
|
-
|
|
3401
|
-
|
|
3402
|
-
} catch {
|
|
3403
|
-
fs3.cpSync(source, target, { recursive: true, dereference: true });
|
|
3404
|
-
}
|
|
3743
|
+
function stripPluginNotes(content) {
|
|
3744
|
+
if (!Array.isArray(content)) return content;
|
|
3745
|
+
const kept = content.filter((part) => !isPluginNote(part));
|
|
3746
|
+
return kept.length === content.length ? content : kept;
|
|
3405
3747
|
}
|
|
3406
|
-
function
|
|
3407
|
-
|
|
3408
|
-
const
|
|
3409
|
-
|
|
3410
|
-
|
|
3411
|
-
|
|
3412
|
-
if (fileExists(manifest)) return root;
|
|
3413
|
-
try {
|
|
3414
|
-
fs3.rmSync(root, { recursive: true, force: true });
|
|
3415
|
-
fs3.mkdirSync(path4.join(root, ".claude-plugin"), { recursive: true });
|
|
3416
|
-
fs3.mkdirSync(path4.join(root, "skills"), { recursive: true });
|
|
3417
|
-
fs3.writeFileSync(
|
|
3418
|
-
manifest,
|
|
3419
|
-
JSON.stringify(
|
|
3420
|
-
{
|
|
3421
|
-
name: SKILL_PLUGIN_NAME,
|
|
3422
|
-
description: "Skills discovered from this opencode installation, bridged into Claude Code."
|
|
3423
|
-
},
|
|
3424
|
-
null,
|
|
3425
|
-
2
|
|
3426
|
-
),
|
|
3427
|
-
{ encoding: "utf8", mode: 384 }
|
|
3428
|
-
);
|
|
3429
|
-
for (const skill of skills) {
|
|
3430
|
-
linkSkill(skill.dir, path4.join(root, "skills", skill.name));
|
|
3748
|
+
function filterSideQuestionHistory(prompt) {
|
|
3749
|
+
let pluginCommand = false;
|
|
3750
|
+
const kept = prompt.filter((message) => {
|
|
3751
|
+
if (message.role === "user") {
|
|
3752
|
+
pluginCommand = parseSideQuestionContent(message.content) !== null || parseDoctorCommandContent(message.content) !== null;
|
|
3753
|
+
return !pluginCommand;
|
|
3431
3754
|
}
|
|
3432
|
-
|
|
3433
|
-
|
|
3434
|
-
|
|
3435
|
-
|
|
3755
|
+
return message.role !== "assistant" || !pluginCommand;
|
|
3756
|
+
});
|
|
3757
|
+
return kept.map(
|
|
3758
|
+
(message) => message.role === "assistant" ? { ...message, content: stripPluginNotes(message.content) } : message
|
|
3759
|
+
);
|
|
3760
|
+
}
|
|
3761
|
+
var SUPPORTED_IMAGE_TYPES = /* @__PURE__ */ new Set([
|
|
3762
|
+
"image/jpeg",
|
|
3763
|
+
"image/png",
|
|
3764
|
+
"image/gif",
|
|
3765
|
+
"image/webp"
|
|
3766
|
+
]);
|
|
3767
|
+
function toImageBlock(part) {
|
|
3768
|
+
const raw = part.image ?? part.data ?? part.url ?? part.source?.data;
|
|
3769
|
+
if (!raw) {
|
|
3770
|
+
log.warn("file part without data, skipping");
|
|
3771
|
+
return null;
|
|
3772
|
+
}
|
|
3773
|
+
let resolvedMediaType = part.mediaType || part.mimeType || part.mime || "";
|
|
3774
|
+
let base64 = null;
|
|
3775
|
+
if (typeof raw === "string") {
|
|
3776
|
+
if (raw.startsWith("data:")) {
|
|
3777
|
+
const match = /^data:([^;,]+)(?:;[^,]*)*(?:;base64)?,(.*)$/s.exec(raw);
|
|
3778
|
+
if (!match) {
|
|
3779
|
+
log.warn("malformed data URI, skipping file part");
|
|
3780
|
+
return null;
|
|
3781
|
+
}
|
|
3782
|
+
resolvedMediaType = resolvedMediaType || match[1];
|
|
3783
|
+
base64 = match[2];
|
|
3784
|
+
} else if (/^https?:\/\//i.test(raw)) {
|
|
3785
|
+
log.warn("remote URL images are not supported by Claude CLI, skipping");
|
|
3786
|
+
return null;
|
|
3787
|
+
} else {
|
|
3788
|
+
base64 = raw;
|
|
3789
|
+
}
|
|
3790
|
+
} else if (raw instanceof URL) {
|
|
3791
|
+
log.warn("remote URL images are not supported by Claude CLI, skipping");
|
|
3792
|
+
return null;
|
|
3793
|
+
} else if (raw instanceof Uint8Array || Buffer.isBuffer(raw)) {
|
|
3794
|
+
base64 = Buffer.from(raw).toString("base64");
|
|
3795
|
+
} else {
|
|
3796
|
+
log.warn("unsupported file part data type", { dataType: typeof raw });
|
|
3797
|
+
return null;
|
|
3798
|
+
}
|
|
3799
|
+
if (!resolvedMediaType || !SUPPORTED_IMAGE_TYPES.has(resolvedMediaType)) {
|
|
3800
|
+
log.warn("unsupported media type for Claude image block, skipping", {
|
|
3801
|
+
mediaType: resolvedMediaType
|
|
3436
3802
|
});
|
|
3437
3803
|
return null;
|
|
3438
3804
|
}
|
|
3439
|
-
return
|
|
3805
|
+
return {
|
|
3806
|
+
type: "image",
|
|
3807
|
+
source: { type: "base64", media_type: resolvedMediaType, data: base64 }
|
|
3808
|
+
};
|
|
3440
3809
|
}
|
|
3441
|
-
|
|
3442
|
-
const
|
|
3443
|
-
|
|
3444
|
-
|
|
3445
|
-
const skills = [...user, ...bundled].sort(byName);
|
|
3446
|
-
if (skills.length === 0) return [];
|
|
3447
|
-
const supported = await detectCliSupportsFlag(opts.cliPath, "--plugin-dir");
|
|
3448
|
-
if (!supported) {
|
|
3449
|
-
log.notice(
|
|
3450
|
-
"claude cli does not support --plugin-dir; opencode skills will not be bridged. Run `npm i -g @anthropic-ai/claude-code` to upgrade.",
|
|
3451
|
-
{ skills: skills.length }
|
|
3452
|
-
);
|
|
3453
|
-
return [];
|
|
3810
|
+
function getToolResultText(part) {
|
|
3811
|
+
const value = part.output ?? part.result;
|
|
3812
|
+
if (typeof value === "string") {
|
|
3813
|
+
return value;
|
|
3454
3814
|
}
|
|
3455
|
-
|
|
3456
|
-
|
|
3457
|
-
|
|
3458
|
-
|
|
3459
|
-
|
|
3460
|
-
|
|
3461
|
-
|
|
3462
|
-
|
|
3463
|
-
|
|
3464
|
-
|
|
3465
|
-
|
|
3466
|
-
|
|
3467
|
-
|
|
3468
|
-
|
|
3469
|
-
|
|
3470
|
-
|
|
3471
|
-
|
|
3472
|
-
|
|
3473
|
-
|
|
3474
|
-
} from "jsonc-parser";
|
|
3475
|
-
var FILE_NAMES = ["opencode.jsonc", "opencode.json", "config.json"];
|
|
3476
|
-
var PROJECT_FILE_NAMES = ["opencode.json", "opencode.jsonc"];
|
|
3477
|
-
function fileExists2(p) {
|
|
3478
|
-
try {
|
|
3479
|
-
return fs4.statSync(p).isFile();
|
|
3480
|
-
} catch {
|
|
3481
|
-
return false;
|
|
3815
|
+
if (!value || typeof value !== "object") {
|
|
3816
|
+
return JSON.stringify(value);
|
|
3817
|
+
}
|
|
3818
|
+
switch (value.type) {
|
|
3819
|
+
case "text":
|
|
3820
|
+
case "error-text":
|
|
3821
|
+
return String(value.value);
|
|
3822
|
+
case "json":
|
|
3823
|
+
case "error-json":
|
|
3824
|
+
return JSON.stringify(value.value);
|
|
3825
|
+
case "execution-denied":
|
|
3826
|
+
return value.reason ? `Execution denied: ${value.reason}` : "Execution denied";
|
|
3827
|
+
case "content":
|
|
3828
|
+
return Array.isArray(value.value) ? value.value.map((item) => {
|
|
3829
|
+
if (item?.type === "text") return item.text;
|
|
3830
|
+
return JSON.stringify(item);
|
|
3831
|
+
}).join("\n") : JSON.stringify(value.value);
|
|
3832
|
+
default:
|
|
3833
|
+
return JSON.stringify(value);
|
|
3482
3834
|
}
|
|
3483
3835
|
}
|
|
3484
|
-
|
|
3836
|
+
var MAX_HISTORY_CHARS = 18e4;
|
|
3837
|
+
var MAX_TOOL_RESULT_CHARS = 1e4;
|
|
3838
|
+
var MAX_TOOL_INPUT_CHARS = 2e3;
|
|
3839
|
+
function clipWithMarker(text, max) {
|
|
3840
|
+
if (text.length <= max) return text;
|
|
3841
|
+
return `${text.slice(0, max)}
|
|
3842
|
+
\u2026[truncated ${text.length - max} chars]`;
|
|
3843
|
+
}
|
|
3844
|
+
function renderToolInput(input) {
|
|
3845
|
+
let raw;
|
|
3485
3846
|
try {
|
|
3486
|
-
|
|
3847
|
+
raw = typeof input === "string" ? input : JSON.stringify(input);
|
|
3487
3848
|
} catch {
|
|
3488
|
-
|
|
3849
|
+
raw = String(input);
|
|
3489
3850
|
}
|
|
3851
|
+
return clipWithMarker(raw, MAX_TOOL_INPUT_CHARS);
|
|
3490
3852
|
}
|
|
3491
|
-
function
|
|
3492
|
-
|
|
3493
|
-
|
|
3494
|
-
|
|
3495
|
-
|
|
3496
|
-
|
|
3497
|
-
|
|
3498
|
-
|
|
3499
|
-
|
|
3500
|
-
|
|
3853
|
+
function renderMessageContentForCompaction(msg) {
|
|
3854
|
+
const lines = [];
|
|
3855
|
+
let toolResultCount = 0;
|
|
3856
|
+
if (typeof msg.content === "string") {
|
|
3857
|
+
return { text: msg.content, toolResultCount: 0 };
|
|
3858
|
+
}
|
|
3859
|
+
if (!Array.isArray(msg.content)) {
|
|
3860
|
+
return { text: "", toolResultCount: 0 };
|
|
3861
|
+
}
|
|
3862
|
+
for (const part of msg.content) {
|
|
3863
|
+
if (!part) continue;
|
|
3864
|
+
switch (part.type) {
|
|
3865
|
+
case "text":
|
|
3866
|
+
if (part.text) lines.push(part.text);
|
|
3867
|
+
break;
|
|
3868
|
+
case "tool-call":
|
|
3869
|
+
lines.push(
|
|
3870
|
+
`[tool_use:${part.toolName ?? "unknown"}(${renderToolInput(part.input)})]`
|
|
3871
|
+
);
|
|
3872
|
+
break;
|
|
3873
|
+
case "tool-result":
|
|
3874
|
+
toolResultCount++;
|
|
3875
|
+
lines.push(
|
|
3876
|
+
`[tool_result:${part.toolName ?? part.toolCallId ?? "unknown"}]
|
|
3877
|
+
${clipWithMarker(
|
|
3878
|
+
getToolResultText(part),
|
|
3879
|
+
MAX_TOOL_RESULT_CHARS
|
|
3880
|
+
)}`
|
|
3881
|
+
);
|
|
3882
|
+
break;
|
|
3883
|
+
case "image":
|
|
3884
|
+
lines.push(
|
|
3885
|
+
`[image: ${part.mediaType ?? part.mimeType ?? "unknown"}]`
|
|
3886
|
+
);
|
|
3887
|
+
break;
|
|
3888
|
+
case "file":
|
|
3889
|
+
lines.push(
|
|
3890
|
+
`[file: ${part.mediaType ?? part.mimeType ?? "unknown"}]`
|
|
3891
|
+
);
|
|
3892
|
+
break;
|
|
3893
|
+
case "reasoning":
|
|
3894
|
+
break;
|
|
3501
3895
|
}
|
|
3502
|
-
return parsed;
|
|
3503
|
-
} catch (e) {
|
|
3504
|
-
log.warn("failed to parse opencode config", {
|
|
3505
|
-
file,
|
|
3506
|
-
error: e instanceof Error ? e.message : String(e)
|
|
3507
|
-
});
|
|
3508
|
-
return null;
|
|
3509
3896
|
}
|
|
3897
|
+
return { text: lines.join("\n"), toolResultCount };
|
|
3510
3898
|
}
|
|
3511
|
-
function
|
|
3512
|
-
|
|
3899
|
+
function compactConversationHistory(prompt, opts = {}) {
|
|
3900
|
+
const mode = opts.mode ?? "fresh-session";
|
|
3901
|
+
prompt = filterSideQuestionHistory(prompt);
|
|
3902
|
+
if (mode === "compaction") {
|
|
3903
|
+
return buildCompactionHistory(prompt);
|
|
3904
|
+
}
|
|
3905
|
+
const conversationMessages = prompt.filter(
|
|
3906
|
+
(m) => m.role === "user" || m.role === "assistant" || m.role === "tool"
|
|
3907
|
+
);
|
|
3908
|
+
if (conversationMessages.length <= 1) {
|
|
3909
|
+
return null;
|
|
3910
|
+
}
|
|
3911
|
+
const historyParts = [];
|
|
3912
|
+
for (let i = 0; i < conversationMessages.length - 1; i++) {
|
|
3913
|
+
const msg = conversationMessages[i];
|
|
3914
|
+
const role = msg.role === "user" ? "User" : msg.role === "assistant" ? "Assistant" : "Tool";
|
|
3915
|
+
const { text } = renderMessageContentForCompaction(msg);
|
|
3916
|
+
if (text.trim()) {
|
|
3917
|
+
const truncated = text.length > 2e3 ? text.slice(0, 2e3) + "..." : text;
|
|
3918
|
+
historyParts.push(`${role}: ${truncated}`);
|
|
3919
|
+
}
|
|
3920
|
+
}
|
|
3921
|
+
if (historyParts.length === 0) {
|
|
3922
|
+
return null;
|
|
3923
|
+
}
|
|
3924
|
+
return historyParts.join("\n\n");
|
|
3925
|
+
}
|
|
3926
|
+
function buildCompactionHistory(prompt) {
|
|
3927
|
+
const entries = [];
|
|
3928
|
+
let total = 0;
|
|
3929
|
+
let totalToolResults = 0;
|
|
3930
|
+
let droppedOldest = 0;
|
|
3931
|
+
const end = prompt.length > 0 && prompt[prompt.length - 1].role === "user" ? prompt.length - 1 : prompt.length;
|
|
3932
|
+
for (let i = end - 1; i >= 0; i--) {
|
|
3933
|
+
const msg = prompt[i];
|
|
3934
|
+
const roleLabel = msg.role === "user" ? "User" : msg.role === "assistant" ? "Assistant" : msg.role === "tool" ? "Tool" : msg.role;
|
|
3935
|
+
const { text, toolResultCount } = renderMessageContentForCompaction(msg);
|
|
3936
|
+
if (!text.trim()) continue;
|
|
3937
|
+
const entry = `${roleLabel}: ${text}`;
|
|
3938
|
+
if (total + entry.length > MAX_HISTORY_CHARS) {
|
|
3939
|
+
droppedOldest = i + 1;
|
|
3940
|
+
break;
|
|
3941
|
+
}
|
|
3942
|
+
entries.push(entry);
|
|
3943
|
+
total += entry.length + 2;
|
|
3944
|
+
totalToolResults += toolResultCount;
|
|
3945
|
+
}
|
|
3946
|
+
if (entries.length === 0) return null;
|
|
3947
|
+
entries.reverse();
|
|
3948
|
+
log.info("built compaction history", {
|
|
3949
|
+
entries: entries.length,
|
|
3950
|
+
chars: total,
|
|
3951
|
+
toolResults: totalToolResults,
|
|
3952
|
+
droppedOldestBefore: droppedOldest
|
|
3953
|
+
});
|
|
3954
|
+
return entries.join("\n\n");
|
|
3955
|
+
}
|
|
3956
|
+
function getClaudeUserMessage(prompt, includeHistoryContext = false, opts = {}) {
|
|
3957
|
+
const compactionMode = opts.compactionMode === true;
|
|
3958
|
+
const cliToolCallIds = opts.cliToolCallIds;
|
|
3959
|
+
const content = [];
|
|
3960
|
+
const pushToolResult = (part) => {
|
|
3961
|
+
const id = part.toolCallId;
|
|
3962
|
+
const text = getToolResultText(part);
|
|
3963
|
+
if (!cliToolCallIds || cliToolCallIds.has(id)) {
|
|
3964
|
+
content.push({ type: "tool_result", tool_use_id: id, content: text });
|
|
3965
|
+
return;
|
|
3966
|
+
}
|
|
3967
|
+
log.info("rendering opencode-side tool result as text", {
|
|
3968
|
+
toolCallId: id,
|
|
3969
|
+
toolName: part.toolName,
|
|
3970
|
+
chars: text.length
|
|
3971
|
+
});
|
|
3972
|
+
content.push({
|
|
3973
|
+
type: "text",
|
|
3974
|
+
text: `<opencode_tool_result tool="${part.toolName ?? "unknown"}">
|
|
3975
|
+
${text}
|
|
3976
|
+
</opencode_tool_result>`
|
|
3977
|
+
});
|
|
3978
|
+
};
|
|
3979
|
+
if (compactionMode) {
|
|
3980
|
+
const transcript = compactConversationHistory(prompt, {
|
|
3981
|
+
mode: "compaction"
|
|
3982
|
+
});
|
|
3983
|
+
if (transcript) {
|
|
3984
|
+
log.info("including compaction transcript", {
|
|
3985
|
+
historyLength: transcript.length
|
|
3986
|
+
});
|
|
3987
|
+
content.push({
|
|
3988
|
+
type: "text",
|
|
3989
|
+
text: `<conversation_transcript>
|
|
3990
|
+
${transcript}
|
|
3991
|
+
</conversation_transcript>
|
|
3992
|
+
|
|
3993
|
+
The complete prior conversation appears above. The synthesis instructions follow below.
|
|
3994
|
+
|
|
3995
|
+
`
|
|
3996
|
+
});
|
|
3997
|
+
}
|
|
3998
|
+
} else if (includeHistoryContext) {
|
|
3999
|
+
const historyContext = compactConversationHistory(prompt);
|
|
4000
|
+
if (historyContext) {
|
|
4001
|
+
log.info("including conversation history context", {
|
|
4002
|
+
historyLength: historyContext.length
|
|
4003
|
+
});
|
|
4004
|
+
content.push({
|
|
4005
|
+
type: "text",
|
|
4006
|
+
text: `<conversation_history>
|
|
4007
|
+
The following is a summary of our conversation so far (from a previous session that couldn't be resumed):
|
|
4008
|
+
|
|
4009
|
+
${historyContext}
|
|
4010
|
+
|
|
4011
|
+
</conversation_history>
|
|
4012
|
+
|
|
4013
|
+
Now continuing with the current message:
|
|
4014
|
+
|
|
4015
|
+
`
|
|
4016
|
+
});
|
|
4017
|
+
}
|
|
4018
|
+
}
|
|
4019
|
+
const messages = [];
|
|
4020
|
+
for (let i = prompt.length - 1; i >= 0; i--) {
|
|
4021
|
+
if (prompt[i].role === "assistant") break;
|
|
4022
|
+
messages.unshift(prompt[i]);
|
|
4023
|
+
}
|
|
4024
|
+
for (const msg of messages) {
|
|
4025
|
+
if (msg.role === "user") {
|
|
4026
|
+
if (parseSideQuestionContent(msg.content) !== null) continue;
|
|
4027
|
+
if (typeof msg.content === "string") {
|
|
4028
|
+
const str2 = msg.content;
|
|
4029
|
+
if (str2.trim()) {
|
|
4030
|
+
content.push({ type: "text", text: str2 });
|
|
4031
|
+
}
|
|
4032
|
+
} else if (Array.isArray(msg.content)) {
|
|
4033
|
+
for (const part of msg.content) {
|
|
4034
|
+
if (part.type === "text") {
|
|
4035
|
+
if (part.text && part.text.trim()) {
|
|
4036
|
+
content.push({ type: "text", text: part.text });
|
|
4037
|
+
}
|
|
4038
|
+
} else if (part.type === "file" || part.type === "image") {
|
|
4039
|
+
const block = toImageBlock(part);
|
|
4040
|
+
if (block) {
|
|
4041
|
+
content.push(block);
|
|
4042
|
+
} else {
|
|
4043
|
+
log.debug("skipped non-image file part", {
|
|
4044
|
+
mediaType: part.mediaType
|
|
4045
|
+
});
|
|
4046
|
+
}
|
|
4047
|
+
} else if (part.type === "tool-result") {
|
|
4048
|
+
pushToolResult(part);
|
|
4049
|
+
}
|
|
4050
|
+
}
|
|
4051
|
+
}
|
|
4052
|
+
} else if (msg.role === "tool") {
|
|
4053
|
+
if (Array.isArray(msg.content)) {
|
|
4054
|
+
for (const part of msg.content) {
|
|
4055
|
+
if (part?.type === "tool-result") {
|
|
4056
|
+
pushToolResult(part);
|
|
4057
|
+
}
|
|
4058
|
+
}
|
|
4059
|
+
}
|
|
4060
|
+
}
|
|
4061
|
+
}
|
|
4062
|
+
if (content.length === 0) {
|
|
4063
|
+
log.warn("empty user content; sending sentinel to satisfy CLI");
|
|
4064
|
+
return JSON.stringify({
|
|
4065
|
+
type: "user",
|
|
4066
|
+
message: {
|
|
4067
|
+
role: "user",
|
|
4068
|
+
content: [{ type: "text", text: "(empty)" }]
|
|
4069
|
+
}
|
|
4070
|
+
});
|
|
4071
|
+
}
|
|
4072
|
+
return JSON.stringify({
|
|
4073
|
+
type: "user",
|
|
4074
|
+
message: {
|
|
4075
|
+
role: "user",
|
|
4076
|
+
content
|
|
4077
|
+
}
|
|
4078
|
+
});
|
|
4079
|
+
}
|
|
4080
|
+
|
|
4081
|
+
// src/agent-models.ts
|
|
4082
|
+
import { readFile, readdir } from "fs/promises";
|
|
4083
|
+
import path5 from "path";
|
|
4084
|
+
|
|
4085
|
+
// src/models.ts
|
|
4086
|
+
var PROVIDER_ID = "claude-code";
|
|
4087
|
+
var NPM = "@khalilgharbaoui/opencode-claude-code-plugin";
|
|
4088
|
+
var reasoningVariants = {
|
|
4089
|
+
low: { reasoningEffort: "low" },
|
|
4090
|
+
medium: { reasoningEffort: "medium" },
|
|
4091
|
+
high: { reasoningEffort: "high" },
|
|
4092
|
+
xhigh: { reasoningEffort: "xhigh" },
|
|
4093
|
+
max: { reasoningEffort: "max" }
|
|
4094
|
+
};
|
|
4095
|
+
var baseCapabilities = {
|
|
4096
|
+
temperature: false,
|
|
4097
|
+
attachment: true,
|
|
4098
|
+
toolcall: true,
|
|
4099
|
+
input: { text: true, audio: false, image: true, video: false, pdf: false },
|
|
4100
|
+
output: { text: true, audio: false, image: false, video: false, pdf: false },
|
|
4101
|
+
interleaved: false
|
|
4102
|
+
};
|
|
4103
|
+
function defineModel(opts) {
|
|
4104
|
+
return {
|
|
4105
|
+
id: opts.id,
|
|
4106
|
+
providerID: PROVIDER_ID,
|
|
4107
|
+
api: { id: opts.id, url: "", npm: NPM },
|
|
4108
|
+
name: `${opts.name} (${opts.multiplier}\xD7)`,
|
|
4109
|
+
family: opts.family,
|
|
4110
|
+
capabilities: { ...baseCapabilities, reasoning: opts.reasoning },
|
|
4111
|
+
cost: {
|
|
4112
|
+
input: opts.cost.input,
|
|
4113
|
+
output: opts.cost.output,
|
|
4114
|
+
cache: { read: opts.cost.cacheRead, write: opts.cost.cacheWrite }
|
|
4115
|
+
},
|
|
4116
|
+
limit: { context: opts.context, output: opts.output },
|
|
4117
|
+
status: opts.status ?? "active",
|
|
4118
|
+
options: {},
|
|
4119
|
+
headers: {},
|
|
4120
|
+
release_date: opts.releaseDate,
|
|
4121
|
+
variants: opts.reasoning ? reasoningVariants : void 0
|
|
4122
|
+
};
|
|
4123
|
+
}
|
|
4124
|
+
var haikuCost = { input: 1, output: 5, cacheRead: 0.1, cacheWrite: 1.25 };
|
|
4125
|
+
var sonnetCost = { input: 3, output: 15, cacheRead: 0.3, cacheWrite: 3.75 };
|
|
4126
|
+
var opusCost = { input: 5, output: 25, cacheRead: 0.5, cacheWrite: 6.25 };
|
|
4127
|
+
var fableCost = { input: 10, output: 50, cacheRead: 1, cacheWrite: 12.5 };
|
|
4128
|
+
var fable51Cost = { input: 10, output: 50, cacheRead: 0.25, cacheWrite: 12.5 };
|
|
4129
|
+
var opusFastCost = { input: 10, output: 50, cacheRead: 1, cacheWrite: 12.5 };
|
|
4130
|
+
function toConfigModel(model) {
|
|
4131
|
+
const inputMods = [];
|
|
4132
|
+
const outputMods = [];
|
|
4133
|
+
for (const [k, v] of Object.entries(model.capabilities.input)) {
|
|
4134
|
+
if (v) inputMods.push(k);
|
|
4135
|
+
}
|
|
4136
|
+
for (const [k, v] of Object.entries(model.capabilities.output)) {
|
|
4137
|
+
if (v) outputMods.push(k);
|
|
4138
|
+
}
|
|
4139
|
+
return {
|
|
4140
|
+
id: model.api.id,
|
|
4141
|
+
name: model.name,
|
|
4142
|
+
status: model.status,
|
|
4143
|
+
family: model.family ?? "",
|
|
4144
|
+
release_date: model.release_date,
|
|
4145
|
+
temperature: model.capabilities.temperature,
|
|
4146
|
+
reasoning: model.capabilities.reasoning,
|
|
4147
|
+
attachment: model.capabilities.attachment,
|
|
4148
|
+
tool_call: model.capabilities.toolcall,
|
|
4149
|
+
modalities: { input: inputMods, output: outputMods },
|
|
4150
|
+
cost: {
|
|
4151
|
+
input: model.cost.input,
|
|
4152
|
+
output: model.cost.output,
|
|
4153
|
+
cache_read: model.cost.cache.read,
|
|
4154
|
+
cache_write: model.cost.cache.write
|
|
4155
|
+
},
|
|
4156
|
+
limit: model.limit,
|
|
4157
|
+
options: model.options,
|
|
4158
|
+
headers: model.headers,
|
|
4159
|
+
variants: model.variants
|
|
4160
|
+
};
|
|
4161
|
+
}
|
|
4162
|
+
var defaultModels = {
|
|
4163
|
+
"claude-haiku-4-5": defineModel({
|
|
4164
|
+
id: "claude-haiku-4-5",
|
|
4165
|
+
name: "Claude Haiku 4.5",
|
|
4166
|
+
family: "haiku",
|
|
4167
|
+
reasoning: false,
|
|
4168
|
+
context: 2e5,
|
|
4169
|
+
output: 64e3,
|
|
4170
|
+
cost: haikuCost,
|
|
4171
|
+
multiplier: 1,
|
|
4172
|
+
releaseDate: "2025-10-01"
|
|
4173
|
+
}),
|
|
4174
|
+
"claude-sonnet-4-5": defineModel({
|
|
4175
|
+
id: "claude-sonnet-4-5",
|
|
4176
|
+
name: "Claude Sonnet 4.5",
|
|
4177
|
+
family: "sonnet",
|
|
4178
|
+
reasoning: true,
|
|
4179
|
+
context: 2e5,
|
|
4180
|
+
output: 64e3,
|
|
4181
|
+
cost: sonnetCost,
|
|
4182
|
+
multiplier: 3,
|
|
4183
|
+
releaseDate: "2025-09-29"
|
|
4184
|
+
}),
|
|
4185
|
+
"claude-sonnet-4-6": defineModel({
|
|
4186
|
+
id: "claude-sonnet-4-6",
|
|
4187
|
+
name: "Claude Sonnet 4.6",
|
|
4188
|
+
family: "sonnet",
|
|
4189
|
+
reasoning: true,
|
|
4190
|
+
context: 1e6,
|
|
4191
|
+
output: 128e3,
|
|
4192
|
+
cost: sonnetCost,
|
|
4193
|
+
multiplier: 3,
|
|
4194
|
+
releaseDate: "2025-06-19"
|
|
4195
|
+
}),
|
|
4196
|
+
"claude-sonnet-5": defineModel({
|
|
4197
|
+
id: "claude-sonnet-5",
|
|
4198
|
+
name: "Claude Sonnet 5",
|
|
4199
|
+
family: "sonnet",
|
|
4200
|
+
reasoning: true,
|
|
4201
|
+
context: 1e6,
|
|
4202
|
+
output: 128e3,
|
|
4203
|
+
cost: sonnetCost,
|
|
4204
|
+
multiplier: 3,
|
|
4205
|
+
releaseDate: "2026-06-30"
|
|
4206
|
+
}),
|
|
4207
|
+
"claude-opus-4-5": defineModel({
|
|
4208
|
+
id: "claude-opus-4-5",
|
|
4209
|
+
name: "Claude Opus 4.5",
|
|
4210
|
+
family: "opus",
|
|
4211
|
+
reasoning: true,
|
|
4212
|
+
context: 2e5,
|
|
4213
|
+
output: 64e3,
|
|
4214
|
+
cost: opusCost,
|
|
4215
|
+
multiplier: 5,
|
|
4216
|
+
releaseDate: "2025-11-01"
|
|
4217
|
+
}),
|
|
4218
|
+
"claude-opus-4-6": defineModel({
|
|
4219
|
+
id: "claude-opus-4-6",
|
|
4220
|
+
name: "Claude Opus 4.6",
|
|
4221
|
+
family: "opus",
|
|
4222
|
+
reasoning: true,
|
|
4223
|
+
context: 1e6,
|
|
4224
|
+
output: 128e3,
|
|
4225
|
+
cost: opusCost,
|
|
4226
|
+
multiplier: 5,
|
|
4227
|
+
releaseDate: "2025-06-19"
|
|
4228
|
+
}),
|
|
4229
|
+
"claude-opus-4-7": defineModel({
|
|
4230
|
+
id: "claude-opus-4-7",
|
|
4231
|
+
name: "Claude Opus 4.7",
|
|
4232
|
+
family: "opus",
|
|
4233
|
+
reasoning: true,
|
|
4234
|
+
context: 1e6,
|
|
4235
|
+
output: 128e3,
|
|
4236
|
+
cost: opusCost,
|
|
4237
|
+
multiplier: 5,
|
|
4238
|
+
releaseDate: "2025-07-16"
|
|
4239
|
+
}),
|
|
4240
|
+
"claude-opus-4-8": defineModel({
|
|
4241
|
+
id: "claude-opus-4-8",
|
|
4242
|
+
name: "Claude Opus 4.8",
|
|
4243
|
+
family: "opus",
|
|
4244
|
+
reasoning: true,
|
|
4245
|
+
context: 1e6,
|
|
4246
|
+
output: 128e3,
|
|
4247
|
+
cost: opusCost,
|
|
4248
|
+
multiplier: 5,
|
|
4249
|
+
releaseDate: "2026-05-28"
|
|
4250
|
+
}),
|
|
4251
|
+
// Fast mode. The `-fast` suffix is OUR marker, not a model name Anthropic
|
|
4252
|
+
// serves: `parseModelId` strips it before `--model` and turns it into
|
|
4253
|
+
// `--settings {"fastMode":true}` on the spawn. Retired `-fast` model strings
|
|
4254
|
+
// (`claude-opus-4-6-fast`) are a different thing and are not registered here.
|
|
4255
|
+
//
|
|
4256
|
+
// Only Opus 4.8 and Opus 5 qualify: the CLI gates fast mode on the resolved
|
|
4257
|
+
// model name containing `opus-4-8` or `opus-5`, so registering a fast entry
|
|
4258
|
+
// for any other model would produce a picker option that silently runs at
|
|
4259
|
+
// standard speed while displaying the 10x price.
|
|
4260
|
+
"claude-opus-4-8-fast": defineModel({
|
|
4261
|
+
id: "claude-opus-4-8-fast",
|
|
4262
|
+
name: "Claude Opus 4.8 Fast",
|
|
4263
|
+
family: "opus",
|
|
4264
|
+
reasoning: true,
|
|
4265
|
+
context: 1e6,
|
|
4266
|
+
output: 128e3,
|
|
4267
|
+
cost: opusFastCost,
|
|
4268
|
+
multiplier: 10,
|
|
4269
|
+
releaseDate: "2026-05-28"
|
|
4270
|
+
}),
|
|
4271
|
+
"claude-opus-5": defineModel({
|
|
4272
|
+
id: "claude-opus-5",
|
|
4273
|
+
name: "Claude Opus 5",
|
|
4274
|
+
family: "opus",
|
|
4275
|
+
reasoning: true,
|
|
4276
|
+
context: 1e6,
|
|
4277
|
+
output: 128e3,
|
|
4278
|
+
cost: opusCost,
|
|
4279
|
+
multiplier: 5,
|
|
4280
|
+
releaseDate: "2026-07-24"
|
|
4281
|
+
}),
|
|
4282
|
+
"claude-opus-5-fast": defineModel({
|
|
4283
|
+
id: "claude-opus-5-fast",
|
|
4284
|
+
name: "Claude Opus 5 Fast",
|
|
4285
|
+
family: "opus",
|
|
4286
|
+
reasoning: true,
|
|
4287
|
+
context: 1e6,
|
|
4288
|
+
output: 128e3,
|
|
4289
|
+
cost: opusFastCost,
|
|
4290
|
+
multiplier: 10,
|
|
4291
|
+
releaseDate: "2026-07-24"
|
|
4292
|
+
}),
|
|
4293
|
+
"claude-fable-5": defineModel({
|
|
4294
|
+
id: "claude-fable-5",
|
|
4295
|
+
name: "Claude Fable 5",
|
|
4296
|
+
family: "fable",
|
|
4297
|
+
reasoning: true,
|
|
4298
|
+
context: 1e6,
|
|
4299
|
+
output: 128e3,
|
|
4300
|
+
cost: fableCost,
|
|
4301
|
+
multiplier: 10,
|
|
4302
|
+
releaseDate: "2026-06-09"
|
|
4303
|
+
}),
|
|
4304
|
+
"claude-fable-5-1": defineModel({
|
|
4305
|
+
id: "claude-fable-5-1",
|
|
4306
|
+
name: "Claude Fable 5.1",
|
|
4307
|
+
family: "fable",
|
|
4308
|
+
reasoning: true,
|
|
4309
|
+
context: 1e6,
|
|
4310
|
+
output: 128e3,
|
|
4311
|
+
cost: fable51Cost,
|
|
4312
|
+
multiplier: 10,
|
|
4313
|
+
releaseDate: "2026-09-01"
|
|
4314
|
+
}),
|
|
4315
|
+
// Mythos 5 and 5.1 share the corresponding Fable models' capabilities and
|
|
4316
|
+
// pricing without the safety classifiers; limited availability via Project
|
|
4317
|
+
// Glasswing. `claude --model` simply errors for accounts without access, so
|
|
4318
|
+
// they are safe to register unconditionally.
|
|
4319
|
+
"claude-mythos-5": defineModel({
|
|
4320
|
+
id: "claude-mythos-5",
|
|
4321
|
+
name: "Claude Mythos 5",
|
|
4322
|
+
family: "mythos",
|
|
4323
|
+
reasoning: true,
|
|
4324
|
+
context: 1e6,
|
|
4325
|
+
output: 128e3,
|
|
4326
|
+
cost: fableCost,
|
|
4327
|
+
multiplier: 10,
|
|
4328
|
+
releaseDate: "2026-06-09"
|
|
4329
|
+
}),
|
|
4330
|
+
"claude-mythos-5-1": defineModel({
|
|
4331
|
+
id: "claude-mythos-5-1",
|
|
4332
|
+
name: "Claude Mythos 5.1",
|
|
4333
|
+
family: "mythos",
|
|
4334
|
+
reasoning: true,
|
|
4335
|
+
context: 1e6,
|
|
4336
|
+
output: 128e3,
|
|
4337
|
+
cost: fable51Cost,
|
|
4338
|
+
multiplier: 10,
|
|
4339
|
+
releaseDate: "2026-09-01"
|
|
4340
|
+
})
|
|
4341
|
+
};
|
|
4342
|
+
var FAST_SUFFIX = "-fast";
|
|
4343
|
+
function parseModelId(modelId) {
|
|
4344
|
+
const at = modelId.indexOf("@");
|
|
4345
|
+
const base = at === -1 ? modelId : modelId.slice(0, at);
|
|
4346
|
+
const account = at === -1 ? "" : modelId.slice(at);
|
|
4347
|
+
if (!base.endsWith(FAST_SUFFIX)) return { model: modelId, fast: false };
|
|
4348
|
+
if (!Object.hasOwn(defaultModels, base)) return { model: modelId, fast: false };
|
|
4349
|
+
return { model: base.slice(0, -FAST_SUFFIX.length) + account, fast: true };
|
|
3513
4350
|
}
|
|
3514
|
-
|
|
3515
|
-
|
|
3516
|
-
|
|
3517
|
-
|
|
3518
|
-
|
|
3519
|
-
|
|
3520
|
-
|
|
3521
|
-
|
|
3522
|
-
|
|
3523
|
-
|
|
3524
|
-
|
|
3525
|
-
|
|
4351
|
+
|
|
4352
|
+
// src/agent-models.ts
|
|
4353
|
+
var AGENT_DIR_NAMES = ["agents", "agent"];
|
|
4354
|
+
var REASONING_EFFORTS = [
|
|
4355
|
+
"minimal",
|
|
4356
|
+
"low",
|
|
4357
|
+
"medium",
|
|
4358
|
+
"high",
|
|
4359
|
+
"xhigh",
|
|
4360
|
+
"max"
|
|
4361
|
+
];
|
|
4362
|
+
var registry = {};
|
|
4363
|
+
var defaultSubagentModel;
|
|
4364
|
+
function setAgentRegistry(records) {
|
|
4365
|
+
registry = records;
|
|
3526
4366
|
}
|
|
3527
|
-
function
|
|
3528
|
-
|
|
3529
|
-
let current = path5.resolve(opts.start);
|
|
3530
|
-
while (true) {
|
|
3531
|
-
for (const target of opts.targets) {
|
|
3532
|
-
const candidate = path5.join(current, target);
|
|
3533
|
-
if (opts.predicate(candidate)) out.push(candidate);
|
|
3534
|
-
}
|
|
3535
|
-
if (opts.stop && current === path5.resolve(opts.stop)) break;
|
|
3536
|
-
const parent = path5.dirname(current);
|
|
3537
|
-
if (parent === current) break;
|
|
3538
|
-
current = parent;
|
|
3539
|
-
}
|
|
3540
|
-
return out;
|
|
4367
|
+
function getAgentRegistry() {
|
|
4368
|
+
return registry;
|
|
3541
4369
|
}
|
|
3542
|
-
function
|
|
3543
|
-
|
|
3544
|
-
if (override) return path5.resolve(override);
|
|
3545
|
-
let current = path5.resolve(cwd);
|
|
3546
|
-
while (true) {
|
|
3547
|
-
const gitPath = path5.join(current, ".git");
|
|
3548
|
-
try {
|
|
3549
|
-
if (fs4.existsSync(gitPath)) return current;
|
|
3550
|
-
} catch {
|
|
3551
|
-
}
|
|
3552
|
-
const parent = path5.dirname(current);
|
|
3553
|
-
if (parent === current) return void 0;
|
|
3554
|
-
current = parent;
|
|
3555
|
-
}
|
|
4370
|
+
function setDefaultSubagentModel(model) {
|
|
4371
|
+
defaultSubagentModel = model?.trim() || void 0;
|
|
3556
4372
|
}
|
|
3557
|
-
function
|
|
3558
|
-
|
|
3559
|
-
return path5.join(xdg, "opencode");
|
|
4373
|
+
function getDefaultSubagentModel() {
|
|
4374
|
+
return defaultSubagentModel;
|
|
3560
4375
|
}
|
|
3561
|
-
function
|
|
3562
|
-
const
|
|
3563
|
-
|
|
3564
|
-
for (const name of FILE_NAMES.slice().reverse()) {
|
|
3565
|
-
const file = path5.join(dir, name);
|
|
3566
|
-
if (!fileExists2(file)) continue;
|
|
3567
|
-
const parsed = readAndParse(file);
|
|
3568
|
-
if (parsed) merged = deepMerge(merged, parsed);
|
|
3569
|
-
}
|
|
3570
|
-
return merged;
|
|
4376
|
+
function accountMarker(modelId) {
|
|
4377
|
+
const at = modelId.indexOf("@");
|
|
4378
|
+
return at === -1 ? "" : modelId.slice(at);
|
|
3571
4379
|
}
|
|
3572
|
-
function
|
|
3573
|
-
|
|
3574
|
-
|
|
3575
|
-
const file = path5.join(dir, name);
|
|
3576
|
-
if (!fileExists2(file)) continue;
|
|
3577
|
-
const parsed = readAndParse(file);
|
|
3578
|
-
if (parsed) merged = deepMerge(merged, parsed);
|
|
3579
|
-
}
|
|
3580
|
-
return merged;
|
|
4380
|
+
function withoutAccountMarker(modelId) {
|
|
4381
|
+
const at = modelId.indexOf("@");
|
|
4382
|
+
return at === -1 ? modelId : modelId.slice(0, at);
|
|
3581
4383
|
}
|
|
3582
|
-
function
|
|
3583
|
-
|
|
3584
|
-
const
|
|
3585
|
-
|
|
3586
|
-
|
|
3587
|
-
|
|
3588
|
-
|
|
3589
|
-
|
|
3590
|
-
|
|
3591
|
-
|
|
3592
|
-
|
|
3593
|
-
|
|
3594
|
-
|
|
3595
|
-
|
|
3596
|
-
|
|
3597
|
-
|
|
3598
|
-
|
|
4384
|
+
function resolveAgentModel(agent, modelId, overrides) {
|
|
4385
|
+
if (!agent) return modelId;
|
|
4386
|
+
const record = (overrides?.records ?? registry)[agent];
|
|
4387
|
+
if (!record) return modelId;
|
|
4388
|
+
if (record.model?.includes("/")) return modelId;
|
|
4389
|
+
const fallback = overrides ? overrides.defaultSubagentModel : defaultSubagentModel;
|
|
4390
|
+
const declared = record.forceModel?.trim();
|
|
4391
|
+
const wanted = declared || (record.mode === "subagent" ? fallback : void 0);
|
|
4392
|
+
if (!wanted) return modelId;
|
|
4393
|
+
const base = withoutAccountMarker(wanted);
|
|
4394
|
+
if (!Object.hasOwn(defaultModels, base)) {
|
|
4395
|
+
log.warn("agent model override refused: unknown model", {
|
|
4396
|
+
agent,
|
|
4397
|
+
wanted: base,
|
|
4398
|
+
keeping: modelId
|
|
4399
|
+
});
|
|
4400
|
+
return modelId;
|
|
3599
4401
|
}
|
|
3600
|
-
const
|
|
3601
|
-
if (
|
|
3602
|
-
|
|
3603
|
-
if (dirExists2(homeDot)) push(homeDot);
|
|
4402
|
+
const resolved = `${base}${accountMarker(modelId)}`;
|
|
4403
|
+
if (resolved !== modelId) {
|
|
4404
|
+
log.debug("agent model override", { agent, from: modelId, to: resolved });
|
|
3604
4405
|
}
|
|
3605
|
-
|
|
3606
|
-
if (envDir && dirExists2(envDir)) push(envDir);
|
|
3607
|
-
return dirs;
|
|
4406
|
+
return resolved;
|
|
3608
4407
|
}
|
|
3609
|
-
function
|
|
3610
|
-
|
|
3611
|
-
|
|
3612
|
-
|
|
3613
|
-
|
|
3614
|
-
|
|
3615
|
-
|
|
4408
|
+
function resolveAgentEffort(agent, inherited, overrides) {
|
|
4409
|
+
if (!agent) return inherited;
|
|
4410
|
+
const record = (overrides?.records ?? registry)[agent];
|
|
4411
|
+
const declared = record?.reasoningEffort?.trim();
|
|
4412
|
+
if (!declared) return inherited;
|
|
4413
|
+
if (!REASONING_EFFORTS.includes(declared)) {
|
|
4414
|
+
log.warn("agent effort override refused: unknown level", {
|
|
4415
|
+
agent,
|
|
4416
|
+
wanted: declared,
|
|
4417
|
+
keeping: inherited
|
|
3616
4418
|
});
|
|
4419
|
+
return inherited;
|
|
3617
4420
|
}
|
|
3618
|
-
|
|
3619
|
-
|
|
3620
|
-
|
|
3621
|
-
|
|
3622
|
-
|
|
3623
|
-
|
|
3624
|
-
const cmd = spec.command;
|
|
3625
|
-
if (!Array.isArray(cmd) || cmd.length === 0) {
|
|
3626
|
-
log.warn("skipping local MCP server with no command", { name });
|
|
3627
|
-
return null;
|
|
3628
|
-
}
|
|
3629
|
-
const out = {
|
|
3630
|
-
type: "stdio",
|
|
3631
|
-
command: String(cmd[0])
|
|
3632
|
-
};
|
|
3633
|
-
if (cmd.length > 1) out.args = cmd.slice(1).map((s) => String(s));
|
|
3634
|
-
if (spec.environment && typeof spec.environment === "object") {
|
|
3635
|
-
out.env = substituteEnvPlaceholders(
|
|
3636
|
-
spec.environment
|
|
3637
|
-
);
|
|
3638
|
-
}
|
|
3639
|
-
return out;
|
|
3640
|
-
}
|
|
3641
|
-
if (type === "remote") {
|
|
3642
|
-
if (typeof spec.url !== "string" || !spec.url) {
|
|
3643
|
-
log.warn("skipping remote MCP server with no url", { name });
|
|
3644
|
-
return null;
|
|
3645
|
-
}
|
|
3646
|
-
const out = {
|
|
3647
|
-
type: "http",
|
|
3648
|
-
url: spec.url
|
|
3649
|
-
};
|
|
3650
|
-
if (spec.headers && typeof spec.headers === "object") {
|
|
3651
|
-
out.headers = substituteEnvPlaceholders(
|
|
3652
|
-
spec.headers
|
|
3653
|
-
);
|
|
3654
|
-
}
|
|
3655
|
-
return out;
|
|
4421
|
+
if (declared !== inherited) {
|
|
4422
|
+
log.debug("agent effort override", {
|
|
4423
|
+
agent,
|
|
4424
|
+
from: inherited,
|
|
4425
|
+
to: declared
|
|
4426
|
+
});
|
|
3656
4427
|
}
|
|
3657
|
-
|
|
3658
|
-
name,
|
|
3659
|
-
type: type ?? null
|
|
3660
|
-
});
|
|
3661
|
-
return null;
|
|
3662
|
-
}
|
|
3663
|
-
function extractMcpBlock(config) {
|
|
3664
|
-
const mcp = config.mcp;
|
|
3665
|
-
if (!mcp || typeof mcp !== "object" || Array.isArray(mcp)) return {};
|
|
3666
|
-
return mcp;
|
|
4428
|
+
return declared;
|
|
3667
4429
|
}
|
|
3668
|
-
function
|
|
3669
|
-
const
|
|
3670
|
-
|
|
3671
|
-
|
|
3672
|
-
|
|
3673
|
-
|
|
3674
|
-
|
|
3675
|
-
|
|
3676
|
-
|
|
3677
|
-
|
|
3678
|
-
|
|
3679
|
-
|
|
3680
|
-
|
|
4430
|
+
function parseAgentFrontmatter(text) {
|
|
4431
|
+
const record = {};
|
|
4432
|
+
if (!text.startsWith("---")) return record;
|
|
4433
|
+
const lines = text.split(/\r?\n/);
|
|
4434
|
+
for (let i = 1; i < lines.length; i++) {
|
|
4435
|
+
const line = lines[i];
|
|
4436
|
+
if (line.trim() === "---") break;
|
|
4437
|
+
const match = /^([A-Za-z_][A-Za-z0-9_-]*):[ \t]*(.*)$/.exec(line);
|
|
4438
|
+
if (!match) continue;
|
|
4439
|
+
const key = match[1];
|
|
4440
|
+
if (key !== "mode" && key !== "model" && key !== "forceModel" && key !== "reasoningEffort")
|
|
4441
|
+
continue;
|
|
4442
|
+
const value = match[2].trim().replace(/^["']|["']$/g, "");
|
|
4443
|
+
if (value) record[key] = value;
|
|
3681
4444
|
}
|
|
3682
|
-
return
|
|
4445
|
+
return record;
|
|
3683
4446
|
}
|
|
3684
|
-
function
|
|
3685
|
-
const {
|
|
3686
|
-
|
|
3687
|
-
|
|
3688
|
-
|
|
3689
|
-
|
|
3690
|
-
|
|
3691
|
-
|
|
3692
|
-
|
|
3693
|
-
|
|
3694
|
-
|
|
3695
|
-
|
|
3696
|
-
|
|
3697
|
-
|
|
3698
|
-
|
|
4447
|
+
async function readAgentMarkdownRecords(directories) {
|
|
4448
|
+
const records = {};
|
|
4449
|
+
for (const directory of directories) {
|
|
4450
|
+
let entries;
|
|
4451
|
+
try {
|
|
4452
|
+
entries = await readdir(directory);
|
|
4453
|
+
} catch {
|
|
4454
|
+
continue;
|
|
4455
|
+
}
|
|
4456
|
+
for (const entry of entries) {
|
|
4457
|
+
if (!entry.endsWith(".md")) continue;
|
|
4458
|
+
const name = entry.slice(0, -3);
|
|
4459
|
+
if (records[name]) continue;
|
|
4460
|
+
try {
|
|
4461
|
+
const text = await readFile(path5.join(directory, entry), "utf8");
|
|
4462
|
+
records[name] = parseAgentFrontmatter(text);
|
|
4463
|
+
} catch (err) {
|
|
4464
|
+
log.debug("failed to read agent markdown", {
|
|
4465
|
+
file: path5.join(directory, entry),
|
|
4466
|
+
error: String(err)
|
|
4467
|
+
});
|
|
4468
|
+
}
|
|
3699
4469
|
}
|
|
3700
4470
|
}
|
|
3701
|
-
return
|
|
3702
|
-
servers,
|
|
3703
|
-
bridgedServerNames,
|
|
3704
|
-
allEnabledServerNames,
|
|
3705
|
-
hash,
|
|
3706
|
-
excludeServers
|
|
3707
|
-
});
|
|
4471
|
+
return records;
|
|
3708
4472
|
}
|
|
3709
|
-
function
|
|
3710
|
-
const
|
|
3711
|
-
|
|
3712
|
-
|
|
3713
|
-
|
|
3714
|
-
if (explicitConfig && fileExists2(explicitConfig)) {
|
|
3715
|
-
const parsed = readAndParse(explicitConfig);
|
|
3716
|
-
if (parsed) merged = mergeMcp(merged, extractMcpBlock(parsed));
|
|
3717
|
-
}
|
|
3718
|
-
const projectFiles = walkUp({
|
|
3719
|
-
start: cwd,
|
|
3720
|
-
stop: worktree,
|
|
3721
|
-
targets: PROJECT_FILE_NAMES,
|
|
3722
|
-
predicate: fileExists2
|
|
3723
|
-
});
|
|
3724
|
-
const projectDirs = [];
|
|
3725
|
-
const seenProjectDirs = /* @__PURE__ */ new Set();
|
|
3726
|
-
for (const f of projectFiles) {
|
|
3727
|
-
const d = path5.dirname(f);
|
|
3728
|
-
if (!seenProjectDirs.has(d)) {
|
|
3729
|
-
seenProjectDirs.add(d);
|
|
3730
|
-
projectDirs.push(d);
|
|
4473
|
+
function agentDirectories(home, projectDirectory) {
|
|
4474
|
+
const directories = [];
|
|
4475
|
+
if (projectDirectory) {
|
|
4476
|
+
for (const name of AGENT_DIR_NAMES) {
|
|
4477
|
+
directories.push(path5.join(projectDirectory, ".opencode", name));
|
|
3731
4478
|
}
|
|
3732
4479
|
}
|
|
3733
|
-
|
|
3734
|
-
|
|
3735
|
-
|
|
3736
|
-
for (const dir of dotOpencodeDirs(cwd, worktree)) {
|
|
3737
|
-
merged = mergeMcp(merged, extractMcpBlock(loadProjectFilesInDir(dir)));
|
|
3738
|
-
}
|
|
3739
|
-
if (runtimeStatus) {
|
|
3740
|
-
for (const name of Object.keys(merged)) {
|
|
3741
|
-
const status = runtimeStatus[name];
|
|
3742
|
-
if (status === void 0) continue;
|
|
3743
|
-
const existing = merged[name];
|
|
3744
|
-
const base = existing && typeof existing === "object" ? existing : {};
|
|
3745
|
-
merged[name] = { ...base, enabled: status === "connected" };
|
|
4480
|
+
if (home) {
|
|
4481
|
+
for (const name of AGENT_DIR_NAMES) {
|
|
4482
|
+
directories.push(path5.join(home, ".config", "opencode", name));
|
|
3746
4483
|
}
|
|
3747
4484
|
}
|
|
3748
|
-
|
|
3749
|
-
for (const [name, spec] of Object.entries(merged)) {
|
|
3750
|
-
if (!spec || typeof spec !== "object") continue;
|
|
3751
|
-
const enabled = spec.enabled;
|
|
3752
|
-
if (enabled === false) continue;
|
|
3753
|
-
enabledServerNames.push(name);
|
|
3754
|
-
}
|
|
3755
|
-
const mergedBody = JSON.stringify({ mcpServers: merged }, null, 2);
|
|
3756
|
-
const hash = crypto3.createHash("sha256").update(mergedBody).digest("hex").slice(0, 12);
|
|
3757
|
-
return { servers: merged, enabledServerNames, hash };
|
|
4485
|
+
return directories;
|
|
3758
4486
|
}
|
|
3759
|
-
|
|
3760
|
-
|
|
3761
|
-
|
|
3762
|
-
|
|
3763
|
-
|
|
3764
|
-
|
|
3765
|
-
|
|
3766
|
-
|
|
3767
|
-
|
|
3768
|
-
allEnabledServerNames
|
|
3769
|
-
};
|
|
3770
|
-
}
|
|
3771
|
-
const body = JSON.stringify({ mcpServers: servers }, null, 2);
|
|
3772
|
-
const outPath = path5.join(
|
|
3773
|
-
pluginTmpDir(),
|
|
3774
|
-
`mcp-${hash}.json`
|
|
3775
|
-
);
|
|
4487
|
+
|
|
4488
|
+
// src/skill-bridge.ts
|
|
4489
|
+
import * as crypto3 from "crypto";
|
|
4490
|
+
import * as fs5 from "fs";
|
|
4491
|
+
import * as os3 from "os";
|
|
4492
|
+
import * as path6 from "path";
|
|
4493
|
+
import { fileURLToPath as fileURLToPath2 } from "url";
|
|
4494
|
+
var SKILL_PLUGIN_NAME = "opencode-skills";
|
|
4495
|
+
function bundledSkillsDir() {
|
|
3776
4496
|
try {
|
|
3777
|
-
|
|
3778
|
-
|
|
3779
|
-
|
|
3780
|
-
} catch
|
|
3781
|
-
log.warn("failed to write bridged MCP config", {
|
|
3782
|
-
error: e instanceof Error ? e.message : String(e)
|
|
3783
|
-
});
|
|
4497
|
+
const here = fileURLToPath2(import.meta.url);
|
|
4498
|
+
const dir = path6.resolve(path6.dirname(here), "..", "skills");
|
|
4499
|
+
return dirExists2(dir) ? dir : null;
|
|
4500
|
+
} catch {
|
|
3784
4501
|
return null;
|
|
3785
4502
|
}
|
|
3786
|
-
log.info("bridged opencode MCP config", {
|
|
3787
|
-
target: outPath,
|
|
3788
|
-
hash,
|
|
3789
|
-
servers: bridgedServerNames,
|
|
3790
|
-
excluded: excludeServers ? Array.from(excludeServers) : []
|
|
3791
|
-
});
|
|
3792
|
-
return {
|
|
3793
|
-
path: outPath,
|
|
3794
|
-
hash,
|
|
3795
|
-
serverNames: bridgedServerNames,
|
|
3796
|
-
allEnabledServerNames
|
|
3797
|
-
};
|
|
3798
4503
|
}
|
|
3799
|
-
|
|
3800
|
-
|
|
3801
|
-
|
|
3802
|
-
|
|
3803
|
-
|
|
3804
|
-
opencodeClient = client;
|
|
4504
|
+
function dirExists2(p) {
|
|
4505
|
+
try {
|
|
4506
|
+
return fs5.statSync(p).isDirectory();
|
|
4507
|
+
} catch {
|
|
4508
|
+
return false;
|
|
3805
4509
|
}
|
|
3806
4510
|
}
|
|
3807
|
-
function
|
|
3808
|
-
|
|
3809
|
-
|
|
3810
|
-
|
|
3811
|
-
|
|
3812
|
-
|
|
4511
|
+
function fileExists2(p) {
|
|
4512
|
+
try {
|
|
4513
|
+
return fs5.statSync(p).isFile();
|
|
4514
|
+
} catch {
|
|
4515
|
+
return false;
|
|
4516
|
+
}
|
|
3813
4517
|
}
|
|
3814
|
-
function
|
|
3815
|
-
|
|
4518
|
+
function skillRoots(cwd) {
|
|
4519
|
+
const roots = [];
|
|
4520
|
+
const seen = /* @__PURE__ */ new Set();
|
|
4521
|
+
const push = (p) => {
|
|
4522
|
+
const abs = path6.resolve(p);
|
|
4523
|
+
if (seen.has(abs)) return;
|
|
4524
|
+
seen.add(abs);
|
|
4525
|
+
if (dirExists2(abs)) roots.push(abs);
|
|
4526
|
+
};
|
|
4527
|
+
let current = path6.resolve(cwd);
|
|
4528
|
+
while (true) {
|
|
4529
|
+
push(path6.join(current, ".opencode", "skills"));
|
|
4530
|
+
const parent = path6.dirname(current);
|
|
4531
|
+
if (parent === current) break;
|
|
4532
|
+
current = parent;
|
|
4533
|
+
}
|
|
4534
|
+
const home = os3.homedir();
|
|
4535
|
+
if (home) push(path6.join(home, ".opencode", "skills"));
|
|
4536
|
+
const envDir = process.env.OPENCODE_CONFIG_DIR;
|
|
4537
|
+
if (envDir) push(path6.join(envDir, "skills"));
|
|
4538
|
+
const xdg = process.env.XDG_CONFIG_HOME ?? (home ? path6.join(home, ".config") : null);
|
|
4539
|
+
if (xdg) push(path6.join(xdg, "opencode", "skills"));
|
|
4540
|
+
return roots;
|
|
3816
4541
|
}
|
|
3817
|
-
function
|
|
3818
|
-
|
|
4542
|
+
function collectSkills(root, claimed, found) {
|
|
4543
|
+
let entries;
|
|
4544
|
+
try {
|
|
4545
|
+
entries = fs5.readdirSync(root, { withFileTypes: true });
|
|
4546
|
+
} catch {
|
|
4547
|
+
return;
|
|
4548
|
+
}
|
|
4549
|
+
for (const entry of entries) {
|
|
4550
|
+
if (!entry.isDirectory() && !entry.isSymbolicLink()) continue;
|
|
4551
|
+
const name = entry.name;
|
|
4552
|
+
if (name.startsWith(".")) continue;
|
|
4553
|
+
if (claimed.has(name)) continue;
|
|
4554
|
+
const dir = path6.join(root, name);
|
|
4555
|
+
if (!fileExists2(path6.join(dir, "SKILL.md"))) continue;
|
|
4556
|
+
claimed.add(name);
|
|
4557
|
+
found.push({ name, dir });
|
|
4558
|
+
}
|
|
3819
4559
|
}
|
|
3820
|
-
|
|
3821
|
-
|
|
3822
|
-
|
|
3823
|
-
|
|
3824
|
-
|
|
4560
|
+
var byName = (a, b) => a.name.localeCompare(b.name);
|
|
4561
|
+
function discoverOpencodeSkills(cwd) {
|
|
4562
|
+
const found = [];
|
|
4563
|
+
const claimed = /* @__PURE__ */ new Set();
|
|
4564
|
+
for (const root of skillRoots(cwd)) collectSkills(root, claimed, found);
|
|
4565
|
+
return found.sort(byName);
|
|
3825
4566
|
}
|
|
3826
|
-
|
|
3827
|
-
|
|
3828
|
-
|
|
3829
|
-
|
|
3830
|
-
|
|
3831
|
-
|
|
3832
|
-
opencodeProjectDirectory,
|
|
3833
|
-
sessionDir
|
|
3834
|
-
);
|
|
4567
|
+
function discoverBundledSkills() {
|
|
4568
|
+
const root = bundledSkillsDir();
|
|
4569
|
+
if (!root) return [];
|
|
4570
|
+
const found = [];
|
|
4571
|
+
collectSkills(root, /* @__PURE__ */ new Set(), found);
|
|
4572
|
+
return found.sort(byName);
|
|
3835
4573
|
}
|
|
3836
|
-
|
|
3837
|
-
|
|
3838
|
-
|
|
3839
|
-
|
|
3840
|
-
|
|
3841
|
-
|
|
3842
|
-
|
|
3843
|
-
|
|
3844
|
-
const dir = data.directory;
|
|
3845
|
-
return isUsableDirectory(dir) ? dir : void 0;
|
|
3846
|
-
} catch (err) {
|
|
3847
|
-
log.warn("failed to fetch opencode session directory", {
|
|
3848
|
-
sessionID,
|
|
3849
|
-
error: err instanceof Error ? err.message : String(err)
|
|
3850
|
-
});
|
|
3851
|
-
return void 0;
|
|
3852
|
-
}
|
|
4574
|
+
function registerBundledSkillPath(config) {
|
|
4575
|
+
const dir = bundledSkillsDir();
|
|
4576
|
+
if (!dir) return false;
|
|
4577
|
+
config.skills ??= {};
|
|
4578
|
+
const paths = config.skills.paths ??= [];
|
|
4579
|
+
if (paths.some((p) => path6.resolve(p) === dir)) return false;
|
|
4580
|
+
paths.push(dir);
|
|
4581
|
+
return true;
|
|
3853
4582
|
}
|
|
3854
|
-
|
|
3855
|
-
const client = opencodeClient;
|
|
3856
|
-
if (!client?.mcp?.status) return void 0;
|
|
4583
|
+
function linkSkill(source, target) {
|
|
3857
4584
|
try {
|
|
3858
|
-
|
|
3859
|
-
|
|
3860
|
-
|
|
3861
|
-
|
|
3862
|
-
for (const [name, entry] of Object.entries(data)) {
|
|
3863
|
-
if (entry && typeof entry === "object") {
|
|
3864
|
-
const status = entry.status;
|
|
3865
|
-
if (typeof status === "string") out[name] = status;
|
|
3866
|
-
}
|
|
3867
|
-
}
|
|
3868
|
-
return out;
|
|
3869
|
-
} catch (err) {
|
|
3870
|
-
log.warn("failed to fetch opencode MCP runtime status", {
|
|
3871
|
-
error: err instanceof Error ? err.message : String(err)
|
|
3872
|
-
});
|
|
3873
|
-
return void 0;
|
|
4585
|
+
fs5.symlinkSync(source, target, process.platform === "win32" ? "junction" : "dir");
|
|
4586
|
+
return;
|
|
4587
|
+
} catch {
|
|
4588
|
+
fs5.cpSync(source, target, { recursive: true, dereference: true });
|
|
3874
4589
|
}
|
|
3875
|
-
}
|
|
3876
|
-
|
|
3877
|
-
|
|
3878
|
-
|
|
3879
|
-
|
|
3880
|
-
|
|
3881
|
-
|
|
3882
|
-
|
|
3883
|
-
|
|
3884
|
-
|
|
3885
|
-
|
|
3886
|
-
|
|
3887
|
-
|
|
3888
|
-
|
|
3889
|
-
|
|
3890
|
-
|
|
3891
|
-
|
|
3892
|
-
|
|
3893
|
-
|
|
4590
|
+
}
|
|
4591
|
+
function buildSkillPluginDir(skills) {
|
|
4592
|
+
if (skills.length === 0) return null;
|
|
4593
|
+
const fingerprint = skills.map((s) => `${s.name}\0${s.dir}`).join("\n");
|
|
4594
|
+
const hash = crypto3.createHash("sha256").update(fingerprint).digest("hex").slice(0, 12);
|
|
4595
|
+
const root = path6.join(pluginTmpDir(), `skills-${hash}`);
|
|
4596
|
+
const manifest = path6.join(root, ".claude-plugin", "plugin.json");
|
|
4597
|
+
if (fileExists2(manifest)) return root;
|
|
4598
|
+
try {
|
|
4599
|
+
fs5.rmSync(root, { recursive: true, force: true });
|
|
4600
|
+
fs5.mkdirSync(path6.join(root, ".claude-plugin"), { recursive: true });
|
|
4601
|
+
fs5.mkdirSync(path6.join(root, "skills"), { recursive: true });
|
|
4602
|
+
fs5.writeFileSync(
|
|
4603
|
+
manifest,
|
|
4604
|
+
JSON.stringify(
|
|
4605
|
+
{
|
|
4606
|
+
name: SKILL_PLUGIN_NAME,
|
|
4607
|
+
description: "Skills discovered from this opencode installation, bridged into Claude Code."
|
|
4608
|
+
},
|
|
4609
|
+
null,
|
|
4610
|
+
2
|
|
4611
|
+
),
|
|
4612
|
+
{ encoding: "utf8", mode: 384 }
|
|
4613
|
+
);
|
|
4614
|
+
for (const skill of skills) {
|
|
4615
|
+
linkSkill(skill.dir, path6.join(root, "skills", skill.name));
|
|
3894
4616
|
}
|
|
3895
|
-
return out;
|
|
3896
4617
|
} catch (err) {
|
|
3897
|
-
log.warn("failed to
|
|
3898
|
-
|
|
3899
|
-
model,
|
|
4618
|
+
log.warn("failed to stage opencode skill plugin dir", {
|
|
4619
|
+
root,
|
|
3900
4620
|
error: err instanceof Error ? err.message : String(err)
|
|
3901
4621
|
});
|
|
3902
|
-
return
|
|
4622
|
+
return null;
|
|
4623
|
+
}
|
|
4624
|
+
return root;
|
|
4625
|
+
}
|
|
4626
|
+
async function resolveSkillPluginDirs(opts) {
|
|
4627
|
+
const user = opts.enabled ? discoverOpencodeSkills(opts.cwd) : [];
|
|
4628
|
+
const claimed = new Set(user.map((s) => s.name));
|
|
4629
|
+
const bundled = discoverBundledSkills().filter((s) => !claimed.has(s.name));
|
|
4630
|
+
const skills = [...user, ...bundled].sort(byName);
|
|
4631
|
+
if (skills.length === 0) return [];
|
|
4632
|
+
const supported = await detectCliSupportsFlag(opts.cliPath, "--plugin-dir");
|
|
4633
|
+
if (!supported) {
|
|
4634
|
+
log.notice(
|
|
4635
|
+
"claude cli does not support --plugin-dir; opencode skills will not be bridged. Run `npm i -g @anthropic-ai/claude-code` to upgrade.",
|
|
4636
|
+
{ skills: skills.length }
|
|
4637
|
+
);
|
|
4638
|
+
return [];
|
|
3903
4639
|
}
|
|
4640
|
+
const dir = buildSkillPluginDir(skills);
|
|
4641
|
+
if (!dir) return [];
|
|
4642
|
+
log.info("bridged opencode skills into claude", {
|
|
4643
|
+
count: skills.length,
|
|
4644
|
+
names: skills.map((s) => s.name),
|
|
4645
|
+
bundled: bundled.map((s) => s.name),
|
|
4646
|
+
pluginDir: dir
|
|
4647
|
+
});
|
|
4648
|
+
return [dir];
|
|
3904
4649
|
}
|
|
3905
4650
|
|
|
3906
4651
|
// src/claude-session-wrapper.ts
|
|
@@ -3909,12 +4654,12 @@ import { unlink as unlink2 } from "fs/promises";
|
|
|
3909
4654
|
|
|
3910
4655
|
// src/claude-session-bun.ts
|
|
3911
4656
|
import * as os4 from "os";
|
|
3912
|
-
import * as
|
|
3913
|
-
import * as
|
|
4657
|
+
import * as fs6 from "fs";
|
|
4658
|
+
import * as path7 from "path";
|
|
3914
4659
|
import { execFileSync } from "child_process";
|
|
3915
4660
|
import { randomUUID as randomUUID4 } from "crypto";
|
|
3916
4661
|
function resolveClaude(cmd = "claude") {
|
|
3917
|
-
if (
|
|
4662
|
+
if (path7.isAbsolute(cmd) && fs6.existsSync(cmd)) return cmd;
|
|
3918
4663
|
const viaBun = Bun.which(cmd);
|
|
3919
4664
|
if (viaBun) return viaBun;
|
|
3920
4665
|
const isWin = os4.platform() === "win32";
|
|
@@ -3923,25 +4668,25 @@ function resolveClaude(cmd = "claude") {
|
|
|
3923
4668
|
encoding: "utf8",
|
|
3924
4669
|
stdio: ["ignore", "pipe", "ignore"]
|
|
3925
4670
|
});
|
|
3926
|
-
const first = out.split(/\r?\n/).map((l) => l.trim()).filter(Boolean).find((p) =>
|
|
4671
|
+
const first = out.split(/\r?\n/).map((l) => l.trim()).filter(Boolean).find((p) => fs6.existsSync(p));
|
|
3927
4672
|
if (first) return first;
|
|
3928
4673
|
} catch {
|
|
3929
4674
|
}
|
|
3930
4675
|
throw new Error(`Could not resolve command on PATH: ${cmd}`);
|
|
3931
4676
|
}
|
|
3932
4677
|
function encodeCwd(cwd) {
|
|
3933
|
-
return
|
|
4678
|
+
return path7.resolve(cwd).replace(/[^a-zA-Z0-9]/g, "-");
|
|
3934
4679
|
}
|
|
3935
4680
|
var TERMINAL_STOP = /* @__PURE__ */ new Set(["end_turn", "stop_sequence", "max_tokens"]);
|
|
3936
4681
|
var delay = (ms) => new Promise((r) => setTimeout(r, ms));
|
|
3937
4682
|
function resolveConfigDir(configDir) {
|
|
3938
4683
|
const value = configDir ?? process.env.CLAUDE_CONFIG_DIR;
|
|
3939
|
-
if (!value) return
|
|
4684
|
+
if (!value) return path7.join(os4.homedir(), ".claude");
|
|
3940
4685
|
if (value === "~") return os4.homedir();
|
|
3941
4686
|
if (value.startsWith("~/") || value.startsWith("~\\")) {
|
|
3942
|
-
return
|
|
4687
|
+
return path7.join(os4.homedir(), value.slice(2));
|
|
3943
4688
|
}
|
|
3944
|
-
return
|
|
4689
|
+
return path7.resolve(value);
|
|
3945
4690
|
}
|
|
3946
4691
|
var ClaudeSession = class {
|
|
3947
4692
|
sessionId;
|
|
@@ -3959,11 +4704,11 @@ var ClaudeSession = class {
|
|
|
3959
4704
|
signal;
|
|
3960
4705
|
o;
|
|
3961
4706
|
constructor(opts = {}) {
|
|
3962
|
-
this.cwd =
|
|
4707
|
+
this.cwd = path7.resolve(opts.cwd ?? process.cwd());
|
|
3963
4708
|
this.configDir = resolveConfigDir(opts.configDir);
|
|
3964
4709
|
this.signal = opts.signal;
|
|
3965
4710
|
this.sessionId = randomUUID4();
|
|
3966
|
-
this.jsonlPath =
|
|
4711
|
+
this.jsonlPath = path7.join(
|
|
3967
4712
|
this.configDir,
|
|
3968
4713
|
"projects",
|
|
3969
4714
|
encodeCwd(this.cwd),
|
|
@@ -4086,7 +4831,7 @@ var ClaudeSession = class {
|
|
|
4086
4831
|
}
|
|
4087
4832
|
readRawLines() {
|
|
4088
4833
|
try {
|
|
4089
|
-
return
|
|
4834
|
+
return fs6.readFileSync(this.jsonlPath, "utf8").split("\n");
|
|
4090
4835
|
} catch {
|
|
4091
4836
|
return [];
|
|
4092
4837
|
}
|
|
@@ -4464,11 +5209,11 @@ function spawnInteractiveProcess(opts) {
|
|
|
4464
5209
|
}
|
|
4465
5210
|
|
|
4466
5211
|
// src/claude-code-language-model.ts
|
|
4467
|
-
import { readFileSync as
|
|
5212
|
+
import { readFileSync as readFileSync4, writeFileSync as writeFileSync4 } from "fs";
|
|
4468
5213
|
import { unlink as unlink3 } from "fs/promises";
|
|
4469
5214
|
import { homedir as homedir5, tmpdir as tmpdir2 } from "os";
|
|
4470
5215
|
import { randomUUID as randomUUID5 } from "crypto";
|
|
4471
|
-
import { dirname as
|
|
5216
|
+
import { dirname as dirname5, join as join8 } from "path";
|
|
4472
5217
|
var DEFAULT_COMPACTION_MODEL = "claude-haiku-4-5";
|
|
4473
5218
|
function resolveCompactionModel(configured) {
|
|
4474
5219
|
const env = process.env.CLAUDE_CODE_COMPACTION_MODEL?.trim();
|
|
@@ -4532,7 +5277,11 @@ var AUTO_CONTINUE_MAX_ATTEMPTS = 8;
|
|
|
4532
5277
|
var AUTO_CONTINUE_MAX_ELAPSED_MS = 10 * 60 * 1e3;
|
|
4533
5278
|
var AUTO_CONTINUE_NO_PROGRESS_LIMIT = 2;
|
|
4534
5279
|
var PROXY_RESULT_BOUNDARY_GRACE_MS = 250;
|
|
5280
|
+
var CHILD_EXIT_STATUS_GRACE_MS = 250;
|
|
4535
5281
|
var AUTO_CONTINUE_PROMPT = "Continue the task from where you stopped. Do not summarize; keep working until the requested task is complete, you need clarification, or you hit a real blocker.";
|
|
5282
|
+
function autoContinueEnabledFor(compactionMode, configured) {
|
|
5283
|
+
return compactionMode ? false : configured;
|
|
5284
|
+
}
|
|
4536
5285
|
function isTruncationStopReason(stopReason) {
|
|
4537
5286
|
return stopReason === "max_tokens" || stopReason === "max_output_tokens";
|
|
4538
5287
|
}
|
|
@@ -4690,7 +5439,7 @@ ${body}`;
|
|
|
4690
5439
|
}
|
|
4691
5440
|
function readPromptFileIfPresent(path9) {
|
|
4692
5441
|
try {
|
|
4693
|
-
const content =
|
|
5442
|
+
const content = readFileSync4(path9, "utf8").trim();
|
|
4694
5443
|
return content || void 0;
|
|
4695
5444
|
} catch {
|
|
4696
5445
|
return void 0;
|
|
@@ -4699,9 +5448,9 @@ function readPromptFileIfPresent(path9) {
|
|
|
4699
5448
|
function nearestWorkspaceAgentsPrompt(cwd) {
|
|
4700
5449
|
let dir = cwd;
|
|
4701
5450
|
while (true) {
|
|
4702
|
-
const content = readPromptFileIfPresent(
|
|
5451
|
+
const content = readPromptFileIfPresent(join8(dir, "AGENTS.md"));
|
|
4703
5452
|
if (content) return content;
|
|
4704
|
-
const parent =
|
|
5453
|
+
const parent = dirname5(dir);
|
|
4705
5454
|
if (parent === dir) return void 0;
|
|
4706
5455
|
dir = parent;
|
|
4707
5456
|
}
|
|
@@ -4785,8 +5534,8 @@ ${options.compressionSummary.trim()}`
|
|
|
4785
5534
|
for (const s of extraSystemContent) {
|
|
4786
5535
|
if (s.trim()) parts.push(s.trim());
|
|
4787
5536
|
}
|
|
4788
|
-
const configRoot = process.env.XDG_CONFIG_HOME ??
|
|
4789
|
-
const globalAgents = readPromptFileIfPresent(
|
|
5537
|
+
const configRoot = process.env.XDG_CONFIG_HOME ?? join8(homedir5(), ".config");
|
|
5538
|
+
const globalAgents = readPromptFileIfPresent(join8(configRoot, "opencode", "AGENTS.md"));
|
|
4790
5539
|
const workspaceAgents = nearestWorkspaceAgentsPrompt(cwd);
|
|
4791
5540
|
const forwarded = extraSystemContent.join("\n\n");
|
|
4792
5541
|
const pushGlobal = !!globalAgents && !forwarded.includes(globalAgents);
|
|
@@ -4797,7 +5546,7 @@ ${options.compressionSummary.trim()}`
|
|
|
4797
5546
|
if (includeMultiStepHint) parts.push(MULTI_STEP_TASK_HINT);
|
|
4798
5547
|
const content = parts.join("\n\n");
|
|
4799
5548
|
if (!content) return void 0;
|
|
4800
|
-
const path9 =
|
|
5549
|
+
const path9 = join8(tmpdir2(), `opencode-cc-sys-${randomUUID5()}.md`);
|
|
4801
5550
|
try {
|
|
4802
5551
|
writeFileSync4(path9, content, "utf8");
|
|
4803
5552
|
return path9;
|
|
@@ -5593,6 +6342,13 @@ var ClaudeCodeLanguageModel = class {
|
|
|
5593
6342
|
setClaudeSessionId(sk, msg.session_id);
|
|
5594
6343
|
}
|
|
5595
6344
|
reportFastModeState(msg, fastMode);
|
|
6345
|
+
reportSystemInit(msg, {
|
|
6346
|
+
ignoreAnthropicApiKey: this.config.ignoreAnthropicApiKey
|
|
6347
|
+
});
|
|
6348
|
+
}
|
|
6349
|
+
if (msg.type === "rate_limit_event") {
|
|
6350
|
+
reportRateLimitEvent(msg);
|
|
6351
|
+
return;
|
|
5596
6352
|
}
|
|
5597
6353
|
if (msg.type === "assistant" && msg.message?.content && !gotPartialEvents) {
|
|
5598
6354
|
for (const block of msg.message.content) {
|
|
@@ -5702,8 +6458,21 @@ ${plan}
|
|
|
5702
6458
|
sessionId: msg.session_id,
|
|
5703
6459
|
costUsd: msg.total_cost_usd,
|
|
5704
6460
|
durationMs: msg.duration_ms,
|
|
5705
|
-
|
|
6461
|
+
durationApiMs: msg.duration_api_ms,
|
|
6462
|
+
numTurns: msg.num_turns,
|
|
6463
|
+
usage: msg.usage,
|
|
6464
|
+
modelUsage: msg.modelUsage,
|
|
6465
|
+
permissionDenials: msg.permission_denials?.map((denial) => ({
|
|
6466
|
+
tool_name: denial.tool_name,
|
|
6467
|
+
tool_use_id: denial.tool_use_id
|
|
6468
|
+
}))
|
|
5706
6469
|
};
|
|
6470
|
+
log.info("conversation result", {
|
|
6471
|
+
sessionId: msg.session_id,
|
|
6472
|
+
isError: msg.is_error,
|
|
6473
|
+
subtype: msg.subtype,
|
|
6474
|
+
...turnStatsLogPayload(extractTurnStats(msg))
|
|
6475
|
+
});
|
|
5707
6476
|
cleanup();
|
|
5708
6477
|
resolve5({
|
|
5709
6478
|
...resultMeta,
|
|
@@ -5851,6 +6620,39 @@ ${plan}
|
|
|
5851
6620
|
const useInteractive = interactivePref && typeof globalThis.Bun?.Terminal === "function";
|
|
5852
6621
|
const interactiveBypassRequested = this.config.interactiveBypass ?? flagOn(process.env.CLAUDE_CODE_INTERACTIVE_BYPASS);
|
|
5853
6622
|
const asideTransportRef = { cliPath, interactive: !!useInteractive };
|
|
6623
|
+
const doctor = !compactionMode && scope !== "no-tools" ? parseDoctorCommand(options.prompt) : null;
|
|
6624
|
+
if (doctor) {
|
|
6625
|
+
const doctorOptions = {
|
|
6626
|
+
cliPath,
|
|
6627
|
+
interactive: !!useInteractive,
|
|
6628
|
+
turnStats: this.config.turnStats === true
|
|
6629
|
+
};
|
|
6630
|
+
const stream2 = new ReadableStream({
|
|
6631
|
+
async start(controller) {
|
|
6632
|
+
controller.enqueue({ type: "stream-start", warnings });
|
|
6633
|
+
try {
|
|
6634
|
+
const text = await buildDoctorReport(doctorOptions);
|
|
6635
|
+
const id = generateId();
|
|
6636
|
+
controller.enqueue({ type: "text-start", id });
|
|
6637
|
+
controller.enqueue({ type: "text-delta", id, delta: text });
|
|
6638
|
+
controller.enqueue({ type: "text-end", id });
|
|
6639
|
+
controller.enqueue({
|
|
6640
|
+
type: "finish",
|
|
6641
|
+
finishReason: toFinishReason("stop"),
|
|
6642
|
+
usage: toUsage({ input_tokens: 0, output_tokens: 0 }),
|
|
6643
|
+
providerMetadata: {
|
|
6644
|
+
"claude-code": { path: "doctor", synthetic: true, usageUnavailable: true }
|
|
6645
|
+
}
|
|
6646
|
+
});
|
|
6647
|
+
} catch (error) {
|
|
6648
|
+
controller.enqueue({ type: "error", error });
|
|
6649
|
+
} finally {
|
|
6650
|
+
controller.close();
|
|
6651
|
+
}
|
|
6652
|
+
}
|
|
6653
|
+
});
|
|
6654
|
+
return { stream: stream2, request: { body: { text: `/${DOCTOR_COMMAND}` } } };
|
|
6655
|
+
}
|
|
5854
6656
|
const aside = !compactionMode && scope !== "no-tools" ? parseSideQuestion(options.prompt) : null;
|
|
5855
6657
|
if (aside) {
|
|
5856
6658
|
const active = getActiveProcess(sk);
|
|
@@ -6289,7 +7091,10 @@ ${plan}
|
|
|
6289
7091
|
let hadProxyActivitySinceContinue = false;
|
|
6290
7092
|
let lastStopReason = null;
|
|
6291
7093
|
const autoContinueState = {
|
|
6292
|
-
enabled:
|
|
7094
|
+
enabled: autoContinueEnabledFor(
|
|
7095
|
+
compactionMode,
|
|
7096
|
+
self.config.autoContinueIncompleteTurns
|
|
7097
|
+
),
|
|
6293
7098
|
attempts: 0,
|
|
6294
7099
|
startedAt: Date.now(),
|
|
6295
7100
|
noProgressCount: 0
|
|
@@ -6436,6 +7241,7 @@ ${plan}
|
|
|
6436
7241
|
const skipResultForIds = /* @__PURE__ */ new Set();
|
|
6437
7242
|
const toolCallsById = /* @__PURE__ */ new Map();
|
|
6438
7243
|
let resultMeta = {};
|
|
7244
|
+
let resultFailure;
|
|
6439
7245
|
const drainBuffer = [];
|
|
6440
7246
|
let drainTimer = null;
|
|
6441
7247
|
const DRAIN_QUIET_MS = 100;
|
|
@@ -6658,11 +7464,12 @@ ${plan}
|
|
|
6658
7464
|
}
|
|
6659
7465
|
controller.enqueue({
|
|
6660
7466
|
type: "finish",
|
|
6661
|
-
finishReason: toFinishReason("stop"),
|
|
7467
|
+
finishReason: resultFailure ? { unified: "error", raw: resultFailure } : toFinishReason("stop"),
|
|
6662
7468
|
usage: toUsage(msg.usage),
|
|
6663
7469
|
providerMetadata: {
|
|
6664
7470
|
"claude-code": {
|
|
6665
7471
|
...resultMeta,
|
|
7472
|
+
...resultFailure ? { resultSubtype: resultFailure } : {},
|
|
6666
7473
|
...compactionMode ? { compactionModel: effectiveModelId } : {}
|
|
6667
7474
|
},
|
|
6668
7475
|
...typeof msg.usage?.cache_creation_input_tokens === "number" ? {
|
|
@@ -6714,6 +7521,24 @@ ${plan}
|
|
|
6714
7521
|
});
|
|
6715
7522
|
}
|
|
6716
7523
|
reportFastModeState(msg, fastMode);
|
|
7524
|
+
reportSystemInit(msg, {
|
|
7525
|
+
ignoreAnthropicApiKey: self.config.ignoreAnthropicApiKey
|
|
7526
|
+
});
|
|
7527
|
+
}
|
|
7528
|
+
if (msg.type === "system" && msg.subtype === "compact_boundary") {
|
|
7529
|
+
const note = reportCompactBoundary(msg);
|
|
7530
|
+
if (note) {
|
|
7531
|
+
controller.enqueue({ type: "text-delta", id: startTextBlock(), delta: note });
|
|
7532
|
+
endTextBlock();
|
|
7533
|
+
}
|
|
7534
|
+
}
|
|
7535
|
+
if (msg.type === "rate_limit_event") {
|
|
7536
|
+
const note = reportRateLimitEvent(msg);
|
|
7537
|
+
if (note) {
|
|
7538
|
+
controller.enqueue({ type: "text-delta", id: startTextBlock(), delta: note });
|
|
7539
|
+
endTextBlock();
|
|
7540
|
+
}
|
|
7541
|
+
return;
|
|
6717
7542
|
}
|
|
6718
7543
|
if (msg.type === "content_block_start" && msg.content_block && msg.index !== void 0) {
|
|
6719
7544
|
const block = msg.content_block;
|
|
@@ -7152,12 +7977,12 @@ ${plan}
|
|
|
7152
7977
|
}
|
|
7153
7978
|
const claudeSessionId = getClaudeSessionId(sk);
|
|
7154
7979
|
if (claudeSessionId) {
|
|
7155
|
-
const
|
|
7980
|
+
const list2 = applyTaskCreateToolResult(
|
|
7156
7981
|
claudeSessionId,
|
|
7157
7982
|
block.tool_use_id,
|
|
7158
7983
|
resultText
|
|
7159
7984
|
);
|
|
7160
|
-
if (
|
|
7985
|
+
if (list2) {
|
|
7161
7986
|
const synthId = `todowrite_${block.tool_use_id}`;
|
|
7162
7987
|
controller.enqueue({
|
|
7163
7988
|
type: "tool-input-start",
|
|
@@ -7170,7 +7995,7 @@ ${plan}
|
|
|
7170
7995
|
toolCallId: synthId,
|
|
7171
7996
|
toolName: "todowrite",
|
|
7172
7997
|
input: JSON.stringify({
|
|
7173
|
-
todos:
|
|
7998
|
+
todos: list2.map((t) => ({
|
|
7174
7999
|
id: t.id,
|
|
7175
8000
|
content: t.content,
|
|
7176
8001
|
status: t.status,
|
|
@@ -7184,6 +8009,7 @@ ${plan}
|
|
|
7184
8009
|
}
|
|
7185
8010
|
const toolCall = toolCallsById.get(block.tool_use_id);
|
|
7186
8011
|
if (toolCall) {
|
|
8012
|
+
const isError = block.is_error === true;
|
|
7187
8013
|
controller.enqueue({
|
|
7188
8014
|
type: "tool-result",
|
|
7189
8015
|
toolCallId: block.tool_use_id,
|
|
@@ -7191,14 +8017,16 @@ ${plan}
|
|
|
7191
8017
|
result: {
|
|
7192
8018
|
output: resultText,
|
|
7193
8019
|
title: toolCall.name,
|
|
7194
|
-
metadata: {}
|
|
8020
|
+
metadata: isError ? { error: true } : {}
|
|
7195
8021
|
},
|
|
8022
|
+
...isError ? { isError: true } : {},
|
|
7196
8023
|
providerExecuted: true
|
|
7197
8024
|
});
|
|
7198
8025
|
noteToolActivity();
|
|
7199
8026
|
log.info("tool result emitted", {
|
|
7200
8027
|
toolUseId: block.tool_use_id,
|
|
7201
|
-
name: toolCall.name
|
|
8028
|
+
name: toolCall.name,
|
|
8029
|
+
isError
|
|
7202
8030
|
});
|
|
7203
8031
|
toolCallsById.delete(block.tool_use_id);
|
|
7204
8032
|
}
|
|
@@ -7221,18 +8049,49 @@ ${plan}
|
|
|
7221
8049
|
delta: msg.result
|
|
7222
8050
|
});
|
|
7223
8051
|
}
|
|
8052
|
+
const failure = describeResultFailure(msg);
|
|
8053
|
+
if (failure) {
|
|
8054
|
+
resultFailure = msg.subtype;
|
|
8055
|
+
controller.enqueue({
|
|
8056
|
+
type: "text-delta",
|
|
8057
|
+
id: startTextBlock(),
|
|
8058
|
+
delta: formatResultFailureNote(failure)
|
|
8059
|
+
});
|
|
8060
|
+
log.warn(failure, { sessionKey: sk, subtype: msg.subtype });
|
|
8061
|
+
}
|
|
8062
|
+
const turnStats = extractTurnStats(msg);
|
|
7224
8063
|
resultMeta = {
|
|
7225
8064
|
sessionId: msg.session_id,
|
|
7226
8065
|
costUsd: msg.total_cost_usd,
|
|
7227
8066
|
durationMs: msg.duration_ms,
|
|
7228
|
-
|
|
8067
|
+
durationApiMs: msg.duration_api_ms,
|
|
8068
|
+
numTurns: msg.num_turns,
|
|
8069
|
+
usage: msg.usage,
|
|
8070
|
+
modelUsage: msg.modelUsage,
|
|
8071
|
+
// Names and ids only: a denial's `tool_input` can be a whole
|
|
8072
|
+
// file write payload and has no business in metadata.
|
|
8073
|
+
permissionDenials: msg.permission_denials?.map((denial) => ({
|
|
8074
|
+
tool_name: denial.tool_name,
|
|
8075
|
+
tool_use_id: denial.tool_use_id
|
|
8076
|
+
}))
|
|
7229
8077
|
};
|
|
7230
8078
|
log.info("conversation result", {
|
|
7231
8079
|
sessionId: msg.session_id,
|
|
7232
|
-
durationMs: msg.duration_ms,
|
|
7233
8080
|
numTurns: msg.num_turns,
|
|
7234
|
-
isError: msg.is_error
|
|
8081
|
+
isError: msg.is_error,
|
|
8082
|
+
subtype: msg.subtype,
|
|
8083
|
+
...turnStatsLogPayload(turnStats)
|
|
7235
8084
|
});
|
|
8085
|
+
if (self.config.turnStats && !compactionMode && !msg.is_error && !failure) {
|
|
8086
|
+
const footer = formatTurnStatsBlock(turnStats);
|
|
8087
|
+
if (footer) {
|
|
8088
|
+
controller.enqueue({
|
|
8089
|
+
type: "text-delta",
|
|
8090
|
+
id: startTextBlock(),
|
|
8091
|
+
delta: footer
|
|
8092
|
+
});
|
|
8093
|
+
}
|
|
8094
|
+
}
|
|
7236
8095
|
turnCompleted = true;
|
|
7237
8096
|
endTextBlock();
|
|
7238
8097
|
const shouldDeferResult = !msg.is_error && !autoContinueState.aborted && !autoContinueState.sawAskUserQuestion;
|
|
@@ -7284,24 +8143,59 @@ ${plan}
|
|
|
7284
8143
|
);
|
|
7285
8144
|
drainBuffer.length = 0;
|
|
7286
8145
|
}
|
|
8146
|
+
const crashed = !turnCompleted && !autoContinueState.aborted;
|
|
7287
8147
|
controllerClosed = true;
|
|
7288
8148
|
cleanupTurn();
|
|
7289
8149
|
endTextBlock();
|
|
7290
|
-
|
|
7291
|
-
|
|
7292
|
-
|
|
7293
|
-
|
|
7294
|
-
|
|
7295
|
-
|
|
7296
|
-
|
|
7297
|
-
|
|
8150
|
+
const finishClose = (exitCode, signal) => {
|
|
8151
|
+
if (crashed) {
|
|
8152
|
+
log.warn("claude process closed without a result", {
|
|
8153
|
+
sessionKey: sk,
|
|
8154
|
+
exitCode,
|
|
8155
|
+
signal,
|
|
8156
|
+
stderrBytes: activeProcess?.lastStderr?.length ?? 0
|
|
8157
|
+
});
|
|
8158
|
+
controller.enqueue({
|
|
8159
|
+
type: "error",
|
|
8160
|
+
error: new Error(
|
|
8161
|
+
describeChildCrash(exitCode, signal, activeProcess?.lastStderr)
|
|
8162
|
+
)
|
|
8163
|
+
});
|
|
8164
|
+
}
|
|
8165
|
+
controller.enqueue({
|
|
8166
|
+
type: "finish",
|
|
8167
|
+
finishReason: toFinishReason(crashed ? "error" : "stop"),
|
|
8168
|
+
usage: toUsage(),
|
|
8169
|
+
providerMetadata: {
|
|
8170
|
+
"claude-code": {
|
|
8171
|
+
...resultMeta,
|
|
8172
|
+
...compactionMode ? { compactionModel: effectiveModelId } : {}
|
|
8173
|
+
}
|
|
7298
8174
|
}
|
|
8175
|
+
});
|
|
8176
|
+
try {
|
|
8177
|
+
controller.close();
|
|
8178
|
+
} catch {
|
|
7299
8179
|
}
|
|
7300
|
-
}
|
|
7301
|
-
|
|
7302
|
-
|
|
7303
|
-
|
|
8180
|
+
};
|
|
8181
|
+
if (crashed && proc.exitCode === null && proc.signalCode === null) {
|
|
8182
|
+
let reported = false;
|
|
8183
|
+
const report = (exitCode, signal) => {
|
|
8184
|
+
if (reported) return;
|
|
8185
|
+
reported = true;
|
|
8186
|
+
clearTimeout(exitGrace);
|
|
8187
|
+
proc.off("exit", onExit);
|
|
8188
|
+
finishClose(exitCode, signal);
|
|
8189
|
+
};
|
|
8190
|
+
const onExit = (code, signal) => report(code, signal);
|
|
8191
|
+
const exitGrace = setTimeout(
|
|
8192
|
+
() => report(proc.exitCode, proc.signalCode),
|
|
8193
|
+
CHILD_EXIT_STATUS_GRACE_MS
|
|
8194
|
+
);
|
|
8195
|
+
proc.once("exit", onExit);
|
|
8196
|
+
return;
|
|
7304
8197
|
}
|
|
8198
|
+
finishClose(proc.exitCode, proc.signalCode);
|
|
7305
8199
|
};
|
|
7306
8200
|
let cleanedUp = false;
|
|
7307
8201
|
const cleanupTurn = () => {
|
|
@@ -7567,7 +8461,7 @@ ${plan}
|
|
|
7567
8461
|
|
|
7568
8462
|
// src/accounts.ts
|
|
7569
8463
|
import { chmod, lstat, mkdir, readlink, symlink, writeFile } from "fs/promises";
|
|
7570
|
-
import
|
|
8464
|
+
import path8 from "path";
|
|
7571
8465
|
var BASE_PROVIDER_ID = "claude-code";
|
|
7572
8466
|
var DEFAULT_ACCOUNT = "default";
|
|
7573
8467
|
var SHARED_CAPABILITY_ITEMS = [
|
|
@@ -7605,7 +8499,7 @@ function expandHome(value) {
|
|
|
7605
8499
|
const home = process.env.HOME ?? process.env.USERPROFILE;
|
|
7606
8500
|
if (value === "~") return home ?? value;
|
|
7607
8501
|
if (value.startsWith("~/") || value.startsWith("~\\")) {
|
|
7608
|
-
return home ?
|
|
8502
|
+
return home ? path8.join(home, value.slice(2)) : value;
|
|
7609
8503
|
}
|
|
7610
8504
|
return value;
|
|
7611
8505
|
}
|
|
@@ -7637,8 +8531,8 @@ async function ensureSharedCapabilities(targetRoot) {
|
|
|
7637
8531
|
}
|
|
7638
8532
|
}
|
|
7639
8533
|
async function ensureSharedCapabilityItem(sourceRoot, targetRoot, item) {
|
|
7640
|
-
const source =
|
|
7641
|
-
const target =
|
|
8534
|
+
const source = path8.join(sourceRoot, item);
|
|
8535
|
+
const target = path8.join(targetRoot, item);
|
|
7642
8536
|
let sourceStat;
|
|
7643
8537
|
try {
|
|
7644
8538
|
sourceStat = await lstat(source);
|
|
@@ -7649,8 +8543,8 @@ async function ensureSharedCapabilityItem(sourceRoot, targetRoot, item) {
|
|
|
7649
8543
|
const targetStat = await lstat(target);
|
|
7650
8544
|
if (targetStat.isSymbolicLink()) {
|
|
7651
8545
|
const current = await readlink(target);
|
|
7652
|
-
const resolvedCurrent =
|
|
7653
|
-
const resolvedSource =
|
|
8546
|
+
const resolvedCurrent = path8.resolve(path8.dirname(target), current);
|
|
8547
|
+
const resolvedSource = path8.resolve(source);
|
|
7654
8548
|
if (resolvedCurrent === resolvedSource) return;
|
|
7655
8549
|
}
|
|
7656
8550
|
log.warn("shared Claude capability already exists; leaving untouched", {
|
|
@@ -7665,11 +8559,11 @@ async function ensureSharedCapabilityItem(sourceRoot, targetRoot, item) {
|
|
|
7665
8559
|
await symlink(source, target, type);
|
|
7666
8560
|
}
|
|
7667
8561
|
async function writeAccountWrapper(account, baseCliPath, configDir) {
|
|
7668
|
-
const cacheRoot =
|
|
8562
|
+
const cacheRoot = path8.join(
|
|
7669
8563
|
process.env.XDG_CACHE_HOME ?? expandHome("~/.cache"),
|
|
7670
8564
|
"opencode-claude-code-plugin"
|
|
7671
8565
|
);
|
|
7672
|
-
const wrapperPath =
|
|
8566
|
+
const wrapperPath = path8.join(cacheRoot, `claude-${account}`);
|
|
7673
8567
|
const suffix = `@${account}`;
|
|
7674
8568
|
await mkdir(cacheRoot, { recursive: true });
|
|
7675
8569
|
const script = `#!/usr/bin/env bash
|
|
@@ -7710,34 +8604,34 @@ function titleizeAccount(account) {
|
|
|
7710
8604
|
// src/cleanup-stale.ts
|
|
7711
8605
|
import {
|
|
7712
8606
|
existsSync as existsSync4,
|
|
7713
|
-
readFileSync as
|
|
8607
|
+
readFileSync as readFileSync5,
|
|
7714
8608
|
realpathSync,
|
|
7715
8609
|
rmSync as rmSync3,
|
|
7716
8610
|
writeFileSync as writeFileSync5
|
|
7717
8611
|
} from "fs";
|
|
7718
8612
|
import { homedir as homedir6 } from "os";
|
|
7719
|
-
import { join as
|
|
7720
|
-
import { fileURLToPath as
|
|
8613
|
+
import { join as join9, resolve as resolve4 } from "path";
|
|
8614
|
+
import { fileURLToPath as fileURLToPath3 } from "url";
|
|
7721
8615
|
var STALE_PACKAGE_NAME = "opencode-claude-code-plugin";
|
|
7722
8616
|
var SUSPECT_DESCRIPTION_TOKEN = "Claude Code";
|
|
7723
8617
|
var alreadyRan = false;
|
|
7724
8618
|
function candidateCacheRoots() {
|
|
7725
8619
|
const xdg = process.env.XDG_CACHE_HOME;
|
|
7726
8620
|
return [
|
|
7727
|
-
xdg ?
|
|
7728
|
-
|
|
7729
|
-
|
|
8621
|
+
xdg ? join9(xdg, "opencode") : null,
|
|
8622
|
+
join9(homedir6(), ".cache", "opencode"),
|
|
8623
|
+
join9(homedir6(), "Library", "Caches", "opencode")
|
|
7730
8624
|
].filter((p) => Boolean(p));
|
|
7731
8625
|
}
|
|
7732
8626
|
function userOpencodeJsonPath() {
|
|
7733
|
-
const xdgConfig = process.env.XDG_CONFIG_HOME ??
|
|
7734
|
-
return
|
|
8627
|
+
const xdgConfig = process.env.XDG_CONFIG_HOME ?? join9(homedir6(), ".config");
|
|
8628
|
+
return join9(xdgConfig, "opencode", "opencode.json");
|
|
7735
8629
|
}
|
|
7736
8630
|
function userIntendsToUseUnscoped() {
|
|
7737
8631
|
const cfg = userOpencodeJsonPath();
|
|
7738
8632
|
if (!existsSync4(cfg)) return false;
|
|
7739
8633
|
try {
|
|
7740
|
-
const json = JSON.parse(
|
|
8634
|
+
const json = JSON.parse(readFileSync5(cfg, "utf8"));
|
|
7741
8635
|
const plugins = json.plugin;
|
|
7742
8636
|
if (!Array.isArray(plugins)) return false;
|
|
7743
8637
|
return plugins.some(
|
|
@@ -7749,7 +8643,7 @@ function userIntendsToUseUnscoped() {
|
|
|
7749
8643
|
}
|
|
7750
8644
|
function ourLoadedDir() {
|
|
7751
8645
|
try {
|
|
7752
|
-
const filePath =
|
|
8646
|
+
const filePath = fileURLToPath3(import.meta.url);
|
|
7753
8647
|
return realpathSync(resolve4(filePath, "..", ".."));
|
|
7754
8648
|
} catch {
|
|
7755
8649
|
return null;
|
|
@@ -7774,7 +8668,7 @@ function cleanupStaleUnscopedInstall() {
|
|
|
7774
8668
|
}
|
|
7775
8669
|
function cleanupOne(cacheRoot, ourDir) {
|
|
7776
8670
|
if (!existsSync4(cacheRoot)) return;
|
|
7777
|
-
const stalePath =
|
|
8671
|
+
const stalePath = join9(cacheRoot, "node_modules", STALE_PACKAGE_NAME);
|
|
7778
8672
|
if (!existsSync4(stalePath)) return;
|
|
7779
8673
|
let realStalePath = stalePath;
|
|
7780
8674
|
try {
|
|
@@ -7782,11 +8676,11 @@ function cleanupOne(cacheRoot, ourDir) {
|
|
|
7782
8676
|
} catch {
|
|
7783
8677
|
}
|
|
7784
8678
|
if (ourDir && realStalePath === ourDir) return;
|
|
7785
|
-
const pkgJsonPath =
|
|
8679
|
+
const pkgJsonPath = join9(stalePath, "package.json");
|
|
7786
8680
|
if (!existsSync4(pkgJsonPath)) return;
|
|
7787
8681
|
let pkg = {};
|
|
7788
8682
|
try {
|
|
7789
|
-
pkg = JSON.parse(
|
|
8683
|
+
pkg = JSON.parse(readFileSync5(pkgJsonPath, "utf8"));
|
|
7790
8684
|
} catch {
|
|
7791
8685
|
return;
|
|
7792
8686
|
}
|
|
@@ -7802,10 +8696,10 @@ function cleanupOne(cacheRoot, ourDir) {
|
|
|
7802
8696
|
});
|
|
7803
8697
|
return;
|
|
7804
8698
|
}
|
|
7805
|
-
const cachePkgJson =
|
|
8699
|
+
const cachePkgJson = join9(cacheRoot, "package.json");
|
|
7806
8700
|
if (!existsSync4(cachePkgJson)) return;
|
|
7807
8701
|
try {
|
|
7808
|
-
const cfg = JSON.parse(
|
|
8702
|
+
const cfg = JSON.parse(readFileSync5(cachePkgJson, "utf8"));
|
|
7809
8703
|
if (cfg?.dependencies?.[STALE_PACKAGE_NAME]) {
|
|
7810
8704
|
delete cfg.dependencies[STALE_PACKAGE_NAME];
|
|
7811
8705
|
writeFileSync5(cachePkgJson, JSON.stringify(cfg, null, 2) + "\n");
|
|
@@ -7818,131 +8712,6 @@ function cleanupOne(cacheRoot, ourDir) {
|
|
|
7818
8712
|
}
|
|
7819
8713
|
}
|
|
7820
8714
|
|
|
7821
|
-
// src/startup-diagnostics.ts
|
|
7822
|
-
import { execFile as execFile2 } from "child_process";
|
|
7823
|
-
import * as fs6 from "fs";
|
|
7824
|
-
import * as path8 from "path";
|
|
7825
|
-
import { promisify as promisify2 } from "util";
|
|
7826
|
-
import { fileURLToPath as fileURLToPath3 } from "url";
|
|
7827
|
-
var cachedPluginVersion;
|
|
7828
|
-
function pluginVersion() {
|
|
7829
|
-
if (cachedPluginVersion) return cachedPluginVersion;
|
|
7830
|
-
try {
|
|
7831
|
-
const here = path8.dirname(fileURLToPath3(import.meta.url));
|
|
7832
|
-
const raw = fs6.readFileSync(path8.join(here, "..", "package.json"), "utf8");
|
|
7833
|
-
const version = JSON.parse(raw).version;
|
|
7834
|
-
cachedPluginVersion = typeof version === "string" ? version : "unknown";
|
|
7835
|
-
} catch {
|
|
7836
|
-
cachedPluginVersion = "unknown";
|
|
7837
|
-
}
|
|
7838
|
-
return cachedPluginVersion;
|
|
7839
|
-
}
|
|
7840
|
-
function pickOpencodeVersion(input) {
|
|
7841
|
-
if (!input || typeof input !== "object") return void 0;
|
|
7842
|
-
const app = input.app;
|
|
7843
|
-
if (app && typeof app === "object") {
|
|
7844
|
-
const version = app.version;
|
|
7845
|
-
if (typeof version === "string" && version.length > 0) return version;
|
|
7846
|
-
}
|
|
7847
|
-
const direct = input.version;
|
|
7848
|
-
if (typeof direct === "string" && direct.length > 0) return direct;
|
|
7849
|
-
return void 0;
|
|
7850
|
-
}
|
|
7851
|
-
var execFileAsync2 = promisify2(execFile2);
|
|
7852
|
-
var opencodeVersionProbe;
|
|
7853
|
-
function detectOpencodeVersion(execPath = process.execPath) {
|
|
7854
|
-
if (opencodeVersionProbe) return opencodeVersionProbe;
|
|
7855
|
-
opencodeVersionProbe = (async () => {
|
|
7856
|
-
if (!path8.basename(execPath).toLowerCase().includes("opencode")) {
|
|
7857
|
-
log.debug("skipping opencode version probe: execPath is not opencode", { execPath });
|
|
7858
|
-
return void 0;
|
|
7859
|
-
}
|
|
7860
|
-
try {
|
|
7861
|
-
const { stdout } = await execFileAsync2(execPath, ["--version"], { timeout: 5e3 });
|
|
7862
|
-
const match = /\d+\.\d+\.\d+\S*/.exec(stdout.trim());
|
|
7863
|
-
return match ? match[0] : void 0;
|
|
7864
|
-
} catch (err) {
|
|
7865
|
-
log.debug("opencode version probe failed", {
|
|
7866
|
-
execPath,
|
|
7867
|
-
error: err instanceof Error ? err.message : String(err)
|
|
7868
|
-
});
|
|
7869
|
-
return void 0;
|
|
7870
|
-
}
|
|
7871
|
-
})();
|
|
7872
|
-
return opencodeVersionProbe;
|
|
7873
|
-
}
|
|
7874
|
-
function describeSpawnCwd(configured, live = process.cwd(), captured = getOpencodeProjectDirectory()) {
|
|
7875
|
-
if (typeof configured === "string" && configured.length > 0) {
|
|
7876
|
-
return { resolved: configured, source: "configured" };
|
|
7877
|
-
}
|
|
7878
|
-
if (isUsableDirectory(live)) return { resolved: live, source: "process" };
|
|
7879
|
-
if (isUsableDirectory(captured)) return { resolved: captured, source: "captured" };
|
|
7880
|
-
return { resolved: live, source: "unresolved" };
|
|
7881
|
-
}
|
|
7882
|
-
function stringList(value) {
|
|
7883
|
-
if (!Array.isArray(value)) return [];
|
|
7884
|
-
return value.filter((entry) => typeof entry === "string");
|
|
7885
|
-
}
|
|
7886
|
-
function firstOption(providers, key) {
|
|
7887
|
-
for (const entry of Object.values(providers)) {
|
|
7888
|
-
const value = entry?.options?.[key];
|
|
7889
|
-
if (value !== void 0) return value;
|
|
7890
|
-
}
|
|
7891
|
-
return void 0;
|
|
7892
|
-
}
|
|
7893
|
-
function collectStartupDiagnostics(providers, opencodeVersion) {
|
|
7894
|
-
const accounts = [];
|
|
7895
|
-
for (const entry of Object.values(providers)) {
|
|
7896
|
-
const account = entry?.options?.account;
|
|
7897
|
-
if (typeof account === "string" && account.length > 0) accounts.push(account);
|
|
7898
|
-
}
|
|
7899
|
-
const cwd = describeSpawnCwd(firstOption(providers, "cwd"));
|
|
7900
|
-
let mcpServers = [];
|
|
7901
|
-
try {
|
|
7902
|
-
mcpServers = mergeOpencodeMcp(cwd.resolved).enabledServerNames;
|
|
7903
|
-
} catch (err) {
|
|
7904
|
-
log.debug("startup diagnostics could not read MCP config", {
|
|
7905
|
-
error: err instanceof Error ? err.message : String(err)
|
|
7906
|
-
});
|
|
7907
|
-
}
|
|
7908
|
-
return {
|
|
7909
|
-
plugin: pluginVersion(),
|
|
7910
|
-
opencode: opencodeVersion ?? process.env.OPENCODE_VERSION ?? "unknown",
|
|
7911
|
-
claudeCliPath: String(firstOption(providers, "cliPath") ?? "claude"),
|
|
7912
|
-
cwd,
|
|
7913
|
-
providers: Object.keys(providers),
|
|
7914
|
-
accounts,
|
|
7915
|
-
proxyTools: stringList(firstOption(providers, "proxyTools")),
|
|
7916
|
-
mcpServers,
|
|
7917
|
-
interactiveTransport: firstOption(providers, "interactive") === true || process.env.CLAUDE_CODE_INTERACTIVE_TRANSPORT === "1",
|
|
7918
|
-
planModeQuestion: firstOption(providers, "planModeQuestion") === true,
|
|
7919
|
-
anthropicApiKeyInEnv: Boolean(
|
|
7920
|
-
process.env.ANTHROPIC_API_KEY || process.env.ANTHROPIC_AUTH_TOKEN
|
|
7921
|
-
)
|
|
7922
|
-
};
|
|
7923
|
-
}
|
|
7924
|
-
var logged = false;
|
|
7925
|
-
function logStartupDiagnostics(providers, opencodeVersion) {
|
|
7926
|
-
if (logged) return;
|
|
7927
|
-
logged = true;
|
|
7928
|
-
void (async () => {
|
|
7929
|
-
try {
|
|
7930
|
-
const version = opencodeVersion ?? process.env.OPENCODE_VERSION ?? await detectOpencodeVersion();
|
|
7931
|
-
const { claudeCliPath, ...rest } = collectStartupDiagnostics(providers, version);
|
|
7932
|
-
const cli = await detectCliVersion(claudeCliPath);
|
|
7933
|
-
const diagnostics = {
|
|
7934
|
-
...rest,
|
|
7935
|
-
claudeCli: { path: claudeCliPath, version: cli?.raw ?? "not detected" }
|
|
7936
|
-
};
|
|
7937
|
-
log.notice("claude-code plugin ready", { ...diagnostics });
|
|
7938
|
-
} catch (err) {
|
|
7939
|
-
log.debug("startup diagnostics failed", {
|
|
7940
|
-
error: err instanceof Error ? err.message : String(err)
|
|
7941
|
-
});
|
|
7942
|
-
}
|
|
7943
|
-
})();
|
|
7944
|
-
}
|
|
7945
|
-
|
|
7946
8715
|
// src/index.ts
|
|
7947
8716
|
function pickOpencodeDirectory(input) {
|
|
7948
8717
|
if (!input || typeof input !== "object") return void 0;
|
|
@@ -7969,6 +8738,15 @@ function registerSideQuestionCommand(config) {
|
|
|
7969
8738
|
};
|
|
7970
8739
|
return true;
|
|
7971
8740
|
}
|
|
8741
|
+
function registerDoctorCommand(config) {
|
|
8742
|
+
config.command ??= {};
|
|
8743
|
+
if (config.command[DOCTOR_COMMAND]) return false;
|
|
8744
|
+
config.command[DOCTOR_COMMAND] = {
|
|
8745
|
+
template: `/${DOCTOR_COMMAND} $ARGUMENTS`,
|
|
8746
|
+
description: "Report what the Claude Code plugin sees: versions, cwd, live processes, pending proxy calls"
|
|
8747
|
+
};
|
|
8748
|
+
return true;
|
|
8749
|
+
}
|
|
7972
8750
|
var ownsSideQuestionCommand = false;
|
|
7973
8751
|
function warnIfAnthropicApiKey(ignore) {
|
|
7974
8752
|
if (warnedAnthropicApiKey) return;
|
|
@@ -8039,6 +8817,7 @@ function createClaudeCode(settings = {}) {
|
|
|
8039
8817
|
ignoreAnthropicApiKey: settings.ignoreAnthropicApiKey,
|
|
8040
8818
|
idleProcessTimeoutMs: settings.idleProcessTimeoutMs,
|
|
8041
8819
|
bridgeOpencodeSkills: settings.bridgeOpencodeSkills === true,
|
|
8820
|
+
turnStats: settings.turnStats === true,
|
|
8042
8821
|
interactive: settings.interactive,
|
|
8043
8822
|
interactiveBypass: settings.interactiveBypass,
|
|
8044
8823
|
interactiveAllowTools: settings.interactiveAllowTools,
|
|
@@ -8238,6 +9017,7 @@ var server = async (input) => {
|
|
|
8238
9017
|
return {
|
|
8239
9018
|
config: async (config) => {
|
|
8240
9019
|
if (registerSideQuestionCommand(config)) ownsSideQuestionCommand = true;
|
|
9020
|
+
registerDoctorCommand(config);
|
|
8241
9021
|
registerBundledSkillPath(config);
|
|
8242
9022
|
config.provider ??= {};
|
|
8243
9023
|
await buildAgentRegistry(config);
|
|
@@ -8323,6 +9103,7 @@ export {
|
|
|
8323
9103
|
defaultModels,
|
|
8324
9104
|
getAgentRegistry,
|
|
8325
9105
|
getDefaultSubagentModel,
|
|
9106
|
+
registerDoctorCommand,
|
|
8326
9107
|
registerSideQuestionCommand,
|
|
8327
9108
|
resolveAgentModel,
|
|
8328
9109
|
warnIfPlanModeCannotExit
|