@khalilgharbaoui/opencode-claude-code-plugin 0.15.4 → 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)) {
@@ -3093,34 +3222,166 @@ function agentDirectories(home, projectDirectory) {
3093
3222
  return directories;
3094
3223
  }
3095
3224
 
3096
- // src/mcp-bridge.ts
3225
+ // src/skill-bridge.ts
3226
+ import * as crypto2 from "crypto";
3097
3227
  import * as fs3 from "fs";
3098
- import * as path4 from "path";
3099
3228
  import * as os2 from "os";
3100
- 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";
3101
3362
  import {
3102
3363
  parse as parseJsonc,
3103
3364
  printParseErrorCode
3104
3365
  } from "jsonc-parser";
3105
3366
  var FILE_NAMES = ["opencode.jsonc", "opencode.json", "config.json"];
3106
3367
  var PROJECT_FILE_NAMES = ["opencode.json", "opencode.jsonc"];
3107
- function fileExists(p) {
3368
+ function fileExists2(p) {
3108
3369
  try {
3109
- return fs3.statSync(p).isFile();
3370
+ return fs4.statSync(p).isFile();
3110
3371
  } catch {
3111
3372
  return false;
3112
3373
  }
3113
3374
  }
3114
- function dirExists(p) {
3375
+ function dirExists2(p) {
3115
3376
  try {
3116
- return fs3.statSync(p).isDirectory();
3377
+ return fs4.statSync(p).isDirectory();
3117
3378
  } catch {
3118
3379
  return false;
3119
3380
  }
3120
3381
  }
3121
3382
  function readAndParse(file) {
3122
3383
  try {
3123
- const raw = fs3.readFileSync(file, "utf8");
3384
+ const raw = fs4.readFileSync(file, "utf8");
3124
3385
  const errors = [];
3125
3386
  const parsed = parseJsonc(raw, errors, { allowTrailingComma: true });
3126
3387
  if (errors.length > 0) {
@@ -3156,14 +3417,14 @@ function deepMerge(target, source) {
3156
3417
  }
3157
3418
  function walkUp(opts) {
3158
3419
  const out = [];
3159
- let current = path4.resolve(opts.start);
3420
+ let current = path5.resolve(opts.start);
3160
3421
  while (true) {
3161
3422
  for (const target of opts.targets) {
3162
- const candidate = path4.join(current, target);
3423
+ const candidate = path5.join(current, target);
3163
3424
  if (opts.predicate(candidate)) out.push(candidate);
3164
3425
  }
3165
- if (opts.stop && current === path4.resolve(opts.stop)) break;
3166
- const parent = path4.dirname(current);
3426
+ if (opts.stop && current === path5.resolve(opts.stop)) break;
3427
+ const parent = path5.dirname(current);
3167
3428
  if (parent === current) break;
3168
3429
  current = parent;
3169
3430
  }
@@ -3171,29 +3432,29 @@ function walkUp(opts) {
3171
3432
  }
3172
3433
  function detectWorktree(cwd) {
3173
3434
  const override = process.env.OPENCODE_WORKTREE;
3174
- if (override) return path4.resolve(override);
3175
- let current = path4.resolve(cwd);
3435
+ if (override) return path5.resolve(override);
3436
+ let current = path5.resolve(cwd);
3176
3437
  while (true) {
3177
- const gitPath = path4.join(current, ".git");
3438
+ const gitPath = path5.join(current, ".git");
3178
3439
  try {
3179
- if (fs3.existsSync(gitPath)) return current;
3440
+ if (fs4.existsSync(gitPath)) return current;
3180
3441
  } catch {
3181
3442
  }
3182
- const parent = path4.dirname(current);
3443
+ const parent = path5.dirname(current);
3183
3444
  if (parent === current) return void 0;
3184
3445
  current = parent;
3185
3446
  }
3186
3447
  }
3187
3448
  function globalConfigDir() {
3188
- const xdg = process.env.XDG_CONFIG_HOME ?? path4.join(os2.homedir(), ".config");
3189
- return path4.join(xdg, "opencode");
3449
+ const xdg = process.env.XDG_CONFIG_HOME ?? path5.join(os3.homedir(), ".config");
3450
+ return path5.join(xdg, "opencode");
3190
3451
  }
3191
3452
  function loadGlobalConfig() {
3192
3453
  const dir = globalConfigDir();
3193
3454
  let merged = {};
3194
3455
  for (const name of FILE_NAMES.slice().reverse()) {
3195
- const file = path4.join(dir, name);
3196
- if (!fileExists(file)) continue;
3456
+ const file = path5.join(dir, name);
3457
+ if (!fileExists2(file)) continue;
3197
3458
  const parsed = readAndParse(file);
3198
3459
  if (parsed) merged = deepMerge(merged, parsed);
3199
3460
  }
@@ -3202,8 +3463,8 @@ function loadGlobalConfig() {
3202
3463
  function loadProjectFilesInDir(dir) {
3203
3464
  let merged = {};
3204
3465
  for (const name of PROJECT_FILE_NAMES) {
3205
- const file = path4.join(dir, name);
3206
- if (!fileExists(file)) continue;
3466
+ const file = path5.join(dir, name);
3467
+ if (!fileExists2(file)) continue;
3207
3468
  const parsed = readAndParse(file);
3208
3469
  if (parsed) merged = deepMerge(merged, parsed);
3209
3470
  }
@@ -3213,8 +3474,8 @@ function dotOpencodeDirs(cwd, worktree) {
3213
3474
  const dirs = [];
3214
3475
  const seen = /* @__PURE__ */ new Set();
3215
3476
  const push = (p) => {
3216
- const abs = path4.resolve(p);
3217
- if (!seen.has(abs) && dirExists(abs)) {
3477
+ const abs = path5.resolve(p);
3478
+ if (!seen.has(abs) && dirExists2(abs)) {
3218
3479
  seen.add(abs);
3219
3480
  dirs.push(abs);
3220
3481
  }
@@ -3223,17 +3484,17 @@ function dotOpencodeDirs(cwd, worktree) {
3223
3484
  start: cwd,
3224
3485
  stop: worktree,
3225
3486
  targets: [".opencode"],
3226
- predicate: dirExists
3487
+ predicate: dirExists2
3227
3488
  })) {
3228
3489
  push(dir);
3229
3490
  }
3230
- const home = os2.homedir();
3491
+ const home = os3.homedir();
3231
3492
  if (home) {
3232
- const homeDot = path4.join(home, ".opencode");
3233
- if (dirExists(homeDot)) push(homeDot);
3493
+ const homeDot = path5.join(home, ".opencode");
3494
+ if (dirExists2(homeDot)) push(homeDot);
3234
3495
  }
3235
3496
  const envDir = process.env.OPENCODE_CONFIG_DIR;
3236
- if (envDir && dirExists(envDir)) push(envDir);
3497
+ if (envDir && dirExists2(envDir)) push(envDir);
3237
3498
  return dirs;
3238
3499
  }
3239
3500
  function substituteEnvPlaceholders(source) {
@@ -3341,7 +3602,7 @@ function mergeOpencodeMcp(cwd, runtimeStatus) {
3341
3602
  let merged = {};
3342
3603
  merged = mergeMcp(merged, extractMcpBlock(loadGlobalConfig()));
3343
3604
  const explicitConfig = process.env.OPENCODE_CONFIG;
3344
- if (explicitConfig && fileExists(explicitConfig)) {
3605
+ if (explicitConfig && fileExists2(explicitConfig)) {
3345
3606
  const parsed = readAndParse(explicitConfig);
3346
3607
  if (parsed) merged = mergeMcp(merged, extractMcpBlock(parsed));
3347
3608
  }
@@ -3349,12 +3610,12 @@ function mergeOpencodeMcp(cwd, runtimeStatus) {
3349
3610
  start: cwd,
3350
3611
  stop: worktree,
3351
3612
  targets: PROJECT_FILE_NAMES,
3352
- predicate: fileExists
3613
+ predicate: fileExists2
3353
3614
  });
3354
3615
  const projectDirs = [];
3355
3616
  const seenProjectDirs = /* @__PURE__ */ new Set();
3356
3617
  for (const f of projectFiles) {
3357
- const d = path4.dirname(f);
3618
+ const d = path5.dirname(f);
3358
3619
  if (!seenProjectDirs.has(d)) {
3359
3620
  seenProjectDirs.add(d);
3360
3621
  projectDirs.push(d);
@@ -3383,7 +3644,7 @@ function mergeOpencodeMcp(cwd, runtimeStatus) {
3383
3644
  enabledServerNames.push(name);
3384
3645
  }
3385
3646
  const mergedBody = JSON.stringify({ mcpServers: merged }, null, 2);
3386
- 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);
3387
3648
  return { servers: merged, enabledServerNames, hash };
3388
3649
  }
3389
3650
  function finishBridge(input) {
@@ -3399,13 +3660,13 @@ function finishBridge(input) {
3399
3660
  };
3400
3661
  }
3401
3662
  const body = JSON.stringify({ mcpServers: servers }, null, 2);
3402
- const outPath = path4.join(
3663
+ const outPath = path5.join(
3403
3664
  pluginTmpDir(),
3404
3665
  `mcp-${hash}.json`
3405
3666
  );
3406
3667
  try {
3407
- if (!fileExists(outPath)) {
3408
- fs3.writeFileSync(outPath, body, { encoding: "utf8", mode: 384 });
3668
+ if (!fileExists2(outPath)) {
3669
+ fs4.writeFileSync(outPath, body, { encoding: "utf8", mode: 384 });
3409
3670
  }
3410
3671
  } catch (e) {
3411
3672
  log.warn("failed to write bridged MCP config", {
@@ -3447,17 +3708,39 @@ function getOpencodeProjectDirectory() {
3447
3708
  function isUsableDirectory(d) {
3448
3709
  return typeof d === "string" && d.length > 1 && d !== "/";
3449
3710
  }
3450
- 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;
3451
3720
  return resolveSpawnCwdFrom(
3452
3721
  configured,
3453
3722
  process.cwd(),
3454
- opencodeProjectDirectory
3723
+ opencodeProjectDirectory,
3724
+ sessionDir
3455
3725
  );
3456
3726
  }
3457
- function resolveSpawnCwdFrom(configured, live, captured) {
3458
- if (configured) return configured;
3459
- if (isUsableDirectory(live)) return live;
3460
- 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
+ }
3461
3744
  }
3462
3745
  async function getRuntimeMcpStatus() {
3463
3746
  const client = opencodeClient;
@@ -3516,40 +3799,40 @@ import { EventEmitter as EventEmitter4 } from "events";
3516
3799
  import { unlink as unlink2 } from "fs/promises";
3517
3800
 
3518
3801
  // src/claude-session-bun.ts
3519
- import * as os3 from "os";
3520
- import * as fs4 from "fs";
3521
- import * as path5 from "path";
3802
+ import * as os4 from "os";
3803
+ import * as fs5 from "fs";
3804
+ import * as path6 from "path";
3522
3805
  import { execFileSync } from "child_process";
3523
- import { randomUUID as randomUUID3 } from "crypto";
3806
+ import { randomUUID as randomUUID4 } from "crypto";
3524
3807
  function resolveClaude(cmd = "claude") {
3525
- if (path5.isAbsolute(cmd) && fs4.existsSync(cmd)) return cmd;
3808
+ if (path6.isAbsolute(cmd) && fs5.existsSync(cmd)) return cmd;
3526
3809
  const viaBun = Bun.which(cmd);
3527
3810
  if (viaBun) return viaBun;
3528
- const isWin = os3.platform() === "win32";
3811
+ const isWin = os4.platform() === "win32";
3529
3812
  try {
3530
3813
  const out = execFileSync(isWin ? "where" : "which", [cmd], {
3531
3814
  encoding: "utf8",
3532
3815
  stdio: ["ignore", "pipe", "ignore"]
3533
3816
  });
3534
- 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));
3535
3818
  if (first) return first;
3536
3819
  } catch {
3537
3820
  }
3538
3821
  throw new Error(`Could not resolve command on PATH: ${cmd}`);
3539
3822
  }
3540
3823
  function encodeCwd(cwd) {
3541
- return path5.resolve(cwd).replace(/[^a-zA-Z0-9]/g, "-");
3824
+ return path6.resolve(cwd).replace(/[^a-zA-Z0-9]/g, "-");
3542
3825
  }
3543
3826
  var TERMINAL_STOP = /* @__PURE__ */ new Set(["end_turn", "stop_sequence", "max_tokens"]);
3544
3827
  var delay = (ms) => new Promise((r) => setTimeout(r, ms));
3545
3828
  function resolveConfigDir(configDir) {
3546
3829
  const value = configDir ?? process.env.CLAUDE_CONFIG_DIR;
3547
- if (!value) return path5.join(os3.homedir(), ".claude");
3548
- if (value === "~") return os3.homedir();
3830
+ if (!value) return path6.join(os4.homedir(), ".claude");
3831
+ if (value === "~") return os4.homedir();
3549
3832
  if (value.startsWith("~/") || value.startsWith("~\\")) {
3550
- return path5.join(os3.homedir(), value.slice(2));
3833
+ return path6.join(os4.homedir(), value.slice(2));
3551
3834
  }
3552
- return path5.resolve(value);
3835
+ return path6.resolve(value);
3553
3836
  }
3554
3837
  var ClaudeSession = class {
3555
3838
  sessionId;
@@ -3567,11 +3850,11 @@ var ClaudeSession = class {
3567
3850
  signal;
3568
3851
  o;
3569
3852
  constructor(opts = {}) {
3570
- this.cwd = path5.resolve(opts.cwd ?? process.cwd());
3853
+ this.cwd = path6.resolve(opts.cwd ?? process.cwd());
3571
3854
  this.configDir = resolveConfigDir(opts.configDir);
3572
3855
  this.signal = opts.signal;
3573
- this.sessionId = randomUUID3();
3574
- this.jsonlPath = path5.join(
3856
+ this.sessionId = randomUUID4();
3857
+ this.jsonlPath = path6.join(
3575
3858
  this.configDir,
3576
3859
  "projects",
3577
3860
  encodeCwd(this.cwd),
@@ -3694,7 +3977,7 @@ var ClaudeSession = class {
3694
3977
  }
3695
3978
  readRawLines() {
3696
3979
  try {
3697
- return fs4.readFileSync(this.jsonlPath, "utf8").split("\n");
3980
+ return fs5.readFileSync(this.jsonlPath, "utf8").split("\n");
3698
3981
  } catch {
3699
3982
  return [];
3700
3983
  }
@@ -4072,11 +4355,11 @@ function spawnInteractiveProcess(opts) {
4072
4355
  }
4073
4356
 
4074
4357
  // src/claude-code-language-model.ts
4075
- import { readFileSync as readFileSync3, writeFileSync as writeFileSync3 } from "fs";
4358
+ import { readFileSync as readFileSync3, writeFileSync as writeFileSync4 } from "fs";
4076
4359
  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";
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";
4080
4363
  var DEFAULT_COMPACTION_MODEL = "claude-haiku-4-5";
4081
4364
  function resolveCompactionModel(configured) {
4082
4365
  const env = process.env.CLAUDE_CODE_COMPACTION_MODEL?.trim();
@@ -4283,9 +4566,9 @@ ${body}`;
4283
4566
  }
4284
4567
  });
4285
4568
  }
4286
- function readPromptFileIfPresent(path8) {
4569
+ function readPromptFileIfPresent(path9) {
4287
4570
  try {
4288
- const content = readFileSync3(path8, "utf8").trim();
4571
+ const content = readFileSync3(path9, "utf8").trim();
4289
4572
  return content || void 0;
4290
4573
  } catch {
4291
4574
  return void 0;
@@ -4294,9 +4577,9 @@ function readPromptFileIfPresent(path8) {
4294
4577
  function nearestWorkspaceAgentsPrompt(cwd) {
4295
4578
  let dir = cwd;
4296
4579
  while (true) {
4297
- const content = readPromptFileIfPresent(join6(dir, "AGENTS.md"));
4580
+ const content = readPromptFileIfPresent(join7(dir, "AGENTS.md"));
4298
4581
  if (content) return content;
4299
- const parent = dirname3(dir);
4582
+ const parent = dirname4(dir);
4300
4583
  if (parent === dir) return void 0;
4301
4584
  dir = parent;
4302
4585
  }
@@ -4379,19 +4662,22 @@ ${options.compressionSummary.trim()}`
4379
4662
  for (const s of extraSystemContent) {
4380
4663
  if (s.trim()) parts.push(s.trim());
4381
4664
  }
4382
- const configRoot = process.env.XDG_CONFIG_HOME ?? join6(homedir4(), ".config");
4383
- 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"));
4384
4667
  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);
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);
4388
4674
  if (includeMultiStepHint) parts.push(MULTI_STEP_TASK_HINT);
4389
4675
  const content = parts.join("\n\n");
4390
4676
  if (!content) return void 0;
4391
- const path8 = join6(tmpdir2(), `opencode-cc-sys-${randomUUID4()}.md`);
4677
+ const path9 = join7(tmpdir2(), `opencode-cc-sys-${randomUUID5()}.md`);
4392
4678
  try {
4393
- writeFileSync3(path8, content, "utf8");
4394
- return path8;
4679
+ writeFileSync4(path9, content, "utf8");
4680
+ return path9;
4395
4681
  } catch (err) {
4396
4682
  log.warn("failed to write system prompt file", { error: String(err) });
4397
4683
  return void 0;
@@ -4972,9 +5258,9 @@ var ClaudeCodeLanguageModel = class {
4972
5258
  return this.doGenerateViaStream(options);
4973
5259
  }
4974
5260
  const warnings = [];
4975
- const cwd = resolveSpawnCwd(this.config.cwd);
4976
5261
  const scope = this.requestScope(options);
4977
5262
  const affinity = this.sessionAffinity(options);
5263
+ const cwd = await resolveSpawnCwdForSession(this.config.cwd, affinity);
4978
5264
  const effectiveModelId = resolveAgentModel(
4979
5265
  this.getOpencodeAgent(options.providerOptions),
4980
5266
  this.modelId
@@ -5113,7 +5399,7 @@ var ClaudeCodeLanguageModel = class {
5113
5399
  const toolCalls = [];
5114
5400
  const toolCallStreams = /* @__PURE__ */ new Map();
5115
5401
  let gotPartialEvents = false;
5116
- const result = await new Promise((resolve4, reject) => {
5402
+ const result = await new Promise((resolve5, reject) => {
5117
5403
  const cleanup = () => {
5118
5404
  try {
5119
5405
  if (!proc.killed && proc.exitCode === null) proc.kill();
@@ -5248,7 +5534,7 @@ ${plan}
5248
5534
  usage: msg.usage
5249
5535
  };
5250
5536
  cleanup();
5251
- resolve4({
5537
+ resolve5({
5252
5538
  ...resultMeta,
5253
5539
  text: responseText,
5254
5540
  thinking: thinkingText,
@@ -5260,7 +5546,7 @@ ${plan}
5260
5546
  });
5261
5547
  rl.on("close", () => {
5262
5548
  cleanup();
5263
- resolve4({
5549
+ resolve5({
5264
5550
  ...resultMeta,
5265
5551
  text: responseText,
5266
5552
  thinking: thinkingText,
@@ -5366,11 +5652,11 @@ ${plan}
5366
5652
  }
5367
5653
  async doStream(options) {
5368
5654
  const warnings = [];
5369
- const cwd = resolveSpawnCwd(this.config.cwd);
5370
5655
  const cliPath = this.config.cliPath;
5371
5656
  const skipPermissions = this.config.skipPermissions !== false;
5372
5657
  const scope = this.requestScope(options);
5373
5658
  const affinity = this.sessionAffinity(options);
5659
+ const cwd = await resolveSpawnCwdForSession(this.config.cwd, affinity);
5374
5660
  const compactionMode = this.isCompactionCall(options);
5375
5661
  const effectiveModelId = compactionMode ? this.resolveCompactionModel() : resolveAgentModel(
5376
5662
  this.getOpencodeAgent(options.providerOptions),
@@ -5743,6 +6029,11 @@ ${plan}
5743
6029
  compressionSummary: getCompressionSummary(sk)
5744
6030
  }
5745
6031
  );
6032
+ const skillPluginDirs = await resolveSkillPluginDirs({
6033
+ cwd,
6034
+ cliPath,
6035
+ enabled: self.config.bridgeOpencodeSkills === true
6036
+ });
5746
6037
  cliArgs = buildCliArgs({
5747
6038
  sessionKey: sk,
5748
6039
  skipPermissions,
@@ -5752,6 +6043,7 @@ ${plan}
5752
6043
  strictMcpConfig: self.config.strictMcpConfig,
5753
6044
  disallowedTools: allDisallowed.length > 0 ? allDisallowed : void 0,
5754
6045
  appendSystemPromptFile: systemPromptFile,
6046
+ pluginDirs: skillPluginDirs,
5755
6047
  ...self.thinkingCliOptions(),
5756
6048
  fastMode,
5757
6049
  cliVersion
@@ -5781,6 +6073,13 @@ ${plan}
5781
6073
  activeProcess = ap;
5782
6074
  }
5783
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
+ }
5784
6083
  controller.enqueue({ type: "stream-start", warnings });
5785
6084
  let currentTextId = null;
5786
6085
  const textBlockIndices = /* @__PURE__ */ new Set();
@@ -5919,7 +6218,10 @@ ${plan}
5919
6218
  lineEmitter.on("close", closeHandler);
5920
6219
  proc.on("error", procErrorHandler);
5921
6220
  try {
5922
- if (!deliverPendingCompletions(true)) proc.stdin?.write(watchdogMessage + "\n");
6221
+ if (!deliverPendingCompletions(true)) {
6222
+ noteTurnStarted(newAp);
6223
+ proc.stdin?.write(watchdogMessage + "\n");
6224
+ }
5923
6225
  log.debug("re-sent user message after respawn", {
5924
6226
  textLength: watchdogMessage.length
5925
6227
  });
@@ -6146,6 +6448,7 @@ ${plan}
6146
6448
  });
6147
6449
  turnCompleted = false;
6148
6450
  resetAutoContinueWindow();
6451
+ if (activeProcess) noteTurnStarted(activeProcess);
6149
6452
  proc.stdin?.write(makeAutoContinueMessage() + "\n");
6150
6453
  return;
6151
6454
  }
@@ -6186,6 +6489,9 @@ ${plan}
6186
6489
  });
6187
6490
  controllerClosed = true;
6188
6491
  cleanupTurn();
6492
+ if (!useInteractive && !compactionMode) {
6493
+ scheduleIdleProcessEviction(sk, self.config.idleProcessTimeoutMs);
6494
+ }
6189
6495
  try {
6190
6496
  controller.close();
6191
6497
  } catch {
@@ -6952,6 +7258,11 @@ ${plan}
6952
7258
  options.abortSignal.addEventListener("abort", () => {
6953
7259
  autoContinueState.aborted = true;
6954
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
+ }
6955
7266
  if (!hasReceivedContent) {
6956
7267
  log.info(
6957
7268
  "abort signal received before content, closing stream immediately",
@@ -7039,6 +7350,7 @@ ${plan}
7039
7350
  );
7040
7351
  }
7041
7352
  }
7353
+ if (activeProcess) noteTurnStarted(activeProcess);
7042
7354
  proc.stdin?.write(userMsg + "\n");
7043
7355
  log.debug("sent user message", { textLength: userMsg.length });
7044
7356
  armStartWatchdog();
@@ -7070,7 +7382,7 @@ ${plan}
7070
7382
 
7071
7383
  // src/accounts.ts
7072
7384
  import { chmod, lstat, mkdir, readlink, symlink, writeFile } from "fs/promises";
7073
- import path6 from "path";
7385
+ import path7 from "path";
7074
7386
  var BASE_PROVIDER_ID = "claude-code";
7075
7387
  var DEFAULT_ACCOUNT = "default";
7076
7388
  var SHARED_CAPABILITY_ITEMS = [
@@ -7108,7 +7420,7 @@ function expandHome(value) {
7108
7420
  const home = process.env.HOME ?? process.env.USERPROFILE;
7109
7421
  if (value === "~") return home ?? value;
7110
7422
  if (value.startsWith("~/") || value.startsWith("~\\")) {
7111
- return home ? path6.join(home, value.slice(2)) : value;
7423
+ return home ? path7.join(home, value.slice(2)) : value;
7112
7424
  }
7113
7425
  return value;
7114
7426
  }
@@ -7140,8 +7452,8 @@ async function ensureSharedCapabilities(targetRoot) {
7140
7452
  }
7141
7453
  }
7142
7454
  async function ensureSharedCapabilityItem(sourceRoot, targetRoot, item) {
7143
- const source = path6.join(sourceRoot, item);
7144
- const target = path6.join(targetRoot, item);
7455
+ const source = path7.join(sourceRoot, item);
7456
+ const target = path7.join(targetRoot, item);
7145
7457
  let sourceStat;
7146
7458
  try {
7147
7459
  sourceStat = await lstat(source);
@@ -7152,8 +7464,8 @@ async function ensureSharedCapabilityItem(sourceRoot, targetRoot, item) {
7152
7464
  const targetStat = await lstat(target);
7153
7465
  if (targetStat.isSymbolicLink()) {
7154
7466
  const current = await readlink(target);
7155
- const resolvedCurrent = path6.resolve(path6.dirname(target), current);
7156
- const resolvedSource = path6.resolve(source);
7467
+ const resolvedCurrent = path7.resolve(path7.dirname(target), current);
7468
+ const resolvedSource = path7.resolve(source);
7157
7469
  if (resolvedCurrent === resolvedSource) return;
7158
7470
  }
7159
7471
  log.warn("shared Claude capability already exists; leaving untouched", {
@@ -7168,11 +7480,11 @@ async function ensureSharedCapabilityItem(sourceRoot, targetRoot, item) {
7168
7480
  await symlink(source, target, type);
7169
7481
  }
7170
7482
  async function writeAccountWrapper(account, baseCliPath, configDir) {
7171
- const cacheRoot = path6.join(
7483
+ const cacheRoot = path7.join(
7172
7484
  process.env.XDG_CACHE_HOME ?? expandHome("~/.cache"),
7173
7485
  "opencode-claude-code-plugin"
7174
7486
  );
7175
- const wrapperPath = path6.join(cacheRoot, `claude-${account}`);
7487
+ const wrapperPath = path7.join(cacheRoot, `claude-${account}`);
7176
7488
  const suffix = `@${account}`;
7177
7489
  await mkdir(cacheRoot, { recursive: true });
7178
7490
  const script = `#!/usr/bin/env bash
@@ -7215,11 +7527,11 @@ import {
7215
7527
  existsSync as existsSync4,
7216
7528
  readFileSync as readFileSync4,
7217
7529
  realpathSync,
7218
- rmSync as rmSync2,
7219
- writeFileSync as writeFileSync4
7530
+ rmSync as rmSync3,
7531
+ writeFileSync as writeFileSync5
7220
7532
  } from "fs";
7221
- import { homedir as homedir5 } from "os";
7222
- 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";
7223
7535
  import { fileURLToPath } from "url";
7224
7536
  var STALE_PACKAGE_NAME = "opencode-claude-code-plugin";
7225
7537
  var SUSPECT_DESCRIPTION_TOKEN = "Claude Code";
@@ -7227,14 +7539,14 @@ var alreadyRan = false;
7227
7539
  function candidateCacheRoots() {
7228
7540
  const xdg = process.env.XDG_CACHE_HOME;
7229
7541
  return [
7230
- xdg ? join7(xdg, "opencode") : null,
7231
- join7(homedir5(), ".cache", "opencode"),
7232
- join7(homedir5(), "Library", "Caches", "opencode")
7542
+ xdg ? join8(xdg, "opencode") : null,
7543
+ join8(homedir6(), ".cache", "opencode"),
7544
+ join8(homedir6(), "Library", "Caches", "opencode")
7233
7545
  ].filter((p) => Boolean(p));
7234
7546
  }
7235
7547
  function userOpencodeJsonPath() {
7236
- const xdgConfig = process.env.XDG_CONFIG_HOME ?? join7(homedir5(), ".config");
7237
- return join7(xdgConfig, "opencode", "opencode.json");
7548
+ const xdgConfig = process.env.XDG_CONFIG_HOME ?? join8(homedir6(), ".config");
7549
+ return join8(xdgConfig, "opencode", "opencode.json");
7238
7550
  }
7239
7551
  function userIntendsToUseUnscoped() {
7240
7552
  const cfg = userOpencodeJsonPath();
@@ -7253,7 +7565,7 @@ function userIntendsToUseUnscoped() {
7253
7565
  function ourLoadedDir() {
7254
7566
  try {
7255
7567
  const filePath = fileURLToPath(import.meta.url);
7256
- return realpathSync(resolve3(filePath, "..", ".."));
7568
+ return realpathSync(resolve4(filePath, "..", ".."));
7257
7569
  } catch {
7258
7570
  return null;
7259
7571
  }
@@ -7277,7 +7589,7 @@ function cleanupStaleUnscopedInstall() {
7277
7589
  }
7278
7590
  function cleanupOne(cacheRoot, ourDir) {
7279
7591
  if (!existsSync4(cacheRoot)) return;
7280
- const stalePath = join7(cacheRoot, "node_modules", STALE_PACKAGE_NAME);
7592
+ const stalePath = join8(cacheRoot, "node_modules", STALE_PACKAGE_NAME);
7281
7593
  if (!existsSync4(stalePath)) return;
7282
7594
  let realStalePath = stalePath;
7283
7595
  try {
@@ -7285,7 +7597,7 @@ function cleanupOne(cacheRoot, ourDir) {
7285
7597
  } catch {
7286
7598
  }
7287
7599
  if (ourDir && realStalePath === ourDir) return;
7288
- const pkgJsonPath = join7(stalePath, "package.json");
7600
+ const pkgJsonPath = join8(stalePath, "package.json");
7289
7601
  if (!existsSync4(pkgJsonPath)) return;
7290
7602
  let pkg = {};
7291
7603
  try {
@@ -7297,7 +7609,7 @@ function cleanupOne(cacheRoot, ourDir) {
7297
7609
  if (!pkg.description?.includes(SUSPECT_DESCRIPTION_TOKEN)) return;
7298
7610
  log.info("cleanup-stale: removing unscoped install", { stalePath });
7299
7611
  try {
7300
- rmSync2(stalePath, { recursive: true, force: true });
7612
+ rmSync3(stalePath, { recursive: true, force: true });
7301
7613
  } catch (err) {
7302
7614
  log.warn("cleanup-stale: rmSync failed", {
7303
7615
  stalePath,
@@ -7305,13 +7617,13 @@ function cleanupOne(cacheRoot, ourDir) {
7305
7617
  });
7306
7618
  return;
7307
7619
  }
7308
- const cachePkgJson = join7(cacheRoot, "package.json");
7620
+ const cachePkgJson = join8(cacheRoot, "package.json");
7309
7621
  if (!existsSync4(cachePkgJson)) return;
7310
7622
  try {
7311
7623
  const cfg = JSON.parse(readFileSync4(cachePkgJson, "utf8"));
7312
7624
  if (cfg?.dependencies?.[STALE_PACKAGE_NAME]) {
7313
7625
  delete cfg.dependencies[STALE_PACKAGE_NAME];
7314
- writeFileSync4(cachePkgJson, JSON.stringify(cfg, null, 2) + "\n");
7626
+ writeFileSync5(cachePkgJson, JSON.stringify(cfg, null, 2) + "\n");
7315
7627
  log.info("cleanup-stale: pruned dep from cache package.json");
7316
7628
  }
7317
7629
  } catch (err) {
@@ -7323,16 +7635,16 @@ function cleanupOne(cacheRoot, ourDir) {
7323
7635
 
7324
7636
  // src/startup-diagnostics.ts
7325
7637
  import { execFile as execFile2 } from "child_process";
7326
- import * as fs5 from "fs";
7327
- import * as path7 from "path";
7638
+ import * as fs6 from "fs";
7639
+ import * as path8 from "path";
7328
7640
  import { promisify as promisify2 } from "util";
7329
7641
  import { fileURLToPath as fileURLToPath2 } from "url";
7330
7642
  var cachedPluginVersion;
7331
7643
  function pluginVersion() {
7332
7644
  if (cachedPluginVersion) return cachedPluginVersion;
7333
7645
  try {
7334
- const here = path7.dirname(fileURLToPath2(import.meta.url));
7335
- 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");
7336
7648
  const version = JSON.parse(raw).version;
7337
7649
  cachedPluginVersion = typeof version === "string" ? version : "unknown";
7338
7650
  } catch {
@@ -7356,7 +7668,7 @@ var opencodeVersionProbe;
7356
7668
  function detectOpencodeVersion(execPath = process.execPath) {
7357
7669
  if (opencodeVersionProbe) return opencodeVersionProbe;
7358
7670
  opencodeVersionProbe = (async () => {
7359
- if (!path7.basename(execPath).toLowerCase().includes("opencode")) {
7671
+ if (!path8.basename(execPath).toLowerCase().includes("opencode")) {
7360
7672
  log.debug("skipping opencode version probe: execPath is not opencode", { execPath });
7361
7673
  return void 0;
7362
7674
  }
@@ -7526,6 +7838,8 @@ function createClaudeCode(settings = {}) {
7526
7838
  autoContinueIncompleteTurns: settings.autoContinueIncompleteTurns ?? "smart",
7527
7839
  compactionModel: settings.compactionModel,
7528
7840
  ignoreAnthropicApiKey: settings.ignoreAnthropicApiKey,
7841
+ idleProcessTimeoutMs: settings.idleProcessTimeoutMs,
7842
+ bridgeOpencodeSkills: settings.bridgeOpencodeSkills === true,
7529
7843
  interactive: settings.interactive,
7530
7844
  interactiveBypass: settings.interactiveBypass,
7531
7845
  interactiveAllowTools: settings.interactiveAllowTools,