@khalilgharbaoui/opencode-claude-code-plugin 0.15.4 → 0.17.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
 
@@ -498,6 +523,8 @@ var PROXY_DEFAULT_TIMEOUT_MS = 10 * 60 * 1e3;
498
523
  var PROXY_PER_TOOL_DEFAULT_TIMEOUT_MS = {
499
524
  task: 60 * 60 * 1e3,
500
525
  // 60 min
526
+ task_batch: 60 * 60 * 1e3,
527
+ // 60 min, same reasoning: it IS task calls
501
528
  question: 30 * 60 * 1e3
502
529
  // 30 min
503
530
  };
@@ -537,14 +564,58 @@ function resolveProxyClientCeilingMs(overrides) {
537
564
  function buildProxyTimeoutError(toolName, ms) {
538
565
  const key = toolName.toLowerCase();
539
566
  const base = `Proxy tool '${toolName}' timed out after ${ms}ms waiting for opencode to resolve the call`;
540
- if (key === "task") {
567
+ if (key === "task" || key === TASK_BATCH_TOOL_NAME) {
541
568
  return new Error(
542
- base + " (the subagent). The subagent may still be running but its result is no longer reachable in this session. Do not declare the dispatch failed, and do not 'schedule a wake-up' or defer -- that mechanism does not apply here. If the result is required, re-dispatch or verify it directly now."
569
+ base + (key === "task" ? " (the subagent)." : " (the subagents).") + " The subagent may still be running but its result is no longer reachable in this session. Do not declare the dispatch failed, and do not 'schedule a wake-up' or defer -- that mechanism does not apply here. If the result is required, re-dispatch or verify it directly now."
543
570
  );
544
571
  }
545
572
  return new Error(base);
546
573
  }
547
- var TASK_PROXY_NOTE = "This is the ONLY tool that dispatches opencode subagents (including user @-mentions). Claude Code's built-in TaskCreate/TaskUpdate manage a local todo list and cannot dispatch subagents. Do not search config files to verify a subagent type exists \u2014 invalid types fail fast with a clear error. Foreground calls block until the subagent finishes; set `background` to request opencode's background execution mode. Task calls get a 60-minute proxy deadline by default (configurable via proxyToolTimeoutMs).";
574
+ var TASK_PROXY_NOTE = "This and task_batch are the ONLY tools that dispatch opencode subagents (including user @-mentions). Claude Code's built-in TaskCreate/TaskUpdate manage a local todo list and cannot dispatch subagents. Do not search config files to verify a subagent type exists: invalid types fail fast with a clear error. Foreground calls block until the subagent finishes; set `background` to request opencode's background execution mode. For two or more independent subagents in one response use task_batch, not several task calls: those run one after another. Task calls get a 60-minute proxy deadline by default (configurable via proxyToolTimeoutMs).";
575
+ var TASK_BATCH_TOOL_NAME = "task_batch";
576
+ var TASK_BATCH_PROXY_NOTE = "Use this instead of several task calls in one response: Claude Code runs MCP tool calls one at a time, so separate task calls run serially even when emitted together, while one task_batch call fans them out as parallel opencode task calls. Each task takes the same fields as the task tool. Results come back in task order, each labelled. Same 60-minute proxy deadline as task (configurable via proxyToolTimeoutMs).";
577
+ var TASK_INPUT_REQUIRED = ["description", "prompt", "subagent_type"];
578
+ function taskBatchInputError(input) {
579
+ const tasks = input?.tasks;
580
+ if (!Array.isArray(tasks) || tasks.length < 2) {
581
+ return "task_batch requires a `tasks` array with at least two items; use `task` for one subagent";
582
+ }
583
+ for (const [index, task] of tasks.entries()) {
584
+ if (task === null || typeof task !== "object" || Array.isArray(task)) {
585
+ return `task_batch tasks[${index}] must be an object`;
586
+ }
587
+ const item = task;
588
+ for (const field of TASK_INPUT_REQUIRED) {
589
+ if (typeof item[field] !== "string") {
590
+ return `task_batch tasks[${index}].${field} must be a string`;
591
+ }
592
+ }
593
+ }
594
+ return null;
595
+ }
596
+ function taskBatchTasks(input) {
597
+ if (taskBatchInputError(input)) return [];
598
+ return input.tasks;
599
+ }
600
+ function taskBatchChildToolCallId(parentToolCallId, index) {
601
+ return `${parentToolCallId}_task_${index}`;
602
+ }
603
+ function formatTaskBatchResults(children) {
604
+ const total = children.length;
605
+ const sections = children.map(({ task, result }, index) => {
606
+ const label = typeof task.description === "string" ? task.description : `task ${index + 1}`;
607
+ const agent = typeof task.subagent_type === "string" ? ` (${task.subagent_type})` : "";
608
+ const header = `## task ${index + 1} of ${total}: ${label}${agent}`;
609
+ if (!result) return `${header}
610
+ [missing] opencode returned no result for this task in the batch`;
611
+ if (result.kind === "error") return `${header}
612
+ [error] ${result.message}`;
613
+ return `${header}
614
+ ${result.isError ? "[error] " : ""}${result.text}`;
615
+ });
616
+ const failed = children.some(({ result }) => !result || result.kind === "error" || result.isError);
617
+ return { kind: "text", text: sections.join("\n\n"), ...failed ? { isError: true } : {} };
618
+ }
548
619
  var AGENT_TYPES_HEADING = "Available agent types";
549
620
  var AGENT_BLURB_LIMIT = 140;
550
621
  var QUESTION_PROXY_NOTE = "This routes structured questions through opencode's native `question` tool, which renders a TUI form with the options you provide and blocks until the operator answers. Claude Code's built-in AskUserQuestion is disabled in this environment; this proxy is the ONLY way to ask the operator for a decision or clarification. Answers come back as arrays of selected labels (set `multiple: true` to allow more than one). If the operator dismisses the form the call returns an error \u2014 treat that as 'no answer' and stop, do not guess. Question calls get a 30-minute proxy deadline by default (configurable via proxyToolTimeoutMs); for long-AFK scenarios prefer fewer, high-signal questions.";
@@ -572,7 +643,7 @@ function overlayTaskProxyDescription(tools, liveDescription) {
572
643
  const agentTypes = extractAgentTypeList(liveDescription);
573
644
  if (!agentTypes) return tools;
574
645
  return tools.map(
575
- (t) => t.name === "task" ? { ...t, description: `${agentTypes}
646
+ (t) => t.name === "task" || t.name === TASK_BATCH_TOOL_NAME ? { ...t, description: `${agentTypes}
576
647
 
577
648
  ${t.description}` } : t
578
649
  );
@@ -590,6 +661,32 @@ function filterQuestionProxyByOpencodeSupport(tools, opencodeHasQuestion) {
590
661
  if (opencodeHasQuestion) return tools;
591
662
  return tools.filter((t) => t.name !== "question");
592
663
  }
664
+ var TASK_INPUT_PROPERTIES = {
665
+ description: {
666
+ type: "string",
667
+ description: "A short (3-5 words) description of the task"
668
+ },
669
+ prompt: {
670
+ type: "string",
671
+ description: "The task for the agent to perform"
672
+ },
673
+ subagent_type: {
674
+ type: "string",
675
+ description: "The type of specialized agent to use for this task"
676
+ },
677
+ task_id: {
678
+ type: "string",
679
+ description: "Set this only if you mean to resume a previous task: pass the prior task_id to continue the same subagent session instead of creating a fresh one."
680
+ },
681
+ command: {
682
+ type: "string",
683
+ description: "The command that triggered this task"
684
+ },
685
+ background: {
686
+ type: "boolean",
687
+ description: "Run the task in the background when supported by opencode"
688
+ }
689
+ };
593
690
  var DEFAULT_PROXY_TOOLS = [
594
691
  {
595
692
  name: "bash",
@@ -683,35 +780,30 @@ var DEFAULT_PROXY_TOOLS = [
683
780
  {
684
781
  name: "task",
685
782
  description: "Launch an opencode subagent to handle a complex multi-step task autonomously. Routed through opencode's task tool so subagent orchestration, permission, and lifecycle are handled by opencode. Use `subagent_type` to pick which configured subagent runs (e.g. `build`, `general`, `explore`, or any custom subagent declared in opencode.json). " + TASK_PROXY_NOTE,
783
+ inputSchema: {
784
+ type: "object",
785
+ properties: TASK_INPUT_PROPERTIES,
786
+ required: TASK_INPUT_REQUIRED
787
+ }
788
+ },
789
+ {
790
+ name: TASK_BATCH_TOOL_NAME,
791
+ description: "Launch two or more independent opencode subagents at the same time and get all their results back together. Put one ordinary task input in `tasks` for each subagent. " + TASK_BATCH_PROXY_NOTE,
686
792
  inputSchema: {
687
793
  type: "object",
688
794
  properties: {
689
- description: {
690
- type: "string",
691
- description: "A short (3-5 words) description of the task"
692
- },
693
- prompt: {
694
- type: "string",
695
- description: "The task for the agent to perform"
696
- },
697
- subagent_type: {
698
- type: "string",
699
- description: "The type of specialized agent to use for this task"
700
- },
701
- task_id: {
702
- type: "string",
703
- description: "Set this only if you mean to resume a previous task \u2014 pass the prior task_id to continue the same subagent session instead of creating a fresh one."
704
- },
705
- command: {
706
- type: "string",
707
- description: "The command that triggered this task"
708
- },
709
- background: {
710
- type: "boolean",
711
- description: "Run the task in the background when supported by opencode"
795
+ tasks: {
796
+ type: "array",
797
+ minItems: 2,
798
+ description: "Independent subagent tasks to run concurrently",
799
+ items: {
800
+ type: "object",
801
+ properties: TASK_INPUT_PROPERTIES,
802
+ required: TASK_INPUT_REQUIRED
803
+ }
712
804
  }
713
805
  },
714
- required: ["description", "prompt", "subagent_type"]
806
+ required: ["tasks"]
715
807
  }
716
808
  },
717
809
  {
@@ -897,6 +989,13 @@ async function createProxyMcpServer(tools = DEFAULT_PROXY_TOOLS, timeoutOverride
897
989
  });
898
990
  return;
899
991
  }
992
+ if (toolName === TASK_BATCH_TOOL_NAME) {
993
+ const problem = taskBatchInputError(input);
994
+ if (problem) {
995
+ writeToolCallResult(res, requestId, { kind: "error", message: problem });
996
+ return;
997
+ }
998
+ }
900
999
  const interceptor = interceptors?.get(toolName);
901
1000
  if (interceptor) {
902
1001
  let intercepted;
@@ -932,12 +1031,12 @@ async function createProxyMcpServer(tools = DEFAULT_PROXY_TOOLS, timeoutOverride
932
1031
  });
933
1032
  let timer = null;
934
1033
  const result = await new Promise(
935
- (resolve4, reject2) => {
1034
+ (resolve5, reject2) => {
936
1035
  const entry = {
937
1036
  id: callId,
938
1037
  toolName,
939
1038
  input,
940
- resolve: resolve4,
1039
+ resolve: resolve5,
941
1040
  reject: reject2,
942
1041
  channel
943
1042
  };
@@ -1019,11 +1118,11 @@ async function createProxyMcpServer(tools = DEFAULT_PROXY_TOOLS, timeoutOverride
1019
1118
  }
1020
1119
  }
1021
1120
  });
1022
- await new Promise((resolve4, reject2) => {
1121
+ await new Promise((resolve5, reject2) => {
1023
1122
  server2.once("error", reject2);
1024
1123
  server2.listen(0, "127.0.0.1", () => {
1025
1124
  server2.off("error", reject2);
1026
- resolve4();
1125
+ resolve5();
1027
1126
  });
1028
1127
  });
1029
1128
  const addr = server2.address();
@@ -1077,8 +1176,8 @@ async function createProxyMcpServer(tools = DEFAULT_PROXY_TOOLS, timeoutOverride
1077
1176
  entry.reject(new Error(SERVER_CLOSED_MESSAGE));
1078
1177
  }
1079
1178
  pending.clear();
1080
- await new Promise((resolve4) => {
1081
- server2.close(() => resolve4());
1179
+ await new Promise((resolve5) => {
1180
+ server2.close(() => resolve5());
1082
1181
  });
1083
1182
  if (configFilePath) {
1084
1183
  try {
@@ -1101,6 +1200,7 @@ function disallowedToolFlags(tools) {
1101
1200
  grep: ["Grep"],
1102
1201
  webfetch: ["WebFetch"],
1103
1202
  task: ["Agent"],
1203
+ task_batch: ["Agent"],
1104
1204
  // `question` disables Claude Code's built-in `AskUserQuestion` so the
1105
1205
  // structured-questions path flows through opencode's native `question`
1106
1206
  // tool instead — same UI/permission/audit benefits as the other
@@ -1136,10 +1236,10 @@ function resolveDisallowedTools(options) {
1136
1236
  return out;
1137
1237
  }
1138
1238
  function readBody(req) {
1139
- return new Promise((resolve4, reject) => {
1239
+ return new Promise((resolve5, reject) => {
1140
1240
  const chunks = [];
1141
1241
  req.on("data", (chunk) => chunks.push(chunk));
1142
- req.on("end", () => resolve4(Buffer.concat(chunks).toString("utf8")));
1242
+ req.on("end", () => resolve5(Buffer.concat(chunks).toString("utf8")));
1143
1243
  req.on("error", reject);
1144
1244
  });
1145
1245
  }
@@ -1621,7 +1721,7 @@ async function requestSideQuestion(activeProcess, question, options) {
1621
1721
  }
1622
1722
  });
1623
1723
  pendingProcesses.add(proc);
1624
- return new Promise((resolve4, reject) => {
1724
+ return new Promise((resolve5, reject) => {
1625
1725
  const event = `side-question:${requestId}`;
1626
1726
  let settled = false;
1627
1727
  let sent = false;
@@ -1676,7 +1776,7 @@ async function requestSideQuestion(activeProcess, question, options) {
1676
1776
  }
1677
1777
  settled = true;
1678
1778
  cleanup();
1679
- resolve4({ response: result.response, synthetic: result.synthetic });
1779
+ resolve5({ response: result.response, synthetic: result.synthetic });
1680
1780
  };
1681
1781
  const timer = setTimeout(() => {
1682
1782
  fail(new Error(`/btw timed out after ${timeoutMs}ms.`), true);
@@ -1731,6 +1831,8 @@ function takeUnattendedLines(ap) {
1731
1831
  }
1732
1832
  var activeProcesses = /* @__PURE__ */ new Map();
1733
1833
  var claudeSessions = /* @__PURE__ */ new Map();
1834
+ var idleEvictionTimers = /* @__PURE__ */ new Map();
1835
+ var MAX_IDLE_TIMEOUT_MS = 2147483647;
1734
1836
  var MAX_ACTIVE_PROCESSES = 16;
1735
1837
  var PROCESS_EXIT_TIMEOUT_MS = 1500;
1736
1838
  var PROCESS_FORCE_EXIT_TIMEOUT_MS = 500;
@@ -1778,15 +1880,107 @@ function evictIfNeeded() {
1778
1880
  deleteActiveProcess(oldestKey);
1779
1881
  }
1780
1882
  }
1883
+ var TURN_INTERRUPT_TIMEOUT_MS = 5e3;
1884
+ function isTerminalResultLine(line) {
1885
+ if (!line.includes('"result"')) return false;
1886
+ try {
1887
+ return JSON.parse(line).type === "result";
1888
+ } catch {
1889
+ return false;
1890
+ }
1891
+ }
1892
+ function settleTurn(ap) {
1893
+ ap.turnInFlight = false;
1894
+ const waiters = ap.turnIdleWaiters ?? [];
1895
+ ap.turnIdleWaiters = [];
1896
+ for (const wake of waiters) wake();
1897
+ }
1898
+ function noteTurnStarted(ap) {
1899
+ if (ap.asideTransport?.interactive) return;
1900
+ ap.turnInFlight = true;
1901
+ }
1902
+ function noteTurnLine(ap, line) {
1903
+ if (!ap.turnInFlight) return;
1904
+ if (isTerminalResultLine(line)) settleTurn(ap);
1905
+ }
1906
+ function isTurnInFlight(ap) {
1907
+ return ap.turnInFlight === true;
1908
+ }
1909
+ function awaitTurnIdle(ap, timeoutMs) {
1910
+ if (!ap.turnInFlight) return Promise.resolve(true);
1911
+ return new Promise((resolve5) => {
1912
+ const wake = () => {
1913
+ clearTimeout(timer);
1914
+ resolve5(true);
1915
+ };
1916
+ const timer = setTimeout(() => {
1917
+ const waiters = ap.turnIdleWaiters ?? [];
1918
+ const at = waiters.indexOf(wake);
1919
+ if (at >= 0) waiters.splice(at, 1);
1920
+ resolve5(false);
1921
+ }, timeoutMs);
1922
+ (ap.turnIdleWaiters ??= []).push(wake);
1923
+ });
1924
+ }
1925
+ function interruptTurn(ap, timeoutMs = TURN_INTERRUPT_TIMEOUT_MS) {
1926
+ if (!ap.turnInFlight) return Promise.resolve(true);
1927
+ const stdin = ap.proc.stdin;
1928
+ if (ap.asideTransport?.interactive || !stdin || !stdin.writable) {
1929
+ log.notice("cannot interrupt this transport; waiting for the turn to end");
1930
+ return awaitTurnIdle(ap, timeoutMs);
1931
+ }
1932
+ try {
1933
+ stdin.write(
1934
+ JSON.stringify({
1935
+ type: "control_request",
1936
+ request_id: randomUUID3(),
1937
+ request: { subtype: "interrupt" }
1938
+ }) + "\n"
1939
+ );
1940
+ } catch (error) {
1941
+ log.warn("failed to write interrupt control request", {
1942
+ error: error instanceof Error ? error.message : String(error)
1943
+ });
1944
+ return Promise.resolve(false);
1945
+ }
1946
+ return awaitTurnIdle(ap, timeoutMs);
1947
+ }
1948
+ function cancelIdleProcessEviction(key) {
1949
+ const timer = idleEvictionTimers.get(key);
1950
+ if (!timer) return;
1951
+ clearTimeout(timer);
1952
+ idleEvictionTimers.delete(key);
1953
+ }
1781
1954
  function getActiveProcess(key) {
1782
1955
  const ap = activeProcesses.get(key);
1783
- if (ap) touch(key);
1956
+ if (ap) {
1957
+ cancelIdleProcessEviction(key);
1958
+ touch(key);
1959
+ }
1784
1960
  return ap;
1785
1961
  }
1786
1962
  function setActiveProcess(key, ap) {
1963
+ cancelIdleProcessEviction(key);
1787
1964
  activeProcesses.set(key, ap);
1788
1965
  }
1966
+ function scheduleIdleProcessEviction(key, timeoutMs) {
1967
+ cancelIdleProcessEviction(key);
1968
+ if (typeof timeoutMs !== "number" || !Number.isFinite(timeoutMs) || timeoutMs <= 0 || timeoutMs > MAX_IDLE_TIMEOUT_MS) {
1969
+ return;
1970
+ }
1971
+ const scheduledProcess = activeProcesses.get(key);
1972
+ if (!scheduledProcess) return;
1973
+ const timer = setTimeout(() => {
1974
+ idleEvictionTimers.delete(key);
1975
+ if (activeProcesses.get(key) !== scheduledProcess) return;
1976
+ log.info("evicting idle claude process", { sessionKey: key, timeoutMs });
1977
+ deleteActiveProcess(key);
1978
+ }, timeoutMs);
1979
+ timer.unref();
1980
+ idleEvictionTimers.set(key, timer);
1981
+ }
1789
1982
  function detachActiveProcess(key) {
1983
+ cancelIdleProcessEviction(key);
1790
1984
  const ap = activeProcesses.get(key);
1791
1985
  if (!ap) return void 0;
1792
1986
  activeProcesses.delete(key);
@@ -1802,14 +1996,14 @@ function hasProcessExited(proc) {
1802
1996
  }
1803
1997
  function waitForProcessExit(proc, timeoutMs) {
1804
1998
  if (hasProcessExited(proc)) return Promise.resolve(true);
1805
- return new Promise((resolve4) => {
1999
+ return new Promise((resolve5) => {
1806
2000
  const onExit = () => {
1807
2001
  clearTimeout(timer);
1808
- resolve4(true);
2002
+ resolve5(true);
1809
2003
  };
1810
2004
  const timer = setTimeout(() => {
1811
2005
  proc.off("exit", onExit);
1812
- resolve4(hasProcessExited(proc));
2006
+ resolve5(hasProcessExited(proc));
1813
2007
  }, timeoutMs);
1814
2008
  proc.once("exit", onExit);
1815
2009
  });
@@ -1905,6 +2099,7 @@ function spawnClaudeProcess(cliPath, cliArgs, cwd, sessionKey2, proxyServer, mcp
1905
2099
  const rl = createInterface({ input: proc.stdout });
1906
2100
  rl.on("line", (line) => {
1907
2101
  if (dispatchSideQuestionResponse(ap, line)) return;
2102
+ noteTurnLine(ap, line);
1908
2103
  if (lineEmitter.listenerCount("line") === 0) {
1909
2104
  bufferUnattendedLine(ap, line);
1910
2105
  return;
@@ -1912,8 +2107,10 @@ function spawnClaudeProcess(cliPath, cliArgs, cwd, sessionKey2, proxyServer, mcp
1912
2107
  lineEmitter.emit("line", line);
1913
2108
  });
1914
2109
  rl.on("close", () => {
2110
+ settleTurn(ap);
1915
2111
  lineEmitter.emit("close");
1916
2112
  });
2113
+ cancelIdleProcessEviction(sessionKey2);
1917
2114
  activeProcesses.set(sessionKey2, ap);
1918
2115
  proc.on("error", (err) => {
1919
2116
  log.error("claude process error", { sessionKey: sessionKey2, error: err.message });
@@ -1926,7 +2123,10 @@ function spawnClaudeProcess(cliPath, cliArgs, cwd, sessionKey2, proxyServer, mcp
1926
2123
  });
1927
2124
  }
1928
2125
  const ownsSessionKey = activeProcesses.get(sessionKey2) === ap;
1929
- if (ownsSessionKey) activeProcesses.delete(sessionKey2);
2126
+ if (ownsSessionKey) {
2127
+ cancelIdleProcessEviction(sessionKey2);
2128
+ activeProcesses.delete(sessionKey2);
2129
+ }
1930
2130
  if (ownsSessionKey && code !== 0 && code !== null) {
1931
2131
  log.info("process exited with error, clearing session", {
1932
2132
  code,
@@ -1997,6 +2197,7 @@ function buildCliArgs(opts) {
1997
2197
  strictMcpConfig,
1998
2198
  disallowedTools,
1999
2199
  appendSystemPromptFile,
2200
+ pluginDirs,
2000
2201
  thinking,
2001
2202
  thinkingDisplay,
2002
2203
  fastMode,
@@ -2045,6 +2246,9 @@ function buildCliArgs(opts) {
2045
2246
  if (appendSystemPromptFile) {
2046
2247
  args.push("--append-system-prompt-file", appendSystemPromptFile);
2047
2248
  }
2249
+ for (const dir of pluginDirs ?? []) {
2250
+ args.push("--plugin-dir", dir);
2251
+ }
2048
2252
  if (fastMode && cliSupportsFastMode(cliVersion ?? null)) {
2049
2253
  args.push("--settings", JSON.stringify({ fastMode: true }));
2050
2254
  }
@@ -2169,7 +2373,7 @@ async function waitForSessionIdle(client, sessionID, options = {}) {
2169
2373
  if (options.stop?.()) return true;
2170
2374
  if (await sessionStatus(client, sessionID) !== "busy") return true;
2171
2375
  if (Date.now() - started >= timeoutMs) return false;
2172
- await new Promise((resolve4) => setTimeout(resolve4, pollMs));
2376
+ await new Promise((resolve5) => setTimeout(resolve5, pollMs));
2173
2377
  }
2174
2378
  }
2175
2379
  async function deliverAsideInline(client, sessionID, text, options = {}) {
@@ -2180,7 +2384,7 @@ async function deliverAsideInline(client, sessionID, text, options = {}) {
2180
2384
  if (emitAsideInline(sessionID, text)) return true;
2181
2385
  if (Date.now() - started >= timeoutMs) return false;
2182
2386
  if (await sessionStatus(client, sessionID) !== "busy") return false;
2183
- await new Promise((resolve4) => setTimeout(resolve4, pollMs));
2387
+ await new Promise((resolve5) => setTimeout(resolve5, pollMs));
2184
2388
  }
2185
2389
  }
2186
2390
  async function waitForAsideProcess(client, sessionID, options = {}) {
@@ -2201,7 +2405,7 @@ async function waitForAsideProcess(client, sessionID, options = {}) {
2201
2405
  log.warn("btw: a turn is running but no claude process appeared for it", { sessionID, waitedMs });
2202
2406
  return void 0;
2203
2407
  }
2204
- await new Promise((resolve4) => setTimeout(resolve4, pollMs));
2408
+ await new Promise((resolve5) => setTimeout(resolve5, pollMs));
2205
2409
  }
2206
2410
  }
2207
2411
  async function settleSessionBusy(client, sessionID, active, options = {}) {
@@ -2213,7 +2417,7 @@ async function settleSessionBusy(client, sessionID, active, options = {}) {
2213
2417
  if (status === "busy") return true;
2214
2418
  if (status === "unknown") return isProcessBusy(active);
2215
2419
  if (Date.now() - started >= settleMs) return false;
2216
- await new Promise((resolve4) => setTimeout(resolve4, pollMs));
2420
+ await new Promise((resolve5) => setTimeout(resolve5, pollMs));
2217
2421
  }
2218
2422
  }
2219
2423
  function errorText(error) {
@@ -2260,10 +2464,10 @@ async function handleBtwCommand(client, input, options = {}) {
2260
2464
  let inlineDone = false;
2261
2465
  let markInlineDelivered = () => {
2262
2466
  };
2263
- const inlineDelivered = new Promise((resolve4) => {
2467
+ const inlineDelivered = new Promise((resolve5) => {
2264
2468
  markInlineDelivered = () => {
2265
2469
  inlineDone = true;
2266
- resolve4("inline");
2470
+ resolve5("inline");
2267
2471
  };
2268
2472
  });
2269
2473
  if (isSideQuestionPending(active)) {
@@ -3093,34 +3297,166 @@ function agentDirectories(home, projectDirectory) {
3093
3297
  return directories;
3094
3298
  }
3095
3299
 
3096
- // src/mcp-bridge.ts
3300
+ // src/skill-bridge.ts
3301
+ import * as crypto2 from "crypto";
3097
3302
  import * as fs3 from "fs";
3098
- import * as path4 from "path";
3099
3303
  import * as os2 from "os";
3100
- import * as crypto2 from "crypto";
3304
+ import * as path4 from "path";
3305
+ var SKILL_PLUGIN_NAME = "opencode-skills";
3306
+ function dirExists(p) {
3307
+ try {
3308
+ return fs3.statSync(p).isDirectory();
3309
+ } catch {
3310
+ return false;
3311
+ }
3312
+ }
3313
+ function fileExists(p) {
3314
+ try {
3315
+ return fs3.statSync(p).isFile();
3316
+ } catch {
3317
+ return false;
3318
+ }
3319
+ }
3320
+ function skillRoots(cwd) {
3321
+ const roots = [];
3322
+ const seen = /* @__PURE__ */ new Set();
3323
+ const push = (p) => {
3324
+ const abs = path4.resolve(p);
3325
+ if (seen.has(abs)) return;
3326
+ seen.add(abs);
3327
+ if (dirExists(abs)) roots.push(abs);
3328
+ };
3329
+ let current = path4.resolve(cwd);
3330
+ while (true) {
3331
+ push(path4.join(current, ".opencode", "skills"));
3332
+ const parent = path4.dirname(current);
3333
+ if (parent === current) break;
3334
+ current = parent;
3335
+ }
3336
+ const home = os2.homedir();
3337
+ if (home) push(path4.join(home, ".opencode", "skills"));
3338
+ const envDir = process.env.OPENCODE_CONFIG_DIR;
3339
+ if (envDir) push(path4.join(envDir, "skills"));
3340
+ const xdg = process.env.XDG_CONFIG_HOME ?? (home ? path4.join(home, ".config") : null);
3341
+ if (xdg) push(path4.join(xdg, "opencode", "skills"));
3342
+ return roots;
3343
+ }
3344
+ function discoverOpencodeSkills(cwd) {
3345
+ const found = [];
3346
+ const claimed = /* @__PURE__ */ new Set();
3347
+ for (const root of skillRoots(cwd)) {
3348
+ let entries;
3349
+ try {
3350
+ entries = fs3.readdirSync(root, { withFileTypes: true });
3351
+ } catch {
3352
+ continue;
3353
+ }
3354
+ for (const entry of entries) {
3355
+ if (!entry.isDirectory() && !entry.isSymbolicLink()) continue;
3356
+ const name = entry.name;
3357
+ if (name.startsWith(".")) continue;
3358
+ if (claimed.has(name)) continue;
3359
+ const dir = path4.join(root, name);
3360
+ if (!fileExists(path4.join(dir, "SKILL.md"))) continue;
3361
+ claimed.add(name);
3362
+ found.push({ name, dir });
3363
+ }
3364
+ }
3365
+ return found.sort((a, b) => a.name.localeCompare(b.name));
3366
+ }
3367
+ function linkSkill(source, target) {
3368
+ try {
3369
+ fs3.symlinkSync(source, target, process.platform === "win32" ? "junction" : "dir");
3370
+ return;
3371
+ } catch {
3372
+ fs3.cpSync(source, target, { recursive: true, dereference: true });
3373
+ }
3374
+ }
3375
+ function buildSkillPluginDir(skills) {
3376
+ if (skills.length === 0) return null;
3377
+ const fingerprint = skills.map((s) => `${s.name}\0${s.dir}`).join("\n");
3378
+ const hash = crypto2.createHash("sha256").update(fingerprint).digest("hex").slice(0, 12);
3379
+ const root = path4.join(pluginTmpDir(), `skills-${hash}`);
3380
+ const manifest = path4.join(root, ".claude-plugin", "plugin.json");
3381
+ if (fileExists(manifest)) return root;
3382
+ try {
3383
+ fs3.rmSync(root, { recursive: true, force: true });
3384
+ fs3.mkdirSync(path4.join(root, ".claude-plugin"), { recursive: true });
3385
+ fs3.mkdirSync(path4.join(root, "skills"), { recursive: true });
3386
+ fs3.writeFileSync(
3387
+ manifest,
3388
+ JSON.stringify(
3389
+ {
3390
+ name: SKILL_PLUGIN_NAME,
3391
+ description: "Skills discovered from this opencode installation, bridged into Claude Code."
3392
+ },
3393
+ null,
3394
+ 2
3395
+ ),
3396
+ { encoding: "utf8", mode: 384 }
3397
+ );
3398
+ for (const skill of skills) {
3399
+ linkSkill(skill.dir, path4.join(root, "skills", skill.name));
3400
+ }
3401
+ } catch (err) {
3402
+ log.warn("failed to stage opencode skill plugin dir", {
3403
+ root,
3404
+ error: err instanceof Error ? err.message : String(err)
3405
+ });
3406
+ return null;
3407
+ }
3408
+ return root;
3409
+ }
3410
+ async function resolveSkillPluginDirs(opts) {
3411
+ if (!opts.enabled) return [];
3412
+ const skills = discoverOpencodeSkills(opts.cwd);
3413
+ if (skills.length === 0) return [];
3414
+ const supported = await detectCliSupportsFlag(opts.cliPath, "--plugin-dir");
3415
+ if (!supported) {
3416
+ log.notice(
3417
+ "claude cli does not support --plugin-dir; opencode skills will not be bridged. Run `npm i -g @anthropic-ai/claude-code` to upgrade.",
3418
+ { skills: skills.length }
3419
+ );
3420
+ return [];
3421
+ }
3422
+ const dir = buildSkillPluginDir(skills);
3423
+ if (!dir) return [];
3424
+ log.info("bridged opencode skills into claude", {
3425
+ count: skills.length,
3426
+ names: skills.map((s) => s.name),
3427
+ pluginDir: dir
3428
+ });
3429
+ return [dir];
3430
+ }
3431
+
3432
+ // src/mcp-bridge.ts
3433
+ import * as fs4 from "fs";
3434
+ import * as path5 from "path";
3435
+ import * as os3 from "os";
3436
+ import * as crypto3 from "crypto";
3101
3437
  import {
3102
3438
  parse as parseJsonc,
3103
3439
  printParseErrorCode
3104
3440
  } from "jsonc-parser";
3105
3441
  var FILE_NAMES = ["opencode.jsonc", "opencode.json", "config.json"];
3106
3442
  var PROJECT_FILE_NAMES = ["opencode.json", "opencode.jsonc"];
3107
- function fileExists(p) {
3443
+ function fileExists2(p) {
3108
3444
  try {
3109
- return fs3.statSync(p).isFile();
3445
+ return fs4.statSync(p).isFile();
3110
3446
  } catch {
3111
3447
  return false;
3112
3448
  }
3113
3449
  }
3114
- function dirExists(p) {
3450
+ function dirExists2(p) {
3115
3451
  try {
3116
- return fs3.statSync(p).isDirectory();
3452
+ return fs4.statSync(p).isDirectory();
3117
3453
  } catch {
3118
3454
  return false;
3119
3455
  }
3120
3456
  }
3121
3457
  function readAndParse(file) {
3122
3458
  try {
3123
- const raw = fs3.readFileSync(file, "utf8");
3459
+ const raw = fs4.readFileSync(file, "utf8");
3124
3460
  const errors = [];
3125
3461
  const parsed = parseJsonc(raw, errors, { allowTrailingComma: true });
3126
3462
  if (errors.length > 0) {
@@ -3156,14 +3492,14 @@ function deepMerge(target, source) {
3156
3492
  }
3157
3493
  function walkUp(opts) {
3158
3494
  const out = [];
3159
- let current = path4.resolve(opts.start);
3495
+ let current = path5.resolve(opts.start);
3160
3496
  while (true) {
3161
3497
  for (const target of opts.targets) {
3162
- const candidate = path4.join(current, target);
3498
+ const candidate = path5.join(current, target);
3163
3499
  if (opts.predicate(candidate)) out.push(candidate);
3164
3500
  }
3165
- if (opts.stop && current === path4.resolve(opts.stop)) break;
3166
- const parent = path4.dirname(current);
3501
+ if (opts.stop && current === path5.resolve(opts.stop)) break;
3502
+ const parent = path5.dirname(current);
3167
3503
  if (parent === current) break;
3168
3504
  current = parent;
3169
3505
  }
@@ -3171,29 +3507,29 @@ function walkUp(opts) {
3171
3507
  }
3172
3508
  function detectWorktree(cwd) {
3173
3509
  const override = process.env.OPENCODE_WORKTREE;
3174
- if (override) return path4.resolve(override);
3175
- let current = path4.resolve(cwd);
3510
+ if (override) return path5.resolve(override);
3511
+ let current = path5.resolve(cwd);
3176
3512
  while (true) {
3177
- const gitPath = path4.join(current, ".git");
3513
+ const gitPath = path5.join(current, ".git");
3178
3514
  try {
3179
- if (fs3.existsSync(gitPath)) return current;
3515
+ if (fs4.existsSync(gitPath)) return current;
3180
3516
  } catch {
3181
3517
  }
3182
- const parent = path4.dirname(current);
3518
+ const parent = path5.dirname(current);
3183
3519
  if (parent === current) return void 0;
3184
3520
  current = parent;
3185
3521
  }
3186
3522
  }
3187
3523
  function globalConfigDir() {
3188
- const xdg = process.env.XDG_CONFIG_HOME ?? path4.join(os2.homedir(), ".config");
3189
- return path4.join(xdg, "opencode");
3524
+ const xdg = process.env.XDG_CONFIG_HOME ?? path5.join(os3.homedir(), ".config");
3525
+ return path5.join(xdg, "opencode");
3190
3526
  }
3191
3527
  function loadGlobalConfig() {
3192
3528
  const dir = globalConfigDir();
3193
3529
  let merged = {};
3194
3530
  for (const name of FILE_NAMES.slice().reverse()) {
3195
- const file = path4.join(dir, name);
3196
- if (!fileExists(file)) continue;
3531
+ const file = path5.join(dir, name);
3532
+ if (!fileExists2(file)) continue;
3197
3533
  const parsed = readAndParse(file);
3198
3534
  if (parsed) merged = deepMerge(merged, parsed);
3199
3535
  }
@@ -3202,8 +3538,8 @@ function loadGlobalConfig() {
3202
3538
  function loadProjectFilesInDir(dir) {
3203
3539
  let merged = {};
3204
3540
  for (const name of PROJECT_FILE_NAMES) {
3205
- const file = path4.join(dir, name);
3206
- if (!fileExists(file)) continue;
3541
+ const file = path5.join(dir, name);
3542
+ if (!fileExists2(file)) continue;
3207
3543
  const parsed = readAndParse(file);
3208
3544
  if (parsed) merged = deepMerge(merged, parsed);
3209
3545
  }
@@ -3213,8 +3549,8 @@ function dotOpencodeDirs(cwd, worktree) {
3213
3549
  const dirs = [];
3214
3550
  const seen = /* @__PURE__ */ new Set();
3215
3551
  const push = (p) => {
3216
- const abs = path4.resolve(p);
3217
- if (!seen.has(abs) && dirExists(abs)) {
3552
+ const abs = path5.resolve(p);
3553
+ if (!seen.has(abs) && dirExists2(abs)) {
3218
3554
  seen.add(abs);
3219
3555
  dirs.push(abs);
3220
3556
  }
@@ -3223,17 +3559,17 @@ function dotOpencodeDirs(cwd, worktree) {
3223
3559
  start: cwd,
3224
3560
  stop: worktree,
3225
3561
  targets: [".opencode"],
3226
- predicate: dirExists
3562
+ predicate: dirExists2
3227
3563
  })) {
3228
3564
  push(dir);
3229
3565
  }
3230
- const home = os2.homedir();
3566
+ const home = os3.homedir();
3231
3567
  if (home) {
3232
- const homeDot = path4.join(home, ".opencode");
3233
- if (dirExists(homeDot)) push(homeDot);
3568
+ const homeDot = path5.join(home, ".opencode");
3569
+ if (dirExists2(homeDot)) push(homeDot);
3234
3570
  }
3235
3571
  const envDir = process.env.OPENCODE_CONFIG_DIR;
3236
- if (envDir && dirExists(envDir)) push(envDir);
3572
+ if (envDir && dirExists2(envDir)) push(envDir);
3237
3573
  return dirs;
3238
3574
  }
3239
3575
  function substituteEnvPlaceholders(source) {
@@ -3341,7 +3677,7 @@ function mergeOpencodeMcp(cwd, runtimeStatus) {
3341
3677
  let merged = {};
3342
3678
  merged = mergeMcp(merged, extractMcpBlock(loadGlobalConfig()));
3343
3679
  const explicitConfig = process.env.OPENCODE_CONFIG;
3344
- if (explicitConfig && fileExists(explicitConfig)) {
3680
+ if (explicitConfig && fileExists2(explicitConfig)) {
3345
3681
  const parsed = readAndParse(explicitConfig);
3346
3682
  if (parsed) merged = mergeMcp(merged, extractMcpBlock(parsed));
3347
3683
  }
@@ -3349,12 +3685,12 @@ function mergeOpencodeMcp(cwd, runtimeStatus) {
3349
3685
  start: cwd,
3350
3686
  stop: worktree,
3351
3687
  targets: PROJECT_FILE_NAMES,
3352
- predicate: fileExists
3688
+ predicate: fileExists2
3353
3689
  });
3354
3690
  const projectDirs = [];
3355
3691
  const seenProjectDirs = /* @__PURE__ */ new Set();
3356
3692
  for (const f of projectFiles) {
3357
- const d = path4.dirname(f);
3693
+ const d = path5.dirname(f);
3358
3694
  if (!seenProjectDirs.has(d)) {
3359
3695
  seenProjectDirs.add(d);
3360
3696
  projectDirs.push(d);
@@ -3383,7 +3719,7 @@ function mergeOpencodeMcp(cwd, runtimeStatus) {
3383
3719
  enabledServerNames.push(name);
3384
3720
  }
3385
3721
  const mergedBody = JSON.stringify({ mcpServers: merged }, null, 2);
3386
- const hash = crypto2.createHash("sha256").update(mergedBody).digest("hex").slice(0, 12);
3722
+ const hash = crypto3.createHash("sha256").update(mergedBody).digest("hex").slice(0, 12);
3387
3723
  return { servers: merged, enabledServerNames, hash };
3388
3724
  }
3389
3725
  function finishBridge(input) {
@@ -3399,13 +3735,13 @@ function finishBridge(input) {
3399
3735
  };
3400
3736
  }
3401
3737
  const body = JSON.stringify({ mcpServers: servers }, null, 2);
3402
- const outPath = path4.join(
3738
+ const outPath = path5.join(
3403
3739
  pluginTmpDir(),
3404
3740
  `mcp-${hash}.json`
3405
3741
  );
3406
3742
  try {
3407
- if (!fileExists(outPath)) {
3408
- fs3.writeFileSync(outPath, body, { encoding: "utf8", mode: 384 });
3743
+ if (!fileExists2(outPath)) {
3744
+ fs4.writeFileSync(outPath, body, { encoding: "utf8", mode: 384 });
3409
3745
  }
3410
3746
  } catch (e) {
3411
3747
  log.warn("failed to write bridged MCP config", {
@@ -3447,17 +3783,39 @@ function getOpencodeProjectDirectory() {
3447
3783
  function isUsableDirectory(d) {
3448
3784
  return typeof d === "string" && d.length > 1 && d !== "/";
3449
3785
  }
3450
- function resolveSpawnCwd(configured) {
3786
+ function resolveSpawnCwdFrom(configured, live, captured, sessionDir) {
3787
+ if (configured) return configured;
3788
+ if (isUsableDirectory(sessionDir)) return sessionDir;
3789
+ if (isUsableDirectory(live)) return live;
3790
+ return captured ?? live;
3791
+ }
3792
+ async function resolveSpawnCwdForSession(configured, sessionID) {
3793
+ if (configured) return configured;
3794
+ const sessionDir = sessionID ? await fetchSessionDirectory(sessionID) : void 0;
3451
3795
  return resolveSpawnCwdFrom(
3452
3796
  configured,
3453
3797
  process.cwd(),
3454
- opencodeProjectDirectory
3798
+ opencodeProjectDirectory,
3799
+ sessionDir
3455
3800
  );
3456
3801
  }
3457
- function resolveSpawnCwdFrom(configured, live, captured) {
3458
- if (configured) return configured;
3459
- if (isUsableDirectory(live)) return live;
3460
- return captured ?? live;
3802
+ async function fetchSessionDirectory(sessionID) {
3803
+ if (!sessionID || sessionID === "default") return void 0;
3804
+ const client = opencodeClient;
3805
+ if (!client?.session?.get) return void 0;
3806
+ try {
3807
+ const res = await client.session.get({ path: { id: sessionID } });
3808
+ const data = res.data;
3809
+ if (!data || typeof data !== "object") return void 0;
3810
+ const dir = data.directory;
3811
+ return isUsableDirectory(dir) ? dir : void 0;
3812
+ } catch (err) {
3813
+ log.warn("failed to fetch opencode session directory", {
3814
+ sessionID,
3815
+ error: err instanceof Error ? err.message : String(err)
3816
+ });
3817
+ return void 0;
3818
+ }
3461
3819
  }
3462
3820
  async function getRuntimeMcpStatus() {
3463
3821
  const client = opencodeClient;
@@ -3516,40 +3874,40 @@ import { EventEmitter as EventEmitter4 } from "events";
3516
3874
  import { unlink as unlink2 } from "fs/promises";
3517
3875
 
3518
3876
  // src/claude-session-bun.ts
3519
- import * as os3 from "os";
3520
- import * as fs4 from "fs";
3521
- import * as path5 from "path";
3877
+ import * as os4 from "os";
3878
+ import * as fs5 from "fs";
3879
+ import * as path6 from "path";
3522
3880
  import { execFileSync } from "child_process";
3523
- import { randomUUID as randomUUID3 } from "crypto";
3881
+ import { randomUUID as randomUUID4 } from "crypto";
3524
3882
  function resolveClaude(cmd = "claude") {
3525
- if (path5.isAbsolute(cmd) && fs4.existsSync(cmd)) return cmd;
3883
+ if (path6.isAbsolute(cmd) && fs5.existsSync(cmd)) return cmd;
3526
3884
  const viaBun = Bun.which(cmd);
3527
3885
  if (viaBun) return viaBun;
3528
- const isWin = os3.platform() === "win32";
3886
+ const isWin = os4.platform() === "win32";
3529
3887
  try {
3530
3888
  const out = execFileSync(isWin ? "where" : "which", [cmd], {
3531
3889
  encoding: "utf8",
3532
3890
  stdio: ["ignore", "pipe", "ignore"]
3533
3891
  });
3534
- const first = out.split(/\r?\n/).map((l) => l.trim()).filter(Boolean).find((p) => fs4.existsSync(p));
3892
+ const first = out.split(/\r?\n/).map((l) => l.trim()).filter(Boolean).find((p) => fs5.existsSync(p));
3535
3893
  if (first) return first;
3536
3894
  } catch {
3537
3895
  }
3538
3896
  throw new Error(`Could not resolve command on PATH: ${cmd}`);
3539
3897
  }
3540
3898
  function encodeCwd(cwd) {
3541
- return path5.resolve(cwd).replace(/[^a-zA-Z0-9]/g, "-");
3899
+ return path6.resolve(cwd).replace(/[^a-zA-Z0-9]/g, "-");
3542
3900
  }
3543
3901
  var TERMINAL_STOP = /* @__PURE__ */ new Set(["end_turn", "stop_sequence", "max_tokens"]);
3544
3902
  var delay = (ms) => new Promise((r) => setTimeout(r, ms));
3545
3903
  function resolveConfigDir(configDir) {
3546
3904
  const value = configDir ?? process.env.CLAUDE_CONFIG_DIR;
3547
- if (!value) return path5.join(os3.homedir(), ".claude");
3548
- if (value === "~") return os3.homedir();
3905
+ if (!value) return path6.join(os4.homedir(), ".claude");
3906
+ if (value === "~") return os4.homedir();
3549
3907
  if (value.startsWith("~/") || value.startsWith("~\\")) {
3550
- return path5.join(os3.homedir(), value.slice(2));
3908
+ return path6.join(os4.homedir(), value.slice(2));
3551
3909
  }
3552
- return path5.resolve(value);
3910
+ return path6.resolve(value);
3553
3911
  }
3554
3912
  var ClaudeSession = class {
3555
3913
  sessionId;
@@ -3567,11 +3925,11 @@ var ClaudeSession = class {
3567
3925
  signal;
3568
3926
  o;
3569
3927
  constructor(opts = {}) {
3570
- this.cwd = path5.resolve(opts.cwd ?? process.cwd());
3928
+ this.cwd = path6.resolve(opts.cwd ?? process.cwd());
3571
3929
  this.configDir = resolveConfigDir(opts.configDir);
3572
3930
  this.signal = opts.signal;
3573
- this.sessionId = randomUUID3();
3574
- this.jsonlPath = path5.join(
3931
+ this.sessionId = randomUUID4();
3932
+ this.jsonlPath = path6.join(
3575
3933
  this.configDir,
3576
3934
  "projects",
3577
3935
  encodeCwd(this.cwd),
@@ -3694,7 +4052,7 @@ var ClaudeSession = class {
3694
4052
  }
3695
4053
  readRawLines() {
3696
4054
  try {
3697
- return fs4.readFileSync(this.jsonlPath, "utf8").split("\n");
4055
+ return fs5.readFileSync(this.jsonlPath, "utf8").split("\n");
3698
4056
  } catch {
3699
4057
  return [];
3700
4058
  }
@@ -4072,11 +4430,11 @@ function spawnInteractiveProcess(opts) {
4072
4430
  }
4073
4431
 
4074
4432
  // src/claude-code-language-model.ts
4075
- import { readFileSync as readFileSync3, writeFileSync as writeFileSync3 } from "fs";
4433
+ import { readFileSync as readFileSync3, writeFileSync as writeFileSync4 } from "fs";
4076
4434
  import { unlink as unlink3 } from "fs/promises";
4077
- import { homedir as homedir4, tmpdir as tmpdir2 } from "os";
4078
- import { randomUUID as randomUUID4 } from "crypto";
4079
- import { dirname as dirname3, join as join6 } from "path";
4435
+ import { homedir as homedir5, tmpdir as tmpdir2 } from "os";
4436
+ import { randomUUID as randomUUID5 } from "crypto";
4437
+ import { dirname as dirname4, join as join7 } from "path";
4080
4438
  var DEFAULT_COMPACTION_MODEL = "claude-haiku-4-5";
4081
4439
  function resolveCompactionModel(configured) {
4082
4440
  const env = process.env.CLAUDE_CODE_COMPACTION_MODEL?.trim();
@@ -4283,9 +4641,9 @@ ${body}`;
4283
4641
  }
4284
4642
  });
4285
4643
  }
4286
- function readPromptFileIfPresent(path8) {
4644
+ function readPromptFileIfPresent(path9) {
4287
4645
  try {
4288
- const content = readFileSync3(path8, "utf8").trim();
4646
+ const content = readFileSync3(path9, "utf8").trim();
4289
4647
  return content || void 0;
4290
4648
  } catch {
4291
4649
  return void 0;
@@ -4294,9 +4652,9 @@ function readPromptFileIfPresent(path8) {
4294
4652
  function nearestWorkspaceAgentsPrompt(cwd) {
4295
4653
  let dir = cwd;
4296
4654
  while (true) {
4297
- const content = readPromptFileIfPresent(join6(dir, "AGENTS.md"));
4655
+ const content = readPromptFileIfPresent(join7(dir, "AGENTS.md"));
4298
4656
  if (content) return content;
4299
- const parent = dirname3(dir);
4657
+ const parent = dirname4(dir);
4300
4658
  if (parent === dir) return void 0;
4301
4659
  dir = parent;
4302
4660
  }
@@ -4316,8 +4674,9 @@ blocker. The user can interrupt or abort at any time; turn endings should
4316
4674
  mark meaningful checkpoints, not every completed substep.`;
4317
4675
  var SUBAGENT_DISPATCH_HINT = `## opencode subagents
4318
4676
 
4319
- Subagent dispatch in this environment goes through exactly one tool: \`mcp__opencode_proxy__task\`.
4677
+ Subagent dispatch in this environment goes through exactly two tools: \`mcp__opencode_proxy__task\` for one subagent and \`mcp__opencode_proxy__task_batch\` for two or more at once.
4320
4678
 
4679
+ - Two or more independent subagents in one response: make ONE \`mcp__opencode_proxy__task_batch\` call with a \`tasks\` array (each item is a normal task input). Claude Code runs MCP calls one at a time, so several \`mcp__opencode_proxy__task\` calls in the same response run serially; \`task_batch\` runs them concurrently in opencode and returns every result together, labelled in order.
4321
4680
  - When the user mentions \`@<agent>\` or an instruction says "call the task tool with subagent: <name>", call \`mcp__opencode_proxy__task\` with \`subagent_type: "<name>"\`.
4322
4681
  - If that tool is not in your visible tool list it is deferred \u2014 load it with ToolSearch (\`select:mcp__opencode_proxy__task\`), then call it.
4323
4682
  - Claude Code's built-in TaskCreate/TaskUpdate/TaskList manage a local todo list. They cannot dispatch subagents; creating a task there runs nothing. Never report a subagent as dispatched unless \`mcp__opencode_proxy__task\` returned its result.
@@ -4379,19 +4738,22 @@ ${options.compressionSummary.trim()}`
4379
4738
  for (const s of extraSystemContent) {
4380
4739
  if (s.trim()) parts.push(s.trim());
4381
4740
  }
4382
- const configRoot = process.env.XDG_CONFIG_HOME ?? join6(homedir4(), ".config");
4383
- const globalAgents = readPromptFileIfPresent(join6(configRoot, "opencode", "AGENTS.md"));
4741
+ const configRoot = process.env.XDG_CONFIG_HOME ?? join7(homedir5(), ".config");
4742
+ const globalAgents = readPromptFileIfPresent(join7(configRoot, "opencode", "AGENTS.md"));
4384
4743
  const workspaceAgents = nearestWorkspaceAgentsPrompt(cwd);
4385
- if (globalAgents) parts.push(globalAgents);
4386
- if (workspaceAgents && workspaceAgents !== globalAgents) parts.push(workspaceAgents);
4387
- if (globalAgents || workspaceAgents) parts.push(AGENTS_MAINTENANCE_HINT);
4744
+ const forwarded = extraSystemContent.join("\n\n");
4745
+ const pushGlobal = !!globalAgents && !forwarded.includes(globalAgents);
4746
+ const pushWorkspace = !!workspaceAgents && workspaceAgents !== globalAgents && !forwarded.includes(workspaceAgents);
4747
+ if (pushGlobal) parts.push(globalAgents);
4748
+ if (pushWorkspace) parts.push(workspaceAgents);
4749
+ if (pushGlobal || pushWorkspace) parts.push(AGENTS_MAINTENANCE_HINT);
4388
4750
  if (includeMultiStepHint) parts.push(MULTI_STEP_TASK_HINT);
4389
4751
  const content = parts.join("\n\n");
4390
4752
  if (!content) return void 0;
4391
- const path8 = join6(tmpdir2(), `opencode-cc-sys-${randomUUID4()}.md`);
4753
+ const path9 = join7(tmpdir2(), `opencode-cc-sys-${randomUUID5()}.md`);
4392
4754
  try {
4393
- writeFileSync3(path8, content, "utf8");
4394
- return path8;
4755
+ writeFileSync4(path9, content, "utf8");
4756
+ return path9;
4395
4757
  } catch (err) {
4396
4758
  log.warn("failed to write system prompt file", { error: String(err) });
4397
4759
  return void 0;
@@ -4518,11 +4880,24 @@ var ClaudeCodeLanguageModel = class {
4518
4880
  DEFAULT_PROXY_TOOLS.map((t) => [t.name.toLowerCase(), t])
4519
4881
  );
4520
4882
  const picked = [];
4883
+ const seen = /* @__PURE__ */ new Set();
4521
4884
  const unknown = [];
4885
+ const pick = (def) => {
4886
+ if (seen.has(def.name)) return;
4887
+ seen.add(def.name);
4888
+ picked.push(def);
4889
+ };
4522
4890
  for (const n of names) {
4523
4891
  const def = defsByName.get(String(n).toLowerCase());
4524
- if (def) picked.push(def);
4525
- else unknown.push(String(n));
4892
+ if (!def) {
4893
+ unknown.push(String(n));
4894
+ continue;
4895
+ }
4896
+ pick(def);
4897
+ if (def.name === "task") {
4898
+ const batch = defsByName.get(TASK_BATCH_TOOL_NAME);
4899
+ if (batch) pick(batch);
4900
+ }
4526
4901
  }
4527
4902
  if (unknown.length > 0) {
4528
4903
  const known = [...defsByName.keys()].join(", ");
@@ -4712,6 +5087,41 @@ var ClaudeCodeLanguageModel = class {
4712
5087
  }
4713
5088
  return null;
4714
5089
  }
5090
+ /**
5091
+ * The result opencode produced for a pending proxy call, if the prompt
5092
+ * carries it. For `task_batch` that means every child's result gathered
5093
+ * back onto the parent: opencode runs the children in one step and hands
5094
+ * all their results to the next call together, so a partial set is not
5095
+ * expected. If it ever happens the batch still resolves, with the gap
5096
+ * named in the text, because leaving the parent pending would send this
5097
+ * turn down the fresh-envelope path and reject the call as orphaned.
5098
+ */
5099
+ extractPendingProxyResultForCall(prompt, call) {
5100
+ if (call.toolName !== TASK_BATCH_TOOL_NAME) {
5101
+ return this.extractPendingProxyResult(prompt, call.toolCallId);
5102
+ }
5103
+ const tasks = taskBatchTasks(call.input);
5104
+ if (tasks.length === 0) {
5105
+ return { kind: "error", message: "task_batch input is not a list of task objects" };
5106
+ }
5107
+ const children = tasks.map((task, index) => ({
5108
+ task,
5109
+ result: this.extractPendingProxyResult(
5110
+ prompt,
5111
+ taskBatchChildToolCallId(call.toolCallId, index)
5112
+ )
5113
+ }));
5114
+ const answered = children.filter((child) => child.result !== null).length;
5115
+ if (answered === 0) return null;
5116
+ if (answered < children.length) {
5117
+ log.warn("task_batch resolving with child results missing", {
5118
+ toolCallId: call.toolCallId,
5119
+ answered,
5120
+ total: children.length
5121
+ });
5122
+ }
5123
+ return formatTaskBatchResults(children);
5124
+ }
4715
5125
  /**
4716
5126
  * Resolve the session affinity token for this LLM call. Delegates to the
4717
5127
  * exported `resolveSessionAffinity` helper so the logic is unit-testable.
@@ -4972,9 +5382,9 @@ var ClaudeCodeLanguageModel = class {
4972
5382
  return this.doGenerateViaStream(options);
4973
5383
  }
4974
5384
  const warnings = [];
4975
- const cwd = resolveSpawnCwd(this.config.cwd);
4976
5385
  const scope = this.requestScope(options);
4977
5386
  const affinity = this.sessionAffinity(options);
5387
+ const cwd = await resolveSpawnCwdForSession(this.config.cwd, affinity);
4978
5388
  const effectiveModelId = resolveAgentModel(
4979
5389
  this.getOpencodeAgent(options.providerOptions),
4980
5390
  this.modelId
@@ -5113,7 +5523,7 @@ var ClaudeCodeLanguageModel = class {
5113
5523
  const toolCalls = [];
5114
5524
  const toolCallStreams = /* @__PURE__ */ new Map();
5115
5525
  let gotPartialEvents = false;
5116
- const result = await new Promise((resolve4, reject) => {
5526
+ const result = await new Promise((resolve5, reject) => {
5117
5527
  const cleanup = () => {
5118
5528
  try {
5119
5529
  if (!proc.killed && proc.exitCode === null) proc.kill();
@@ -5248,7 +5658,7 @@ ${plan}
5248
5658
  usage: msg.usage
5249
5659
  };
5250
5660
  cleanup();
5251
- resolve4({
5661
+ resolve5({
5252
5662
  ...resultMeta,
5253
5663
  text: responseText,
5254
5664
  thinking: thinkingText,
@@ -5260,7 +5670,7 @@ ${plan}
5260
5670
  });
5261
5671
  rl.on("close", () => {
5262
5672
  cleanup();
5263
- resolve4({
5673
+ resolve5({
5264
5674
  ...resultMeta,
5265
5675
  text: responseText,
5266
5676
  thinking: thinkingText,
@@ -5366,11 +5776,11 @@ ${plan}
5366
5776
  }
5367
5777
  async doStream(options) {
5368
5778
  const warnings = [];
5369
- const cwd = resolveSpawnCwd(this.config.cwd);
5370
5779
  const cliPath = this.config.cliPath;
5371
5780
  const skipPermissions = this.config.skipPermissions !== false;
5372
5781
  const scope = this.requestScope(options);
5373
5782
  const affinity = this.sessionAffinity(options);
5783
+ const cwd = await resolveSpawnCwdForSession(this.config.cwd, affinity);
5374
5784
  const compactionMode = this.isCompactionCall(options);
5375
5785
  const effectiveModelId = compactionMode ? this.resolveCompactionModel() : resolveAgentModel(
5376
5786
  this.getOpencodeAgent(options.providerOptions),
@@ -5523,7 +5933,7 @@ ${plan}
5523
5933
  const self = this;
5524
5934
  const previousPendingProxyMatches = previousPendingProxyCalls.map((call) => ({
5525
5935
  call,
5526
- result: this.extractPendingProxyResult(options.prompt, call.toolCallId)
5936
+ result: this.extractPendingProxyResultForCall(options.prompt, call)
5527
5937
  }));
5528
5938
  const hasMatchedPendingResults = previousPendingProxyMatches.some(
5529
5939
  (m) => m.result !== null
@@ -5743,6 +6153,11 @@ ${plan}
5743
6153
  compressionSummary: getCompressionSummary(sk)
5744
6154
  }
5745
6155
  );
6156
+ const skillPluginDirs = await resolveSkillPluginDirs({
6157
+ cwd,
6158
+ cliPath,
6159
+ enabled: self.config.bridgeOpencodeSkills === true
6160
+ });
5746
6161
  cliArgs = buildCliArgs({
5747
6162
  sessionKey: sk,
5748
6163
  skipPermissions,
@@ -5752,6 +6167,7 @@ ${plan}
5752
6167
  strictMcpConfig: self.config.strictMcpConfig,
5753
6168
  disallowedTools: allDisallowed.length > 0 ? allDisallowed : void 0,
5754
6169
  appendSystemPromptFile: systemPromptFile,
6170
+ pluginDirs: skillPluginDirs,
5755
6171
  ...self.thinkingCliOptions(),
5756
6172
  fastMode,
5757
6173
  cliVersion
@@ -5781,6 +6197,13 @@ ${plan}
5781
6197
  activeProcess = ap;
5782
6198
  }
5783
6199
  }
6200
+ if (activeProcess && !hasMatchedPendingResults && isTurnInFlight(activeProcess)) {
6201
+ log.warn("previous turn still in flight; interrupting it", { sk });
6202
+ const idle = await interruptTurn(activeProcess);
6203
+ if (!idle) {
6204
+ log.warn("previous turn did not stop in time; this turn may see stale output", { sk });
6205
+ }
6206
+ }
5784
6207
  controller.enqueue({ type: "stream-start", warnings });
5785
6208
  let currentTextId = null;
5786
6209
  const textBlockIndices = /* @__PURE__ */ new Set();
@@ -5919,7 +6342,10 @@ ${plan}
5919
6342
  lineEmitter.on("close", closeHandler);
5920
6343
  proc.on("error", procErrorHandler);
5921
6344
  try {
5922
- if (!deliverPendingCompletions(true)) proc.stdin?.write(watchdogMessage + "\n");
6345
+ if (!deliverPendingCompletions(true)) {
6346
+ noteTurnStarted(newAp);
6347
+ proc.stdin?.write(watchdogMessage + "\n");
6348
+ }
5923
6349
  log.debug("re-sent user message after respawn", {
5924
6350
  textLength: watchdogMessage.length
5925
6351
  });
@@ -5969,20 +6395,34 @@ ${plan}
5969
6395
  const finishWithToolCalls = (calls) => {
5970
6396
  if (controllerClosed) return;
5971
6397
  if (calls.length === 0) return;
5972
- for (const call of calls) {
6398
+ const enqueueToolCall = (toolCallId, toolName, input) => {
5973
6399
  controller.enqueue({
5974
6400
  type: "tool-input-start",
5975
- id: call.toolCallId,
5976
- toolName: call.toolName
6401
+ id: toolCallId,
6402
+ toolName
5977
6403
  });
5978
6404
  controller.enqueue({
5979
6405
  type: "tool-call",
5980
- toolCallId: call.toolCallId,
5981
- toolName: call.toolName,
5982
- input: JSON.stringify(call.input),
6406
+ toolCallId,
6407
+ toolName,
6408
+ input: JSON.stringify(input),
5983
6409
  providerExecuted: false
5984
6410
  });
5985
- skipResultForIds.add(call.toolCallId);
6411
+ skipResultForIds.add(toolCallId);
6412
+ };
6413
+ for (const call of calls) {
6414
+ if (call.toolName === TASK_BATCH_TOOL_NAME) {
6415
+ for (const [index, task] of taskBatchTasks(call.input).entries()) {
6416
+ enqueueToolCall(
6417
+ taskBatchChildToolCallId(call.toolCallId, index),
6418
+ "task",
6419
+ task
6420
+ );
6421
+ }
6422
+ skipResultForIds.add(call.toolCallId);
6423
+ } else {
6424
+ enqueueToolCall(call.toolCallId, call.toolName, call.input);
6425
+ }
5986
6426
  markPendingProxyCallEmitted(call.toolCallId);
5987
6427
  }
5988
6428
  controller.enqueue({
@@ -6146,6 +6586,7 @@ ${plan}
6146
6586
  });
6147
6587
  turnCompleted = false;
6148
6588
  resetAutoContinueWindow();
6589
+ if (activeProcess) noteTurnStarted(activeProcess);
6149
6590
  proc.stdin?.write(makeAutoContinueMessage() + "\n");
6150
6591
  return;
6151
6592
  }
@@ -6186,6 +6627,9 @@ ${plan}
6186
6627
  });
6187
6628
  controllerClosed = true;
6188
6629
  cleanupTurn();
6630
+ if (!useInteractive && !compactionMode) {
6631
+ scheduleIdleProcessEviction(sk, self.config.idleProcessTimeoutMs);
6632
+ }
6189
6633
  try {
6190
6634
  controller.close();
6191
6635
  } catch {
@@ -6952,6 +7396,11 @@ ${plan}
6952
7396
  options.abortSignal.addEventListener("abort", () => {
6953
7397
  autoContinueState.aborted = true;
6954
7398
  if (turnCompleted || controllerClosed) return;
7399
+ if (activeProcess) {
7400
+ void interruptTurn(activeProcess).then((idle) => {
7401
+ log.info("interrupt sent for aborted turn", { sk, idle });
7402
+ });
7403
+ }
6955
7404
  if (!hasReceivedContent) {
6956
7405
  log.info(
6957
7406
  "abort signal received before content, closing stream immediately",
@@ -7039,6 +7488,7 @@ ${plan}
7039
7488
  );
7040
7489
  }
7041
7490
  }
7491
+ if (activeProcess) noteTurnStarted(activeProcess);
7042
7492
  proc.stdin?.write(userMsg + "\n");
7043
7493
  log.debug("sent user message", { textLength: userMsg.length });
7044
7494
  armStartWatchdog();
@@ -7070,7 +7520,7 @@ ${plan}
7070
7520
 
7071
7521
  // src/accounts.ts
7072
7522
  import { chmod, lstat, mkdir, readlink, symlink, writeFile } from "fs/promises";
7073
- import path6 from "path";
7523
+ import path7 from "path";
7074
7524
  var BASE_PROVIDER_ID = "claude-code";
7075
7525
  var DEFAULT_ACCOUNT = "default";
7076
7526
  var SHARED_CAPABILITY_ITEMS = [
@@ -7108,7 +7558,7 @@ function expandHome(value) {
7108
7558
  const home = process.env.HOME ?? process.env.USERPROFILE;
7109
7559
  if (value === "~") return home ?? value;
7110
7560
  if (value.startsWith("~/") || value.startsWith("~\\")) {
7111
- return home ? path6.join(home, value.slice(2)) : value;
7561
+ return home ? path7.join(home, value.slice(2)) : value;
7112
7562
  }
7113
7563
  return value;
7114
7564
  }
@@ -7140,8 +7590,8 @@ async function ensureSharedCapabilities(targetRoot) {
7140
7590
  }
7141
7591
  }
7142
7592
  async function ensureSharedCapabilityItem(sourceRoot, targetRoot, item) {
7143
- const source = path6.join(sourceRoot, item);
7144
- const target = path6.join(targetRoot, item);
7593
+ const source = path7.join(sourceRoot, item);
7594
+ const target = path7.join(targetRoot, item);
7145
7595
  let sourceStat;
7146
7596
  try {
7147
7597
  sourceStat = await lstat(source);
@@ -7152,8 +7602,8 @@ async function ensureSharedCapabilityItem(sourceRoot, targetRoot, item) {
7152
7602
  const targetStat = await lstat(target);
7153
7603
  if (targetStat.isSymbolicLink()) {
7154
7604
  const current = await readlink(target);
7155
- const resolvedCurrent = path6.resolve(path6.dirname(target), current);
7156
- const resolvedSource = path6.resolve(source);
7605
+ const resolvedCurrent = path7.resolve(path7.dirname(target), current);
7606
+ const resolvedSource = path7.resolve(source);
7157
7607
  if (resolvedCurrent === resolvedSource) return;
7158
7608
  }
7159
7609
  log.warn("shared Claude capability already exists; leaving untouched", {
@@ -7168,11 +7618,11 @@ async function ensureSharedCapabilityItem(sourceRoot, targetRoot, item) {
7168
7618
  await symlink(source, target, type);
7169
7619
  }
7170
7620
  async function writeAccountWrapper(account, baseCliPath, configDir) {
7171
- const cacheRoot = path6.join(
7621
+ const cacheRoot = path7.join(
7172
7622
  process.env.XDG_CACHE_HOME ?? expandHome("~/.cache"),
7173
7623
  "opencode-claude-code-plugin"
7174
7624
  );
7175
- const wrapperPath = path6.join(cacheRoot, `claude-${account}`);
7625
+ const wrapperPath = path7.join(cacheRoot, `claude-${account}`);
7176
7626
  const suffix = `@${account}`;
7177
7627
  await mkdir(cacheRoot, { recursive: true });
7178
7628
  const script = `#!/usr/bin/env bash
@@ -7215,11 +7665,11 @@ import {
7215
7665
  existsSync as existsSync4,
7216
7666
  readFileSync as readFileSync4,
7217
7667
  realpathSync,
7218
- rmSync as rmSync2,
7219
- writeFileSync as writeFileSync4
7668
+ rmSync as rmSync3,
7669
+ writeFileSync as writeFileSync5
7220
7670
  } from "fs";
7221
- import { homedir as homedir5 } from "os";
7222
- import { join as join7, resolve as resolve3 } from "path";
7671
+ import { homedir as homedir6 } from "os";
7672
+ import { join as join8, resolve as resolve4 } from "path";
7223
7673
  import { fileURLToPath } from "url";
7224
7674
  var STALE_PACKAGE_NAME = "opencode-claude-code-plugin";
7225
7675
  var SUSPECT_DESCRIPTION_TOKEN = "Claude Code";
@@ -7227,14 +7677,14 @@ var alreadyRan = false;
7227
7677
  function candidateCacheRoots() {
7228
7678
  const xdg = process.env.XDG_CACHE_HOME;
7229
7679
  return [
7230
- xdg ? join7(xdg, "opencode") : null,
7231
- join7(homedir5(), ".cache", "opencode"),
7232
- join7(homedir5(), "Library", "Caches", "opencode")
7680
+ xdg ? join8(xdg, "opencode") : null,
7681
+ join8(homedir6(), ".cache", "opencode"),
7682
+ join8(homedir6(), "Library", "Caches", "opencode")
7233
7683
  ].filter((p) => Boolean(p));
7234
7684
  }
7235
7685
  function userOpencodeJsonPath() {
7236
- const xdgConfig = process.env.XDG_CONFIG_HOME ?? join7(homedir5(), ".config");
7237
- return join7(xdgConfig, "opencode", "opencode.json");
7686
+ const xdgConfig = process.env.XDG_CONFIG_HOME ?? join8(homedir6(), ".config");
7687
+ return join8(xdgConfig, "opencode", "opencode.json");
7238
7688
  }
7239
7689
  function userIntendsToUseUnscoped() {
7240
7690
  const cfg = userOpencodeJsonPath();
@@ -7253,7 +7703,7 @@ function userIntendsToUseUnscoped() {
7253
7703
  function ourLoadedDir() {
7254
7704
  try {
7255
7705
  const filePath = fileURLToPath(import.meta.url);
7256
- return realpathSync(resolve3(filePath, "..", ".."));
7706
+ return realpathSync(resolve4(filePath, "..", ".."));
7257
7707
  } catch {
7258
7708
  return null;
7259
7709
  }
@@ -7277,7 +7727,7 @@ function cleanupStaleUnscopedInstall() {
7277
7727
  }
7278
7728
  function cleanupOne(cacheRoot, ourDir) {
7279
7729
  if (!existsSync4(cacheRoot)) return;
7280
- const stalePath = join7(cacheRoot, "node_modules", STALE_PACKAGE_NAME);
7730
+ const stalePath = join8(cacheRoot, "node_modules", STALE_PACKAGE_NAME);
7281
7731
  if (!existsSync4(stalePath)) return;
7282
7732
  let realStalePath = stalePath;
7283
7733
  try {
@@ -7285,7 +7735,7 @@ function cleanupOne(cacheRoot, ourDir) {
7285
7735
  } catch {
7286
7736
  }
7287
7737
  if (ourDir && realStalePath === ourDir) return;
7288
- const pkgJsonPath = join7(stalePath, "package.json");
7738
+ const pkgJsonPath = join8(stalePath, "package.json");
7289
7739
  if (!existsSync4(pkgJsonPath)) return;
7290
7740
  let pkg = {};
7291
7741
  try {
@@ -7297,7 +7747,7 @@ function cleanupOne(cacheRoot, ourDir) {
7297
7747
  if (!pkg.description?.includes(SUSPECT_DESCRIPTION_TOKEN)) return;
7298
7748
  log.info("cleanup-stale: removing unscoped install", { stalePath });
7299
7749
  try {
7300
- rmSync2(stalePath, { recursive: true, force: true });
7750
+ rmSync3(stalePath, { recursive: true, force: true });
7301
7751
  } catch (err) {
7302
7752
  log.warn("cleanup-stale: rmSync failed", {
7303
7753
  stalePath,
@@ -7305,13 +7755,13 @@ function cleanupOne(cacheRoot, ourDir) {
7305
7755
  });
7306
7756
  return;
7307
7757
  }
7308
- const cachePkgJson = join7(cacheRoot, "package.json");
7758
+ const cachePkgJson = join8(cacheRoot, "package.json");
7309
7759
  if (!existsSync4(cachePkgJson)) return;
7310
7760
  try {
7311
7761
  const cfg = JSON.parse(readFileSync4(cachePkgJson, "utf8"));
7312
7762
  if (cfg?.dependencies?.[STALE_PACKAGE_NAME]) {
7313
7763
  delete cfg.dependencies[STALE_PACKAGE_NAME];
7314
- writeFileSync4(cachePkgJson, JSON.stringify(cfg, null, 2) + "\n");
7764
+ writeFileSync5(cachePkgJson, JSON.stringify(cfg, null, 2) + "\n");
7315
7765
  log.info("cleanup-stale: pruned dep from cache package.json");
7316
7766
  }
7317
7767
  } catch (err) {
@@ -7323,16 +7773,16 @@ function cleanupOne(cacheRoot, ourDir) {
7323
7773
 
7324
7774
  // src/startup-diagnostics.ts
7325
7775
  import { execFile as execFile2 } from "child_process";
7326
- import * as fs5 from "fs";
7327
- import * as path7 from "path";
7776
+ import * as fs6 from "fs";
7777
+ import * as path8 from "path";
7328
7778
  import { promisify as promisify2 } from "util";
7329
7779
  import { fileURLToPath as fileURLToPath2 } from "url";
7330
7780
  var cachedPluginVersion;
7331
7781
  function pluginVersion() {
7332
7782
  if (cachedPluginVersion) return cachedPluginVersion;
7333
7783
  try {
7334
- const here = path7.dirname(fileURLToPath2(import.meta.url));
7335
- const raw = fs5.readFileSync(path7.join(here, "..", "package.json"), "utf8");
7784
+ const here = path8.dirname(fileURLToPath2(import.meta.url));
7785
+ const raw = fs6.readFileSync(path8.join(here, "..", "package.json"), "utf8");
7336
7786
  const version = JSON.parse(raw).version;
7337
7787
  cachedPluginVersion = typeof version === "string" ? version : "unknown";
7338
7788
  } catch {
@@ -7356,7 +7806,7 @@ var opencodeVersionProbe;
7356
7806
  function detectOpencodeVersion(execPath = process.execPath) {
7357
7807
  if (opencodeVersionProbe) return opencodeVersionProbe;
7358
7808
  opencodeVersionProbe = (async () => {
7359
- if (!path7.basename(execPath).toLowerCase().includes("opencode")) {
7809
+ if (!path8.basename(execPath).toLowerCase().includes("opencode")) {
7360
7810
  log.debug("skipping opencode version probe: execPath is not opencode", { execPath });
7361
7811
  return void 0;
7362
7812
  }
@@ -7526,6 +7976,8 @@ function createClaudeCode(settings = {}) {
7526
7976
  autoContinueIncompleteTurns: settings.autoContinueIncompleteTurns ?? "smart",
7527
7977
  compactionModel: settings.compactionModel,
7528
7978
  ignoreAnthropicApiKey: settings.ignoreAnthropicApiKey,
7979
+ idleProcessTimeoutMs: settings.idleProcessTimeoutMs,
7980
+ bridgeOpencodeSkills: settings.bridgeOpencodeSkills === true,
7529
7981
  interactive: settings.interactive,
7530
7982
  interactiveBypass: settings.interactiveBypass,
7531
7983
  interactiveAllowTools: settings.interactiveAllowTools,