@khalilgharbaoui/opencode-claude-code-plugin 0.2.6 → 0.3.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -311,7 +311,7 @@ Set `permissionMode: "plan"` to forward `--permission-mode plan` to Claude. The
311
311
 
312
312
  - **Empty text blocks are dropped.** Claude sometimes opens a `content_block_start` for text but never sends a delta. The plugin no longer emits the empty block (which was triggering Anthropic 400s like `cache_control cannot be set for empty text blocks`).
313
313
  - **`AskUserQuestion`** from the CLI is converted into plain text content rather than forwarded as a tool call.
314
- - **Result fallback timer.** If the CLI finishes a text block but never sends a `result` message, the stream closes gracefully after 5 seconds rather than hanging.
314
+ - **Wire-inactivity watchdog.** Once the CLI has produced any content, the stream closes gracefully if stdout goes silent for 60 seconds without a `result` message arriving. Resets on every line received, so long mid-turn pauses (Sonnet between text-end and the next tool_use, for example) are tolerated. On a user-initiated abort, the watchdog shortens to 5 seconds.
315
315
  - **Per-iteration usage.** When the CLI internally retries with tools, the plugin only counts the last iteration's usage so opencode's context accounting stays accurate.
316
316
  - **Lazy `cwd`.** The working directory is re-resolved at every request, so opencode's project-aware behavior works without restarting the plugin.
317
317
  - **Variants survive merge.** opencode recalculates variant lists after the plugin loads; the plugin re-injects defaults into runtime config so your variants don't disappear.
package/dist/index.d.ts CHANGED
@@ -201,6 +201,7 @@ interface ClaudeStreamMessage {
201
201
  type: string;
202
202
  subtype?: string;
203
203
  request_id?: string;
204
+ event?: ClaudeStreamMessage;
204
205
  request?: {
205
206
  subtype?: string;
206
207
  tool_name?: string;
package/dist/index.js CHANGED
@@ -19,7 +19,7 @@ var log = {
19
19
  console.error(fmt("NOTICE", msg, data));
20
20
  },
21
21
  warn(msg, data) {
22
- if (DEBUG) console.error(fmt("WARN", msg, data));
22
+ console.error(fmt("WARN", msg, data));
23
23
  },
24
24
  error(msg, data) {
25
25
  console.error(fmt("ERROR", msg, data));
@@ -145,7 +145,7 @@ function mapTool(name, input, opts) {
145
145
  const toolName = parts.slice(1).join("_");
146
146
  const openCodeName = `${serverName}_${toolName}`;
147
147
  log.debug("mapping MCP tool", { original: name, mapped: openCodeName });
148
- return { name: openCodeName, input, executed: false };
148
+ return { name: openCodeName, input, executed: true };
149
149
  }
150
150
  }
151
151
  if (OPENCODE_HANDLED_TOOLS.has(name)) {
@@ -381,22 +381,49 @@ Now continuing with the current message:
381
381
  }
382
382
 
383
383
  // src/mcp-bridge.ts
384
+ import * as fs2 from "fs";
385
+ import * as path2 from "path";
386
+ import * as os2 from "os";
387
+ import * as crypto from "crypto";
388
+
389
+ // src/tmp.ts
384
390
  import * as fs from "fs";
385
- import * as path from "path";
386
391
  import * as os from "os";
387
- import * as crypto from "crypto";
392
+ import * as path from "path";
393
+ var PLUGIN_TMP_DIR = path.join(
394
+ os.tmpdir(),
395
+ `opencode-claude-code-${process.pid}`
396
+ );
397
+ var registered = false;
398
+ function pluginTmpDir() {
399
+ if (!fs.existsSync(PLUGIN_TMP_DIR)) {
400
+ fs.mkdirSync(PLUGIN_TMP_DIR, { recursive: true });
401
+ }
402
+ if (!registered) {
403
+ registered = true;
404
+ process.on("exit", () => {
405
+ try {
406
+ fs.rmSync(PLUGIN_TMP_DIR, { recursive: true, force: true });
407
+ } catch {
408
+ }
409
+ });
410
+ }
411
+ return PLUGIN_TMP_DIR;
412
+ }
413
+
414
+ // src/mcp-bridge.ts
388
415
  var FILE_NAMES = ["opencode.jsonc", "opencode.json", "config.json"];
389
416
  var PROJECT_FILE_NAMES = ["opencode.json", "opencode.jsonc"];
390
417
  function fileExists(p) {
391
418
  try {
392
- return fs.statSync(p).isFile();
419
+ return fs2.statSync(p).isFile();
393
420
  } catch {
394
421
  return false;
395
422
  }
396
423
  }
397
424
  function dirExists(p) {
398
425
  try {
399
- return fs.statSync(p).isDirectory();
426
+ return fs2.statSync(p).isDirectory();
400
427
  } catch {
401
428
  return false;
402
429
  }
@@ -442,7 +469,7 @@ function stripJsonComments(text) {
442
469
  }
443
470
  function readAndParse(file) {
444
471
  try {
445
- const raw = fs.readFileSync(file, "utf8");
472
+ const raw = fs2.readFileSync(file, "utf8");
446
473
  return JSON.parse(stripJsonComments(raw));
447
474
  } catch (e) {
448
475
  log.warn("failed to parse opencode config", {
@@ -470,14 +497,14 @@ function deepMerge(target, source) {
470
497
  }
471
498
  function walkUp(opts) {
472
499
  const out = [];
473
- let current = path.resolve(opts.start);
500
+ let current = path2.resolve(opts.start);
474
501
  while (true) {
475
502
  for (const target of opts.targets) {
476
- const candidate = path.join(current, target);
503
+ const candidate = path2.join(current, target);
477
504
  if (opts.predicate(candidate)) out.push(candidate);
478
505
  }
479
- if (opts.stop && current === path.resolve(opts.stop)) break;
480
- const parent = path.dirname(current);
506
+ if (opts.stop && current === path2.resolve(opts.stop)) break;
507
+ const parent = path2.dirname(current);
481
508
  if (parent === current) break;
482
509
  current = parent;
483
510
  }
@@ -485,28 +512,28 @@ function walkUp(opts) {
485
512
  }
486
513
  function detectWorktree(cwd) {
487
514
  const override = process.env.OPENCODE_WORKTREE;
488
- if (override) return path.resolve(override);
489
- let current = path.resolve(cwd);
515
+ if (override) return path2.resolve(override);
516
+ let current = path2.resolve(cwd);
490
517
  while (true) {
491
- const gitPath = path.join(current, ".git");
518
+ const gitPath = path2.join(current, ".git");
492
519
  try {
493
- if (fs.existsSync(gitPath)) return current;
520
+ if (fs2.existsSync(gitPath)) return current;
494
521
  } catch {
495
522
  }
496
- const parent = path.dirname(current);
523
+ const parent = path2.dirname(current);
497
524
  if (parent === current) return void 0;
498
525
  current = parent;
499
526
  }
500
527
  }
501
528
  function globalConfigDir() {
502
- const xdg = process.env.XDG_CONFIG_HOME ?? path.join(os.homedir(), ".config");
503
- return path.join(xdg, "opencode");
529
+ const xdg = process.env.XDG_CONFIG_HOME ?? path2.join(os2.homedir(), ".config");
530
+ return path2.join(xdg, "opencode");
504
531
  }
505
532
  function loadGlobalConfig() {
506
533
  const dir = globalConfigDir();
507
534
  let merged = {};
508
535
  for (const name of FILE_NAMES.slice().reverse()) {
509
- const file = path.join(dir, name);
536
+ const file = path2.join(dir, name);
510
537
  if (!fileExists(file)) continue;
511
538
  const parsed = readAndParse(file);
512
539
  if (parsed) merged = deepMerge(merged, parsed);
@@ -516,7 +543,7 @@ function loadGlobalConfig() {
516
543
  function loadProjectFilesInDir(dir) {
517
544
  let merged = {};
518
545
  for (const name of PROJECT_FILE_NAMES) {
519
- const file = path.join(dir, name);
546
+ const file = path2.join(dir, name);
520
547
  if (!fileExists(file)) continue;
521
548
  const parsed = readAndParse(file);
522
549
  if (parsed) merged = deepMerge(merged, parsed);
@@ -527,7 +554,7 @@ function dotOpencodeDirs(cwd, worktree) {
527
554
  const dirs = [];
528
555
  const seen = /* @__PURE__ */ new Set();
529
556
  const push = (p) => {
530
- const abs = path.resolve(p);
557
+ const abs = path2.resolve(p);
531
558
  if (!seen.has(abs) && dirExists(abs)) {
532
559
  seen.add(abs);
533
560
  dirs.push(abs);
@@ -541,9 +568,9 @@ function dotOpencodeDirs(cwd, worktree) {
541
568
  })) {
542
569
  push(dir);
543
570
  }
544
- const home = os.homedir();
571
+ const home = os2.homedir();
545
572
  if (home) {
546
- const homeDot = path.join(home, ".opencode");
573
+ const homeDot = path2.join(home, ".opencode");
547
574
  if (dirExists(homeDot)) push(homeDot);
548
575
  }
549
576
  const envDir = process.env.OPENCODE_CONFIG_DIR;
@@ -628,7 +655,7 @@ function bridgeOpencodeMcp(cwd, runtimeStatus) {
628
655
  const projectDirs = [];
629
656
  const seenProjectDirs = /* @__PURE__ */ new Set();
630
657
  for (const f of projectFiles) {
631
- const d = path.dirname(f);
658
+ const d = path2.dirname(f);
632
659
  if (!seenProjectDirs.has(d)) {
633
660
  seenProjectDirs.add(d);
634
661
  projectDirs.push(d);
@@ -658,13 +685,13 @@ function bridgeOpencodeMcp(cwd, runtimeStatus) {
658
685
  if (Object.keys(servers).length === 0) return null;
659
686
  const body = JSON.stringify({ mcpServers: servers }, null, 2);
660
687
  const hash = crypto.createHash("sha256").update(body).digest("hex").slice(0, 12);
661
- const outPath = path.join(
662
- os.tmpdir(),
663
- `opencode-claude-code-mcp-${hash}.json`
688
+ const outPath = path2.join(
689
+ pluginTmpDir(),
690
+ `mcp-${hash}.json`
664
691
  );
665
692
  try {
666
693
  if (!fileExists(outPath)) {
667
- fs.writeFileSync(outPath, body, { encoding: "utf8", mode: 384 });
694
+ fs2.writeFileSync(outPath, body, { encoding: "utf8", mode: 384 });
668
695
  }
669
696
  } catch (e) {
670
697
  log.warn("failed to write bridged MCP config", {
@@ -825,10 +852,12 @@ function buildCliArgs(opts) {
825
852
  appendSystemPromptFile
826
853
  } = opts;
827
854
  const args = [
855
+ "--print",
828
856
  "--output-format",
829
857
  "stream-json",
830
858
  "--input-format",
831
859
  "stream-json",
860
+ "--include-partial-messages",
832
861
  "--verbose"
833
862
  ];
834
863
  if (model) {
@@ -870,14 +899,14 @@ function sessionKey(cwd, modelId) {
870
899
 
871
900
  // src/proxy-mcp.ts
872
901
  import { createServer } from "http";
873
- import * as fs2 from "fs";
874
- import * as path2 from "path";
875
- import * as os2 from "os";
902
+ import * as fs3 from "fs";
903
+ import * as path3 from "path";
876
904
  import * as crypto2 from "crypto";
877
905
  import { EventEmitter as EventEmitter2 } from "events";
878
906
  var PROTOCOL_VERSION = "2024-11-05";
879
907
  var SERVER_NAME = "opencode_proxy";
880
908
  var PROXY_TOOL_PREFIX = `mcp__${SERVER_NAME}__`;
909
+ var PROXY_CALL_TIMEOUT_MS = 10 * 60 * 1e3;
881
910
  var DEFAULT_PROXY_TOOLS = [
882
911
  {
883
912
  name: "bash",
@@ -1048,6 +1077,7 @@ async function createProxyMcpServer(tools = DEFAULT_PROXY_TOOLS) {
1048
1077
  toolName,
1049
1078
  hasInput: input != null
1050
1079
  });
1080
+ let timer = null;
1051
1081
  const result = await new Promise(
1052
1082
  (resolve3, reject) => {
1053
1083
  const entry = {
@@ -1058,9 +1088,24 @@ async function createProxyMcpServer(tools = DEFAULT_PROXY_TOOLS) {
1058
1088
  reject
1059
1089
  };
1060
1090
  pending.set(callId, entry);
1091
+ timer = setTimeout(() => {
1092
+ if (!pending.has(callId)) return;
1093
+ pending.delete(callId);
1094
+ log.warn("proxy-mcp tool call timed out", {
1095
+ callId,
1096
+ toolName,
1097
+ timeoutMs: PROXY_CALL_TIMEOUT_MS
1098
+ });
1099
+ reject(
1100
+ new Error(
1101
+ `Proxy tool '${toolName}' timed out after ${PROXY_CALL_TIMEOUT_MS}ms waiting for opencode to resolve the call`
1102
+ )
1103
+ );
1104
+ }, PROXY_CALL_TIMEOUT_MS);
1061
1105
  calls.emit("call", entry);
1062
1106
  }
1063
1107
  ).finally(() => {
1108
+ if (timer) clearTimeout(timer);
1064
1109
  pending.delete(callId);
1065
1110
  });
1066
1111
  if (result.kind === "error") {
@@ -1149,11 +1194,11 @@ async function createProxyMcpServer(tools = DEFAULT_PROXY_TOOLS) {
1149
1194
  2
1150
1195
  );
1151
1196
  const hash = crypto2.createHash("sha256").update(body).digest("hex").slice(0, 12);
1152
- const outPath = path2.join(
1153
- os2.tmpdir(),
1154
- `opencode-claude-code-proxy-${hash}.json`
1197
+ const outPath = path3.join(
1198
+ pluginTmpDir(),
1199
+ `proxy-${hash}.json`
1155
1200
  );
1156
- fs2.writeFileSync(outPath, body, { encoding: "utf8", mode: 384 });
1201
+ fs3.writeFileSync(outPath, body, { encoding: "utf8", mode: 384 });
1157
1202
  configFilePath = outPath;
1158
1203
  return outPath;
1159
1204
  },
@@ -1165,6 +1210,13 @@ async function createProxyMcpServer(tools = DEFAULT_PROXY_TOOLS) {
1165
1210
  await new Promise((resolve3) => {
1166
1211
  server2.close(() => resolve3());
1167
1212
  });
1213
+ if (configFilePath) {
1214
+ try {
1215
+ fs3.unlinkSync(configFilePath);
1216
+ } catch {
1217
+ }
1218
+ configFilePath = null;
1219
+ }
1168
1220
  }
1169
1221
  };
1170
1222
  return api;
@@ -1264,12 +1316,32 @@ function resolvePendingProxyCall(sessionKey2, result) {
1264
1316
  // src/claude-code-language-model.ts
1265
1317
  import { readFileSync as readFileSync2, writeFileSync as writeFileSync3 } from "fs";
1266
1318
  import { unlink as unlink2 } from "fs/promises";
1267
- import { homedir as homedir2, tmpdir as tmpdir3 } from "os";
1319
+ import { homedir as homedir2, tmpdir as tmpdir2 } from "os";
1268
1320
  import { randomUUID as randomUUID2 } from "crypto";
1269
- import { dirname as dirname2, join as join3 } from "path";
1270
- function readPromptFileIfPresent(path4) {
1321
+ import { dirname as dirname2, join as join4 } from "path";
1322
+ function hasNewUserContent(prompt) {
1323
+ for (let i = prompt.length - 1; i >= 0; i--) {
1324
+ const msg = prompt[i];
1325
+ if (msg.role === "assistant") return false;
1326
+ if (msg.role !== "user") continue;
1327
+ const content = msg.content;
1328
+ if (typeof content === "string") {
1329
+ if (content.trim()) return true;
1330
+ continue;
1331
+ }
1332
+ if (Array.isArray(content)) {
1333
+ for (const part of content) {
1334
+ if (part.type === "text" && part.text && part.text.trim()) return true;
1335
+ if (part.type === "tool-result") return true;
1336
+ if (part.type === "image" || part.type === "file") return true;
1337
+ }
1338
+ }
1339
+ }
1340
+ return false;
1341
+ }
1342
+ function readPromptFileIfPresent(path5) {
1271
1343
  try {
1272
- const content = readFileSync2(path4, "utf8").trim();
1344
+ const content = readFileSync2(path5, "utf8").trim();
1273
1345
  return content || void 0;
1274
1346
  } catch {
1275
1347
  return void 0;
@@ -1278,7 +1350,7 @@ function readPromptFileIfPresent(path4) {
1278
1350
  function nearestWorkspaceAgentsPrompt(cwd) {
1279
1351
  let dir = cwd;
1280
1352
  while (true) {
1281
- const content = readPromptFileIfPresent(join3(dir, "AGENTS.md"));
1353
+ const content = readPromptFileIfPresent(join4(dir, "AGENTS.md"));
1282
1354
  if (content) return content;
1283
1355
  const parent = dirname2(dir);
1284
1356
  if (parent === dir) return void 0;
@@ -1287,17 +1359,17 @@ function nearestWorkspaceAgentsPrompt(cwd) {
1287
1359
  }
1288
1360
  function buildAppendedSystemPrompt(cwd) {
1289
1361
  const parts = [];
1290
- const configRoot = process.env.XDG_CONFIG_HOME ?? join3(homedir2(), ".config");
1291
- const globalAgents = readPromptFileIfPresent(join3(configRoot, "opencode", "AGENTS.md"));
1362
+ const configRoot = process.env.XDG_CONFIG_HOME ?? join4(homedir2(), ".config");
1363
+ const globalAgents = readPromptFileIfPresent(join4(configRoot, "opencode", "AGENTS.md"));
1292
1364
  const workspaceAgents = nearestWorkspaceAgentsPrompt(cwd);
1293
1365
  if (globalAgents) parts.push(globalAgents);
1294
1366
  if (workspaceAgents && workspaceAgents !== globalAgents) parts.push(workspaceAgents);
1295
1367
  const content = parts.join("\n\n");
1296
1368
  if (!content) return void 0;
1297
- const path4 = join3(tmpdir3(), `opencode-cc-sys-${randomUUID2()}.md`);
1369
+ const path5 = join4(tmpdir2(), `opencode-cc-sys-${randomUUID2()}.md`);
1298
1370
  try {
1299
- writeFileSync3(path4, content, "utf8");
1300
- return path4;
1371
+ writeFileSync3(path5, content, "utf8");
1372
+ return path5;
1301
1373
  } catch (err) {
1302
1374
  log.warn("failed to write system prompt file", { error: String(err) });
1303
1375
  return void 0;
@@ -1692,6 +1764,24 @@ var ClaudeCodeLanguageModel = class {
1692
1764
  warnings
1693
1765
  };
1694
1766
  }
1767
+ if (!hasNewUserContent(options.prompt)) {
1768
+ log.info("doGenerate short-circuit: no new user content");
1769
+ return {
1770
+ content: [],
1771
+ finishReason: this.toFinishReason("stop"),
1772
+ usage: this.toUsage({ input_tokens: 0, output_tokens: 0 }),
1773
+ request: { body: { text: "" } },
1774
+ response: {
1775
+ id: generateId(),
1776
+ timestamp: /* @__PURE__ */ new Date(),
1777
+ modelId: this.modelId
1778
+ },
1779
+ providerMetadata: {
1780
+ "claude-code": { synthetic: true, path: "no-new-user-content" }
1781
+ },
1782
+ warnings
1783
+ };
1784
+ }
1695
1785
  const hasPriorConversation = options.prompt.filter((m) => m.role === "user" || m.role === "assistant").length > 1;
1696
1786
  if (!hasPriorConversation) {
1697
1787
  deleteClaudeSessionId(sk);
@@ -1743,11 +1833,16 @@ var ClaudeCodeLanguageModel = class {
1743
1833
  let thinkingText = "";
1744
1834
  let resultMeta = {};
1745
1835
  const toolCalls = [];
1836
+ let gotPartialEvents = false;
1746
1837
  const result = await new Promise((resolve3, reject) => {
1747
1838
  rl.on("line", (line) => {
1748
1839
  if (!line.trim()) return;
1749
1840
  try {
1750
- const msg = JSON.parse(line);
1841
+ const outer = JSON.parse(line);
1842
+ const msg = outer.type === "stream_event" && outer.event ? { ...outer.event, session_id: outer.session_id } : outer;
1843
+ if (outer.type === "stream_event") {
1844
+ gotPartialEvents = true;
1845
+ }
1751
1846
  if (this.handleControlRequest(msg, proc)) {
1752
1847
  return;
1753
1848
  }
@@ -1756,7 +1851,7 @@ var ClaudeCodeLanguageModel = class {
1756
1851
  setClaudeSessionId(sk, msg.session_id);
1757
1852
  }
1758
1853
  }
1759
- if (msg.type === "assistant" && msg.message?.content) {
1854
+ if (msg.type === "assistant" && msg.message?.content && !gotPartialEvents) {
1760
1855
  for (const block of msg.message.content) {
1761
1856
  if (block.type === "text" && block.text) {
1762
1857
  responseText += block.text;
@@ -1975,6 +2070,24 @@ ${plan}
1975
2070
  request: { body: { text: "" } }
1976
2071
  };
1977
2072
  }
2073
+ if (!hasNewUserContent(options.prompt)) {
2074
+ log.info("doStream short-circuit: no new user content");
2075
+ const stream2 = new ReadableStream({
2076
+ start(controller) {
2077
+ controller.enqueue({ type: "stream-start", warnings });
2078
+ controller.enqueue({
2079
+ type: "finish",
2080
+ finishReason: toFinishReason("stop"),
2081
+ usage: toUsage({ input_tokens: 0, output_tokens: 0 }),
2082
+ providerMetadata: {
2083
+ "claude-code": { synthetic: true, path: "no-new-user-content" }
2084
+ }
2085
+ });
2086
+ controller.close();
2087
+ }
2088
+ });
2089
+ return { stream: stream2, request: { body: { text: "" } } };
2090
+ }
1978
2091
  const hasPriorConversation = options.prompt.filter((m) => m.role === "user" || m.role === "assistant").length > 1;
1979
2092
  if (!hasPriorConversation) {
1980
2093
  deleteClaudeSessionId(sk);
@@ -2141,12 +2254,17 @@ ${plan}
2141
2254
  } catch {
2142
2255
  }
2143
2256
  };
2257
+ let gotPartialEvents = false;
2144
2258
  const lineHandler = (line) => {
2145
2259
  if (!line.trim()) return;
2146
2260
  if (controllerClosed) return;
2147
2261
  startResultFallback();
2148
2262
  try {
2149
- const msg = JSON.parse(line);
2263
+ const outer = JSON.parse(line);
2264
+ const msg = outer.type === "stream_event" && outer.event ? { ...outer.event, session_id: outer.session_id } : outer;
2265
+ if (outer.type === "stream_event") {
2266
+ gotPartialEvents = true;
2267
+ }
2150
2268
  if (handleControlRequest(msg, proc)) {
2151
2269
  return;
2152
2270
  }
@@ -2338,7 +2456,7 @@ ${plan}
2338
2456
  }
2339
2457
  }
2340
2458
  }
2341
- if (msg.type === "assistant" && msg.message?.content) {
2459
+ if (msg.type === "assistant" && msg.message?.content && !gotPartialEvents) {
2342
2460
  const hasText = msg.message.content.some(
2343
2461
  (b) => b.type === "text" && b.text
2344
2462
  );
@@ -2822,7 +2940,7 @@ var defaultModels = {
2822
2940
 
2823
2941
  // src/accounts.ts
2824
2942
  import { chmod, lstat, mkdir, readlink, symlink, writeFile } from "fs/promises";
2825
- import path3 from "path";
2943
+ import path4 from "path";
2826
2944
  var BASE_PROVIDER_ID = "claude-code";
2827
2945
  var DEFAULT_ACCOUNT = "default";
2828
2946
  var SHARED_CAPABILITY_ITEMS = [
@@ -2860,7 +2978,7 @@ function expandHome(value) {
2860
2978
  const home = process.env.HOME ?? process.env.USERPROFILE;
2861
2979
  if (value === "~") return home ?? value;
2862
2980
  if (value.startsWith("~/") || value.startsWith("~\\")) {
2863
- return home ? path3.join(home, value.slice(2)) : value;
2981
+ return home ? path4.join(home, value.slice(2)) : value;
2864
2982
  }
2865
2983
  return value;
2866
2984
  }
@@ -2892,8 +3010,8 @@ async function ensureSharedCapabilities(targetRoot) {
2892
3010
  }
2893
3011
  }
2894
3012
  async function ensureSharedCapabilityItem(sourceRoot, targetRoot, item) {
2895
- const source = path3.join(sourceRoot, item);
2896
- const target = path3.join(targetRoot, item);
3013
+ const source = path4.join(sourceRoot, item);
3014
+ const target = path4.join(targetRoot, item);
2897
3015
  let sourceStat;
2898
3016
  try {
2899
3017
  sourceStat = await lstat(source);
@@ -2904,8 +3022,8 @@ async function ensureSharedCapabilityItem(sourceRoot, targetRoot, item) {
2904
3022
  const targetStat = await lstat(target);
2905
3023
  if (targetStat.isSymbolicLink()) {
2906
3024
  const current = await readlink(target);
2907
- const resolvedCurrent = path3.resolve(path3.dirname(target), current);
2908
- const resolvedSource = path3.resolve(source);
3025
+ const resolvedCurrent = path4.resolve(path4.dirname(target), current);
3026
+ const resolvedSource = path4.resolve(source);
2909
3027
  if (resolvedCurrent === resolvedSource) return;
2910
3028
  }
2911
3029
  log.warn("shared Claude capability already exists; leaving untouched", {
@@ -2920,11 +3038,11 @@ async function ensureSharedCapabilityItem(sourceRoot, targetRoot, item) {
2920
3038
  await symlink(source, target, type);
2921
3039
  }
2922
3040
  async function writeAccountWrapper(account, baseCliPath, configDir) {
2923
- const cacheRoot = path3.join(
3041
+ const cacheRoot = path4.join(
2924
3042
  process.env.XDG_CACHE_HOME ?? expandHome("~/.cache"),
2925
3043
  "opencode-claude-code-plugin"
2926
3044
  );
2927
- const wrapperPath = path3.join(cacheRoot, `claude-${account}`);
3045
+ const wrapperPath = path4.join(cacheRoot, `claude-${account}`);
2928
3046
  const suffix = `@${account}`;
2929
3047
  await mkdir(cacheRoot, { recursive: true });
2930
3048
  const script = `#!/usr/bin/env bash
@@ -2964,14 +3082,14 @@ function titleizeAccount(account) {
2964
3082
 
2965
3083
  // src/cleanup-stale.ts
2966
3084
  import {
2967
- existsSync as existsSync2,
3085
+ existsSync as existsSync3,
2968
3086
  readFileSync as readFileSync3,
2969
3087
  realpathSync,
2970
- rmSync,
3088
+ rmSync as rmSync2,
2971
3089
  writeFileSync as writeFileSync4
2972
3090
  } from "fs";
2973
3091
  import { homedir as homedir3 } from "os";
2974
- import { join as join4, resolve as resolve2 } from "path";
3092
+ import { join as join5, resolve as resolve2 } from "path";
2975
3093
  import { fileURLToPath } from "url";
2976
3094
  var STALE_PACKAGE_NAME = "opencode-claude-code-plugin";
2977
3095
  var SUSPECT_DESCRIPTION_TOKEN = "Claude Code";
@@ -2979,18 +3097,18 @@ var alreadyRan = false;
2979
3097
  function candidateCacheRoots() {
2980
3098
  const xdg = process.env.XDG_CACHE_HOME;
2981
3099
  return [
2982
- xdg ? join4(xdg, "opencode") : null,
2983
- join4(homedir3(), ".cache", "opencode"),
2984
- join4(homedir3(), "Library", "Caches", "opencode")
3100
+ xdg ? join5(xdg, "opencode") : null,
3101
+ join5(homedir3(), ".cache", "opencode"),
3102
+ join5(homedir3(), "Library", "Caches", "opencode")
2985
3103
  ].filter((p) => Boolean(p));
2986
3104
  }
2987
3105
  function userOpencodeJsonPath() {
2988
- const xdgConfig = process.env.XDG_CONFIG_HOME ?? join4(homedir3(), ".config");
2989
- return join4(xdgConfig, "opencode", "opencode.json");
3106
+ const xdgConfig = process.env.XDG_CONFIG_HOME ?? join5(homedir3(), ".config");
3107
+ return join5(xdgConfig, "opencode", "opencode.json");
2990
3108
  }
2991
3109
  function userIntendsToUseUnscoped() {
2992
3110
  const cfg = userOpencodeJsonPath();
2993
- if (!existsSync2(cfg)) return false;
3111
+ if (!existsSync3(cfg)) return false;
2994
3112
  try {
2995
3113
  const json = JSON.parse(readFileSync3(cfg, "utf8"));
2996
3114
  const plugins = json.plugin;
@@ -3028,17 +3146,17 @@ function cleanupStaleUnscopedInstall() {
3028
3146
  }
3029
3147
  }
3030
3148
  function cleanupOne(cacheRoot, ourDir) {
3031
- if (!existsSync2(cacheRoot)) return;
3032
- const stalePath = join4(cacheRoot, "node_modules", STALE_PACKAGE_NAME);
3033
- if (!existsSync2(stalePath)) return;
3149
+ if (!existsSync3(cacheRoot)) return;
3150
+ const stalePath = join5(cacheRoot, "node_modules", STALE_PACKAGE_NAME);
3151
+ if (!existsSync3(stalePath)) return;
3034
3152
  let realStalePath = stalePath;
3035
3153
  try {
3036
3154
  realStalePath = realpathSync(stalePath);
3037
3155
  } catch {
3038
3156
  }
3039
3157
  if (ourDir && realStalePath === ourDir) return;
3040
- const pkgJsonPath = join4(stalePath, "package.json");
3041
- if (!existsSync2(pkgJsonPath)) return;
3158
+ const pkgJsonPath = join5(stalePath, "package.json");
3159
+ if (!existsSync3(pkgJsonPath)) return;
3042
3160
  let pkg = {};
3043
3161
  try {
3044
3162
  pkg = JSON.parse(readFileSync3(pkgJsonPath, "utf8"));
@@ -3049,7 +3167,7 @@ function cleanupOne(cacheRoot, ourDir) {
3049
3167
  if (!pkg.description?.includes(SUSPECT_DESCRIPTION_TOKEN)) return;
3050
3168
  log.info("cleanup-stale: removing unscoped install", { stalePath });
3051
3169
  try {
3052
- rmSync(stalePath, { recursive: true, force: true });
3170
+ rmSync2(stalePath, { recursive: true, force: true });
3053
3171
  } catch (err) {
3054
3172
  log.warn("cleanup-stale: rmSync failed", {
3055
3173
  stalePath,
@@ -3057,8 +3175,8 @@ function cleanupOne(cacheRoot, ourDir) {
3057
3175
  });
3058
3176
  return;
3059
3177
  }
3060
- const cachePkgJson = join4(cacheRoot, "package.json");
3061
- if (!existsSync2(cachePkgJson)) return;
3178
+ const cachePkgJson = join5(cacheRoot, "package.json");
3179
+ if (!existsSync3(cachePkgJson)) return;
3062
3180
  try {
3063
3181
  const cfg = JSON.parse(readFileSync3(cachePkgJson, "utf8"));
3064
3182
  if (cfg?.dependencies?.[STALE_PACKAGE_NAME]) {
@@ -3275,12 +3393,12 @@ var server = async (input) => {
3275
3393
  config.provider ??= {};
3276
3394
  const expanded = await expandAccountProviders(config);
3277
3395
  if (expanded) {
3278
- const registered = Object.entries(config.provider).filter(([id]) => id === PROVIDER_ID2 || id.startsWith(`${PROVIDER_ID2}-`)).map(([id, p]) => ({
3396
+ const registered2 = Object.entries(config.provider).filter(([id]) => id === PROVIDER_ID2 || id.startsWith(`${PROVIDER_ID2}-`)).map(([id, p]) => ({
3279
3397
  id,
3280
3398
  name: p?.name ?? id,
3281
3399
  cwd: p?.options?.cwd
3282
3400
  }));
3283
- log.notice("registered claude-code providers", { providers: registered });
3401
+ log.notice("registered claude-code providers", { providers: registered2 });
3284
3402
  return;
3285
3403
  }
3286
3404
  const existing = config.provider[PROVIDER_ID2];