@khalilgharbaoui/opencode-claude-code-plugin 0.15.3 → 0.16.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
@@ -440,10 +440,35 @@ function cliSupportsThinking(v) {
440
440
  if (!v) return false;
441
441
  return gte(v, { major: 2, minor: 0, patch: 0 });
442
442
  }
443
+ var flagSupport = /* @__PURE__ */ new Map();
444
+ function detectCliSupportsFlag(cliPath, flag) {
445
+ const key = `${cliPath}\0${flag}`;
446
+ const cached = flagSupport.get(key);
447
+ if (cached) return cached;
448
+ const promise = (async () => {
449
+ try {
450
+ const { stdout } = await execFileAsync(cliPath, ["--help"], {
451
+ timeout: 5e3,
452
+ maxBuffer: 4 * 1024 * 1024
453
+ });
454
+ return stdout.includes(flag);
455
+ } catch (err) {
456
+ log.warn("failed to probe claude cli flag support", {
457
+ cliPath,
458
+ flag,
459
+ error: err instanceof Error ? err.message : String(err)
460
+ });
461
+ return false;
462
+ }
463
+ })();
464
+ flagSupport.set(key, promise);
465
+ return promise;
466
+ }
443
467
 
444
468
  // src/session-manager.ts
445
469
  import { spawn } from "child_process";
446
470
  import { createInterface } from "readline";
471
+ import { randomUUID as randomUUID3 } from "crypto";
447
472
  import { EventEmitter as EventEmitter3 } from "events";
448
473
  import { unlink } from "fs/promises";
449
474
 
@@ -932,12 +957,12 @@ async function createProxyMcpServer(tools = DEFAULT_PROXY_TOOLS, timeoutOverride
932
957
  });
933
958
  let timer = null;
934
959
  const result = await new Promise(
935
- (resolve4, reject2) => {
960
+ (resolve5, reject2) => {
936
961
  const entry = {
937
962
  id: callId,
938
963
  toolName,
939
964
  input,
940
- resolve: resolve4,
965
+ resolve: resolve5,
941
966
  reject: reject2,
942
967
  channel
943
968
  };
@@ -1019,11 +1044,11 @@ async function createProxyMcpServer(tools = DEFAULT_PROXY_TOOLS, timeoutOverride
1019
1044
  }
1020
1045
  }
1021
1046
  });
1022
- await new Promise((resolve4, reject2) => {
1047
+ await new Promise((resolve5, reject2) => {
1023
1048
  server2.once("error", reject2);
1024
1049
  server2.listen(0, "127.0.0.1", () => {
1025
1050
  server2.off("error", reject2);
1026
- resolve4();
1051
+ resolve5();
1027
1052
  });
1028
1053
  });
1029
1054
  const addr = server2.address();
@@ -1077,8 +1102,8 @@ async function createProxyMcpServer(tools = DEFAULT_PROXY_TOOLS, timeoutOverride
1077
1102
  entry.reject(new Error(SERVER_CLOSED_MESSAGE));
1078
1103
  }
1079
1104
  pending.clear();
1080
- await new Promise((resolve4) => {
1081
- server2.close(() => resolve4());
1105
+ await new Promise((resolve5) => {
1106
+ server2.close(() => resolve5());
1082
1107
  });
1083
1108
  if (configFilePath) {
1084
1109
  try {
@@ -1136,10 +1161,10 @@ function resolveDisallowedTools(options) {
1136
1161
  return out;
1137
1162
  }
1138
1163
  function readBody(req) {
1139
- return new Promise((resolve4, reject) => {
1164
+ return new Promise((resolve5, reject) => {
1140
1165
  const chunks = [];
1141
1166
  req.on("data", (chunk) => chunks.push(chunk));
1142
- req.on("end", () => resolve4(Buffer.concat(chunks).toString("utf8")));
1167
+ req.on("end", () => resolve5(Buffer.concat(chunks).toString("utf8")));
1143
1168
  req.on("error", reject);
1144
1169
  });
1145
1170
  }
@@ -1621,7 +1646,7 @@ async function requestSideQuestion(activeProcess, question, options) {
1621
1646
  }
1622
1647
  });
1623
1648
  pendingProcesses.add(proc);
1624
- return new Promise((resolve4, reject) => {
1649
+ return new Promise((resolve5, reject) => {
1625
1650
  const event = `side-question:${requestId}`;
1626
1651
  let settled = false;
1627
1652
  let sent = false;
@@ -1676,7 +1701,7 @@ async function requestSideQuestion(activeProcess, question, options) {
1676
1701
  }
1677
1702
  settled = true;
1678
1703
  cleanup();
1679
- resolve4({ response: result.response, synthetic: result.synthetic });
1704
+ resolve5({ response: result.response, synthetic: result.synthetic });
1680
1705
  };
1681
1706
  const timer = setTimeout(() => {
1682
1707
  fail(new Error(`/btw timed out after ${timeoutMs}ms.`), true);
@@ -1731,6 +1756,8 @@ function takeUnattendedLines(ap) {
1731
1756
  }
1732
1757
  var activeProcesses = /* @__PURE__ */ new Map();
1733
1758
  var claudeSessions = /* @__PURE__ */ new Map();
1759
+ var idleEvictionTimers = /* @__PURE__ */ new Map();
1760
+ var MAX_IDLE_TIMEOUT_MS = 2147483647;
1734
1761
  var MAX_ACTIVE_PROCESSES = 16;
1735
1762
  var PROCESS_EXIT_TIMEOUT_MS = 1500;
1736
1763
  var PROCESS_FORCE_EXIT_TIMEOUT_MS = 500;
@@ -1778,15 +1805,107 @@ function evictIfNeeded() {
1778
1805
  deleteActiveProcess(oldestKey);
1779
1806
  }
1780
1807
  }
1808
+ var TURN_INTERRUPT_TIMEOUT_MS = 5e3;
1809
+ function isTerminalResultLine(line) {
1810
+ if (!line.includes('"result"')) return false;
1811
+ try {
1812
+ return JSON.parse(line).type === "result";
1813
+ } catch {
1814
+ return false;
1815
+ }
1816
+ }
1817
+ function settleTurn(ap) {
1818
+ ap.turnInFlight = false;
1819
+ const waiters = ap.turnIdleWaiters ?? [];
1820
+ ap.turnIdleWaiters = [];
1821
+ for (const wake of waiters) wake();
1822
+ }
1823
+ function noteTurnStarted(ap) {
1824
+ if (ap.asideTransport?.interactive) return;
1825
+ ap.turnInFlight = true;
1826
+ }
1827
+ function noteTurnLine(ap, line) {
1828
+ if (!ap.turnInFlight) return;
1829
+ if (isTerminalResultLine(line)) settleTurn(ap);
1830
+ }
1831
+ function isTurnInFlight(ap) {
1832
+ return ap.turnInFlight === true;
1833
+ }
1834
+ function awaitTurnIdle(ap, timeoutMs) {
1835
+ if (!ap.turnInFlight) return Promise.resolve(true);
1836
+ return new Promise((resolve5) => {
1837
+ const wake = () => {
1838
+ clearTimeout(timer);
1839
+ resolve5(true);
1840
+ };
1841
+ const timer = setTimeout(() => {
1842
+ const waiters = ap.turnIdleWaiters ?? [];
1843
+ const at = waiters.indexOf(wake);
1844
+ if (at >= 0) waiters.splice(at, 1);
1845
+ resolve5(false);
1846
+ }, timeoutMs);
1847
+ (ap.turnIdleWaiters ??= []).push(wake);
1848
+ });
1849
+ }
1850
+ function interruptTurn(ap, timeoutMs = TURN_INTERRUPT_TIMEOUT_MS) {
1851
+ if (!ap.turnInFlight) return Promise.resolve(true);
1852
+ const stdin = ap.proc.stdin;
1853
+ if (ap.asideTransport?.interactive || !stdin || !stdin.writable) {
1854
+ log.notice("cannot interrupt this transport; waiting for the turn to end");
1855
+ return awaitTurnIdle(ap, timeoutMs);
1856
+ }
1857
+ try {
1858
+ stdin.write(
1859
+ JSON.stringify({
1860
+ type: "control_request",
1861
+ request_id: randomUUID3(),
1862
+ request: { subtype: "interrupt" }
1863
+ }) + "\n"
1864
+ );
1865
+ } catch (error) {
1866
+ log.warn("failed to write interrupt control request", {
1867
+ error: error instanceof Error ? error.message : String(error)
1868
+ });
1869
+ return Promise.resolve(false);
1870
+ }
1871
+ return awaitTurnIdle(ap, timeoutMs);
1872
+ }
1873
+ function cancelIdleProcessEviction(key) {
1874
+ const timer = idleEvictionTimers.get(key);
1875
+ if (!timer) return;
1876
+ clearTimeout(timer);
1877
+ idleEvictionTimers.delete(key);
1878
+ }
1781
1879
  function getActiveProcess(key) {
1782
1880
  const ap = activeProcesses.get(key);
1783
- if (ap) touch(key);
1881
+ if (ap) {
1882
+ cancelIdleProcessEviction(key);
1883
+ touch(key);
1884
+ }
1784
1885
  return ap;
1785
1886
  }
1786
1887
  function setActiveProcess(key, ap) {
1888
+ cancelIdleProcessEviction(key);
1787
1889
  activeProcesses.set(key, ap);
1788
1890
  }
1891
+ function scheduleIdleProcessEviction(key, timeoutMs) {
1892
+ cancelIdleProcessEviction(key);
1893
+ if (typeof timeoutMs !== "number" || !Number.isFinite(timeoutMs) || timeoutMs <= 0 || timeoutMs > MAX_IDLE_TIMEOUT_MS) {
1894
+ return;
1895
+ }
1896
+ const scheduledProcess = activeProcesses.get(key);
1897
+ if (!scheduledProcess) return;
1898
+ const timer = setTimeout(() => {
1899
+ idleEvictionTimers.delete(key);
1900
+ if (activeProcesses.get(key) !== scheduledProcess) return;
1901
+ log.info("evicting idle claude process", { sessionKey: key, timeoutMs });
1902
+ deleteActiveProcess(key);
1903
+ }, timeoutMs);
1904
+ timer.unref();
1905
+ idleEvictionTimers.set(key, timer);
1906
+ }
1789
1907
  function detachActiveProcess(key) {
1908
+ cancelIdleProcessEviction(key);
1790
1909
  const ap = activeProcesses.get(key);
1791
1910
  if (!ap) return void 0;
1792
1911
  activeProcesses.delete(key);
@@ -1802,14 +1921,14 @@ function hasProcessExited(proc) {
1802
1921
  }
1803
1922
  function waitForProcessExit(proc, timeoutMs) {
1804
1923
  if (hasProcessExited(proc)) return Promise.resolve(true);
1805
- return new Promise((resolve4) => {
1924
+ return new Promise((resolve5) => {
1806
1925
  const onExit = () => {
1807
1926
  clearTimeout(timer);
1808
- resolve4(true);
1927
+ resolve5(true);
1809
1928
  };
1810
1929
  const timer = setTimeout(() => {
1811
1930
  proc.off("exit", onExit);
1812
- resolve4(hasProcessExited(proc));
1931
+ resolve5(hasProcessExited(proc));
1813
1932
  }, timeoutMs);
1814
1933
  proc.once("exit", onExit);
1815
1934
  });
@@ -1905,6 +2024,7 @@ function spawnClaudeProcess(cliPath, cliArgs, cwd, sessionKey2, proxyServer, mcp
1905
2024
  const rl = createInterface({ input: proc.stdout });
1906
2025
  rl.on("line", (line) => {
1907
2026
  if (dispatchSideQuestionResponse(ap, line)) return;
2027
+ noteTurnLine(ap, line);
1908
2028
  if (lineEmitter.listenerCount("line") === 0) {
1909
2029
  bufferUnattendedLine(ap, line);
1910
2030
  return;
@@ -1912,8 +2032,10 @@ function spawnClaudeProcess(cliPath, cliArgs, cwd, sessionKey2, proxyServer, mcp
1912
2032
  lineEmitter.emit("line", line);
1913
2033
  });
1914
2034
  rl.on("close", () => {
2035
+ settleTurn(ap);
1915
2036
  lineEmitter.emit("close");
1916
2037
  });
2038
+ cancelIdleProcessEviction(sessionKey2);
1917
2039
  activeProcesses.set(sessionKey2, ap);
1918
2040
  proc.on("error", (err) => {
1919
2041
  log.error("claude process error", { sessionKey: sessionKey2, error: err.message });
@@ -1926,7 +2048,10 @@ function spawnClaudeProcess(cliPath, cliArgs, cwd, sessionKey2, proxyServer, mcp
1926
2048
  });
1927
2049
  }
1928
2050
  const ownsSessionKey = activeProcesses.get(sessionKey2) === ap;
1929
- if (ownsSessionKey) activeProcesses.delete(sessionKey2);
2051
+ if (ownsSessionKey) {
2052
+ cancelIdleProcessEviction(sessionKey2);
2053
+ activeProcesses.delete(sessionKey2);
2054
+ }
1930
2055
  if (ownsSessionKey && code !== 0 && code !== null) {
1931
2056
  log.info("process exited with error, clearing session", {
1932
2057
  code,
@@ -1997,6 +2122,7 @@ function buildCliArgs(opts) {
1997
2122
  strictMcpConfig,
1998
2123
  disallowedTools,
1999
2124
  appendSystemPromptFile,
2125
+ pluginDirs,
2000
2126
  thinking,
2001
2127
  thinkingDisplay,
2002
2128
  fastMode,
@@ -2045,6 +2171,9 @@ function buildCliArgs(opts) {
2045
2171
  if (appendSystemPromptFile) {
2046
2172
  args.push("--append-system-prompt-file", appendSystemPromptFile);
2047
2173
  }
2174
+ for (const dir of pluginDirs ?? []) {
2175
+ args.push("--plugin-dir", dir);
2176
+ }
2048
2177
  if (fastMode && cliSupportsFastMode(cliVersion ?? null)) {
2049
2178
  args.push("--settings", JSON.stringify({ fastMode: true }));
2050
2179
  }
@@ -2169,7 +2298,7 @@ async function waitForSessionIdle(client, sessionID, options = {}) {
2169
2298
  if (options.stop?.()) return true;
2170
2299
  if (await sessionStatus(client, sessionID) !== "busy") return true;
2171
2300
  if (Date.now() - started >= timeoutMs) return false;
2172
- await new Promise((resolve4) => setTimeout(resolve4, pollMs));
2301
+ await new Promise((resolve5) => setTimeout(resolve5, pollMs));
2173
2302
  }
2174
2303
  }
2175
2304
  async function deliverAsideInline(client, sessionID, text, options = {}) {
@@ -2180,7 +2309,7 @@ async function deliverAsideInline(client, sessionID, text, options = {}) {
2180
2309
  if (emitAsideInline(sessionID, text)) return true;
2181
2310
  if (Date.now() - started >= timeoutMs) return false;
2182
2311
  if (await sessionStatus(client, sessionID) !== "busy") return false;
2183
- await new Promise((resolve4) => setTimeout(resolve4, pollMs));
2312
+ await new Promise((resolve5) => setTimeout(resolve5, pollMs));
2184
2313
  }
2185
2314
  }
2186
2315
  async function waitForAsideProcess(client, sessionID, options = {}) {
@@ -2201,7 +2330,7 @@ async function waitForAsideProcess(client, sessionID, options = {}) {
2201
2330
  log.warn("btw: a turn is running but no claude process appeared for it", { sessionID, waitedMs });
2202
2331
  return void 0;
2203
2332
  }
2204
- await new Promise((resolve4) => setTimeout(resolve4, pollMs));
2333
+ await new Promise((resolve5) => setTimeout(resolve5, pollMs));
2205
2334
  }
2206
2335
  }
2207
2336
  async function settleSessionBusy(client, sessionID, active, options = {}) {
@@ -2213,7 +2342,7 @@ async function settleSessionBusy(client, sessionID, active, options = {}) {
2213
2342
  if (status === "busy") return true;
2214
2343
  if (status === "unknown") return isProcessBusy(active);
2215
2344
  if (Date.now() - started >= settleMs) return false;
2216
- await new Promise((resolve4) => setTimeout(resolve4, pollMs));
2345
+ await new Promise((resolve5) => setTimeout(resolve5, pollMs));
2217
2346
  }
2218
2347
  }
2219
2348
  function errorText(error) {
@@ -2260,10 +2389,10 @@ async function handleBtwCommand(client, input, options = {}) {
2260
2389
  let inlineDone = false;
2261
2390
  let markInlineDelivered = () => {
2262
2391
  };
2263
- const inlineDelivered = new Promise((resolve4) => {
2392
+ const inlineDelivered = new Promise((resolve5) => {
2264
2393
  markInlineDelivered = () => {
2265
2394
  inlineDone = true;
2266
- resolve4("inline");
2395
+ resolve5("inline");
2267
2396
  };
2268
2397
  });
2269
2398
  if (isSideQuestionPending(active)) {
@@ -2511,7 +2640,7 @@ function compactConversationHistory(prompt, opts = {}) {
2511
2640
  return buildCompactionHistory(prompt);
2512
2641
  }
2513
2642
  const conversationMessages = prompt.filter(
2514
- (m) => m.role === "user" || m.role === "assistant"
2643
+ (m) => m.role === "user" || m.role === "assistant" || m.role === "tool"
2515
2644
  );
2516
2645
  if (conversationMessages.length <= 1) {
2517
2646
  return null;
@@ -2519,28 +2648,8 @@ function compactConversationHistory(prompt, opts = {}) {
2519
2648
  const historyParts = [];
2520
2649
  for (let i = 0; i < conversationMessages.length - 1; i++) {
2521
2650
  const msg = conversationMessages[i];
2522
- const role = msg.role === "user" ? "User" : "Assistant";
2523
- let text = "";
2524
- if (typeof msg.content === "string") {
2525
- text = msg.content;
2526
- } else if (Array.isArray(msg.content)) {
2527
- const textParts = msg.content.filter((p) => p.type === "text" && p.text).map((p) => p.text);
2528
- text = textParts.join("\n");
2529
- const toolCalls = msg.content.filter(
2530
- (p) => p.type === "tool-call"
2531
- );
2532
- const toolResults = msg.content.filter(
2533
- (p) => p.type === "tool-result"
2534
- );
2535
- if (toolCalls.length > 0) {
2536
- text += `
2537
- [Called ${toolCalls.length} tool(s): ${toolCalls.map((t) => t.toolName).join(", ")}]`;
2538
- }
2539
- if (toolResults.length > 0) {
2540
- text += `
2541
- [Received ${toolResults.length} tool result(s)]`;
2542
- }
2543
- }
2651
+ const role = msg.role === "user" ? "User" : msg.role === "assistant" ? "Assistant" : "Tool";
2652
+ const { text } = renderMessageContentForCompaction(msg);
2544
2653
  if (text.trim()) {
2545
2654
  const truncated = text.length > 2e3 ? text.slice(0, 2e3) + "..." : text;
2546
2655
  historyParts.push(`${role}: ${truncated}`);
@@ -2583,7 +2692,27 @@ function buildCompactionHistory(prompt) {
2583
2692
  }
2584
2693
  function getClaudeUserMessage(prompt, includeHistoryContext = false, opts = {}) {
2585
2694
  const compactionMode = opts.compactionMode === true;
2695
+ const cliToolCallIds = opts.cliToolCallIds;
2586
2696
  const content = [];
2697
+ const pushToolResult = (part) => {
2698
+ const id = part.toolCallId;
2699
+ const text = getToolResultText(part);
2700
+ if (!cliToolCallIds || cliToolCallIds.has(id)) {
2701
+ content.push({ type: "tool_result", tool_use_id: id, content: text });
2702
+ return;
2703
+ }
2704
+ log.info("rendering opencode-side tool result as text", {
2705
+ toolCallId: id,
2706
+ toolName: part.toolName,
2707
+ chars: text.length
2708
+ });
2709
+ content.push({
2710
+ type: "text",
2711
+ text: `<opencode_tool_result tool="${part.toolName ?? "unknown"}">
2712
+ ${text}
2713
+ </opencode_tool_result>`
2714
+ });
2715
+ };
2587
2716
  if (compactionMode) {
2588
2717
  const transcript = compactConversationHistory(prompt, {
2589
2718
  mode: "compaction"
@@ -2653,12 +2782,7 @@ Now continuing with the current message:
2653
2782
  });
2654
2783
  }
2655
2784
  } else if (part.type === "tool-result") {
2656
- const p = part;
2657
- content.push({
2658
- type: "tool_result",
2659
- tool_use_id: p.toolCallId,
2660
- content: getToolResultText(p)
2661
- });
2785
+ pushToolResult(part);
2662
2786
  }
2663
2787
  }
2664
2788
  }
@@ -2666,12 +2790,7 @@ Now continuing with the current message:
2666
2790
  if (Array.isArray(msg.content)) {
2667
2791
  for (const part of msg.content) {
2668
2792
  if (part?.type === "tool-result") {
2669
- const p = part;
2670
- content.push({
2671
- type: "tool_result",
2672
- tool_use_id: p.toolCallId,
2673
- content: getToolResultText(p)
2674
- });
2793
+ pushToolResult(part);
2675
2794
  }
2676
2795
  }
2677
2796
  }
@@ -3103,34 +3222,166 @@ function agentDirectories(home, projectDirectory) {
3103
3222
  return directories;
3104
3223
  }
3105
3224
 
3106
- // src/mcp-bridge.ts
3225
+ // src/skill-bridge.ts
3226
+ import * as crypto2 from "crypto";
3107
3227
  import * as fs3 from "fs";
3108
- import * as path4 from "path";
3109
3228
  import * as os2 from "os";
3110
- import * as crypto2 from "crypto";
3229
+ import * as path4 from "path";
3230
+ var SKILL_PLUGIN_NAME = "opencode-skills";
3231
+ function dirExists(p) {
3232
+ try {
3233
+ return fs3.statSync(p).isDirectory();
3234
+ } catch {
3235
+ return false;
3236
+ }
3237
+ }
3238
+ function fileExists(p) {
3239
+ try {
3240
+ return fs3.statSync(p).isFile();
3241
+ } catch {
3242
+ return false;
3243
+ }
3244
+ }
3245
+ function skillRoots(cwd) {
3246
+ const roots = [];
3247
+ const seen = /* @__PURE__ */ new Set();
3248
+ const push = (p) => {
3249
+ const abs = path4.resolve(p);
3250
+ if (seen.has(abs)) return;
3251
+ seen.add(abs);
3252
+ if (dirExists(abs)) roots.push(abs);
3253
+ };
3254
+ let current = path4.resolve(cwd);
3255
+ while (true) {
3256
+ push(path4.join(current, ".opencode", "skills"));
3257
+ const parent = path4.dirname(current);
3258
+ if (parent === current) break;
3259
+ current = parent;
3260
+ }
3261
+ const home = os2.homedir();
3262
+ if (home) push(path4.join(home, ".opencode", "skills"));
3263
+ const envDir = process.env.OPENCODE_CONFIG_DIR;
3264
+ if (envDir) push(path4.join(envDir, "skills"));
3265
+ const xdg = process.env.XDG_CONFIG_HOME ?? (home ? path4.join(home, ".config") : null);
3266
+ if (xdg) push(path4.join(xdg, "opencode", "skills"));
3267
+ return roots;
3268
+ }
3269
+ function discoverOpencodeSkills(cwd) {
3270
+ const found = [];
3271
+ const claimed = /* @__PURE__ */ new Set();
3272
+ for (const root of skillRoots(cwd)) {
3273
+ let entries;
3274
+ try {
3275
+ entries = fs3.readdirSync(root, { withFileTypes: true });
3276
+ } catch {
3277
+ continue;
3278
+ }
3279
+ for (const entry of entries) {
3280
+ if (!entry.isDirectory() && !entry.isSymbolicLink()) continue;
3281
+ const name = entry.name;
3282
+ if (name.startsWith(".")) continue;
3283
+ if (claimed.has(name)) continue;
3284
+ const dir = path4.join(root, name);
3285
+ if (!fileExists(path4.join(dir, "SKILL.md"))) continue;
3286
+ claimed.add(name);
3287
+ found.push({ name, dir });
3288
+ }
3289
+ }
3290
+ return found.sort((a, b) => a.name.localeCompare(b.name));
3291
+ }
3292
+ function linkSkill(source, target) {
3293
+ try {
3294
+ fs3.symlinkSync(source, target, process.platform === "win32" ? "junction" : "dir");
3295
+ return;
3296
+ } catch {
3297
+ fs3.cpSync(source, target, { recursive: true, dereference: true });
3298
+ }
3299
+ }
3300
+ function buildSkillPluginDir(skills) {
3301
+ if (skills.length === 0) return null;
3302
+ const fingerprint = skills.map((s) => `${s.name}\0${s.dir}`).join("\n");
3303
+ const hash = crypto2.createHash("sha256").update(fingerprint).digest("hex").slice(0, 12);
3304
+ const root = path4.join(pluginTmpDir(), `skills-${hash}`);
3305
+ const manifest = path4.join(root, ".claude-plugin", "plugin.json");
3306
+ if (fileExists(manifest)) return root;
3307
+ try {
3308
+ fs3.rmSync(root, { recursive: true, force: true });
3309
+ fs3.mkdirSync(path4.join(root, ".claude-plugin"), { recursive: true });
3310
+ fs3.mkdirSync(path4.join(root, "skills"), { recursive: true });
3311
+ fs3.writeFileSync(
3312
+ manifest,
3313
+ JSON.stringify(
3314
+ {
3315
+ name: SKILL_PLUGIN_NAME,
3316
+ description: "Skills discovered from this opencode installation, bridged into Claude Code."
3317
+ },
3318
+ null,
3319
+ 2
3320
+ ),
3321
+ { encoding: "utf8", mode: 384 }
3322
+ );
3323
+ for (const skill of skills) {
3324
+ linkSkill(skill.dir, path4.join(root, "skills", skill.name));
3325
+ }
3326
+ } catch (err) {
3327
+ log.warn("failed to stage opencode skill plugin dir", {
3328
+ root,
3329
+ error: err instanceof Error ? err.message : String(err)
3330
+ });
3331
+ return null;
3332
+ }
3333
+ return root;
3334
+ }
3335
+ async function resolveSkillPluginDirs(opts) {
3336
+ if (!opts.enabled) return [];
3337
+ const skills = discoverOpencodeSkills(opts.cwd);
3338
+ if (skills.length === 0) return [];
3339
+ const supported = await detectCliSupportsFlag(opts.cliPath, "--plugin-dir");
3340
+ if (!supported) {
3341
+ log.notice(
3342
+ "claude cli does not support --plugin-dir; opencode skills will not be bridged. Run `npm i -g @anthropic-ai/claude-code` to upgrade.",
3343
+ { skills: skills.length }
3344
+ );
3345
+ return [];
3346
+ }
3347
+ const dir = buildSkillPluginDir(skills);
3348
+ if (!dir) return [];
3349
+ log.info("bridged opencode skills into claude", {
3350
+ count: skills.length,
3351
+ names: skills.map((s) => s.name),
3352
+ pluginDir: dir
3353
+ });
3354
+ return [dir];
3355
+ }
3356
+
3357
+ // src/mcp-bridge.ts
3358
+ import * as fs4 from "fs";
3359
+ import * as path5 from "path";
3360
+ import * as os3 from "os";
3361
+ import * as crypto3 from "crypto";
3111
3362
  import {
3112
3363
  parse as parseJsonc,
3113
3364
  printParseErrorCode
3114
3365
  } from "jsonc-parser";
3115
3366
  var FILE_NAMES = ["opencode.jsonc", "opencode.json", "config.json"];
3116
3367
  var PROJECT_FILE_NAMES = ["opencode.json", "opencode.jsonc"];
3117
- function fileExists(p) {
3368
+ function fileExists2(p) {
3118
3369
  try {
3119
- return fs3.statSync(p).isFile();
3370
+ return fs4.statSync(p).isFile();
3120
3371
  } catch {
3121
3372
  return false;
3122
3373
  }
3123
3374
  }
3124
- function dirExists(p) {
3375
+ function dirExists2(p) {
3125
3376
  try {
3126
- return fs3.statSync(p).isDirectory();
3377
+ return fs4.statSync(p).isDirectory();
3127
3378
  } catch {
3128
3379
  return false;
3129
3380
  }
3130
3381
  }
3131
3382
  function readAndParse(file) {
3132
3383
  try {
3133
- const raw = fs3.readFileSync(file, "utf8");
3384
+ const raw = fs4.readFileSync(file, "utf8");
3134
3385
  const errors = [];
3135
3386
  const parsed = parseJsonc(raw, errors, { allowTrailingComma: true });
3136
3387
  if (errors.length > 0) {
@@ -3166,14 +3417,14 @@ function deepMerge(target, source) {
3166
3417
  }
3167
3418
  function walkUp(opts) {
3168
3419
  const out = [];
3169
- let current = path4.resolve(opts.start);
3420
+ let current = path5.resolve(opts.start);
3170
3421
  while (true) {
3171
3422
  for (const target of opts.targets) {
3172
- const candidate = path4.join(current, target);
3423
+ const candidate = path5.join(current, target);
3173
3424
  if (opts.predicate(candidate)) out.push(candidate);
3174
3425
  }
3175
- if (opts.stop && current === path4.resolve(opts.stop)) break;
3176
- const parent = path4.dirname(current);
3426
+ if (opts.stop && current === path5.resolve(opts.stop)) break;
3427
+ const parent = path5.dirname(current);
3177
3428
  if (parent === current) break;
3178
3429
  current = parent;
3179
3430
  }
@@ -3181,29 +3432,29 @@ function walkUp(opts) {
3181
3432
  }
3182
3433
  function detectWorktree(cwd) {
3183
3434
  const override = process.env.OPENCODE_WORKTREE;
3184
- if (override) return path4.resolve(override);
3185
- let current = path4.resolve(cwd);
3435
+ if (override) return path5.resolve(override);
3436
+ let current = path5.resolve(cwd);
3186
3437
  while (true) {
3187
- const gitPath = path4.join(current, ".git");
3438
+ const gitPath = path5.join(current, ".git");
3188
3439
  try {
3189
- if (fs3.existsSync(gitPath)) return current;
3440
+ if (fs4.existsSync(gitPath)) return current;
3190
3441
  } catch {
3191
3442
  }
3192
- const parent = path4.dirname(current);
3443
+ const parent = path5.dirname(current);
3193
3444
  if (parent === current) return void 0;
3194
3445
  current = parent;
3195
3446
  }
3196
3447
  }
3197
3448
  function globalConfigDir() {
3198
- const xdg = process.env.XDG_CONFIG_HOME ?? path4.join(os2.homedir(), ".config");
3199
- return path4.join(xdg, "opencode");
3449
+ const xdg = process.env.XDG_CONFIG_HOME ?? path5.join(os3.homedir(), ".config");
3450
+ return path5.join(xdg, "opencode");
3200
3451
  }
3201
3452
  function loadGlobalConfig() {
3202
3453
  const dir = globalConfigDir();
3203
3454
  let merged = {};
3204
3455
  for (const name of FILE_NAMES.slice().reverse()) {
3205
- const file = path4.join(dir, name);
3206
- if (!fileExists(file)) continue;
3456
+ const file = path5.join(dir, name);
3457
+ if (!fileExists2(file)) continue;
3207
3458
  const parsed = readAndParse(file);
3208
3459
  if (parsed) merged = deepMerge(merged, parsed);
3209
3460
  }
@@ -3212,8 +3463,8 @@ function loadGlobalConfig() {
3212
3463
  function loadProjectFilesInDir(dir) {
3213
3464
  let merged = {};
3214
3465
  for (const name of PROJECT_FILE_NAMES) {
3215
- const file = path4.join(dir, name);
3216
- if (!fileExists(file)) continue;
3466
+ const file = path5.join(dir, name);
3467
+ if (!fileExists2(file)) continue;
3217
3468
  const parsed = readAndParse(file);
3218
3469
  if (parsed) merged = deepMerge(merged, parsed);
3219
3470
  }
@@ -3223,8 +3474,8 @@ function dotOpencodeDirs(cwd, worktree) {
3223
3474
  const dirs = [];
3224
3475
  const seen = /* @__PURE__ */ new Set();
3225
3476
  const push = (p) => {
3226
- const abs = path4.resolve(p);
3227
- if (!seen.has(abs) && dirExists(abs)) {
3477
+ const abs = path5.resolve(p);
3478
+ if (!seen.has(abs) && dirExists2(abs)) {
3228
3479
  seen.add(abs);
3229
3480
  dirs.push(abs);
3230
3481
  }
@@ -3233,17 +3484,17 @@ function dotOpencodeDirs(cwd, worktree) {
3233
3484
  start: cwd,
3234
3485
  stop: worktree,
3235
3486
  targets: [".opencode"],
3236
- predicate: dirExists
3487
+ predicate: dirExists2
3237
3488
  })) {
3238
3489
  push(dir);
3239
3490
  }
3240
- const home = os2.homedir();
3491
+ const home = os3.homedir();
3241
3492
  if (home) {
3242
- const homeDot = path4.join(home, ".opencode");
3243
- if (dirExists(homeDot)) push(homeDot);
3493
+ const homeDot = path5.join(home, ".opencode");
3494
+ if (dirExists2(homeDot)) push(homeDot);
3244
3495
  }
3245
3496
  const envDir = process.env.OPENCODE_CONFIG_DIR;
3246
- if (envDir && dirExists(envDir)) push(envDir);
3497
+ if (envDir && dirExists2(envDir)) push(envDir);
3247
3498
  return dirs;
3248
3499
  }
3249
3500
  function substituteEnvPlaceholders(source) {
@@ -3351,7 +3602,7 @@ function mergeOpencodeMcp(cwd, runtimeStatus) {
3351
3602
  let merged = {};
3352
3603
  merged = mergeMcp(merged, extractMcpBlock(loadGlobalConfig()));
3353
3604
  const explicitConfig = process.env.OPENCODE_CONFIG;
3354
- if (explicitConfig && fileExists(explicitConfig)) {
3605
+ if (explicitConfig && fileExists2(explicitConfig)) {
3355
3606
  const parsed = readAndParse(explicitConfig);
3356
3607
  if (parsed) merged = mergeMcp(merged, extractMcpBlock(parsed));
3357
3608
  }
@@ -3359,12 +3610,12 @@ function mergeOpencodeMcp(cwd, runtimeStatus) {
3359
3610
  start: cwd,
3360
3611
  stop: worktree,
3361
3612
  targets: PROJECT_FILE_NAMES,
3362
- predicate: fileExists
3613
+ predicate: fileExists2
3363
3614
  });
3364
3615
  const projectDirs = [];
3365
3616
  const seenProjectDirs = /* @__PURE__ */ new Set();
3366
3617
  for (const f of projectFiles) {
3367
- const d = path4.dirname(f);
3618
+ const d = path5.dirname(f);
3368
3619
  if (!seenProjectDirs.has(d)) {
3369
3620
  seenProjectDirs.add(d);
3370
3621
  projectDirs.push(d);
@@ -3393,7 +3644,7 @@ function mergeOpencodeMcp(cwd, runtimeStatus) {
3393
3644
  enabledServerNames.push(name);
3394
3645
  }
3395
3646
  const mergedBody = JSON.stringify({ mcpServers: merged }, null, 2);
3396
- const hash = crypto2.createHash("sha256").update(mergedBody).digest("hex").slice(0, 12);
3647
+ const hash = crypto3.createHash("sha256").update(mergedBody).digest("hex").slice(0, 12);
3397
3648
  return { servers: merged, enabledServerNames, hash };
3398
3649
  }
3399
3650
  function finishBridge(input) {
@@ -3409,13 +3660,13 @@ function finishBridge(input) {
3409
3660
  };
3410
3661
  }
3411
3662
  const body = JSON.stringify({ mcpServers: servers }, null, 2);
3412
- const outPath = path4.join(
3663
+ const outPath = path5.join(
3413
3664
  pluginTmpDir(),
3414
3665
  `mcp-${hash}.json`
3415
3666
  );
3416
3667
  try {
3417
- if (!fileExists(outPath)) {
3418
- fs3.writeFileSync(outPath, body, { encoding: "utf8", mode: 384 });
3668
+ if (!fileExists2(outPath)) {
3669
+ fs4.writeFileSync(outPath, body, { encoding: "utf8", mode: 384 });
3419
3670
  }
3420
3671
  } catch (e) {
3421
3672
  log.warn("failed to write bridged MCP config", {
@@ -3457,17 +3708,39 @@ function getOpencodeProjectDirectory() {
3457
3708
  function isUsableDirectory(d) {
3458
3709
  return typeof d === "string" && d.length > 1 && d !== "/";
3459
3710
  }
3460
- function resolveSpawnCwd(configured) {
3711
+ function resolveSpawnCwdFrom(configured, live, captured, sessionDir) {
3712
+ if (configured) return configured;
3713
+ if (isUsableDirectory(sessionDir)) return sessionDir;
3714
+ if (isUsableDirectory(live)) return live;
3715
+ return captured ?? live;
3716
+ }
3717
+ async function resolveSpawnCwdForSession(configured, sessionID) {
3718
+ if (configured) return configured;
3719
+ const sessionDir = sessionID ? await fetchSessionDirectory(sessionID) : void 0;
3461
3720
  return resolveSpawnCwdFrom(
3462
3721
  configured,
3463
3722
  process.cwd(),
3464
- opencodeProjectDirectory
3723
+ opencodeProjectDirectory,
3724
+ sessionDir
3465
3725
  );
3466
3726
  }
3467
- function resolveSpawnCwdFrom(configured, live, captured) {
3468
- if (configured) return configured;
3469
- if (isUsableDirectory(live)) return live;
3470
- return captured ?? live;
3727
+ async function fetchSessionDirectory(sessionID) {
3728
+ if (!sessionID || sessionID === "default") return void 0;
3729
+ const client = opencodeClient;
3730
+ if (!client?.session?.get) return void 0;
3731
+ try {
3732
+ const res = await client.session.get({ path: { id: sessionID } });
3733
+ const data = res.data;
3734
+ if (!data || typeof data !== "object") return void 0;
3735
+ const dir = data.directory;
3736
+ return isUsableDirectory(dir) ? dir : void 0;
3737
+ } catch (err) {
3738
+ log.warn("failed to fetch opencode session directory", {
3739
+ sessionID,
3740
+ error: err instanceof Error ? err.message : String(err)
3741
+ });
3742
+ return void 0;
3743
+ }
3471
3744
  }
3472
3745
  async function getRuntimeMcpStatus() {
3473
3746
  const client = opencodeClient;
@@ -3526,40 +3799,40 @@ import { EventEmitter as EventEmitter4 } from "events";
3526
3799
  import { unlink as unlink2 } from "fs/promises";
3527
3800
 
3528
3801
  // src/claude-session-bun.ts
3529
- import * as os3 from "os";
3530
- import * as fs4 from "fs";
3531
- import * as path5 from "path";
3802
+ import * as os4 from "os";
3803
+ import * as fs5 from "fs";
3804
+ import * as path6 from "path";
3532
3805
  import { execFileSync } from "child_process";
3533
- import { randomUUID as randomUUID3 } from "crypto";
3806
+ import { randomUUID as randomUUID4 } from "crypto";
3534
3807
  function resolveClaude(cmd = "claude") {
3535
- if (path5.isAbsolute(cmd) && fs4.existsSync(cmd)) return cmd;
3808
+ if (path6.isAbsolute(cmd) && fs5.existsSync(cmd)) return cmd;
3536
3809
  const viaBun = Bun.which(cmd);
3537
3810
  if (viaBun) return viaBun;
3538
- const isWin = os3.platform() === "win32";
3811
+ const isWin = os4.platform() === "win32";
3539
3812
  try {
3540
3813
  const out = execFileSync(isWin ? "where" : "which", [cmd], {
3541
3814
  encoding: "utf8",
3542
3815
  stdio: ["ignore", "pipe", "ignore"]
3543
3816
  });
3544
- const first = out.split(/\r?\n/).map((l) => l.trim()).filter(Boolean).find((p) => fs4.existsSync(p));
3817
+ const first = out.split(/\r?\n/).map((l) => l.trim()).filter(Boolean).find((p) => fs5.existsSync(p));
3545
3818
  if (first) return first;
3546
3819
  } catch {
3547
3820
  }
3548
3821
  throw new Error(`Could not resolve command on PATH: ${cmd}`);
3549
3822
  }
3550
3823
  function encodeCwd(cwd) {
3551
- return path5.resolve(cwd).replace(/[^a-zA-Z0-9]/g, "-");
3824
+ return path6.resolve(cwd).replace(/[^a-zA-Z0-9]/g, "-");
3552
3825
  }
3553
3826
  var TERMINAL_STOP = /* @__PURE__ */ new Set(["end_turn", "stop_sequence", "max_tokens"]);
3554
3827
  var delay = (ms) => new Promise((r) => setTimeout(r, ms));
3555
3828
  function resolveConfigDir(configDir) {
3556
3829
  const value = configDir ?? process.env.CLAUDE_CONFIG_DIR;
3557
- if (!value) return path5.join(os3.homedir(), ".claude");
3558
- if (value === "~") return os3.homedir();
3830
+ if (!value) return path6.join(os4.homedir(), ".claude");
3831
+ if (value === "~") return os4.homedir();
3559
3832
  if (value.startsWith("~/") || value.startsWith("~\\")) {
3560
- return path5.join(os3.homedir(), value.slice(2));
3833
+ return path6.join(os4.homedir(), value.slice(2));
3561
3834
  }
3562
- return path5.resolve(value);
3835
+ return path6.resolve(value);
3563
3836
  }
3564
3837
  var ClaudeSession = class {
3565
3838
  sessionId;
@@ -3577,11 +3850,11 @@ var ClaudeSession = class {
3577
3850
  signal;
3578
3851
  o;
3579
3852
  constructor(opts = {}) {
3580
- this.cwd = path5.resolve(opts.cwd ?? process.cwd());
3853
+ this.cwd = path6.resolve(opts.cwd ?? process.cwd());
3581
3854
  this.configDir = resolveConfigDir(opts.configDir);
3582
3855
  this.signal = opts.signal;
3583
- this.sessionId = randomUUID3();
3584
- this.jsonlPath = path5.join(
3856
+ this.sessionId = randomUUID4();
3857
+ this.jsonlPath = path6.join(
3585
3858
  this.configDir,
3586
3859
  "projects",
3587
3860
  encodeCwd(this.cwd),
@@ -3704,7 +3977,7 @@ var ClaudeSession = class {
3704
3977
  }
3705
3978
  readRawLines() {
3706
3979
  try {
3707
- return fs4.readFileSync(this.jsonlPath, "utf8").split("\n");
3980
+ return fs5.readFileSync(this.jsonlPath, "utf8").split("\n");
3708
3981
  } catch {
3709
3982
  return [];
3710
3983
  }
@@ -4082,11 +4355,11 @@ function spawnInteractiveProcess(opts) {
4082
4355
  }
4083
4356
 
4084
4357
  // src/claude-code-language-model.ts
4085
- import { readFileSync as readFileSync3, writeFileSync as writeFileSync3 } from "fs";
4358
+ import { readFileSync as readFileSync3, writeFileSync as writeFileSync4 } from "fs";
4086
4359
  import { unlink as unlink3 } from "fs/promises";
4087
- import { homedir as homedir4, tmpdir as tmpdir2 } from "os";
4088
- import { randomUUID as randomUUID4 } from "crypto";
4089
- import { dirname as dirname3, join as join6 } from "path";
4360
+ import { homedir as homedir5, tmpdir as tmpdir2 } from "os";
4361
+ import { randomUUID as randomUUID5 } from "crypto";
4362
+ import { dirname as dirname4, join as join7 } from "path";
4090
4363
  var DEFAULT_COMPACTION_MODEL = "claude-haiku-4-5";
4091
4364
  function resolveCompactionModel(configured) {
4092
4365
  const env = process.env.CLAUDE_CODE_COMPACTION_MODEL?.trim();
@@ -4293,9 +4566,9 @@ ${body}`;
4293
4566
  }
4294
4567
  });
4295
4568
  }
4296
- function readPromptFileIfPresent(path8) {
4569
+ function readPromptFileIfPresent(path9) {
4297
4570
  try {
4298
- const content = readFileSync3(path8, "utf8").trim();
4571
+ const content = readFileSync3(path9, "utf8").trim();
4299
4572
  return content || void 0;
4300
4573
  } catch {
4301
4574
  return void 0;
@@ -4304,9 +4577,9 @@ function readPromptFileIfPresent(path8) {
4304
4577
  function nearestWorkspaceAgentsPrompt(cwd) {
4305
4578
  let dir = cwd;
4306
4579
  while (true) {
4307
- const content = readPromptFileIfPresent(join6(dir, "AGENTS.md"));
4580
+ const content = readPromptFileIfPresent(join7(dir, "AGENTS.md"));
4308
4581
  if (content) return content;
4309
- const parent = dirname3(dir);
4582
+ const parent = dirname4(dir);
4310
4583
  if (parent === dir) return void 0;
4311
4584
  dir = parent;
4312
4585
  }
@@ -4389,19 +4662,22 @@ ${options.compressionSummary.trim()}`
4389
4662
  for (const s of extraSystemContent) {
4390
4663
  if (s.trim()) parts.push(s.trim());
4391
4664
  }
4392
- const configRoot = process.env.XDG_CONFIG_HOME ?? join6(homedir4(), ".config");
4393
- const globalAgents = readPromptFileIfPresent(join6(configRoot, "opencode", "AGENTS.md"));
4665
+ const configRoot = process.env.XDG_CONFIG_HOME ?? join7(homedir5(), ".config");
4666
+ const globalAgents = readPromptFileIfPresent(join7(configRoot, "opencode", "AGENTS.md"));
4394
4667
  const workspaceAgents = nearestWorkspaceAgentsPrompt(cwd);
4395
- if (globalAgents) parts.push(globalAgents);
4396
- if (workspaceAgents && workspaceAgents !== globalAgents) parts.push(workspaceAgents);
4397
- if (globalAgents || workspaceAgents) parts.push(AGENTS_MAINTENANCE_HINT);
4668
+ const forwarded = extraSystemContent.join("\n\n");
4669
+ const pushGlobal = !!globalAgents && !forwarded.includes(globalAgents);
4670
+ const pushWorkspace = !!workspaceAgents && workspaceAgents !== globalAgents && !forwarded.includes(workspaceAgents);
4671
+ if (pushGlobal) parts.push(globalAgents);
4672
+ if (pushWorkspace) parts.push(workspaceAgents);
4673
+ if (pushGlobal || pushWorkspace) parts.push(AGENTS_MAINTENANCE_HINT);
4398
4674
  if (includeMultiStepHint) parts.push(MULTI_STEP_TASK_HINT);
4399
4675
  const content = parts.join("\n\n");
4400
4676
  if (!content) return void 0;
4401
- const path8 = join6(tmpdir2(), `opencode-cc-sys-${randomUUID4()}.md`);
4677
+ const path9 = join7(tmpdir2(), `opencode-cc-sys-${randomUUID5()}.md`);
4402
4678
  try {
4403
- writeFileSync3(path8, content, "utf8");
4404
- return path8;
4679
+ writeFileSync4(path9, content, "utf8");
4680
+ return path9;
4405
4681
  } catch (err) {
4406
4682
  log.warn("failed to write system prompt file", { error: String(err) });
4407
4683
  return void 0;
@@ -4982,9 +5258,9 @@ var ClaudeCodeLanguageModel = class {
4982
5258
  return this.doGenerateViaStream(options);
4983
5259
  }
4984
5260
  const warnings = [];
4985
- const cwd = resolveSpawnCwd(this.config.cwd);
4986
5261
  const scope = this.requestScope(options);
4987
5262
  const affinity = this.sessionAffinity(options);
5263
+ const cwd = await resolveSpawnCwdForSession(this.config.cwd, affinity);
4988
5264
  const effectiveModelId = resolveAgentModel(
4989
5265
  this.getOpencodeAgent(options.providerOptions),
4990
5266
  this.modelId
@@ -5058,7 +5334,12 @@ var ClaudeCodeLanguageModel = class {
5058
5334
  }
5059
5335
  const hasExistingSession = !!getClaudeSessionId(sk);
5060
5336
  const includeHistoryContext = !hasExistingSession && hasPriorConversation;
5061
- const userMsg = consumeExitPlanModeQuestionResult(sk, options.prompt) ?? getClaudeUserMessage(options.prompt, includeHistoryContext);
5337
+ const userMsg = consumeExitPlanModeQuestionResult(sk, options.prompt) ?? // doGenerate has no proxy wiring, so this process issued no tool calls
5338
+ // at all: every tool result reaching it belongs to opencode and must be
5339
+ // rendered as text rather than an orphaned `tool_result` (issue #29).
5340
+ getClaudeUserMessage(options.prompt, includeHistoryContext, {
5341
+ cliToolCallIds: /* @__PURE__ */ new Set()
5342
+ });
5062
5343
  const [runtimeStatus, cliVersion, planModeQuestionActive] = await Promise.all([
5063
5344
  getRuntimeMcpStatus(),
5064
5345
  detectCliVersion(this.config.cliPath),
@@ -5118,7 +5399,7 @@ var ClaudeCodeLanguageModel = class {
5118
5399
  const toolCalls = [];
5119
5400
  const toolCallStreams = /* @__PURE__ */ new Map();
5120
5401
  let gotPartialEvents = false;
5121
- const result = await new Promise((resolve4, reject) => {
5402
+ const result = await new Promise((resolve5, reject) => {
5122
5403
  const cleanup = () => {
5123
5404
  try {
5124
5405
  if (!proc.killed && proc.exitCode === null) proc.kill();
@@ -5253,7 +5534,7 @@ ${plan}
5253
5534
  usage: msg.usage
5254
5535
  };
5255
5536
  cleanup();
5256
- resolve4({
5537
+ resolve5({
5257
5538
  ...resultMeta,
5258
5539
  text: responseText,
5259
5540
  thinking: thinkingText,
@@ -5265,7 +5546,7 @@ ${plan}
5265
5546
  });
5266
5547
  rl.on("close", () => {
5267
5548
  cleanup();
5268
- resolve4({
5549
+ resolve5({
5269
5550
  ...resultMeta,
5270
5551
  text: responseText,
5271
5552
  thinking: thinkingText,
@@ -5371,11 +5652,11 @@ ${plan}
5371
5652
  }
5372
5653
  async doStream(options) {
5373
5654
  const warnings = [];
5374
- const cwd = resolveSpawnCwd(this.config.cwd);
5375
5655
  const cliPath = this.config.cliPath;
5376
5656
  const skipPermissions = this.config.skipPermissions !== false;
5377
5657
  const scope = this.requestScope(options);
5378
5658
  const affinity = this.sessionAffinity(options);
5659
+ const cwd = await resolveSpawnCwdForSession(this.config.cwd, affinity);
5379
5660
  const compactionMode = this.isCompactionCall(options);
5380
5661
  const effectiveModelId = compactionMode ? this.resolveCompactionModel() : resolveAgentModel(
5381
5662
  this.getOpencodeAgent(options.providerOptions),
@@ -5514,8 +5795,10 @@ ${plan}
5514
5795
  if (exitPlanModeQuestionResult) {
5515
5796
  log.info("sending plan approval decision to claude", { sk });
5516
5797
  }
5798
+ const previousPendingProxyCalls = compactionMode ? [] : getPendingProxyCalls(sk);
5517
5799
  const userMsg = exitPlanModeQuestionResult ?? getClaudeUserMessage(options.prompt, includeHistoryContext, {
5518
- compactionMode
5800
+ compactionMode,
5801
+ cliToolCallIds: new Set(previousPendingProxyCalls.map((c) => c.toolCallId))
5519
5802
  });
5520
5803
  const resolvedProxy = compactionMode ? null : this.resolvedProxyTools();
5521
5804
  const loadLiveToolInfo = this.createLiveToolInfoLoader();
@@ -5524,7 +5807,6 @@ ${plan}
5524
5807
  loadLiveToolInfo
5525
5808
  );
5526
5809
  const self = this;
5527
- const previousPendingProxyCalls = compactionMode ? [] : getPendingProxyCalls(sk);
5528
5810
  const previousPendingProxyMatches = previousPendingProxyCalls.map((call) => ({
5529
5811
  call,
5530
5812
  result: this.extractPendingProxyResult(options.prompt, call.toolCallId)
@@ -5747,6 +6029,11 @@ ${plan}
5747
6029
  compressionSummary: getCompressionSummary(sk)
5748
6030
  }
5749
6031
  );
6032
+ const skillPluginDirs = await resolveSkillPluginDirs({
6033
+ cwd,
6034
+ cliPath,
6035
+ enabled: self.config.bridgeOpencodeSkills === true
6036
+ });
5750
6037
  cliArgs = buildCliArgs({
5751
6038
  sessionKey: sk,
5752
6039
  skipPermissions,
@@ -5756,6 +6043,7 @@ ${plan}
5756
6043
  strictMcpConfig: self.config.strictMcpConfig,
5757
6044
  disallowedTools: allDisallowed.length > 0 ? allDisallowed : void 0,
5758
6045
  appendSystemPromptFile: systemPromptFile,
6046
+ pluginDirs: skillPluginDirs,
5759
6047
  ...self.thinkingCliOptions(),
5760
6048
  fastMode,
5761
6049
  cliVersion
@@ -5785,6 +6073,13 @@ ${plan}
5785
6073
  activeProcess = ap;
5786
6074
  }
5787
6075
  }
6076
+ if (activeProcess && !hasMatchedPendingResults && isTurnInFlight(activeProcess)) {
6077
+ log.warn("previous turn still in flight; interrupting it", { sk });
6078
+ const idle = await interruptTurn(activeProcess);
6079
+ if (!idle) {
6080
+ log.warn("previous turn did not stop in time; this turn may see stale output", { sk });
6081
+ }
6082
+ }
5788
6083
  controller.enqueue({ type: "stream-start", warnings });
5789
6084
  let currentTextId = null;
5790
6085
  const textBlockIndices = /* @__PURE__ */ new Set();
@@ -5923,7 +6218,10 @@ ${plan}
5923
6218
  lineEmitter.on("close", closeHandler);
5924
6219
  proc.on("error", procErrorHandler);
5925
6220
  try {
5926
- if (!deliverPendingCompletions(true)) proc.stdin?.write(watchdogMessage + "\n");
6221
+ if (!deliverPendingCompletions(true)) {
6222
+ noteTurnStarted(newAp);
6223
+ proc.stdin?.write(watchdogMessage + "\n");
6224
+ }
5927
6225
  log.debug("re-sent user message after respawn", {
5928
6226
  textLength: watchdogMessage.length
5929
6227
  });
@@ -6150,6 +6448,7 @@ ${plan}
6150
6448
  });
6151
6449
  turnCompleted = false;
6152
6450
  resetAutoContinueWindow();
6451
+ if (activeProcess) noteTurnStarted(activeProcess);
6153
6452
  proc.stdin?.write(makeAutoContinueMessage() + "\n");
6154
6453
  return;
6155
6454
  }
@@ -6190,6 +6489,9 @@ ${plan}
6190
6489
  });
6191
6490
  controllerClosed = true;
6192
6491
  cleanupTurn();
6492
+ if (!useInteractive && !compactionMode) {
6493
+ scheduleIdleProcessEviction(sk, self.config.idleProcessTimeoutMs);
6494
+ }
6193
6495
  try {
6194
6496
  controller.close();
6195
6497
  } catch {
@@ -6956,6 +7258,11 @@ ${plan}
6956
7258
  options.abortSignal.addEventListener("abort", () => {
6957
7259
  autoContinueState.aborted = true;
6958
7260
  if (turnCompleted || controllerClosed) return;
7261
+ if (activeProcess) {
7262
+ void interruptTurn(activeProcess).then((idle) => {
7263
+ log.info("interrupt sent for aborted turn", { sk, idle });
7264
+ });
7265
+ }
6959
7266
  if (!hasReceivedContent) {
6960
7267
  log.info(
6961
7268
  "abort signal received before content, closing stream immediately",
@@ -7043,6 +7350,7 @@ ${plan}
7043
7350
  );
7044
7351
  }
7045
7352
  }
7353
+ if (activeProcess) noteTurnStarted(activeProcess);
7046
7354
  proc.stdin?.write(userMsg + "\n");
7047
7355
  log.debug("sent user message", { textLength: userMsg.length });
7048
7356
  armStartWatchdog();
@@ -7074,7 +7382,7 @@ ${plan}
7074
7382
 
7075
7383
  // src/accounts.ts
7076
7384
  import { chmod, lstat, mkdir, readlink, symlink, writeFile } from "fs/promises";
7077
- import path6 from "path";
7385
+ import path7 from "path";
7078
7386
  var BASE_PROVIDER_ID = "claude-code";
7079
7387
  var DEFAULT_ACCOUNT = "default";
7080
7388
  var SHARED_CAPABILITY_ITEMS = [
@@ -7112,7 +7420,7 @@ function expandHome(value) {
7112
7420
  const home = process.env.HOME ?? process.env.USERPROFILE;
7113
7421
  if (value === "~") return home ?? value;
7114
7422
  if (value.startsWith("~/") || value.startsWith("~\\")) {
7115
- return home ? path6.join(home, value.slice(2)) : value;
7423
+ return home ? path7.join(home, value.slice(2)) : value;
7116
7424
  }
7117
7425
  return value;
7118
7426
  }
@@ -7144,8 +7452,8 @@ async function ensureSharedCapabilities(targetRoot) {
7144
7452
  }
7145
7453
  }
7146
7454
  async function ensureSharedCapabilityItem(sourceRoot, targetRoot, item) {
7147
- const source = path6.join(sourceRoot, item);
7148
- const target = path6.join(targetRoot, item);
7455
+ const source = path7.join(sourceRoot, item);
7456
+ const target = path7.join(targetRoot, item);
7149
7457
  let sourceStat;
7150
7458
  try {
7151
7459
  sourceStat = await lstat(source);
@@ -7156,8 +7464,8 @@ async function ensureSharedCapabilityItem(sourceRoot, targetRoot, item) {
7156
7464
  const targetStat = await lstat(target);
7157
7465
  if (targetStat.isSymbolicLink()) {
7158
7466
  const current = await readlink(target);
7159
- const resolvedCurrent = path6.resolve(path6.dirname(target), current);
7160
- const resolvedSource = path6.resolve(source);
7467
+ const resolvedCurrent = path7.resolve(path7.dirname(target), current);
7468
+ const resolvedSource = path7.resolve(source);
7161
7469
  if (resolvedCurrent === resolvedSource) return;
7162
7470
  }
7163
7471
  log.warn("shared Claude capability already exists; leaving untouched", {
@@ -7172,11 +7480,11 @@ async function ensureSharedCapabilityItem(sourceRoot, targetRoot, item) {
7172
7480
  await symlink(source, target, type);
7173
7481
  }
7174
7482
  async function writeAccountWrapper(account, baseCliPath, configDir) {
7175
- const cacheRoot = path6.join(
7483
+ const cacheRoot = path7.join(
7176
7484
  process.env.XDG_CACHE_HOME ?? expandHome("~/.cache"),
7177
7485
  "opencode-claude-code-plugin"
7178
7486
  );
7179
- const wrapperPath = path6.join(cacheRoot, `claude-${account}`);
7487
+ const wrapperPath = path7.join(cacheRoot, `claude-${account}`);
7180
7488
  const suffix = `@${account}`;
7181
7489
  await mkdir(cacheRoot, { recursive: true });
7182
7490
  const script = `#!/usr/bin/env bash
@@ -7219,11 +7527,11 @@ import {
7219
7527
  existsSync as existsSync4,
7220
7528
  readFileSync as readFileSync4,
7221
7529
  realpathSync,
7222
- rmSync as rmSync2,
7223
- writeFileSync as writeFileSync4
7530
+ rmSync as rmSync3,
7531
+ writeFileSync as writeFileSync5
7224
7532
  } from "fs";
7225
- import { homedir as homedir5 } from "os";
7226
- import { join as join7, resolve as resolve3 } from "path";
7533
+ import { homedir as homedir6 } from "os";
7534
+ import { join as join8, resolve as resolve4 } from "path";
7227
7535
  import { fileURLToPath } from "url";
7228
7536
  var STALE_PACKAGE_NAME = "opencode-claude-code-plugin";
7229
7537
  var SUSPECT_DESCRIPTION_TOKEN = "Claude Code";
@@ -7231,14 +7539,14 @@ var alreadyRan = false;
7231
7539
  function candidateCacheRoots() {
7232
7540
  const xdg = process.env.XDG_CACHE_HOME;
7233
7541
  return [
7234
- xdg ? join7(xdg, "opencode") : null,
7235
- join7(homedir5(), ".cache", "opencode"),
7236
- join7(homedir5(), "Library", "Caches", "opencode")
7542
+ xdg ? join8(xdg, "opencode") : null,
7543
+ join8(homedir6(), ".cache", "opencode"),
7544
+ join8(homedir6(), "Library", "Caches", "opencode")
7237
7545
  ].filter((p) => Boolean(p));
7238
7546
  }
7239
7547
  function userOpencodeJsonPath() {
7240
- const xdgConfig = process.env.XDG_CONFIG_HOME ?? join7(homedir5(), ".config");
7241
- return join7(xdgConfig, "opencode", "opencode.json");
7548
+ const xdgConfig = process.env.XDG_CONFIG_HOME ?? join8(homedir6(), ".config");
7549
+ return join8(xdgConfig, "opencode", "opencode.json");
7242
7550
  }
7243
7551
  function userIntendsToUseUnscoped() {
7244
7552
  const cfg = userOpencodeJsonPath();
@@ -7257,7 +7565,7 @@ function userIntendsToUseUnscoped() {
7257
7565
  function ourLoadedDir() {
7258
7566
  try {
7259
7567
  const filePath = fileURLToPath(import.meta.url);
7260
- return realpathSync(resolve3(filePath, "..", ".."));
7568
+ return realpathSync(resolve4(filePath, "..", ".."));
7261
7569
  } catch {
7262
7570
  return null;
7263
7571
  }
@@ -7281,7 +7589,7 @@ function cleanupStaleUnscopedInstall() {
7281
7589
  }
7282
7590
  function cleanupOne(cacheRoot, ourDir) {
7283
7591
  if (!existsSync4(cacheRoot)) return;
7284
- const stalePath = join7(cacheRoot, "node_modules", STALE_PACKAGE_NAME);
7592
+ const stalePath = join8(cacheRoot, "node_modules", STALE_PACKAGE_NAME);
7285
7593
  if (!existsSync4(stalePath)) return;
7286
7594
  let realStalePath = stalePath;
7287
7595
  try {
@@ -7289,7 +7597,7 @@ function cleanupOne(cacheRoot, ourDir) {
7289
7597
  } catch {
7290
7598
  }
7291
7599
  if (ourDir && realStalePath === ourDir) return;
7292
- const pkgJsonPath = join7(stalePath, "package.json");
7600
+ const pkgJsonPath = join8(stalePath, "package.json");
7293
7601
  if (!existsSync4(pkgJsonPath)) return;
7294
7602
  let pkg = {};
7295
7603
  try {
@@ -7301,7 +7609,7 @@ function cleanupOne(cacheRoot, ourDir) {
7301
7609
  if (!pkg.description?.includes(SUSPECT_DESCRIPTION_TOKEN)) return;
7302
7610
  log.info("cleanup-stale: removing unscoped install", { stalePath });
7303
7611
  try {
7304
- rmSync2(stalePath, { recursive: true, force: true });
7612
+ rmSync3(stalePath, { recursive: true, force: true });
7305
7613
  } catch (err) {
7306
7614
  log.warn("cleanup-stale: rmSync failed", {
7307
7615
  stalePath,
@@ -7309,13 +7617,13 @@ function cleanupOne(cacheRoot, ourDir) {
7309
7617
  });
7310
7618
  return;
7311
7619
  }
7312
- const cachePkgJson = join7(cacheRoot, "package.json");
7620
+ const cachePkgJson = join8(cacheRoot, "package.json");
7313
7621
  if (!existsSync4(cachePkgJson)) return;
7314
7622
  try {
7315
7623
  const cfg = JSON.parse(readFileSync4(cachePkgJson, "utf8"));
7316
7624
  if (cfg?.dependencies?.[STALE_PACKAGE_NAME]) {
7317
7625
  delete cfg.dependencies[STALE_PACKAGE_NAME];
7318
- writeFileSync4(cachePkgJson, JSON.stringify(cfg, null, 2) + "\n");
7626
+ writeFileSync5(cachePkgJson, JSON.stringify(cfg, null, 2) + "\n");
7319
7627
  log.info("cleanup-stale: pruned dep from cache package.json");
7320
7628
  }
7321
7629
  } catch (err) {
@@ -7327,16 +7635,16 @@ function cleanupOne(cacheRoot, ourDir) {
7327
7635
 
7328
7636
  // src/startup-diagnostics.ts
7329
7637
  import { execFile as execFile2 } from "child_process";
7330
- import * as fs5 from "fs";
7331
- import * as path7 from "path";
7638
+ import * as fs6 from "fs";
7639
+ import * as path8 from "path";
7332
7640
  import { promisify as promisify2 } from "util";
7333
7641
  import { fileURLToPath as fileURLToPath2 } from "url";
7334
7642
  var cachedPluginVersion;
7335
7643
  function pluginVersion() {
7336
7644
  if (cachedPluginVersion) return cachedPluginVersion;
7337
7645
  try {
7338
- const here = path7.dirname(fileURLToPath2(import.meta.url));
7339
- const raw = fs5.readFileSync(path7.join(here, "..", "package.json"), "utf8");
7646
+ const here = path8.dirname(fileURLToPath2(import.meta.url));
7647
+ const raw = fs6.readFileSync(path8.join(here, "..", "package.json"), "utf8");
7340
7648
  const version = JSON.parse(raw).version;
7341
7649
  cachedPluginVersion = typeof version === "string" ? version : "unknown";
7342
7650
  } catch {
@@ -7360,7 +7668,7 @@ var opencodeVersionProbe;
7360
7668
  function detectOpencodeVersion(execPath = process.execPath) {
7361
7669
  if (opencodeVersionProbe) return opencodeVersionProbe;
7362
7670
  opencodeVersionProbe = (async () => {
7363
- if (!path7.basename(execPath).toLowerCase().includes("opencode")) {
7671
+ if (!path8.basename(execPath).toLowerCase().includes("opencode")) {
7364
7672
  log.debug("skipping opencode version probe: execPath is not opencode", { execPath });
7365
7673
  return void 0;
7366
7674
  }
@@ -7530,6 +7838,8 @@ function createClaudeCode(settings = {}) {
7530
7838
  autoContinueIncompleteTurns: settings.autoContinueIncompleteTurns ?? "smart",
7531
7839
  compactionModel: settings.compactionModel,
7532
7840
  ignoreAnthropicApiKey: settings.ignoreAnthropicApiKey,
7841
+ idleProcessTimeoutMs: settings.idleProcessTimeoutMs,
7842
+ bridgeOpencodeSkills: settings.bridgeOpencodeSkills === true,
7533
7843
  interactive: settings.interactive,
7534
7844
  interactiveBypass: settings.interactiveBypass,
7535
7845
  interactiveAllowTools: settings.interactiveAllowTools,