@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/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 list = applyTaskUpdate(opts.sessionId, input);
328
- if (list !== null) return emitTodoWrite(list);
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
- const oldestKey = activeProcesses.keys().next().value;
1881
- if (!oldestKey) break;
1882
- log.info("evicting LRU claude process", { sessionKey: oldestKey });
1883
- deleteActiveProcess(oldestKey);
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/message-builder.ts
2552
- var ASIDE_MARKERS = [INLINE_ASIDE_MARKER, ...LEGACY_INLINE_ASIDE_MARKERS];
2553
- function isInlineAside(part) {
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 stripInlineAsides(content) {
2559
- if (!Array.isArray(content)) return content;
2560
- const kept = content.filter((part) => !isInlineAside(part));
2561
- return kept.length === content.length ? content : kept;
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 filterSideQuestionHistory(prompt) {
2564
- let aside = false;
2565
- const kept = prompt.filter((message) => {
2566
- if (message.role === "user") {
2567
- aside = parseSideQuestionContent(message.content) !== null;
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
- var SUPPORTED_IMAGE_TYPES = /* @__PURE__ */ new Set([
2577
- "image/jpeg",
2578
- "image/png",
2579
- "image/gif",
2580
- "image/webp"
2581
- ]);
2582
- function toImageBlock(part) {
2583
- const raw = part.image ?? part.data ?? part.url ?? part.source?.data;
2584
- if (!raw) {
2585
- log.warn("file part without data, skipping");
2586
- return null;
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
- let resolvedMediaType = part.mediaType || part.mimeType || part.mime || "";
2589
- let base64 = null;
2590
- if (typeof raw === "string") {
2591
- if (raw.startsWith("data:")) {
2592
- const match = /^data:([^;,]+)(?:;[^,]*)*(?:;base64)?,(.*)$/s.exec(raw);
2593
- if (!match) {
2594
- log.warn("malformed data URI, skipping file part");
2595
- return null;
2596
- }
2597
- resolvedMediaType = resolvedMediaType || match[1];
2598
- base64 = match[2];
2599
- } else if (/^https?:\/\//i.test(raw)) {
2600
- log.warn("remote URL images are not supported by Claude CLI, skipping");
2601
- return null;
2602
- } else {
2603
- base64 = raw;
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
- if (!resolvedMediaType || !SUPPORTED_IMAGE_TYPES.has(resolvedMediaType)) {
2615
- log.warn("unsupported media type for Claude image block, skipping", {
2616
- mediaType: resolvedMediaType
2617
- });
2618
- return null;
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
- type: "image",
2622
- source: { type: "base64", media_type: resolvedMediaType, data: base64 }
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 getToolResultText(part) {
2626
- const value = part.output ?? part.result;
2627
- if (typeof value === "string") {
2628
- return value;
2629
- }
2630
- if (!value || typeof value !== "object") {
2631
- return JSON.stringify(value);
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
- switch (value.type) {
2634
- case "text":
2635
- case "error-text":
2636
- return String(value.value);
2637
- case "json":
2638
- case "error-json":
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 MAX_HISTORY_CHARS = 18e4;
2652
- var MAX_TOOL_RESULT_CHARS = 1e4;
2653
- var MAX_TOOL_INPUT_CHARS = 2e3;
2654
- function clipWithMarker(text, max) {
2655
- if (text.length <= max) return text;
2656
- return `${text.slice(0, max)}
2657
- \u2026[truncated ${text.length - max} chars]`;
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 renderToolInput(input) {
2660
- let raw;
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
- raw = typeof input === "string" ? input : JSON.stringify(input);
2884
+ return fs3.statSync(p).isFile();
2663
2885
  } catch {
2664
- raw = String(input);
2886
+ return false;
2665
2887
  }
2666
- return clipWithMarker(raw, MAX_TOOL_INPUT_CHARS);
2667
2888
  }
2668
- function renderMessageContentForCompaction(msg) {
2669
- const lines = [];
2670
- let toolResultCount = 0;
2671
- if (typeof msg.content === "string") {
2672
- return { text: msg.content, toolResultCount: 0 };
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 compactConversationHistory(prompt, opts = {}) {
2715
- const mode = opts.mode ?? "fresh-session";
2716
- prompt = filterSideQuestionHistory(prompt);
2717
- if (mode === "compaction") {
2718
- return buildCompactionHistory(prompt);
2719
- }
2720
- const conversationMessages = prompt.filter(
2721
- (m) => m.role === "user" || m.role === "assistant" || m.role === "tool"
2722
- );
2723
- if (conversationMessages.length <= 1) {
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
- if (historyParts.length === 0) {
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 buildCompactionHistory(prompt) {
2742
- const entries = [];
2743
- let total = 0;
2744
- let totalToolResults = 0;
2745
- let droppedOldest = 0;
2746
- const end = prompt.length > 0 && prompt[prompt.length - 1].role === "user" ? prompt.length - 1 : prompt.length;
2747
- for (let i = end - 1; i >= 0; i--) {
2748
- const msg = prompt[i];
2749
- const roleLabel = msg.role === "user" ? "User" : msg.role === "assistant" ? "Assistant" : msg.role === "tool" ? "Tool" : msg.role;
2750
- const { text, toolResultCount } = renderMessageContentForCompaction(msg);
2751
- if (!text.trim()) continue;
2752
- const entry = `${roleLabel}: ${text}`;
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
- if (entries.length === 0) return null;
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 getClaudeUserMessage(prompt, includeHistoryContext = false, opts = {}) {
2772
- const compactionMode = opts.compactionMode === true;
2773
- const cliToolCallIds = opts.cliToolCallIds;
2774
- const content = [];
2775
- const pushToolResult = (part) => {
2776
- const id = part.toolCallId;
2777
- const text = getToolResultText(part);
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
- } else if (includeHistoryContext) {
2814
- const historyContext = compactConversationHistory(prompt);
2815
- if (historyContext) {
2816
- log.info("including conversation history context", {
2817
- historyLength: historyContext.length
2818
- });
2819
- content.push({
2820
- type: "text",
2821
- text: `<conversation_history>
2822
- The following is a summary of our conversation so far (from a previous session that couldn't be resumed):
2823
-
2824
- ${historyContext}
2825
-
2826
- </conversation_history>
2827
-
2828
- Now continuing with the current message:
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
- const messages = [];
2835
- for (let i = prompt.length - 1; i >= 0; i--) {
2836
- if (prompt[i].role === "assistant") break;
2837
- messages.unshift(prompt[i]);
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
- for (const msg of messages) {
2840
- if (msg.role === "user") {
2841
- if (parseSideQuestionContent(msg.content) !== null) continue;
2842
- if (typeof msg.content === "string") {
2843
- const str = msg.content;
2844
- if (str.trim()) {
2845
- content.push({ type: "text", text: str });
2846
- }
2847
- } else if (Array.isArray(msg.content)) {
2848
- for (const part of msg.content) {
2849
- if (part.type === "text") {
2850
- if (part.text && part.text.trim()) {
2851
- content.push({ type: "text", text: part.text });
2852
- }
2853
- } else if (part.type === "file" || part.type === "image") {
2854
- const block = toImageBlock(part);
2855
- if (block) {
2856
- content.push(block);
2857
- } else {
2858
- log.debug("skipped non-image file part", {
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
- if (content.length === 0) {
2878
- log.warn("empty user content; sending sentinel to satisfy CLI");
2879
- return JSON.stringify({
2880
- type: "user",
2881
- message: {
2882
- role: "user",
2883
- content: [{ type: "text", text: "(empty)" }]
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 JSON.stringify({
2888
- type: "user",
2889
- message: {
2890
- role: "user",
2891
- content
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
- // src/agent-models.ts
2897
- import { readFile, readdir } from "fs/promises";
2898
- import path3 from "path";
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
- var haikuCost = { input: 1, output: 5, cacheRead: 0.1, cacheWrite: 1.25 };
2940
- var sonnetCost = { input: 3, output: 15, cacheRead: 0.3, cacheWrite: 3.75 };
2941
- var opusCost = { input: 5, output: 25, cacheRead: 0.5, cacheWrite: 6.25 };
2942
- var fableCost = { input: 10, output: 50, cacheRead: 1, cacheWrite: 12.5 };
2943
- var fable51Cost = { input: 10, output: 50, cacheRead: 0.25, cacheWrite: 12.5 };
2944
- var opusFastCost = { input: 10, output: 50, cacheRead: 1, cacheWrite: 12.5 };
2945
- function toConfigModel(model) {
2946
- const inputMods = [];
2947
- const outputMods = [];
2948
- for (const [k, v] of Object.entries(model.capabilities.input)) {
2949
- if (v) inputMods.push(k);
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
- for (const [k, v] of Object.entries(model.capabilities.output)) {
2952
- if (v) outputMods.push(k);
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
- id: model.api.id,
2956
- name: model.name,
2957
- status: model.status,
2958
- family: model.family ?? "",
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/agent-models.ts
3168
- var AGENT_DIR_NAMES = ["agents", "agent"];
3169
- var REASONING_EFFORTS = [
3170
- "minimal",
3171
- "low",
3172
- "medium",
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 getAgentRegistry() {
3183
- return registry;
3212
+ function getOpencodeClient() {
3213
+ return opencodeClient;
3184
3214
  }
3185
- function setDefaultSubagentModel(model) {
3186
- defaultSubagentModel = model?.trim() || void 0;
3215
+ var opencodeProjectDirectory;
3216
+ function setOpencodeProjectDirectory(dir) {
3217
+ opencodeProjectDirectory = dir;
3187
3218
  }
3188
- function getDefaultSubagentModel() {
3189
- return defaultSubagentModel;
3219
+ function getOpencodeProjectDirectory() {
3220
+ return opencodeProjectDirectory;
3190
3221
  }
3191
- function accountMarker(modelId) {
3192
- const at = modelId.indexOf("@");
3193
- return at === -1 ? "" : modelId.slice(at);
3222
+ function isUsableDirectory(d) {
3223
+ return typeof d === "string" && d.length > 1 && d !== "/";
3194
3224
  }
3195
- function withoutAccountMarker(modelId) {
3196
- const at = modelId.indexOf("@");
3197
- return at === -1 ? modelId : modelId.slice(0, at);
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 resolveAgentModel(agent, modelId, overrides) {
3200
- if (!agent) return modelId;
3201
- const record = (overrides?.records ?? registry)[agent];
3202
- if (!record) return modelId;
3203
- if (record.model?.includes("/")) return modelId;
3204
- const fallback = overrides ? overrides.defaultSubagentModel : defaultSubagentModel;
3205
- const declared = record.forceModel?.trim();
3206
- const wanted = declared || (record.mode === "subagent" ? fallback : void 0);
3207
- if (!wanted) return modelId;
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 resolveAgentEffort(agent, inherited, overrides) {
3224
- if (!agent) return inherited;
3225
- const record = (overrides?.records ?? registry)[agent];
3226
- const declared = record?.reasoningEffort?.trim();
3227
- if (!declared) return inherited;
3228
- if (!REASONING_EFFORTS.includes(declared)) {
3229
- log.warn("agent effort override refused: unknown level", {
3230
- agent,
3231
- wanted: declared,
3232
- keeping: inherited
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 inherited;
3256
+ return void 0;
3235
3257
  }
3236
- if (declared !== inherited) {
3237
- log.debug("agent effort override", {
3238
- agent,
3239
- from: inherited,
3240
- to: declared
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 parseAgentFrontmatter(text) {
3246
- const record = {};
3247
- if (!text.startsWith("---")) return record;
3248
- const lines = text.split(/\r?\n/);
3249
- for (let i = 1; i < lines.length; i++) {
3250
- const line = lines[i];
3251
- if (line.trim() === "---") break;
3252
- const match = /^([A-Za-z_][A-Za-z0-9_-]*):[ \t]*(.*)$/.exec(line);
3253
- if (!match) continue;
3254
- const key = match[1];
3255
- if (key !== "mode" && key !== "model" && key !== "forceModel" && key !== "reasoningEffort")
3256
- continue;
3257
- const value = match[2].trim().replace(/^["']|["']$/g, "");
3258
- if (value) record[key] = value;
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
- async function readAgentMarkdownRecords(directories) {
3263
- const records = {};
3264
- for (const directory of directories) {
3265
- let entries;
3266
- try {
3267
- entries = await readdir(directory);
3268
- } catch {
3269
- continue;
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
- for (const entry of entries) {
3272
- if (!entry.endsWith(".md")) continue;
3273
- const name = entry.slice(0, -3);
3274
- if (records[name]) continue;
3275
- try {
3276
- const text = await readFile(path3.join(directory, entry), "utf8");
3277
- records[name] = parseAgentFrontmatter(text);
3278
- } catch (err) {
3279
- log.debug("failed to read agent markdown", {
3280
- file: path3.join(directory, entry),
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 records;
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 agentDirectories(home, projectDirectory) {
3289
- const directories = [];
3290
- if (projectDirectory) {
3291
- for (const name of AGENT_DIR_NAMES) {
3292
- directories.push(path3.join(projectDirectory, ".opencode", name));
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
- if (home) {
3296
- for (const name of AGENT_DIR_NAMES) {
3297
- directories.push(path3.join(home, ".config", "opencode", name));
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
- return directories;
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/skill-bridge.ts
3304
- import * as crypto2 from "crypto";
3305
- import * as fs3 from "fs";
3306
- import * as os2 from "os";
3307
- import * as path4 from "path";
3308
- import { fileURLToPath } from "url";
3309
- var SKILL_PLUGIN_NAME = "opencode-skills";
3310
- function bundledSkillsDir() {
3311
- try {
3312
- const here = fileURLToPath(import.meta.url);
3313
- const dir = path4.resolve(path4.dirname(here), "..", "skills");
3314
- return dirExists(dir) ? dir : null;
3315
- } catch {
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 dirExists(p) {
3320
- try {
3321
- return fs3.statSync(p).isDirectory();
3322
- } catch {
3323
- return false;
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 fileExists(p) {
3569
+ async function checkProxyAuth(url, fetchImpl = fetch, timeoutMs = 3e3) {
3570
+ let authority;
3327
3571
  try {
3328
- return fs3.statSync(p).isFile();
3572
+ authority = new URL(url).host;
3329
3573
  } catch {
3330
- return false;
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 skillRoots(cwd) {
3334
- const roots = [];
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 push = (p) => {
3337
- const abs = path4.resolve(p);
3338
- if (seen.has(abs)) return;
3339
- seen.add(abs);
3340
- if (dirExists(abs)) roots.push(abs);
3341
- };
3342
- let current = path4.resolve(cwd);
3343
- while (true) {
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
- const home = os2.homedir();
3350
- if (home) push(path4.join(home, ".opencode", "skills"));
3351
- const envDir = process.env.OPENCODE_CONFIG_DIR;
3352
- if (envDir) push(path4.join(envDir, "skills"));
3353
- const xdg = process.env.XDG_CONFIG_HOME ?? (home ? path4.join(home, ".config") : null);
3354
- if (xdg) push(path4.join(xdg, "opencode", "skills"));
3355
- return roots;
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 collectSkills(root, claimed, found) {
3358
- let entries;
3631
+ async function buildDoctorReport(options) {
3359
3632
  try {
3360
- entries = fs3.readdirSync(root, { withFileTypes: true });
3361
- } catch {
3362
- return;
3363
- }
3364
- for (const entry of entries) {
3365
- if (!entry.isDirectory() && !entry.isSymbolicLink()) continue;
3366
- const name = entry.name;
3367
- if (name.startsWith(".")) continue;
3368
- if (claimed.has(name)) continue;
3369
- const dir = path4.join(root, name);
3370
- if (!fileExists(path4.join(dir, "SKILL.md"))) continue;
3371
- claimed.add(name);
3372
- found.push({ name, dir });
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
- var byName = (a, b) => a.name.localeCompare(b.name);
3376
- function discoverOpencodeSkills(cwd) {
3377
- const found = [];
3378
- const claimed = /* @__PURE__ */ new Set();
3379
- for (const root of skillRoots(cwd)) collectSkills(root, claimed, found);
3380
- return found.sort(byName);
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 discoverBundledSkills() {
3383
- const root = bundledSkillsDir();
3384
- if (!root) return [];
3385
- const found = [];
3386
- collectSkills(root, /* @__PURE__ */ new Set(), found);
3387
- return found.sort(byName);
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
- function registerBundledSkillPath(config) {
3390
- const dir = bundledSkillsDir();
3391
- if (!dir) return false;
3392
- config.skills ??= {};
3393
- const paths = config.skills.paths ??= [];
3394
- if (paths.some((p) => path4.resolve(p) === dir)) return false;
3395
- paths.push(dir);
3396
- return true;
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 linkSkill(source, target) {
3399
- try {
3400
- fs3.symlinkSync(source, target, process.platform === "win32" ? "junction" : "dir");
3401
- return;
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 buildSkillPluginDir(skills) {
3407
- if (skills.length === 0) return null;
3408
- const fingerprint = skills.map((s) => `${s.name}\0${s.dir}`).join("\n");
3409
- const hash = crypto2.createHash("sha256").update(fingerprint).digest("hex").slice(0, 12);
3410
- const root = path4.join(pluginTmpDir(), `skills-${hash}`);
3411
- const manifest = path4.join(root, ".claude-plugin", "plugin.json");
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
- } catch (err) {
3433
- log.warn("failed to stage opencode skill plugin dir", {
3434
- root,
3435
- error: err instanceof Error ? err.message : String(err)
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 root;
3805
+ return {
3806
+ type: "image",
3807
+ source: { type: "base64", media_type: resolvedMediaType, data: base64 }
3808
+ };
3440
3809
  }
3441
- async function resolveSkillPluginDirs(opts) {
3442
- const user = opts.enabled ? discoverOpencodeSkills(opts.cwd) : [];
3443
- const claimed = new Set(user.map((s) => s.name));
3444
- const bundled = discoverBundledSkills().filter((s) => !claimed.has(s.name));
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
- const dir = buildSkillPluginDir(skills);
3456
- if (!dir) return [];
3457
- log.info("bridged opencode skills into claude", {
3458
- count: skills.length,
3459
- names: skills.map((s) => s.name),
3460
- bundled: bundled.map((s) => s.name),
3461
- pluginDir: dir
3462
- });
3463
- return [dir];
3464
- }
3465
-
3466
- // src/mcp-bridge.ts
3467
- import * as fs4 from "fs";
3468
- import * as path5 from "path";
3469
- import * as os3 from "os";
3470
- import * as crypto3 from "crypto";
3471
- import {
3472
- parse as parseJsonc,
3473
- printParseErrorCode
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
- function dirExists2(p) {
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
- return fs4.statSync(p).isDirectory();
3847
+ raw = typeof input === "string" ? input : JSON.stringify(input);
3487
3848
  } catch {
3488
- return false;
3849
+ raw = String(input);
3489
3850
  }
3851
+ return clipWithMarker(raw, MAX_TOOL_INPUT_CHARS);
3490
3852
  }
3491
- function readAndParse(file) {
3492
- try {
3493
- const raw = fs4.readFileSync(file, "utf8");
3494
- const errors = [];
3495
- const parsed = parseJsonc(raw, errors, { allowTrailingComma: true });
3496
- if (errors.length > 0) {
3497
- const first = errors[0];
3498
- throw new Error(
3499
- `${printParseErrorCode(first.error)} at offset ${first.offset}`
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 isPlainObject(x) {
3512
- return typeof x === "object" && x !== null && !Array.isArray(x);
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
- function deepMerge(target, source) {
3515
- const out = { ...target };
3516
- for (const [k, v] of Object.entries(source)) {
3517
- if (v === void 0) continue;
3518
- const existing = out[k];
3519
- if (isPlainObject(existing) && isPlainObject(v)) {
3520
- out[k] = deepMerge(existing, v);
3521
- } else {
3522
- out[k] = v;
3523
- }
3524
- }
3525
- return out;
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 walkUp(opts) {
3528
- const out = [];
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 detectWorktree(cwd) {
3543
- const override = process.env.OPENCODE_WORKTREE;
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 globalConfigDir() {
3558
- const xdg = process.env.XDG_CONFIG_HOME ?? path5.join(os3.homedir(), ".config");
3559
- return path5.join(xdg, "opencode");
4373
+ function getDefaultSubagentModel() {
4374
+ return defaultSubagentModel;
3560
4375
  }
3561
- function loadGlobalConfig() {
3562
- const dir = globalConfigDir();
3563
- let merged = {};
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 loadProjectFilesInDir(dir) {
3573
- let merged = {};
3574
- for (const name of PROJECT_FILE_NAMES) {
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 dotOpencodeDirs(cwd, worktree) {
3583
- const dirs = [];
3584
- const seen = /* @__PURE__ */ new Set();
3585
- const push = (p) => {
3586
- const abs = path5.resolve(p);
3587
- if (!seen.has(abs) && dirExists2(abs)) {
3588
- seen.add(abs);
3589
- dirs.push(abs);
3590
- }
3591
- };
3592
- for (const dir of walkUp({
3593
- start: cwd,
3594
- stop: worktree,
3595
- targets: [".opencode"],
3596
- predicate: dirExists2
3597
- })) {
3598
- push(dir);
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 home = os3.homedir();
3601
- if (home) {
3602
- const homeDot = path5.join(home, ".opencode");
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
- const envDir = process.env.OPENCODE_CONFIG_DIR;
3606
- if (envDir && dirExists2(envDir)) push(envDir);
3607
- return dirs;
4406
+ return resolved;
3608
4407
  }
3609
- function substituteEnvPlaceholders(source) {
3610
- const out = {};
3611
- for (const [k, v] of Object.entries(source)) {
3612
- if (typeof v !== "string") continue;
3613
- out[k] = v.replace(/\{env:([A-Za-z_][A-Za-z0-9_]*)\}/g, (_match, name) => {
3614
- const resolved = process.env[name];
3615
- return typeof resolved === "string" ? resolved : "";
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
- return out;
3619
- }
3620
- function translateServer(name, spec) {
3621
- if (spec.enabled === false) return null;
3622
- const type = spec.type;
3623
- if (type === "local") {
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
- log.warn("skipping MCP server with unknown type", {
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 mergeMcp(target, source) {
3669
- const out = { ...target };
3670
- for (const [name, spec] of Object.entries(source)) {
3671
- if (!spec || typeof spec !== "object") continue;
3672
- const existing = out[name];
3673
- if (existing && typeof existing === "object") {
3674
- out[name] = deepMerge(
3675
- existing,
3676
- spec
3677
- );
3678
- } else {
3679
- out[name] = spec;
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 out;
4445
+ return record;
3683
4446
  }
3684
- function bridgeOpencodeMcp(cwd, runtimeStatus, excludeServers) {
3685
- const {
3686
- servers: merged,
3687
- enabledServerNames: allEnabledServerNames,
3688
- hash
3689
- } = mergeOpencodeMcp(cwd, runtimeStatus);
3690
- const servers = {};
3691
- const bridgedServerNames = [];
3692
- for (const [name, spec] of Object.entries(merged)) {
3693
- if (!spec || typeof spec !== "object") continue;
3694
- if (excludeServers?.has(name)) continue;
3695
- const translated = translateServer(name, spec);
3696
- if (translated) {
3697
- servers[name] = translated;
3698
- bridgedServerNames.push(name);
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 finishBridge({
3702
- servers,
3703
- bridgedServerNames,
3704
- allEnabledServerNames,
3705
- hash,
3706
- excludeServers
3707
- });
4471
+ return records;
3708
4472
  }
3709
- function mergeOpencodeMcp(cwd, runtimeStatus) {
3710
- const worktree = detectWorktree(cwd);
3711
- let merged = {};
3712
- merged = mergeMcp(merged, extractMcpBlock(loadGlobalConfig()));
3713
- const explicitConfig = process.env.OPENCODE_CONFIG;
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
- for (const dir of projectDirs.slice().reverse()) {
3734
- merged = mergeMcp(merged, extractMcpBlock(loadProjectFilesInDir(dir)));
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
- const enabledServerNames = [];
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
- function finishBridge(input) {
3760
- const { servers, bridgedServerNames, allEnabledServerNames, hash, excludeServers } = input;
3761
- if (Object.keys(servers).length === 0) {
3762
- const allEnabledServersExcluded = excludeServers && allEnabledServerNames.length > 0 && allEnabledServerNames.every((name) => excludeServers.has(name));
3763
- if (!allEnabledServersExcluded) return null;
3764
- return {
3765
- path: "",
3766
- hash,
3767
- serverNames: [],
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
- if (!fileExists2(outPath)) {
3778
- fs4.writeFileSync(outPath, body, { encoding: "utf8", mode: 384 });
3779
- }
3780
- } catch (e) {
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
- // src/runtime-status.ts
3801
- var opencodeClient = null;
3802
- function setOpencodeClient(client) {
3803
- if (client && typeof client === "object") {
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 getOpencodeClient() {
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
- const res = await client.session.get({ path: { id: sessionID } });
3842
- const data = res.data;
3843
- if (!data || typeof data !== "object") return void 0;
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
- async function getRuntimeMcpStatus() {
3855
- const client = opencodeClient;
3856
- if (!client?.mcp?.status) return void 0;
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
- const res = await client.mcp.status();
3859
- const data = res.data;
3860
- if (!data || typeof data !== "object") return void 0;
3861
- const out = {};
3862
- for (const [name, entry] of Object.entries(data)) {
3863
- if (entry && typeof entry === "object") {
3864
- const status = entry.status;
3865
- if (typeof status === "string") out[name] = status;
3866
- }
3867
- }
3868
- return out;
3869
- } catch (err) {
3870
- log.warn("failed to fetch opencode MCP runtime status", {
3871
- error: err instanceof Error ? err.message : String(err)
3872
- });
3873
- return void 0;
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
- async function fetchOpencodeToolList(provider, model, directory) {
3877
- const client = opencodeClient;
3878
- if (!client?.tool?.list) return void 0;
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
- const res = await client.tool.list({
3881
- query: { provider, model, ...directory ? { directory } : {} }
3882
- });
3883
- const data = res.data;
3884
- if (!Array.isArray(data)) return void 0;
3885
- const out = [];
3886
- for (const entry of data) {
3887
- if (!entry || typeof entry !== "object") continue;
3888
- const e = entry;
3889
- const id = typeof e.id === "string" ? e.id : null;
3890
- const description = typeof e.description === "string" ? e.description : "";
3891
- const parameters = e.parameters && typeof e.parameters === "object" ? e.parameters : {};
3892
- if (!id) continue;
3893
- out.push({ id, description, parameters });
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 fetch opencode tool list", {
3898
- provider,
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 void 0;
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 fs5 from "fs";
3913
- import * as path6 from "path";
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 (path6.isAbsolute(cmd) && fs5.existsSync(cmd)) return cmd;
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) => fs5.existsSync(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 path6.resolve(cwd).replace(/[^a-zA-Z0-9]/g, "-");
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 path6.join(os4.homedir(), ".claude");
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 path6.join(os4.homedir(), value.slice(2));
4687
+ return path7.join(os4.homedir(), value.slice(2));
3943
4688
  }
3944
- return path6.resolve(value);
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 = path6.resolve(opts.cwd ?? process.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 = path6.join(
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 fs5.readFileSync(this.jsonlPath, "utf8").split("\n");
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 readFileSync3, writeFileSync as writeFileSync4 } from "fs";
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 dirname4, join as join7 } from "path";
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 = readFileSync3(path9, "utf8").trim();
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(join7(dir, "AGENTS.md"));
5451
+ const content = readPromptFileIfPresent(join8(dir, "AGENTS.md"));
4706
5452
  if (content) return content;
4707
- const parent = dirname4(dir);
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 ?? join7(homedir5(), ".config");
4792
- const globalAgents = readPromptFileIfPresent(join7(configRoot, "opencode", "AGENTS.md"));
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 = join7(tmpdir2(), `opencode-cc-sys-${randomUUID5()}.md`);
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
- usage: msg.usage
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 list = applyTaskCreateToolResult(
7980
+ const list2 = applyTaskCreateToolResult(
7162
7981
  claudeSessionId,
7163
7982
  block.tool_use_id,
7164
7983
  resultText
7165
7984
  );
7166
- if (list) {
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: list.map((t) => ({
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
- usage: msg.usage
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
- controller.enqueue({
7297
- type: "finish",
7298
- finishReason: toFinishReason("stop"),
7299
- usage: toUsage(),
7300
- providerMetadata: {
7301
- "claude-code": {
7302
- ...resultMeta,
7303
- ...compactionMode ? { compactionModel: effectiveModelId } : {}
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
- try {
7308
- controller.close();
7309
- } catch {
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 path7 from "path";
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 ? path7.join(home, value.slice(2)) : value;
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 = path7.join(sourceRoot, item);
7647
- const target = path7.join(targetRoot, item);
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 = path7.resolve(path7.dirname(target), current);
7659
- const resolvedSource = path7.resolve(source);
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 = path7.join(
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 = path7.join(cacheRoot, `claude-${account}`);
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 readFileSync4,
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 join8, resolve as resolve4 } from "path";
7726
- import { fileURLToPath as fileURLToPath2 } from "url";
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 ? join8(xdg, "opencode") : null,
7734
- join8(homedir6(), ".cache", "opencode"),
7735
- join8(homedir6(), "Library", "Caches", "opencode")
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 ?? join8(homedir6(), ".config");
7740
- return join8(xdgConfig, "opencode", "opencode.json");
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(readFileSync4(cfg, "utf8"));
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 = fileURLToPath2(import.meta.url);
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 = join8(cacheRoot, "node_modules", STALE_PACKAGE_NAME);
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 = join8(stalePath, "package.json");
8679
+ const pkgJsonPath = join9(stalePath, "package.json");
7792
8680
  if (!existsSync4(pkgJsonPath)) return;
7793
8681
  let pkg = {};
7794
8682
  try {
7795
- pkg = JSON.parse(readFileSync4(pkgJsonPath, "utf8"));
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 = join8(cacheRoot, "package.json");
8699
+ const cachePkgJson = join9(cacheRoot, "package.json");
7812
8700
  if (!existsSync4(cachePkgJson)) return;
7813
8701
  try {
7814
- const cfg = JSON.parse(readFileSync4(cachePkgJson, "utf8"));
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