@khalilgharbaoui/opencode-claude-code-plugin 0.1.6 → 0.2.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/dist/index.js CHANGED
@@ -102,7 +102,7 @@ var CLAUDE_INTERNAL_TOOLS = /* @__PURE__ */ new Set([
102
102
  "Agent",
103
103
  "AskFollowupQuestion"
104
104
  ]);
105
- function mapTool(name, input) {
105
+ function mapTool(name, input, opts) {
106
106
  if (CLAUDE_INTERNAL_TOOLS.has(name)) {
107
107
  log.debug("skipping Claude CLI internal tool", { name });
108
108
  return { name, input, executed: true, skip: true };
@@ -115,8 +115,13 @@ function mapTool(name, input) {
115
115
  }
116
116
  if (name === "WebSearch" || name === "web_search") {
117
117
  const mappedInput = input?.query ? { query: input.query } : input;
118
- log.debug("mapping WebSearch", { originalInput: input, mappedInput });
119
- return { name: "websearch_web_search_exa", input: mappedInput, executed: false };
118
+ const route = opts?.webSearch;
119
+ if (route && route !== "claude" && route !== "disabled") {
120
+ log.debug("routing WebSearch to opencode tool", { target: route, mappedInput });
121
+ return { name: route, input: mappedInput, executed: false };
122
+ }
123
+ log.debug("WebSearch executed by Claude CLI", { mappedInput });
124
+ return { name: "WebSearch", input: mappedInput, executed: true };
120
125
  }
121
126
  if (name === "TaskOutput") {
122
127
  if (!input) return { name: "bash", executed: false };
@@ -377,7 +382,8 @@ import * as fs from "fs";
377
382
  import * as path from "path";
378
383
  import * as os from "os";
379
384
  import * as crypto from "crypto";
380
- var CONFIG_NAMES = ["opencode.jsonc", "opencode.json", "config.json"];
385
+ var FILE_NAMES = ["opencode.jsonc", "opencode.json", "config.json"];
386
+ var PROJECT_FILE_NAMES = ["opencode.json", "opencode.jsonc"];
381
387
  function fileExists(p) {
382
388
  try {
383
389
  return fs.statSync(p).isFile();
@@ -385,35 +391,12 @@ function fileExists(p) {
385
391
  return false;
386
392
  }
387
393
  }
388
- function findConfigInDir(dir) {
389
- for (const name of CONFIG_NAMES) {
390
- const p = path.join(dir, name);
391
- if (fileExists(p)) return p;
394
+ function dirExists(p) {
395
+ try {
396
+ return fs.statSync(p).isDirectory();
397
+ } catch {
398
+ return false;
392
399
  }
393
- return null;
394
- }
395
- function walkUpForConfig(startDir) {
396
- const closestFirst = [];
397
- let dir = path.resolve(startDir);
398
- while (true) {
399
- const hit = findConfigInDir(dir);
400
- if (hit) closestFirst.push(hit);
401
- const dotdir = path.join(dir, ".opencode");
402
- const dothit = findConfigInDir(dotdir);
403
- if (dothit) closestFirst.push(dothit);
404
- const parent = path.dirname(dir);
405
- if (parent === dir) break;
406
- dir = parent;
407
- }
408
- return closestFirst.reverse();
409
- }
410
- function globalConfigs() {
411
- const out = [];
412
- const xdg = process.env.XDG_CONFIG_HOME ?? path.join(os.homedir(), ".config");
413
- const dir = path.join(xdg, "opencode");
414
- const hit = findConfigInDir(dir);
415
- if (hit) out.push(hit);
416
- return out;
417
400
  }
418
401
  function stripJsonComments(text) {
419
402
  let out = "";
@@ -454,26 +437,120 @@ function stripJsonComments(text) {
454
437
  }
455
438
  return out;
456
439
  }
457
- function discoverConfigFiles(cwd) {
458
- const files = [];
459
- files.push(...globalConfigs());
460
- files.push(...walkUpForConfig(cwd));
461
- const dir = process.env.OPENCODE_CONFIG_DIR;
462
- if (dir) {
463
- const hit = findConfigInDir(dir);
464
- if (hit) files.push(hit);
465
- }
466
- const explicit = process.env.OPENCODE_CONFIG;
467
- if (explicit && fileExists(explicit)) files.push(explicit);
468
- const resolvedOrder = files.map((f) => path.resolve(f));
469
- const lastIndex = /* @__PURE__ */ new Map();
470
- resolvedOrder.forEach((f, i) => lastIndex.set(f, i));
471
- return resolvedOrder.filter((f, i) => lastIndex.get(f) === i);
440
+ function readAndParse(file) {
441
+ try {
442
+ const raw = fs.readFileSync(file, "utf8");
443
+ return JSON.parse(stripJsonComments(raw));
444
+ } catch (e) {
445
+ log.warn("failed to parse opencode config", {
446
+ file,
447
+ error: e instanceof Error ? e.message : String(e)
448
+ });
449
+ return null;
450
+ }
451
+ }
452
+ function isPlainObject(x) {
453
+ return typeof x === "object" && x !== null && !Array.isArray(x);
454
+ }
455
+ function deepMerge(target, source) {
456
+ const out = { ...target };
457
+ for (const [k, v] of Object.entries(source)) {
458
+ if (v === void 0) continue;
459
+ const existing = out[k];
460
+ if (isPlainObject(existing) && isPlainObject(v)) {
461
+ out[k] = deepMerge(existing, v);
462
+ } else {
463
+ out[k] = v;
464
+ }
465
+ }
466
+ return out;
467
+ }
468
+ function walkUp(opts) {
469
+ const out = [];
470
+ let current = path.resolve(opts.start);
471
+ while (true) {
472
+ for (const target of opts.targets) {
473
+ const candidate = path.join(current, target);
474
+ if (opts.predicate(candidate)) out.push(candidate);
475
+ }
476
+ if (opts.stop && current === path.resolve(opts.stop)) break;
477
+ const parent = path.dirname(current);
478
+ if (parent === current) break;
479
+ current = parent;
480
+ }
481
+ return out;
482
+ }
483
+ function detectWorktree(cwd) {
484
+ const override = process.env.OPENCODE_WORKTREE;
485
+ if (override) return path.resolve(override);
486
+ let current = path.resolve(cwd);
487
+ while (true) {
488
+ const gitPath = path.join(current, ".git");
489
+ try {
490
+ if (fs.existsSync(gitPath)) return current;
491
+ } catch {
492
+ }
493
+ const parent = path.dirname(current);
494
+ if (parent === current) return void 0;
495
+ current = parent;
496
+ }
497
+ }
498
+ function globalConfigDir() {
499
+ const xdg = process.env.XDG_CONFIG_HOME ?? path.join(os.homedir(), ".config");
500
+ return path.join(xdg, "opencode");
501
+ }
502
+ function loadGlobalConfig() {
503
+ const dir = globalConfigDir();
504
+ let merged = {};
505
+ for (const name of FILE_NAMES.slice().reverse()) {
506
+ const file = path.join(dir, name);
507
+ if (!fileExists(file)) continue;
508
+ const parsed = readAndParse(file);
509
+ if (parsed) merged = deepMerge(merged, parsed);
510
+ }
511
+ return merged;
512
+ }
513
+ function loadProjectFilesInDir(dir) {
514
+ let merged = {};
515
+ for (const name of PROJECT_FILE_NAMES) {
516
+ const file = path.join(dir, name);
517
+ if (!fileExists(file)) continue;
518
+ const parsed = readAndParse(file);
519
+ if (parsed) merged = deepMerge(merged, parsed);
520
+ }
521
+ return merged;
522
+ }
523
+ function dotOpencodeDirs(cwd, worktree) {
524
+ const dirs = [];
525
+ const seen = /* @__PURE__ */ new Set();
526
+ const push = (p) => {
527
+ const abs = path.resolve(p);
528
+ if (!seen.has(abs) && dirExists(abs)) {
529
+ seen.add(abs);
530
+ dirs.push(abs);
531
+ }
532
+ };
533
+ for (const dir of walkUp({
534
+ start: cwd,
535
+ stop: worktree,
536
+ targets: [".opencode"],
537
+ predicate: dirExists
538
+ })) {
539
+ push(dir);
540
+ }
541
+ const home = os.homedir();
542
+ if (home) {
543
+ const homeDot = path.join(home, ".opencode");
544
+ if (dirExists(homeDot)) push(homeDot);
545
+ }
546
+ const envDir = process.env.OPENCODE_CONFIG_DIR;
547
+ if (envDir && dirExists(envDir)) push(envDir);
548
+ return dirs;
472
549
  }
473
550
  function translateServer(name, spec) {
474
- if (!spec || typeof spec !== "object") return null;
475
551
  if (spec.enabled === false) return null;
476
- if (spec.type === "local") {
552
+ const type = spec.type;
553
+ if (type === "local") {
477
554
  const cmd = spec.command;
478
555
  if (!Array.isArray(cmd) || cmd.length === 0) {
479
556
  log.warn("skipping local MCP server with no command", { name });
@@ -489,8 +566,8 @@ function translateServer(name, spec) {
489
566
  }
490
567
  return out;
491
568
  }
492
- if (spec.type === "remote") {
493
- if (!spec.url || typeof spec.url !== "string") {
569
+ if (type === "remote") {
570
+ if (typeof spec.url !== "string" || !spec.url) {
494
571
  log.warn("skipping remote MCP server with no url", { name });
495
572
  return null;
496
573
  }
@@ -505,36 +582,73 @@ function translateServer(name, spec) {
505
582
  }
506
583
  log.warn("skipping MCP server with unknown type", {
507
584
  name,
508
- type: spec?.type
585
+ type: type ?? null
509
586
  });
510
587
  return null;
511
588
  }
512
- function readAndParse(file) {
513
- try {
514
- const raw = fs.readFileSync(file, "utf8");
515
- return JSON.parse(stripJsonComments(raw));
516
- } catch (e) {
517
- log.warn("failed to parse opencode config", {
518
- file,
519
- error: e instanceof Error ? e.message : String(e)
520
- });
521
- return null;
589
+ function extractMcpBlock(config) {
590
+ const mcp = config.mcp;
591
+ if (!mcp || typeof mcp !== "object" || Array.isArray(mcp)) return {};
592
+ return mcp;
593
+ }
594
+ function mergeMcp(target, source) {
595
+ const out = { ...target };
596
+ for (const [name, spec] of Object.entries(source)) {
597
+ if (!spec || typeof spec !== "object") continue;
598
+ const existing = out[name];
599
+ if (existing && typeof existing === "object") {
600
+ out[name] = deepMerge(
601
+ existing,
602
+ spec
603
+ );
604
+ } else {
605
+ out[name] = spec;
606
+ }
522
607
  }
608
+ return out;
523
609
  }
524
- function bridgeOpencodeMcp(cwd) {
525
- const files = discoverConfigFiles(cwd);
526
- if (files.length === 0) return null;
527
- const merged = {};
528
- for (const file of files) {
529
- const parsed = readAndParse(file);
530
- const mcp = parsed?.mcp ?? null;
531
- if (!mcp || typeof mcp !== "object") continue;
532
- for (const [name, spec] of Object.entries(mcp)) {
533
- merged[name] = spec;
610
+ function bridgeOpencodeMcp(cwd, runtimeStatus) {
611
+ const worktree = detectWorktree(cwd);
612
+ let merged = {};
613
+ merged = mergeMcp(merged, extractMcpBlock(loadGlobalConfig()));
614
+ const explicitConfig = process.env.OPENCODE_CONFIG;
615
+ if (explicitConfig && fileExists(explicitConfig)) {
616
+ const parsed = readAndParse(explicitConfig);
617
+ if (parsed) merged = mergeMcp(merged, extractMcpBlock(parsed));
618
+ }
619
+ const projectFiles = walkUp({
620
+ start: cwd,
621
+ stop: worktree,
622
+ targets: PROJECT_FILE_NAMES,
623
+ predicate: fileExists
624
+ });
625
+ const projectDirs = [];
626
+ const seenProjectDirs = /* @__PURE__ */ new Set();
627
+ for (const f of projectFiles) {
628
+ const d = path.dirname(f);
629
+ if (!seenProjectDirs.has(d)) {
630
+ seenProjectDirs.add(d);
631
+ projectDirs.push(d);
632
+ }
633
+ }
634
+ for (const dir of projectDirs.slice().reverse()) {
635
+ merged = mergeMcp(merged, extractMcpBlock(loadProjectFilesInDir(dir)));
636
+ }
637
+ for (const dir of dotOpencodeDirs(cwd, worktree)) {
638
+ merged = mergeMcp(merged, extractMcpBlock(loadProjectFilesInDir(dir)));
639
+ }
640
+ if (runtimeStatus) {
641
+ for (const name of Object.keys(merged)) {
642
+ const status = runtimeStatus[name];
643
+ if (status === void 0) continue;
644
+ const existing = merged[name];
645
+ const base = existing && typeof existing === "object" ? existing : {};
646
+ merged[name] = { ...base, enabled: status === "connected" };
534
647
  }
535
648
  }
536
649
  const servers = {};
537
650
  for (const [name, spec] of Object.entries(merged)) {
651
+ if (!spec || typeof spec !== "object") continue;
538
652
  const translated = translateServer(name, spec);
539
653
  if (translated) servers[name] = translated;
540
654
  }
@@ -556,11 +670,41 @@ function bridgeOpencodeMcp(cwd) {
556
670
  return null;
557
671
  }
558
672
  log.info("bridged opencode MCP config", {
559
- sources: files,
560
673
  target: outPath,
674
+ hash,
561
675
  servers: Object.keys(servers)
562
676
  });
563
- return outPath;
677
+ return { path: outPath, hash };
678
+ }
679
+
680
+ // src/runtime-status.ts
681
+ var opencodeClient = null;
682
+ function setOpencodeClient(client) {
683
+ if (client && typeof client === "object") {
684
+ opencodeClient = client;
685
+ }
686
+ }
687
+ async function getRuntimeMcpStatus() {
688
+ const client = opencodeClient;
689
+ if (!client?.mcp?.status) return void 0;
690
+ try {
691
+ const res = await client.mcp.status();
692
+ const data = res.data;
693
+ if (!data || typeof data !== "object") return void 0;
694
+ const out = {};
695
+ for (const [name, entry] of Object.entries(data)) {
696
+ if (entry && typeof entry === "object") {
697
+ const status = entry.status;
698
+ if (typeof status === "string") out[name] = status;
699
+ }
700
+ }
701
+ return out;
702
+ } catch (err) {
703
+ log.warn("failed to fetch opencode MCP runtime status", {
704
+ error: err instanceof Error ? err.message : String(err)
705
+ });
706
+ return void 0;
707
+ }
564
708
  }
565
709
 
566
710
  // src/session-manager.ts
@@ -607,7 +751,7 @@ function setClaudeSessionId(key, sessionId) {
607
751
  function deleteClaudeSessionId(key) {
608
752
  claudeSessions.delete(key);
609
753
  }
610
- function spawnClaudeProcess(cliPath, cliArgs, cwd, sessionKey2, proxyServer) {
754
+ function spawnClaudeProcess(cliPath, cliArgs, cwd, sessionKey2, proxyServer, mcpHash) {
611
755
  evictIfNeeded();
612
756
  log.info("spawning new claude process", { cliPath, cliArgs, cwd, sessionKey: sessionKey2 });
613
757
  const proc = spawn(cliPath, cliArgs, {
@@ -624,8 +768,16 @@ function spawnClaudeProcess(cliPath, cliArgs, cwd, sessionKey2, proxyServer) {
624
768
  rl.on("close", () => {
625
769
  lineEmitter.emit("close");
626
770
  });
627
- const ap = { proc, lineEmitter, proxyServer: proxyServer ?? null };
771
+ const ap = {
772
+ proc,
773
+ lineEmitter,
774
+ proxyServer: proxyServer ?? null,
775
+ mcpHash
776
+ };
628
777
  activeProcesses.set(sessionKey2, ap);
778
+ proc.on("error", (err) => {
779
+ log.error("claude process error", { sessionKey: sessionKey2, error: err.message });
780
+ });
629
781
  proc.on("exit", (code, signal) => {
630
782
  log.info("claude process exited", { code, signal, sessionKey: sessionKey2 });
631
783
  void proxyServer?.close();
@@ -884,12 +1036,12 @@ async function createProxyMcpServer(tools = DEFAULT_PROXY_TOOLS) {
884
1036
  hasInput: input != null
885
1037
  });
886
1038
  const result = await new Promise(
887
- (resolve2, reject) => {
1039
+ (resolve3, reject) => {
888
1040
  const entry = {
889
1041
  id: callId,
890
1042
  toolName,
891
1043
  input,
892
- resolve: resolve2,
1044
+ resolve: resolve3,
893
1045
  reject
894
1046
  };
895
1047
  pending.set(callId, entry);
@@ -946,11 +1098,11 @@ async function createProxyMcpServer(tools = DEFAULT_PROXY_TOOLS) {
946
1098
  }
947
1099
  }
948
1100
  });
949
- await new Promise((resolve2, reject) => {
1101
+ await new Promise((resolve3, reject) => {
950
1102
  server2.once("error", reject);
951
1103
  server2.listen(0, "127.0.0.1", () => {
952
1104
  server2.off("error", reject);
953
- resolve2();
1105
+ resolve3();
954
1106
  });
955
1107
  });
956
1108
  const addr = server2.address();
@@ -997,8 +1149,8 @@ async function createProxyMcpServer(tools = DEFAULT_PROXY_TOOLS) {
997
1149
  entry.reject(new Error("proxy MCP server closed"));
998
1150
  }
999
1151
  pending.clear();
1000
- await new Promise((resolve2) => {
1001
- server2.close(() => resolve2());
1152
+ await new Promise((resolve3) => {
1153
+ server2.close(() => resolve3());
1002
1154
  });
1003
1155
  }
1004
1156
  };
@@ -1022,10 +1174,10 @@ function disallowedToolFlags(tools) {
1022
1174
  return out;
1023
1175
  }
1024
1176
  function readBody(req) {
1025
- return new Promise((resolve2, reject) => {
1177
+ return new Promise((resolve3, reject) => {
1026
1178
  const chunks = [];
1027
1179
  req.on("data", (chunk) => chunks.push(chunk));
1028
- req.on("end", () => resolve2(Buffer.concat(chunks).toString("utf8")));
1180
+ req.on("end", () => resolve3(Buffer.concat(chunks).toString("utf8")));
1029
1181
  req.on("error", reject);
1030
1182
  });
1031
1183
  }
@@ -1139,18 +1291,27 @@ var ClaudeCodeLanguageModel = class {
1139
1291
  return "no-tools";
1140
1292
  }
1141
1293
  /**
1142
- * Build the combined `--mcp-config` list: user-configured paths plus the
1143
- * auto-bridged opencode MCP config (when enabled and present) and the
1144
- * proxy MCP scratch file (when proxyTools are enabled).
1294
+ * Build the combined `--mcp-config` list and return both the list and the
1295
+ * hash of the bridged opencode MCP block (or null when bridging is off /
1296
+ * yields nothing). The hash is used to detect mid-session config changes
1297
+ * and respawn the underlying claude process.
1298
+ *
1299
+ * `runtimeStatus` is a snapshot of opencode's `client.mcp.status()`. When
1300
+ * provided it overlays opencode's UI-toggled state on top of disk config
1301
+ * so `/mcps` toggles propagate without a config file write.
1145
1302
  */
1146
- effectiveMcpConfig(cwd, proxyConfigPath) {
1147
- const user = Array.isArray(this.config.mcpConfig) ? this.config.mcpConfig.slice() : this.config.mcpConfig ? [this.config.mcpConfig] : [];
1303
+ effectiveMcpConfig(cwd, proxyConfigPath, runtimeStatus) {
1304
+ const paths = Array.isArray(this.config.mcpConfig) ? this.config.mcpConfig.slice() : this.config.mcpConfig ? [this.config.mcpConfig] : [];
1305
+ let bridgedHash = null;
1148
1306
  if (this.config.bridgeOpencodeMcp !== false) {
1149
- const bridged = bridgeOpencodeMcp(cwd);
1150
- if (bridged) user.push(bridged);
1307
+ const bridged = bridgeOpencodeMcp(cwd, runtimeStatus);
1308
+ if (bridged) {
1309
+ paths.push(bridged.path);
1310
+ bridgedHash = bridged.hash;
1311
+ }
1151
1312
  }
1152
- if (proxyConfigPath) user.push(proxyConfigPath);
1153
- return user;
1313
+ if (proxyConfigPath) paths.push(proxyConfigPath);
1314
+ return { paths, bridgedHash };
1154
1315
  }
1155
1316
  /** Resolve ProxyToolDef[] for the configured proxyTools names. */
1156
1317
  resolvedProxyTools() {
@@ -1484,14 +1645,16 @@ var ClaudeCodeLanguageModel = class {
1484
1645
  includeHistoryContext,
1485
1646
  reasoningEffort
1486
1647
  );
1648
+ const runtimeStatus = await getRuntimeMcpStatus();
1487
1649
  const cliArgs = buildCliArgs({
1488
1650
  sessionKey: sk,
1489
1651
  skipPermissions: this.config.skipPermissions !== false,
1490
1652
  includeSessionId: false,
1491
1653
  model: this.modelId,
1492
1654
  permissionMode: this.config.permissionMode,
1493
- mcpConfig: this.effectiveMcpConfig(cwd),
1494
- strictMcpConfig: this.config.strictMcpConfig
1655
+ mcpConfig: this.effectiveMcpConfig(cwd, void 0, runtimeStatus).paths,
1656
+ strictMcpConfig: this.config.strictMcpConfig,
1657
+ disallowedTools: this.config.webSearch === "disabled" ? ["WebSearch"] : void 0
1495
1658
  });
1496
1659
  log.info("doGenerate starting", {
1497
1660
  cwd,
@@ -1512,7 +1675,7 @@ var ClaudeCodeLanguageModel = class {
1512
1675
  let thinkingText = "";
1513
1676
  let resultMeta = {};
1514
1677
  const toolCalls = [];
1515
- const result = await new Promise((resolve2, reject) => {
1678
+ const result = await new Promise((resolve3, reject) => {
1516
1679
  rl.on("line", (line) => {
1517
1680
  if (!line.trim()) return;
1518
1681
  try {
@@ -1603,7 +1766,7 @@ ${plan}
1603
1766
  durationMs: msg.duration_ms,
1604
1767
  usage: msg.usage
1605
1768
  };
1606
- resolve2({
1769
+ resolve3({
1607
1770
  ...resultMeta,
1608
1771
  text: responseText,
1609
1772
  thinking: thinkingText,
@@ -1614,7 +1777,7 @@ ${plan}
1614
1777
  }
1615
1778
  });
1616
1779
  rl.on("close", () => {
1617
- resolve2({
1780
+ resolve3({
1618
1781
  ...resultMeta,
1619
1782
  text: responseText,
1620
1783
  thinking: thinkingText,
@@ -1661,7 +1824,7 @@ ${plan}
1661
1824
  input: mappedInput,
1662
1825
  executed,
1663
1826
  skip
1664
- } = mapTool(tc.name, tc.args);
1827
+ } = mapTool(tc.name, tc.args, { webSearch: this.config.webSearch });
1665
1828
  if (skip) continue;
1666
1829
  content.push({
1667
1830
  type: "tool-call",
@@ -1762,6 +1925,7 @@ ${plan}
1762
1925
  const self = this;
1763
1926
  const pendingProxyCall = getPendingProxyCall(sk);
1764
1927
  const pendingProxyResult = pendingProxyCall ? this.extractPendingProxyResult(options.prompt, pendingProxyCall.toolCallId) : null;
1928
+ const runtimeStatus = await getRuntimeMcpStatus();
1765
1929
  log.info("doStream starting", {
1766
1930
  cwd,
1767
1931
  model: this.modelId,
@@ -1777,25 +1941,55 @@ ${plan}
1777
1941
  let proc;
1778
1942
  let lineEmitter;
1779
1943
  let proxyServer = activeProcess?.proxyServer ?? null;
1944
+ if (activeProcess && self.config.hotReloadMcp !== false && self.config.bridgeOpencodeMcp !== false) {
1945
+ const probe = self.effectiveMcpConfig(cwd, void 0, runtimeStatus);
1946
+ const previousHash = activeProcess.mcpHash ?? null;
1947
+ if (previousHash !== probe.bridgedHash) {
1948
+ log.info("opencode MCP config changed, respawning claude", {
1949
+ sk,
1950
+ previousHash,
1951
+ currentHash: probe.bridgedHash
1952
+ });
1953
+ deleteActiveProcess(sk);
1954
+ activeProcess = void 0;
1955
+ proxyServer = null;
1956
+ }
1957
+ }
1780
1958
  const setup = async () => {
1781
1959
  if (!proxyServer && resolvedProxy) {
1782
1960
  proxyServer = await self.ensureProxyServer(resolvedProxy, sk);
1783
1961
  }
1962
+ const proxyDisallowed = resolvedProxy ? disallowedToolFlags(resolvedProxy) : [];
1963
+ const extraDisallowed = [];
1964
+ if (self.config.webSearch === "disabled") extraDisallowed.push("WebSearch");
1965
+ const allDisallowed = [...proxyDisallowed, ...extraDisallowed];
1966
+ const mcp = self.effectiveMcpConfig(
1967
+ cwd,
1968
+ proxyServer?.configPath(),
1969
+ runtimeStatus
1970
+ );
1784
1971
  const cliArgs = buildCliArgs({
1785
1972
  sessionKey: sk,
1786
1973
  skipPermissions,
1787
1974
  model: self.modelId,
1788
1975
  permissionMode: self.config.permissionMode,
1789
- mcpConfig: self.effectiveMcpConfig(cwd, proxyServer?.configPath()),
1976
+ mcpConfig: mcp.paths,
1790
1977
  strictMcpConfig: self.config.strictMcpConfig,
1791
- disallowedTools: resolvedProxy ? disallowedToolFlags(resolvedProxy) : void 0
1978
+ disallowedTools: allDisallowed.length > 0 ? allDisallowed : void 0
1792
1979
  });
1793
1980
  if (activeProcess) {
1794
1981
  proc = activeProcess.proc;
1795
1982
  lineEmitter = activeProcess.lineEmitter;
1796
1983
  log.debug("reusing active process", { sk });
1797
1984
  } else {
1798
- const ap = spawnClaudeProcess(cliPath, cliArgs, cwd, sk, proxyServer);
1985
+ const ap = spawnClaudeProcess(
1986
+ cliPath,
1987
+ cliArgs,
1988
+ cwd,
1989
+ sk,
1990
+ proxyServer,
1991
+ mcp.bridgedHash
1992
+ );
1799
1993
  proc = ap.proc;
1800
1994
  lineEmitter = ap.lineEmitter;
1801
1995
  activeProcess = ap;
@@ -1868,10 +2062,7 @@ ${plan}
1868
2062
  }
1869
2063
  });
1870
2064
  controllerClosed = true;
1871
- lineEmitter.off("line", lineHandler);
1872
- lineEmitter.off("close", closeHandler);
1873
- pendingProxyUnsubscribe?.();
1874
- pendingProxyUnsubscribe = null;
2065
+ cleanupTurn();
1875
2066
  try {
1876
2067
  controller.close();
1877
2068
  } catch {
@@ -1930,7 +2121,11 @@ ${plan}
1930
2121
  inputJson: ""
1931
2122
  });
1932
2123
  if (block.name !== "AskUserQuestion" && block.name !== "ask_user_question" && block.name !== "ExitPlanMode" && !block.name.startsWith(PROXY_TOOL_PREFIX)) {
1933
- const { name: mappedName, skip, executed } = mapTool(block.name);
2124
+ const { name: mappedName, skip, executed } = mapTool(
2125
+ block.name,
2126
+ void 0,
2127
+ { webSearch: self.config.webSearch }
2128
+ );
1934
2129
  if (!skip) {
1935
2130
  controller.enqueue({
1936
2131
  type: "tool-input-start",
@@ -2047,7 +2242,7 @@ ${plan}
2047
2242
  input: mappedInput,
2048
2243
  executed,
2049
2244
  skip
2050
- } = mapTool(tc.name, parsedInput);
2245
+ } = mapTool(tc.name, parsedInput, { webSearch: self.config.webSearch });
2051
2246
  if (!skip) {
2052
2247
  toolCallsById.set(tc.id, {
2053
2248
  id: tc.id,
@@ -2167,7 +2362,7 @@ ${plan}
2167
2362
  input: mappedInput,
2168
2363
  executed,
2169
2364
  skip
2170
- } = mapTool(block.name, parsedInput);
2365
+ } = mapTool(block.name, parsedInput, { webSearch: self.config.webSearch });
2171
2366
  if (!skip) {
2172
2367
  if (!executed) skipResultForIds.add(block.id);
2173
2368
  controller.enqueue({
@@ -2287,8 +2482,7 @@ ${plan}
2287
2482
  }
2288
2483
  });
2289
2484
  controllerClosed = true;
2290
- lineEmitter.off("line", lineHandler);
2291
- lineEmitter.off("close", closeHandler);
2485
+ cleanupTurn();
2292
2486
  try {
2293
2487
  controller.close();
2294
2488
  } catch {
@@ -2303,12 +2497,8 @@ ${plan}
2303
2497
  const closeHandler = () => {
2304
2498
  log.debug("readline closed");
2305
2499
  if (controllerClosed) return;
2306
- clearFallbackTimer();
2307
2500
  controllerClosed = true;
2308
- lineEmitter.off("line", lineHandler);
2309
- lineEmitter.off("close", closeHandler);
2310
- pendingProxyUnsubscribe?.();
2311
- pendingProxyUnsubscribe = null;
2501
+ cleanupTurn();
2312
2502
  endTextBlock();
2313
2503
  controller.enqueue({
2314
2504
  type: "finish",
@@ -2323,6 +2513,28 @@ ${plan}
2323
2513
  } catch {
2324
2514
  }
2325
2515
  };
2516
+ let cleanedUp = false;
2517
+ const cleanupTurn = () => {
2518
+ if (cleanedUp) return;
2519
+ cleanedUp = true;
2520
+ clearFallbackTimer();
2521
+ lineEmitter.off("line", lineHandler);
2522
+ lineEmitter.off("close", closeHandler);
2523
+ pendingProxyUnsubscribe?.();
2524
+ pendingProxyUnsubscribe = null;
2525
+ proc.off("error", procErrorHandler);
2526
+ };
2527
+ const procErrorHandler = (err) => {
2528
+ log.error("process error", { error: err.message });
2529
+ if (controllerClosed) return;
2530
+ controllerClosed = true;
2531
+ cleanupTurn();
2532
+ controller.enqueue({ type: "error", error: err });
2533
+ try {
2534
+ controller.close();
2535
+ } catch {
2536
+ }
2537
+ };
2326
2538
  lineEmitter.on("line", lineHandler);
2327
2539
  lineEmitter.on("close", closeHandler);
2328
2540
  pendingProxyUnsubscribe = onPendingProxyCall(sk, (call) => {
@@ -2333,19 +2545,7 @@ ${plan}
2333
2545
  });
2334
2546
  finishWithToolCall(call);
2335
2547
  });
2336
- proc.on("error", (err) => {
2337
- log.error("process error", { error: err.message });
2338
- clearFallbackTimer();
2339
- if (controllerClosed) return;
2340
- controllerClosed = true;
2341
- pendingProxyUnsubscribe?.();
2342
- pendingProxyUnsubscribe = null;
2343
- controller.enqueue({ type: "error", error: err });
2344
- try {
2345
- controller.close();
2346
- } catch {
2347
- }
2348
- });
2548
+ proc.on("error", procErrorHandler);
2349
2549
  if (options.abortSignal) {
2350
2550
  options.abortSignal.addEventListener("abort", () => {
2351
2551
  if (turnCompleted || controllerClosed) return;
@@ -2355,10 +2555,7 @@ ${plan}
2355
2555
  { cwd }
2356
2556
  );
2357
2557
  controllerClosed = true;
2358
- lineEmitter.off("line", lineHandler);
2359
- lineEmitter.off("close", closeHandler);
2360
- pendingProxyUnsubscribe?.();
2361
- pendingProxyUnsubscribe = null;
2558
+ cleanupTurn();
2362
2559
  try {
2363
2560
  controller.close();
2364
2561
  } catch {
@@ -2695,6 +2892,117 @@ function titleizeAccount(account) {
2695
2892
  return normalizeAccountName(account).split("-").filter(Boolean).map((part) => part.charAt(0).toUpperCase() + part.slice(1)).join(" ");
2696
2893
  }
2697
2894
 
2895
+ // src/cleanup-stale.ts
2896
+ import {
2897
+ existsSync as existsSync2,
2898
+ readFileSync as readFileSync2,
2899
+ realpathSync,
2900
+ rmSync,
2901
+ writeFileSync as writeFileSync3
2902
+ } from "fs";
2903
+ import { homedir as homedir2 } from "os";
2904
+ import { join as join3, resolve as resolve2 } from "path";
2905
+ import { fileURLToPath } from "url";
2906
+ var STALE_PACKAGE_NAME = "opencode-claude-code-plugin";
2907
+ var SUSPECT_DESCRIPTION_TOKEN = "Claude Code";
2908
+ var alreadyRan = false;
2909
+ function candidateCacheRoots() {
2910
+ const xdg = process.env.XDG_CACHE_HOME;
2911
+ return [
2912
+ xdg ? join3(xdg, "opencode") : null,
2913
+ join3(homedir2(), ".cache", "opencode"),
2914
+ join3(homedir2(), "Library", "Caches", "opencode")
2915
+ ].filter((p) => Boolean(p));
2916
+ }
2917
+ function userOpencodeJsonPath() {
2918
+ const xdgConfig = process.env.XDG_CONFIG_HOME ?? join3(homedir2(), ".config");
2919
+ return join3(xdgConfig, "opencode", "opencode.json");
2920
+ }
2921
+ function userIntendsToUseUnscoped() {
2922
+ const cfg = userOpencodeJsonPath();
2923
+ if (!existsSync2(cfg)) return false;
2924
+ try {
2925
+ const json = JSON.parse(readFileSync2(cfg, "utf8"));
2926
+ const plugins = json.plugin;
2927
+ if (!Array.isArray(plugins)) return false;
2928
+ return plugins.some(
2929
+ (entry) => typeof entry === "string" && /^opencode-claude-code-plugin(@[^/]+)?$/.test(entry)
2930
+ );
2931
+ } catch {
2932
+ return false;
2933
+ }
2934
+ }
2935
+ function ourLoadedDir() {
2936
+ try {
2937
+ const filePath = fileURLToPath(import.meta.url);
2938
+ return realpathSync(resolve2(filePath, "..", ".."));
2939
+ } catch {
2940
+ return null;
2941
+ }
2942
+ }
2943
+ function cleanupStaleUnscopedInstall() {
2944
+ if (alreadyRan) return;
2945
+ alreadyRan = true;
2946
+ if (process.env.OPENCODE_CLAUDE_CODE_PLUGIN_NO_CLEANUP === "1") return;
2947
+ if (userIntendsToUseUnscoped()) return;
2948
+ const ourDir = ourLoadedDir();
2949
+ for (const cacheRoot of candidateCacheRoots()) {
2950
+ try {
2951
+ cleanupOne(cacheRoot, ourDir);
2952
+ } catch (err) {
2953
+ log.warn("cleanup-stale: error processing cache root", {
2954
+ cacheRoot,
2955
+ error: String(err)
2956
+ });
2957
+ }
2958
+ }
2959
+ }
2960
+ function cleanupOne(cacheRoot, ourDir) {
2961
+ if (!existsSync2(cacheRoot)) return;
2962
+ const stalePath = join3(cacheRoot, "node_modules", STALE_PACKAGE_NAME);
2963
+ if (!existsSync2(stalePath)) return;
2964
+ let realStalePath = stalePath;
2965
+ try {
2966
+ realStalePath = realpathSync(stalePath);
2967
+ } catch {
2968
+ }
2969
+ if (ourDir && realStalePath === ourDir) return;
2970
+ const pkgJsonPath = join3(stalePath, "package.json");
2971
+ if (!existsSync2(pkgJsonPath)) return;
2972
+ let pkg = {};
2973
+ try {
2974
+ pkg = JSON.parse(readFileSync2(pkgJsonPath, "utf8"));
2975
+ } catch {
2976
+ return;
2977
+ }
2978
+ if (pkg.name !== STALE_PACKAGE_NAME) return;
2979
+ if (!pkg.description?.includes(SUSPECT_DESCRIPTION_TOKEN)) return;
2980
+ log.info("cleanup-stale: removing unscoped install", { stalePath });
2981
+ try {
2982
+ rmSync(stalePath, { recursive: true, force: true });
2983
+ } catch (err) {
2984
+ log.warn("cleanup-stale: rmSync failed", {
2985
+ stalePath,
2986
+ error: String(err)
2987
+ });
2988
+ return;
2989
+ }
2990
+ const cachePkgJson = join3(cacheRoot, "package.json");
2991
+ if (!existsSync2(cachePkgJson)) return;
2992
+ try {
2993
+ const cfg = JSON.parse(readFileSync2(cachePkgJson, "utf8"));
2994
+ if (cfg?.dependencies?.[STALE_PACKAGE_NAME]) {
2995
+ delete cfg.dependencies[STALE_PACKAGE_NAME];
2996
+ writeFileSync3(cachePkgJson, JSON.stringify(cfg, null, 2) + "\n");
2997
+ log.info("cleanup-stale: pruned dep from cache package.json");
2998
+ }
2999
+ } catch (err) {
3000
+ log.warn("cleanup-stale: cache package.json update failed", {
3001
+ error: String(err)
3002
+ });
3003
+ }
3004
+ }
3005
+
2698
3006
  // src/index.ts
2699
3007
  function createClaudeCode(settings = {}) {
2700
3008
  const cliPath = settings.cliPath ?? process.env.CLAUDE_CLI_PATH ?? "claude";
@@ -2716,7 +3024,9 @@ function createClaudeCode(settings = {}) {
2716
3024
  controlRequestBehavior: settings.controlRequestBehavior ?? "allow",
2717
3025
  controlRequestToolBehaviors: settings.controlRequestToolBehaviors,
2718
3026
  controlRequestDenyMessage: settings.controlRequestDenyMessage,
2719
- proxyTools
3027
+ proxyTools,
3028
+ webSearch: settings.webSearch,
3029
+ hotReloadMcp: settings.hotReloadMcp ?? true
2720
3030
  });
2721
3031
  };
2722
3032
  const provider = function(modelId) {
@@ -2872,22 +3182,32 @@ async function expandAccountProviders(config) {
2872
3182
  }
2873
3183
  return expandedCount > 0;
2874
3184
  }
2875
- var server = async () => ({
2876
- config: async (config) => {
2877
- config.provider ??= {};
2878
- const expanded = await expandAccountProviders(config);
2879
- if (expanded) return;
2880
- const existing = config.provider[PROVIDER_ID2];
2881
- config.provider[PROVIDER_ID2] = {
2882
- ...existing,
2883
- ...await providerConfig(existing)
2884
- };
2885
- },
2886
- provider: {
2887
- id: PROVIDER_ID2,
2888
- models: async (provider) => defaultModelsForProvider(provider.models)
3185
+ var server = async (input) => {
3186
+ cleanupStaleUnscopedInstall();
3187
+ if (input && typeof input === "object" && "client" in input) {
3188
+ setOpencodeClient(input.client);
2889
3189
  }
2890
- });
3190
+ return {
3191
+ config: async (config) => {
3192
+ config.provider ??= {};
3193
+ const expanded = await expandAccountProviders(config);
3194
+ if (expanded) return;
3195
+ const existing = config.provider[PROVIDER_ID2];
3196
+ config.provider[PROVIDER_ID2] = {
3197
+ ...existing,
3198
+ ...await providerConfig(existing)
3199
+ };
3200
+ },
3201
+ // No `event` hook: MCP config drift is detected at turn start by the
3202
+ // hot-reload check in `claude-code-language-model.ts`, which respawns
3203
+ // claude safely between turns. Eviction on `global.disposed` would kill
3204
+ // an in-flight stream and abort the user's current turn.
3205
+ provider: {
3206
+ id: PROVIDER_ID2,
3207
+ models: async (provider) => defaultModelsForProvider(provider.models)
3208
+ }
3209
+ };
3210
+ };
2891
3211
  var index_default = {
2892
3212
  id: "@khalilgharbaoui/opencode-claude-code-plugin",
2893
3213
  server