@khalilgharbaoui/opencode-claude-code-plugin 0.18.3 → 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 +2180 -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
|
-
return opencodeClient;
|
|
3809
|
-
}
|
|
3810
|
-
var opencodeProjectDirectory;
|
|
3811
|
-
function setOpencodeProjectDirectory(dir) {
|
|
3812
|
-
opencodeProjectDirectory = dir;
|
|
3813
|
-
}
|
|
3814
|
-
function getOpencodeProjectDirectory() {
|
|
3815
|
-
return opencodeProjectDirectory;
|
|
3816
|
-
}
|
|
3817
|
-
function isUsableDirectory(d) {
|
|
3818
|
-
return typeof d === "string" && d.length > 1 && d !== "/";
|
|
3819
|
-
}
|
|
3820
|
-
function resolveSpawnCwdFrom(configured, live, captured, sessionDir) {
|
|
3821
|
-
if (configured) return configured;
|
|
3822
|
-
if (isUsableDirectory(sessionDir)) return sessionDir;
|
|
3823
|
-
if (isUsableDirectory(live)) return live;
|
|
3824
|
-
return captured ?? live;
|
|
3825
|
-
}
|
|
3826
|
-
async function resolveSpawnCwdForSession(configured, sessionID) {
|
|
3827
|
-
if (configured) return configured;
|
|
3828
|
-
const sessionDir = sessionID ? await fetchSessionDirectory(sessionID) : void 0;
|
|
3829
|
-
return resolveSpawnCwdFrom(
|
|
3830
|
-
configured,
|
|
3831
|
-
process.cwd(),
|
|
3832
|
-
opencodeProjectDirectory,
|
|
3833
|
-
sessionDir
|
|
3834
|
-
);
|
|
3835
|
-
}
|
|
3836
|
-
async function fetchSessionDirectory(sessionID) {
|
|
3837
|
-
if (!sessionID || sessionID === "default") return void 0;
|
|
3838
|
-
const client = opencodeClient;
|
|
3839
|
-
if (!client?.session?.get) return void 0;
|
|
4511
|
+
function fileExists2(p) {
|
|
3840
4512
|
try {
|
|
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;
|
|
4513
|
+
return fs5.statSync(p).isFile();
|
|
4514
|
+
} catch {
|
|
4515
|
+
return false;
|
|
3852
4516
|
}
|
|
3853
4517
|
}
|
|
3854
|
-
|
|
3855
|
-
const
|
|
3856
|
-
|
|
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;
|
|
4541
|
+
}
|
|
4542
|
+
function collectSkills(root, claimed, found) {
|
|
4543
|
+
let entries;
|
|
3857
4544
|
try {
|
|
3858
|
-
|
|
3859
|
-
|
|
3860
|
-
|
|
3861
|
-
|
|
3862
|
-
|
|
3863
|
-
|
|
3864
|
-
|
|
3865
|
-
|
|
3866
|
-
|
|
3867
|
-
|
|
3868
|
-
|
|
3869
|
-
|
|
3870
|
-
|
|
3871
|
-
error: err instanceof Error ? err.message : String(err)
|
|
3872
|
-
});
|
|
3873
|
-
return void 0;
|
|
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 });
|
|
3874
4558
|
}
|
|
3875
4559
|
}
|
|
3876
|
-
|
|
3877
|
-
|
|
3878
|
-
|
|
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);
|
|
4566
|
+
}
|
|
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);
|
|
4573
|
+
}
|
|
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;
|
|
4582
|
+
}
|
|
4583
|
+
function linkSkill(source, target) {
|
|
3879
4584
|
try {
|
|
3880
|
-
|
|
3881
|
-
|
|
3882
|
-
|
|
3883
|
-
|
|
3884
|
-
|
|
3885
|
-
|
|
3886
|
-
|
|
3887
|
-
|
|
3888
|
-
|
|
3889
|
-
|
|
3890
|
-
|
|
3891
|
-
|
|
3892
|
-
|
|
3893
|
-
|
|
4585
|
+
fs5.symlinkSync(source, target, process.platform === "win32" ? "junction" : "dir");
|
|
4586
|
+
return;
|
|
4587
|
+
} catch {
|
|
4588
|
+
fs5.cpSync(source, target, { recursive: true, dereference: true });
|
|
4589
|
+
}
|
|
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,6 +5277,7 @@ 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.";
|
|
4536
5282
|
function autoContinueEnabledFor(compactionMode, configured) {
|
|
4537
5283
|
return compactionMode ? false : configured;
|
|
@@ -4693,7 +5439,7 @@ ${body}`;
|
|
|
4693
5439
|
}
|
|
4694
5440
|
function readPromptFileIfPresent(path9) {
|
|
4695
5441
|
try {
|
|
4696
|
-
const content =
|
|
5442
|
+
const content = readFileSync4(path9, "utf8").trim();
|
|
4697
5443
|
return content || void 0;
|
|
4698
5444
|
} catch {
|
|
4699
5445
|
return void 0;
|
|
@@ -4702,9 +5448,9 @@ function readPromptFileIfPresent(path9) {
|
|
|
4702
5448
|
function nearestWorkspaceAgentsPrompt(cwd) {
|
|
4703
5449
|
let dir = cwd;
|
|
4704
5450
|
while (true) {
|
|
4705
|
-
const content = readPromptFileIfPresent(
|
|
5451
|
+
const content = readPromptFileIfPresent(join8(dir, "AGENTS.md"));
|
|
4706
5452
|
if (content) return content;
|
|
4707
|
-
const parent =
|
|
5453
|
+
const parent = dirname5(dir);
|
|
4708
5454
|
if (parent === dir) return void 0;
|
|
4709
5455
|
dir = parent;
|
|
4710
5456
|
}
|
|
@@ -4788,8 +5534,8 @@ ${options.compressionSummary.trim()}`
|
|
|
4788
5534
|
for (const s of extraSystemContent) {
|
|
4789
5535
|
if (s.trim()) parts.push(s.trim());
|
|
4790
5536
|
}
|
|
4791
|
-
const configRoot = process.env.XDG_CONFIG_HOME ??
|
|
4792
|
-
const globalAgents = readPromptFileIfPresent(
|
|
5537
|
+
const configRoot = process.env.XDG_CONFIG_HOME ?? join8(homedir5(), ".config");
|
|
5538
|
+
const globalAgents = readPromptFileIfPresent(join8(configRoot, "opencode", "AGENTS.md"));
|
|
4793
5539
|
const workspaceAgents = nearestWorkspaceAgentsPrompt(cwd);
|
|
4794
5540
|
const forwarded = extraSystemContent.join("\n\n");
|
|
4795
5541
|
const pushGlobal = !!globalAgents && !forwarded.includes(globalAgents);
|
|
@@ -4800,7 +5546,7 @@ ${options.compressionSummary.trim()}`
|
|
|
4800
5546
|
if (includeMultiStepHint) parts.push(MULTI_STEP_TASK_HINT);
|
|
4801
5547
|
const content = parts.join("\n\n");
|
|
4802
5548
|
if (!content) return void 0;
|
|
4803
|
-
const path9 =
|
|
5549
|
+
const path9 = join8(tmpdir2(), `opencode-cc-sys-${randomUUID5()}.md`);
|
|
4804
5550
|
try {
|
|
4805
5551
|
writeFileSync4(path9, content, "utf8");
|
|
4806
5552
|
return path9;
|
|
@@ -5596,6 +6342,13 @@ var ClaudeCodeLanguageModel = class {
|
|
|
5596
6342
|
setClaudeSessionId(sk, msg.session_id);
|
|
5597
6343
|
}
|
|
5598
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;
|
|
5599
6352
|
}
|
|
5600
6353
|
if (msg.type === "assistant" && msg.message?.content && !gotPartialEvents) {
|
|
5601
6354
|
for (const block of msg.message.content) {
|
|
@@ -5705,8 +6458,21 @@ ${plan}
|
|
|
5705
6458
|
sessionId: msg.session_id,
|
|
5706
6459
|
costUsd: msg.total_cost_usd,
|
|
5707
6460
|
durationMs: msg.duration_ms,
|
|
5708
|
-
|
|
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
|
+
}))
|
|
5709
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
|
+
});
|
|
5710
6476
|
cleanup();
|
|
5711
6477
|
resolve5({
|
|
5712
6478
|
...resultMeta,
|
|
@@ -5854,6 +6620,39 @@ ${plan}
|
|
|
5854
6620
|
const useInteractive = interactivePref && typeof globalThis.Bun?.Terminal === "function";
|
|
5855
6621
|
const interactiveBypassRequested = this.config.interactiveBypass ?? flagOn(process.env.CLAUDE_CODE_INTERACTIVE_BYPASS);
|
|
5856
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
|
+
}
|
|
5857
6656
|
const aside = !compactionMode && scope !== "no-tools" ? parseSideQuestion(options.prompt) : null;
|
|
5858
6657
|
if (aside) {
|
|
5859
6658
|
const active = getActiveProcess(sk);
|
|
@@ -6442,6 +7241,7 @@ ${plan}
|
|
|
6442
7241
|
const skipResultForIds = /* @__PURE__ */ new Set();
|
|
6443
7242
|
const toolCallsById = /* @__PURE__ */ new Map();
|
|
6444
7243
|
let resultMeta = {};
|
|
7244
|
+
let resultFailure;
|
|
6445
7245
|
const drainBuffer = [];
|
|
6446
7246
|
let drainTimer = null;
|
|
6447
7247
|
const DRAIN_QUIET_MS = 100;
|
|
@@ -6664,11 +7464,12 @@ ${plan}
|
|
|
6664
7464
|
}
|
|
6665
7465
|
controller.enqueue({
|
|
6666
7466
|
type: "finish",
|
|
6667
|
-
finishReason: toFinishReason("stop"),
|
|
7467
|
+
finishReason: resultFailure ? { unified: "error", raw: resultFailure } : toFinishReason("stop"),
|
|
6668
7468
|
usage: toUsage(msg.usage),
|
|
6669
7469
|
providerMetadata: {
|
|
6670
7470
|
"claude-code": {
|
|
6671
7471
|
...resultMeta,
|
|
7472
|
+
...resultFailure ? { resultSubtype: resultFailure } : {},
|
|
6672
7473
|
...compactionMode ? { compactionModel: effectiveModelId } : {}
|
|
6673
7474
|
},
|
|
6674
7475
|
...typeof msg.usage?.cache_creation_input_tokens === "number" ? {
|
|
@@ -6720,6 +7521,24 @@ ${plan}
|
|
|
6720
7521
|
});
|
|
6721
7522
|
}
|
|
6722
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;
|
|
6723
7542
|
}
|
|
6724
7543
|
if (msg.type === "content_block_start" && msg.content_block && msg.index !== void 0) {
|
|
6725
7544
|
const block = msg.content_block;
|
|
@@ -7158,12 +7977,12 @@ ${plan}
|
|
|
7158
7977
|
}
|
|
7159
7978
|
const claudeSessionId = getClaudeSessionId(sk);
|
|
7160
7979
|
if (claudeSessionId) {
|
|
7161
|
-
const
|
|
7980
|
+
const list2 = applyTaskCreateToolResult(
|
|
7162
7981
|
claudeSessionId,
|
|
7163
7982
|
block.tool_use_id,
|
|
7164
7983
|
resultText
|
|
7165
7984
|
);
|
|
7166
|
-
if (
|
|
7985
|
+
if (list2) {
|
|
7167
7986
|
const synthId = `todowrite_${block.tool_use_id}`;
|
|
7168
7987
|
controller.enqueue({
|
|
7169
7988
|
type: "tool-input-start",
|
|
@@ -7176,7 +7995,7 @@ ${plan}
|
|
|
7176
7995
|
toolCallId: synthId,
|
|
7177
7996
|
toolName: "todowrite",
|
|
7178
7997
|
input: JSON.stringify({
|
|
7179
|
-
todos:
|
|
7998
|
+
todos: list2.map((t) => ({
|
|
7180
7999
|
id: t.id,
|
|
7181
8000
|
content: t.content,
|
|
7182
8001
|
status: t.status,
|
|
@@ -7190,6 +8009,7 @@ ${plan}
|
|
|
7190
8009
|
}
|
|
7191
8010
|
const toolCall = toolCallsById.get(block.tool_use_id);
|
|
7192
8011
|
if (toolCall) {
|
|
8012
|
+
const isError = block.is_error === true;
|
|
7193
8013
|
controller.enqueue({
|
|
7194
8014
|
type: "tool-result",
|
|
7195
8015
|
toolCallId: block.tool_use_id,
|
|
@@ -7197,14 +8017,16 @@ ${plan}
|
|
|
7197
8017
|
result: {
|
|
7198
8018
|
output: resultText,
|
|
7199
8019
|
title: toolCall.name,
|
|
7200
|
-
metadata: {}
|
|
8020
|
+
metadata: isError ? { error: true } : {}
|
|
7201
8021
|
},
|
|
8022
|
+
...isError ? { isError: true } : {},
|
|
7202
8023
|
providerExecuted: true
|
|
7203
8024
|
});
|
|
7204
8025
|
noteToolActivity();
|
|
7205
8026
|
log.info("tool result emitted", {
|
|
7206
8027
|
toolUseId: block.tool_use_id,
|
|
7207
|
-
name: toolCall.name
|
|
8028
|
+
name: toolCall.name,
|
|
8029
|
+
isError
|
|
7208
8030
|
});
|
|
7209
8031
|
toolCallsById.delete(block.tool_use_id);
|
|
7210
8032
|
}
|
|
@@ -7227,18 +8049,49 @@ ${plan}
|
|
|
7227
8049
|
delta: msg.result
|
|
7228
8050
|
});
|
|
7229
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);
|
|
7230
8063
|
resultMeta = {
|
|
7231
8064
|
sessionId: msg.session_id,
|
|
7232
8065
|
costUsd: msg.total_cost_usd,
|
|
7233
8066
|
durationMs: msg.duration_ms,
|
|
7234
|
-
|
|
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
|
+
}))
|
|
7235
8077
|
};
|
|
7236
8078
|
log.info("conversation result", {
|
|
7237
8079
|
sessionId: msg.session_id,
|
|
7238
|
-
durationMs: msg.duration_ms,
|
|
7239
8080
|
numTurns: msg.num_turns,
|
|
7240
|
-
isError: msg.is_error
|
|
8081
|
+
isError: msg.is_error,
|
|
8082
|
+
subtype: msg.subtype,
|
|
8083
|
+
...turnStatsLogPayload(turnStats)
|
|
7241
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
|
+
}
|
|
7242
8095
|
turnCompleted = true;
|
|
7243
8096
|
endTextBlock();
|
|
7244
8097
|
const shouldDeferResult = !msg.is_error && !autoContinueState.aborted && !autoContinueState.sawAskUserQuestion;
|
|
@@ -7290,24 +8143,59 @@ ${plan}
|
|
|
7290
8143
|
);
|
|
7291
8144
|
drainBuffer.length = 0;
|
|
7292
8145
|
}
|
|
8146
|
+
const crashed = !turnCompleted && !autoContinueState.aborted;
|
|
7293
8147
|
controllerClosed = true;
|
|
7294
8148
|
cleanupTurn();
|
|
7295
8149
|
endTextBlock();
|
|
7296
|
-
|
|
7297
|
-
|
|
7298
|
-
|
|
7299
|
-
|
|
7300
|
-
|
|
7301
|
-
|
|
7302
|
-
|
|
7303
|
-
|
|
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
|
+
}
|
|
7304
8174
|
}
|
|
8175
|
+
});
|
|
8176
|
+
try {
|
|
8177
|
+
controller.close();
|
|
8178
|
+
} catch {
|
|
7305
8179
|
}
|
|
7306
|
-
}
|
|
7307
|
-
|
|
7308
|
-
|
|
7309
|
-
|
|
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;
|
|
7310
8197
|
}
|
|
8198
|
+
finishClose(proc.exitCode, proc.signalCode);
|
|
7311
8199
|
};
|
|
7312
8200
|
let cleanedUp = false;
|
|
7313
8201
|
const cleanupTurn = () => {
|
|
@@ -7573,7 +8461,7 @@ ${plan}
|
|
|
7573
8461
|
|
|
7574
8462
|
// src/accounts.ts
|
|
7575
8463
|
import { chmod, lstat, mkdir, readlink, symlink, writeFile } from "fs/promises";
|
|
7576
|
-
import
|
|
8464
|
+
import path8 from "path";
|
|
7577
8465
|
var BASE_PROVIDER_ID = "claude-code";
|
|
7578
8466
|
var DEFAULT_ACCOUNT = "default";
|
|
7579
8467
|
var SHARED_CAPABILITY_ITEMS = [
|
|
@@ -7611,7 +8499,7 @@ function expandHome(value) {
|
|
|
7611
8499
|
const home = process.env.HOME ?? process.env.USERPROFILE;
|
|
7612
8500
|
if (value === "~") return home ?? value;
|
|
7613
8501
|
if (value.startsWith("~/") || value.startsWith("~\\")) {
|
|
7614
|
-
return home ?
|
|
8502
|
+
return home ? path8.join(home, value.slice(2)) : value;
|
|
7615
8503
|
}
|
|
7616
8504
|
return value;
|
|
7617
8505
|
}
|
|
@@ -7643,8 +8531,8 @@ async function ensureSharedCapabilities(targetRoot) {
|
|
|
7643
8531
|
}
|
|
7644
8532
|
}
|
|
7645
8533
|
async function ensureSharedCapabilityItem(sourceRoot, targetRoot, item) {
|
|
7646
|
-
const source =
|
|
7647
|
-
const target =
|
|
8534
|
+
const source = path8.join(sourceRoot, item);
|
|
8535
|
+
const target = path8.join(targetRoot, item);
|
|
7648
8536
|
let sourceStat;
|
|
7649
8537
|
try {
|
|
7650
8538
|
sourceStat = await lstat(source);
|
|
@@ -7655,8 +8543,8 @@ async function ensureSharedCapabilityItem(sourceRoot, targetRoot, item) {
|
|
|
7655
8543
|
const targetStat = await lstat(target);
|
|
7656
8544
|
if (targetStat.isSymbolicLink()) {
|
|
7657
8545
|
const current = await readlink(target);
|
|
7658
|
-
const resolvedCurrent =
|
|
7659
|
-
const resolvedSource =
|
|
8546
|
+
const resolvedCurrent = path8.resolve(path8.dirname(target), current);
|
|
8547
|
+
const resolvedSource = path8.resolve(source);
|
|
7660
8548
|
if (resolvedCurrent === resolvedSource) return;
|
|
7661
8549
|
}
|
|
7662
8550
|
log.warn("shared Claude capability already exists; leaving untouched", {
|
|
@@ -7671,11 +8559,11 @@ async function ensureSharedCapabilityItem(sourceRoot, targetRoot, item) {
|
|
|
7671
8559
|
await symlink(source, target, type);
|
|
7672
8560
|
}
|
|
7673
8561
|
async function writeAccountWrapper(account, baseCliPath, configDir) {
|
|
7674
|
-
const cacheRoot =
|
|
8562
|
+
const cacheRoot = path8.join(
|
|
7675
8563
|
process.env.XDG_CACHE_HOME ?? expandHome("~/.cache"),
|
|
7676
8564
|
"opencode-claude-code-plugin"
|
|
7677
8565
|
);
|
|
7678
|
-
const wrapperPath =
|
|
8566
|
+
const wrapperPath = path8.join(cacheRoot, `claude-${account}`);
|
|
7679
8567
|
const suffix = `@${account}`;
|
|
7680
8568
|
await mkdir(cacheRoot, { recursive: true });
|
|
7681
8569
|
const script = `#!/usr/bin/env bash
|
|
@@ -7716,34 +8604,34 @@ function titleizeAccount(account) {
|
|
|
7716
8604
|
// src/cleanup-stale.ts
|
|
7717
8605
|
import {
|
|
7718
8606
|
existsSync as existsSync4,
|
|
7719
|
-
readFileSync as
|
|
8607
|
+
readFileSync as readFileSync5,
|
|
7720
8608
|
realpathSync,
|
|
7721
8609
|
rmSync as rmSync3,
|
|
7722
8610
|
writeFileSync as writeFileSync5
|
|
7723
8611
|
} from "fs";
|
|
7724
8612
|
import { homedir as homedir6 } from "os";
|
|
7725
|
-
import { join as
|
|
7726
|
-
import { fileURLToPath as
|
|
8613
|
+
import { join as join9, resolve as resolve4 } from "path";
|
|
8614
|
+
import { fileURLToPath as fileURLToPath3 } from "url";
|
|
7727
8615
|
var STALE_PACKAGE_NAME = "opencode-claude-code-plugin";
|
|
7728
8616
|
var SUSPECT_DESCRIPTION_TOKEN = "Claude Code";
|
|
7729
8617
|
var alreadyRan = false;
|
|
7730
8618
|
function candidateCacheRoots() {
|
|
7731
8619
|
const xdg = process.env.XDG_CACHE_HOME;
|
|
7732
8620
|
return [
|
|
7733
|
-
xdg ?
|
|
7734
|
-
|
|
7735
|
-
|
|
8621
|
+
xdg ? join9(xdg, "opencode") : null,
|
|
8622
|
+
join9(homedir6(), ".cache", "opencode"),
|
|
8623
|
+
join9(homedir6(), "Library", "Caches", "opencode")
|
|
7736
8624
|
].filter((p) => Boolean(p));
|
|
7737
8625
|
}
|
|
7738
8626
|
function userOpencodeJsonPath() {
|
|
7739
|
-
const xdgConfig = process.env.XDG_CONFIG_HOME ??
|
|
7740
|
-
return
|
|
8627
|
+
const xdgConfig = process.env.XDG_CONFIG_HOME ?? join9(homedir6(), ".config");
|
|
8628
|
+
return join9(xdgConfig, "opencode", "opencode.json");
|
|
7741
8629
|
}
|
|
7742
8630
|
function userIntendsToUseUnscoped() {
|
|
7743
8631
|
const cfg = userOpencodeJsonPath();
|
|
7744
8632
|
if (!existsSync4(cfg)) return false;
|
|
7745
8633
|
try {
|
|
7746
|
-
const json = JSON.parse(
|
|
8634
|
+
const json = JSON.parse(readFileSync5(cfg, "utf8"));
|
|
7747
8635
|
const plugins = json.plugin;
|
|
7748
8636
|
if (!Array.isArray(plugins)) return false;
|
|
7749
8637
|
return plugins.some(
|
|
@@ -7755,7 +8643,7 @@ function userIntendsToUseUnscoped() {
|
|
|
7755
8643
|
}
|
|
7756
8644
|
function ourLoadedDir() {
|
|
7757
8645
|
try {
|
|
7758
|
-
const filePath =
|
|
8646
|
+
const filePath = fileURLToPath3(import.meta.url);
|
|
7759
8647
|
return realpathSync(resolve4(filePath, "..", ".."));
|
|
7760
8648
|
} catch {
|
|
7761
8649
|
return null;
|
|
@@ -7780,7 +8668,7 @@ function cleanupStaleUnscopedInstall() {
|
|
|
7780
8668
|
}
|
|
7781
8669
|
function cleanupOne(cacheRoot, ourDir) {
|
|
7782
8670
|
if (!existsSync4(cacheRoot)) return;
|
|
7783
|
-
const stalePath =
|
|
8671
|
+
const stalePath = join9(cacheRoot, "node_modules", STALE_PACKAGE_NAME);
|
|
7784
8672
|
if (!existsSync4(stalePath)) return;
|
|
7785
8673
|
let realStalePath = stalePath;
|
|
7786
8674
|
try {
|
|
@@ -7788,11 +8676,11 @@ function cleanupOne(cacheRoot, ourDir) {
|
|
|
7788
8676
|
} catch {
|
|
7789
8677
|
}
|
|
7790
8678
|
if (ourDir && realStalePath === ourDir) return;
|
|
7791
|
-
const pkgJsonPath =
|
|
8679
|
+
const pkgJsonPath = join9(stalePath, "package.json");
|
|
7792
8680
|
if (!existsSync4(pkgJsonPath)) return;
|
|
7793
8681
|
let pkg = {};
|
|
7794
8682
|
try {
|
|
7795
|
-
pkg = JSON.parse(
|
|
8683
|
+
pkg = JSON.parse(readFileSync5(pkgJsonPath, "utf8"));
|
|
7796
8684
|
} catch {
|
|
7797
8685
|
return;
|
|
7798
8686
|
}
|
|
@@ -7808,10 +8696,10 @@ function cleanupOne(cacheRoot, ourDir) {
|
|
|
7808
8696
|
});
|
|
7809
8697
|
return;
|
|
7810
8698
|
}
|
|
7811
|
-
const cachePkgJson =
|
|
8699
|
+
const cachePkgJson = join9(cacheRoot, "package.json");
|
|
7812
8700
|
if (!existsSync4(cachePkgJson)) return;
|
|
7813
8701
|
try {
|
|
7814
|
-
const cfg = JSON.parse(
|
|
8702
|
+
const cfg = JSON.parse(readFileSync5(cachePkgJson, "utf8"));
|
|
7815
8703
|
if (cfg?.dependencies?.[STALE_PACKAGE_NAME]) {
|
|
7816
8704
|
delete cfg.dependencies[STALE_PACKAGE_NAME];
|
|
7817
8705
|
writeFileSync5(cachePkgJson, JSON.stringify(cfg, null, 2) + "\n");
|
|
@@ -7824,131 +8712,6 @@ function cleanupOne(cacheRoot, ourDir) {
|
|
|
7824
8712
|
}
|
|
7825
8713
|
}
|
|
7826
8714
|
|
|
7827
|
-
// src/startup-diagnostics.ts
|
|
7828
|
-
import { execFile as execFile2 } from "child_process";
|
|
7829
|
-
import * as fs6 from "fs";
|
|
7830
|
-
import * as path8 from "path";
|
|
7831
|
-
import { promisify as promisify2 } from "util";
|
|
7832
|
-
import { fileURLToPath as fileURLToPath3 } from "url";
|
|
7833
|
-
var cachedPluginVersion;
|
|
7834
|
-
function pluginVersion() {
|
|
7835
|
-
if (cachedPluginVersion) return cachedPluginVersion;
|
|
7836
|
-
try {
|
|
7837
|
-
const here = path8.dirname(fileURLToPath3(import.meta.url));
|
|
7838
|
-
const raw = fs6.readFileSync(path8.join(here, "..", "package.json"), "utf8");
|
|
7839
|
-
const version = JSON.parse(raw).version;
|
|
7840
|
-
cachedPluginVersion = typeof version === "string" ? version : "unknown";
|
|
7841
|
-
} catch {
|
|
7842
|
-
cachedPluginVersion = "unknown";
|
|
7843
|
-
}
|
|
7844
|
-
return cachedPluginVersion;
|
|
7845
|
-
}
|
|
7846
|
-
function pickOpencodeVersion(input) {
|
|
7847
|
-
if (!input || typeof input !== "object") return void 0;
|
|
7848
|
-
const app = input.app;
|
|
7849
|
-
if (app && typeof app === "object") {
|
|
7850
|
-
const version = app.version;
|
|
7851
|
-
if (typeof version === "string" && version.length > 0) return version;
|
|
7852
|
-
}
|
|
7853
|
-
const direct = input.version;
|
|
7854
|
-
if (typeof direct === "string" && direct.length > 0) return direct;
|
|
7855
|
-
return void 0;
|
|
7856
|
-
}
|
|
7857
|
-
var execFileAsync2 = promisify2(execFile2);
|
|
7858
|
-
var opencodeVersionProbe;
|
|
7859
|
-
function detectOpencodeVersion(execPath = process.execPath) {
|
|
7860
|
-
if (opencodeVersionProbe) return opencodeVersionProbe;
|
|
7861
|
-
opencodeVersionProbe = (async () => {
|
|
7862
|
-
if (!path8.basename(execPath).toLowerCase().includes("opencode")) {
|
|
7863
|
-
log.debug("skipping opencode version probe: execPath is not opencode", { execPath });
|
|
7864
|
-
return void 0;
|
|
7865
|
-
}
|
|
7866
|
-
try {
|
|
7867
|
-
const { stdout } = await execFileAsync2(execPath, ["--version"], { timeout: 5e3 });
|
|
7868
|
-
const match = /\d+\.\d+\.\d+\S*/.exec(stdout.trim());
|
|
7869
|
-
return match ? match[0] : void 0;
|
|
7870
|
-
} catch (err) {
|
|
7871
|
-
log.debug("opencode version probe failed", {
|
|
7872
|
-
execPath,
|
|
7873
|
-
error: err instanceof Error ? err.message : String(err)
|
|
7874
|
-
});
|
|
7875
|
-
return void 0;
|
|
7876
|
-
}
|
|
7877
|
-
})();
|
|
7878
|
-
return opencodeVersionProbe;
|
|
7879
|
-
}
|
|
7880
|
-
function describeSpawnCwd(configured, live = process.cwd(), captured = getOpencodeProjectDirectory()) {
|
|
7881
|
-
if (typeof configured === "string" && configured.length > 0) {
|
|
7882
|
-
return { resolved: configured, source: "configured" };
|
|
7883
|
-
}
|
|
7884
|
-
if (isUsableDirectory(live)) return { resolved: live, source: "process" };
|
|
7885
|
-
if (isUsableDirectory(captured)) return { resolved: captured, source: "captured" };
|
|
7886
|
-
return { resolved: live, source: "unresolved" };
|
|
7887
|
-
}
|
|
7888
|
-
function stringList(value) {
|
|
7889
|
-
if (!Array.isArray(value)) return [];
|
|
7890
|
-
return value.filter((entry) => typeof entry === "string");
|
|
7891
|
-
}
|
|
7892
|
-
function firstOption(providers, key) {
|
|
7893
|
-
for (const entry of Object.values(providers)) {
|
|
7894
|
-
const value = entry?.options?.[key];
|
|
7895
|
-
if (value !== void 0) return value;
|
|
7896
|
-
}
|
|
7897
|
-
return void 0;
|
|
7898
|
-
}
|
|
7899
|
-
function collectStartupDiagnostics(providers, opencodeVersion) {
|
|
7900
|
-
const accounts = [];
|
|
7901
|
-
for (const entry of Object.values(providers)) {
|
|
7902
|
-
const account = entry?.options?.account;
|
|
7903
|
-
if (typeof account === "string" && account.length > 0) accounts.push(account);
|
|
7904
|
-
}
|
|
7905
|
-
const cwd = describeSpawnCwd(firstOption(providers, "cwd"));
|
|
7906
|
-
let mcpServers = [];
|
|
7907
|
-
try {
|
|
7908
|
-
mcpServers = mergeOpencodeMcp(cwd.resolved).enabledServerNames;
|
|
7909
|
-
} catch (err) {
|
|
7910
|
-
log.debug("startup diagnostics could not read MCP config", {
|
|
7911
|
-
error: err instanceof Error ? err.message : String(err)
|
|
7912
|
-
});
|
|
7913
|
-
}
|
|
7914
|
-
return {
|
|
7915
|
-
plugin: pluginVersion(),
|
|
7916
|
-
opencode: opencodeVersion ?? process.env.OPENCODE_VERSION ?? "unknown",
|
|
7917
|
-
claudeCliPath: String(firstOption(providers, "cliPath") ?? "claude"),
|
|
7918
|
-
cwd,
|
|
7919
|
-
providers: Object.keys(providers),
|
|
7920
|
-
accounts,
|
|
7921
|
-
proxyTools: stringList(firstOption(providers, "proxyTools")),
|
|
7922
|
-
mcpServers,
|
|
7923
|
-
interactiveTransport: firstOption(providers, "interactive") === true || process.env.CLAUDE_CODE_INTERACTIVE_TRANSPORT === "1",
|
|
7924
|
-
planModeQuestion: firstOption(providers, "planModeQuestion") === true,
|
|
7925
|
-
anthropicApiKeyInEnv: Boolean(
|
|
7926
|
-
process.env.ANTHROPIC_API_KEY || process.env.ANTHROPIC_AUTH_TOKEN
|
|
7927
|
-
)
|
|
7928
|
-
};
|
|
7929
|
-
}
|
|
7930
|
-
var logged = false;
|
|
7931
|
-
function logStartupDiagnostics(providers, opencodeVersion) {
|
|
7932
|
-
if (logged) return;
|
|
7933
|
-
logged = true;
|
|
7934
|
-
void (async () => {
|
|
7935
|
-
try {
|
|
7936
|
-
const version = opencodeVersion ?? process.env.OPENCODE_VERSION ?? await detectOpencodeVersion();
|
|
7937
|
-
const { claudeCliPath, ...rest } = collectStartupDiagnostics(providers, version);
|
|
7938
|
-
const cli = await detectCliVersion(claudeCliPath);
|
|
7939
|
-
const diagnostics = {
|
|
7940
|
-
...rest,
|
|
7941
|
-
claudeCli: { path: claudeCliPath, version: cli?.raw ?? "not detected" }
|
|
7942
|
-
};
|
|
7943
|
-
log.notice("claude-code plugin ready", { ...diagnostics });
|
|
7944
|
-
} catch (err) {
|
|
7945
|
-
log.debug("startup diagnostics failed", {
|
|
7946
|
-
error: err instanceof Error ? err.message : String(err)
|
|
7947
|
-
});
|
|
7948
|
-
}
|
|
7949
|
-
})();
|
|
7950
|
-
}
|
|
7951
|
-
|
|
7952
8715
|
// src/index.ts
|
|
7953
8716
|
function pickOpencodeDirectory(input) {
|
|
7954
8717
|
if (!input || typeof input !== "object") return void 0;
|
|
@@ -7975,6 +8738,15 @@ function registerSideQuestionCommand(config) {
|
|
|
7975
8738
|
};
|
|
7976
8739
|
return true;
|
|
7977
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
|
+
}
|
|
7978
8750
|
var ownsSideQuestionCommand = false;
|
|
7979
8751
|
function warnIfAnthropicApiKey(ignore) {
|
|
7980
8752
|
if (warnedAnthropicApiKey) return;
|
|
@@ -8045,6 +8817,7 @@ function createClaudeCode(settings = {}) {
|
|
|
8045
8817
|
ignoreAnthropicApiKey: settings.ignoreAnthropicApiKey,
|
|
8046
8818
|
idleProcessTimeoutMs: settings.idleProcessTimeoutMs,
|
|
8047
8819
|
bridgeOpencodeSkills: settings.bridgeOpencodeSkills === true,
|
|
8820
|
+
turnStats: settings.turnStats === true,
|
|
8048
8821
|
interactive: settings.interactive,
|
|
8049
8822
|
interactiveBypass: settings.interactiveBypass,
|
|
8050
8823
|
interactiveAllowTools: settings.interactiveAllowTools,
|
|
@@ -8244,6 +9017,7 @@ var server = async (input) => {
|
|
|
8244
9017
|
return {
|
|
8245
9018
|
config: async (config) => {
|
|
8246
9019
|
if (registerSideQuestionCommand(config)) ownsSideQuestionCommand = true;
|
|
9020
|
+
registerDoctorCommand(config);
|
|
8247
9021
|
registerBundledSkillPath(config);
|
|
8248
9022
|
config.provider ??= {};
|
|
8249
9023
|
await buildAgentRegistry(config);
|
|
@@ -8329,6 +9103,7 @@ export {
|
|
|
8329
9103
|
defaultModels,
|
|
8330
9104
|
getAgentRegistry,
|
|
8331
9105
|
getDefaultSubagentModel,
|
|
9106
|
+
registerDoctorCommand,
|
|
8332
9107
|
registerSideQuestionCommand,
|
|
8333
9108
|
resolveAgentModel,
|
|
8334
9109
|
warnIfPlanModeCannotExit
|