@khalilgharbaoui/opencode-claude-code-plugin 0.1.5 → 0.2.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
@@ -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
@@ -598,6 +742,15 @@ function deleteActiveProcess(key) {
598
742
  activeProcesses.delete(key);
599
743
  }
600
744
  }
745
+ function evictAllSessions(reason) {
746
+ const count = activeProcesses.size;
747
+ if (count === 0) return 0;
748
+ log.info("evicting all claude processes", { reason, count });
749
+ for (const key of Array.from(activeProcesses.keys())) {
750
+ deleteActiveProcess(key);
751
+ }
752
+ return count;
753
+ }
601
754
  function getClaudeSessionId(key) {
602
755
  return claudeSessions.get(key);
603
756
  }
@@ -607,7 +760,7 @@ function setClaudeSessionId(key, sessionId) {
607
760
  function deleteClaudeSessionId(key) {
608
761
  claudeSessions.delete(key);
609
762
  }
610
- function spawnClaudeProcess(cliPath, cliArgs, cwd, sessionKey2, proxyServer) {
763
+ function spawnClaudeProcess(cliPath, cliArgs, cwd, sessionKey2, proxyServer, mcpHash) {
611
764
  evictIfNeeded();
612
765
  log.info("spawning new claude process", { cliPath, cliArgs, cwd, sessionKey: sessionKey2 });
613
766
  const proc = spawn(cliPath, cliArgs, {
@@ -624,8 +777,16 @@ function spawnClaudeProcess(cliPath, cliArgs, cwd, sessionKey2, proxyServer) {
624
777
  rl.on("close", () => {
625
778
  lineEmitter.emit("close");
626
779
  });
627
- const ap = { proc, lineEmitter, proxyServer: proxyServer ?? null };
780
+ const ap = {
781
+ proc,
782
+ lineEmitter,
783
+ proxyServer: proxyServer ?? null,
784
+ mcpHash
785
+ };
628
786
  activeProcesses.set(sessionKey2, ap);
787
+ proc.on("error", (err) => {
788
+ log.error("claude process error", { sessionKey: sessionKey2, error: err.message });
789
+ });
629
790
  proc.on("exit", (code, signal) => {
630
791
  log.info("claude process exited", { code, signal, sessionKey: sessionKey2 });
631
792
  void proxyServer?.close();
@@ -1139,18 +1300,27 @@ var ClaudeCodeLanguageModel = class {
1139
1300
  return "no-tools";
1140
1301
  }
1141
1302
  /**
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).
1303
+ * Build the combined `--mcp-config` list and return both the list and the
1304
+ * hash of the bridged opencode MCP block (or null when bridging is off /
1305
+ * yields nothing). The hash is used to detect mid-session config changes
1306
+ * and respawn the underlying claude process.
1307
+ *
1308
+ * `runtimeStatus` is a snapshot of opencode's `client.mcp.status()`. When
1309
+ * provided it overlays opencode's UI-toggled state on top of disk config
1310
+ * so `/mcps` toggles propagate without a config file write.
1145
1311
  */
1146
- effectiveMcpConfig(cwd, proxyConfigPath) {
1147
- const user = Array.isArray(this.config.mcpConfig) ? this.config.mcpConfig.slice() : this.config.mcpConfig ? [this.config.mcpConfig] : [];
1312
+ effectiveMcpConfig(cwd, proxyConfigPath, runtimeStatus) {
1313
+ const paths = Array.isArray(this.config.mcpConfig) ? this.config.mcpConfig.slice() : this.config.mcpConfig ? [this.config.mcpConfig] : [];
1314
+ let bridgedHash = null;
1148
1315
  if (this.config.bridgeOpencodeMcp !== false) {
1149
- const bridged = bridgeOpencodeMcp(cwd);
1150
- if (bridged) user.push(bridged);
1316
+ const bridged = bridgeOpencodeMcp(cwd, runtimeStatus);
1317
+ if (bridged) {
1318
+ paths.push(bridged.path);
1319
+ bridgedHash = bridged.hash;
1320
+ }
1151
1321
  }
1152
- if (proxyConfigPath) user.push(proxyConfigPath);
1153
- return user;
1322
+ if (proxyConfigPath) paths.push(proxyConfigPath);
1323
+ return { paths, bridgedHash };
1154
1324
  }
1155
1325
  /** Resolve ProxyToolDef[] for the configured proxyTools names. */
1156
1326
  resolvedProxyTools() {
@@ -1484,14 +1654,16 @@ var ClaudeCodeLanguageModel = class {
1484
1654
  includeHistoryContext,
1485
1655
  reasoningEffort
1486
1656
  );
1657
+ const runtimeStatus = await getRuntimeMcpStatus();
1487
1658
  const cliArgs = buildCliArgs({
1488
1659
  sessionKey: sk,
1489
1660
  skipPermissions: this.config.skipPermissions !== false,
1490
1661
  includeSessionId: false,
1491
1662
  model: this.modelId,
1492
1663
  permissionMode: this.config.permissionMode,
1493
- mcpConfig: this.effectiveMcpConfig(cwd),
1494
- strictMcpConfig: this.config.strictMcpConfig
1664
+ mcpConfig: this.effectiveMcpConfig(cwd, void 0, runtimeStatus).paths,
1665
+ strictMcpConfig: this.config.strictMcpConfig,
1666
+ disallowedTools: this.config.webSearch === "disabled" ? ["WebSearch"] : void 0
1495
1667
  });
1496
1668
  log.info("doGenerate starting", {
1497
1669
  cwd,
@@ -1661,7 +1833,7 @@ ${plan}
1661
1833
  input: mappedInput,
1662
1834
  executed,
1663
1835
  skip
1664
- } = mapTool(tc.name, tc.args);
1836
+ } = mapTool(tc.name, tc.args, { webSearch: this.config.webSearch });
1665
1837
  if (skip) continue;
1666
1838
  content.push({
1667
1839
  type: "tool-call",
@@ -1762,6 +1934,7 @@ ${plan}
1762
1934
  const self = this;
1763
1935
  const pendingProxyCall = getPendingProxyCall(sk);
1764
1936
  const pendingProxyResult = pendingProxyCall ? this.extractPendingProxyResult(options.prompt, pendingProxyCall.toolCallId) : null;
1937
+ const runtimeStatus = await getRuntimeMcpStatus();
1765
1938
  log.info("doStream starting", {
1766
1939
  cwd,
1767
1940
  model: this.modelId,
@@ -1777,25 +1950,55 @@ ${plan}
1777
1950
  let proc;
1778
1951
  let lineEmitter;
1779
1952
  let proxyServer = activeProcess?.proxyServer ?? null;
1953
+ if (activeProcess && self.config.hotReloadMcp !== false && self.config.bridgeOpencodeMcp !== false) {
1954
+ const probe = self.effectiveMcpConfig(cwd, void 0, runtimeStatus);
1955
+ const previousHash = activeProcess.mcpHash ?? null;
1956
+ if (previousHash !== probe.bridgedHash) {
1957
+ log.info("opencode MCP config changed, respawning claude", {
1958
+ sk,
1959
+ previousHash,
1960
+ currentHash: probe.bridgedHash
1961
+ });
1962
+ deleteActiveProcess(sk);
1963
+ activeProcess = void 0;
1964
+ proxyServer = null;
1965
+ }
1966
+ }
1780
1967
  const setup = async () => {
1781
1968
  if (!proxyServer && resolvedProxy) {
1782
1969
  proxyServer = await self.ensureProxyServer(resolvedProxy, sk);
1783
1970
  }
1971
+ const proxyDisallowed = resolvedProxy ? disallowedToolFlags(resolvedProxy) : [];
1972
+ const extraDisallowed = [];
1973
+ if (self.config.webSearch === "disabled") extraDisallowed.push("WebSearch");
1974
+ const allDisallowed = [...proxyDisallowed, ...extraDisallowed];
1975
+ const mcp = self.effectiveMcpConfig(
1976
+ cwd,
1977
+ proxyServer?.configPath(),
1978
+ runtimeStatus
1979
+ );
1784
1980
  const cliArgs = buildCliArgs({
1785
1981
  sessionKey: sk,
1786
1982
  skipPermissions,
1787
1983
  model: self.modelId,
1788
1984
  permissionMode: self.config.permissionMode,
1789
- mcpConfig: self.effectiveMcpConfig(cwd, proxyServer?.configPath()),
1985
+ mcpConfig: mcp.paths,
1790
1986
  strictMcpConfig: self.config.strictMcpConfig,
1791
- disallowedTools: resolvedProxy ? disallowedToolFlags(resolvedProxy) : void 0
1987
+ disallowedTools: allDisallowed.length > 0 ? allDisallowed : void 0
1792
1988
  });
1793
1989
  if (activeProcess) {
1794
1990
  proc = activeProcess.proc;
1795
1991
  lineEmitter = activeProcess.lineEmitter;
1796
1992
  log.debug("reusing active process", { sk });
1797
1993
  } else {
1798
- const ap = spawnClaudeProcess(cliPath, cliArgs, cwd, sk, proxyServer);
1994
+ const ap = spawnClaudeProcess(
1995
+ cliPath,
1996
+ cliArgs,
1997
+ cwd,
1998
+ sk,
1999
+ proxyServer,
2000
+ mcp.bridgedHash
2001
+ );
1799
2002
  proc = ap.proc;
1800
2003
  lineEmitter = ap.lineEmitter;
1801
2004
  activeProcess = ap;
@@ -1868,10 +2071,7 @@ ${plan}
1868
2071
  }
1869
2072
  });
1870
2073
  controllerClosed = true;
1871
- lineEmitter.off("line", lineHandler);
1872
- lineEmitter.off("close", closeHandler);
1873
- pendingProxyUnsubscribe?.();
1874
- pendingProxyUnsubscribe = null;
2074
+ cleanupTurn();
1875
2075
  try {
1876
2076
  controller.close();
1877
2077
  } catch {
@@ -1930,7 +2130,11 @@ ${plan}
1930
2130
  inputJson: ""
1931
2131
  });
1932
2132
  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);
2133
+ const { name: mappedName, skip, executed } = mapTool(
2134
+ block.name,
2135
+ void 0,
2136
+ { webSearch: self.config.webSearch }
2137
+ );
1934
2138
  if (!skip) {
1935
2139
  controller.enqueue({
1936
2140
  type: "tool-input-start",
@@ -2047,7 +2251,7 @@ ${plan}
2047
2251
  input: mappedInput,
2048
2252
  executed,
2049
2253
  skip
2050
- } = mapTool(tc.name, parsedInput);
2254
+ } = mapTool(tc.name, parsedInput, { webSearch: self.config.webSearch });
2051
2255
  if (!skip) {
2052
2256
  toolCallsById.set(tc.id, {
2053
2257
  id: tc.id,
@@ -2167,7 +2371,7 @@ ${plan}
2167
2371
  input: mappedInput,
2168
2372
  executed,
2169
2373
  skip
2170
- } = mapTool(block.name, parsedInput);
2374
+ } = mapTool(block.name, parsedInput, { webSearch: self.config.webSearch });
2171
2375
  if (!skip) {
2172
2376
  if (!executed) skipResultForIds.add(block.id);
2173
2377
  controller.enqueue({
@@ -2287,8 +2491,7 @@ ${plan}
2287
2491
  }
2288
2492
  });
2289
2493
  controllerClosed = true;
2290
- lineEmitter.off("line", lineHandler);
2291
- lineEmitter.off("close", closeHandler);
2494
+ cleanupTurn();
2292
2495
  try {
2293
2496
  controller.close();
2294
2497
  } catch {
@@ -2303,12 +2506,8 @@ ${plan}
2303
2506
  const closeHandler = () => {
2304
2507
  log.debug("readline closed");
2305
2508
  if (controllerClosed) return;
2306
- clearFallbackTimer();
2307
2509
  controllerClosed = true;
2308
- lineEmitter.off("line", lineHandler);
2309
- lineEmitter.off("close", closeHandler);
2310
- pendingProxyUnsubscribe?.();
2311
- pendingProxyUnsubscribe = null;
2510
+ cleanupTurn();
2312
2511
  endTextBlock();
2313
2512
  controller.enqueue({
2314
2513
  type: "finish",
@@ -2323,6 +2522,28 @@ ${plan}
2323
2522
  } catch {
2324
2523
  }
2325
2524
  };
2525
+ let cleanedUp = false;
2526
+ const cleanupTurn = () => {
2527
+ if (cleanedUp) return;
2528
+ cleanedUp = true;
2529
+ clearFallbackTimer();
2530
+ lineEmitter.off("line", lineHandler);
2531
+ lineEmitter.off("close", closeHandler);
2532
+ pendingProxyUnsubscribe?.();
2533
+ pendingProxyUnsubscribe = null;
2534
+ proc.off("error", procErrorHandler);
2535
+ };
2536
+ const procErrorHandler = (err) => {
2537
+ log.error("process error", { error: err.message });
2538
+ if (controllerClosed) return;
2539
+ controllerClosed = true;
2540
+ cleanupTurn();
2541
+ controller.enqueue({ type: "error", error: err });
2542
+ try {
2543
+ controller.close();
2544
+ } catch {
2545
+ }
2546
+ };
2326
2547
  lineEmitter.on("line", lineHandler);
2327
2548
  lineEmitter.on("close", closeHandler);
2328
2549
  pendingProxyUnsubscribe = onPendingProxyCall(sk, (call) => {
@@ -2333,19 +2554,7 @@ ${plan}
2333
2554
  });
2334
2555
  finishWithToolCall(call);
2335
2556
  });
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
- });
2557
+ proc.on("error", procErrorHandler);
2349
2558
  if (options.abortSignal) {
2350
2559
  options.abortSignal.addEventListener("abort", () => {
2351
2560
  if (turnCompleted || controllerClosed) return;
@@ -2355,10 +2564,7 @@ ${plan}
2355
2564
  { cwd }
2356
2565
  );
2357
2566
  controllerClosed = true;
2358
- lineEmitter.off("line", lineHandler);
2359
- lineEmitter.off("close", closeHandler);
2360
- pendingProxyUnsubscribe?.();
2361
- pendingProxyUnsubscribe = null;
2567
+ cleanupTurn();
2362
2568
  try {
2363
2569
  controller.close();
2364
2570
  } catch {
@@ -2457,10 +2663,43 @@ function defineModel(opts) {
2457
2663
  var haikuCost = { input: 1e-6, output: 5e-6, cacheRead: 1e-7, cacheWrite: 125e-8 };
2458
2664
  var sonnetCost = { input: 3e-6, output: 15e-6, cacheRead: 3e-7, cacheWrite: 375e-8 };
2459
2665
  var opusCost = { input: 15e-6, output: 75e-6, cacheRead: 15e-7, cacheWrite: 1875e-8 };
2666
+ function toConfigModel(model) {
2667
+ const inputMods = [];
2668
+ const outputMods = [];
2669
+ for (const [k, v] of Object.entries(model.capabilities.input)) {
2670
+ if (v) inputMods.push(k);
2671
+ }
2672
+ for (const [k, v] of Object.entries(model.capabilities.output)) {
2673
+ if (v) outputMods.push(k);
2674
+ }
2675
+ return {
2676
+ id: model.api.id,
2677
+ name: model.name,
2678
+ status: model.status,
2679
+ family: model.family ?? "",
2680
+ release_date: model.release_date,
2681
+ temperature: model.capabilities.temperature,
2682
+ reasoning: model.capabilities.reasoning,
2683
+ attachment: model.capabilities.attachment,
2684
+ tool_call: model.capabilities.toolcall,
2685
+ modalities: { input: inputMods, output: outputMods },
2686
+ interleaved: model.capabilities.interleaved,
2687
+ cost: {
2688
+ input: model.cost.input,
2689
+ output: model.cost.output,
2690
+ cache_read: model.cost.cache.read,
2691
+ cache_write: model.cost.cache.write
2692
+ },
2693
+ limit: model.limit,
2694
+ options: model.options,
2695
+ headers: model.headers,
2696
+ variants: model.variants
2697
+ };
2698
+ }
2460
2699
  var defaultModels = {
2461
2700
  "claude-haiku-4-5": defineModel({
2462
2701
  id: "claude-haiku-4-5",
2463
- name: "Claude Code Haiku 4.5",
2702
+ name: "Claude Haiku 4.5",
2464
2703
  family: "haiku",
2465
2704
  reasoning: false,
2466
2705
  context: 2e5,
@@ -2470,7 +2709,7 @@ var defaultModels = {
2470
2709
  }),
2471
2710
  "claude-sonnet-4-5": defineModel({
2472
2711
  id: "claude-sonnet-4-5",
2473
- name: "Claude Code Sonnet 4.5",
2712
+ name: "Claude Sonnet 4.5",
2474
2713
  family: "sonnet",
2475
2714
  reasoning: true,
2476
2715
  context: 1e6,
@@ -2480,7 +2719,7 @@ var defaultModels = {
2480
2719
  }),
2481
2720
  "claude-sonnet-4-6": defineModel({
2482
2721
  id: "claude-sonnet-4-6",
2483
- name: "Claude Code Sonnet 4.6",
2722
+ name: "Claude Sonnet 4.6",
2484
2723
  family: "sonnet",
2485
2724
  reasoning: true,
2486
2725
  context: 1e6,
@@ -2490,7 +2729,7 @@ var defaultModels = {
2490
2729
  }),
2491
2730
  "claude-opus-4-5": defineModel({
2492
2731
  id: "claude-opus-4-5",
2493
- name: "Claude Code Opus 4.5",
2732
+ name: "Claude Opus 4.5",
2494
2733
  family: "opus",
2495
2734
  reasoning: true,
2496
2735
  context: 1e6,
@@ -2500,7 +2739,7 @@ var defaultModels = {
2500
2739
  }),
2501
2740
  "claude-opus-4-6": defineModel({
2502
2741
  id: "claude-opus-4-6",
2503
- name: "Claude Code Opus 4.6",
2742
+ name: "Claude Opus 4.6",
2504
2743
  family: "opus",
2505
2744
  reasoning: true,
2506
2745
  context: 1e6,
@@ -2510,7 +2749,7 @@ var defaultModels = {
2510
2749
  }),
2511
2750
  "claude-opus-4-7": defineModel({
2512
2751
  id: "claude-opus-4-7",
2513
- name: "Claude Code Opus 4.7",
2752
+ name: "Claude Opus 4.7",
2514
2753
  family: "opus",
2515
2754
  reasoning: true,
2516
2755
  context: 1e6,
@@ -2569,7 +2808,15 @@ async function ensureAccountRuntime(account, baseCliPath) {
2569
2808
  if (!configDir) return { cliPath: baseCliPath };
2570
2809
  const expandedConfigDir = expandHome(configDir);
2571
2810
  await mkdir(expandedConfigDir, { recursive: true });
2572
- await ensureSharedCapabilities(expandedConfigDir);
2811
+ try {
2812
+ await ensureSharedCapabilities(expandedConfigDir);
2813
+ } catch (err) {
2814
+ log.warn("failed to symlink shared capabilities; continuing anyway", {
2815
+ account,
2816
+ configDir: expandedConfigDir,
2817
+ error: String(err)
2818
+ });
2819
+ }
2573
2820
  const cliPath = await writeAccountWrapper(
2574
2821
  normalizeAccountName(account),
2575
2822
  baseCliPath,
@@ -2675,7 +2922,9 @@ function createClaudeCode(settings = {}) {
2675
2922
  controlRequestBehavior: settings.controlRequestBehavior ?? "allow",
2676
2923
  controlRequestToolBehaviors: settings.controlRequestToolBehaviors,
2677
2924
  controlRequestDenyMessage: settings.controlRequestDenyMessage,
2678
- proxyTools
2925
+ proxyTools,
2926
+ webSearch: settings.webSearch,
2927
+ hotReloadMcp: settings.hotReloadMcp ?? true
2679
2928
  });
2680
2929
  };
2681
2930
  const provider = function(modelId) {
@@ -2742,6 +2991,31 @@ function defaultModelsForProvider(providerModels, providerID = PROVIDER_ID2, mod
2742
2991
  }
2743
2992
  return models;
2744
2993
  }
2994
+ function configModelsForProvider(providerModels, providerID, modelSuffix) {
2995
+ const models = {};
2996
+ for (const [id, model] of Object.entries(defaultModels)) {
2997
+ const modelId = modelSuffix ? `${id}@${modelSuffix}` : id;
2998
+ const existing = providerModels[id] ?? providerModels[modelId];
2999
+ const full = {
3000
+ ...model,
3001
+ id: modelId,
3002
+ providerID,
3003
+ api: {
3004
+ ...model.api,
3005
+ id: modelId,
3006
+ npm: existing?.api?.npm ?? model.api.npm,
3007
+ url: existing?.api?.url ?? model.api.url
3008
+ }
3009
+ };
3010
+ models[modelId] = toConfigModel(full);
3011
+ }
3012
+ for (const [id, model] of Object.entries(providerModels)) {
3013
+ if (!(id in models)) {
3014
+ models[id] = toConfigModel({ ...model, providerID });
3015
+ }
3016
+ }
3017
+ return models;
3018
+ }
2745
3019
  async function providerConfig(existing, providerID = PROVIDER_ID2, optionDefaults = {}, displayName) {
2746
3020
  const mergedOptions = {
2747
3021
  cliPath: "claude",
@@ -2769,47 +3043,80 @@ async function expandAccountProviders(config) {
2769
3043
  if (!accounts) return false;
2770
3044
  config.provider ??= {};
2771
3045
  const seedOptions = cleanProviderOptions(seed?.options);
3046
+ let expandedCount = 0;
2772
3047
  for (const account of accounts) {
2773
3048
  const providerID = accountProviderId(account);
2774
- const existing = config.provider[providerID];
2775
- const modelSuffix = accountModelSuffix(account);
2776
- config.provider[providerID] = {
2777
- ...existing,
2778
- ...await providerConfig(
2779
- existing,
2780
- providerID,
2781
- {
2782
- ...seedOptions,
2783
- account
2784
- },
2785
- accountDisplayName(account)
2786
- ),
2787
- models: defaultModelsForProvider(
2788
- existing?.models ?? seed?.models ?? {},
3049
+ try {
3050
+ const existing = config.provider[providerID];
3051
+ const modelSuffix = accountModelSuffix(account);
3052
+ config.provider[providerID] = {
3053
+ ...existing,
3054
+ ...await providerConfig(
3055
+ existing,
3056
+ providerID,
3057
+ {
3058
+ ...seedOptions,
3059
+ account
3060
+ },
3061
+ accountDisplayName(account)
3062
+ ),
3063
+ models: configModelsForProvider(
3064
+ existing?.models ?? seed?.models ?? {},
3065
+ providerID,
3066
+ modelSuffix
3067
+ )
3068
+ };
3069
+ expandedCount++;
3070
+ } catch (err) {
3071
+ log.error("failed to expand account provider", {
3072
+ account,
2789
3073
  providerID,
2790
- modelSuffix
2791
- )
2792
- };
3074
+ error: String(err)
3075
+ });
3076
+ }
2793
3077
  }
2794
- delete config.provider[PROVIDER_ID2];
2795
- return true;
3078
+ if (expandedCount > 0) {
3079
+ delete config.provider[PROVIDER_ID2];
3080
+ }
3081
+ return expandedCount > 0;
2796
3082
  }
2797
- var server = async () => ({
2798
- config: async (config) => {
2799
- config.provider ??= {};
2800
- const expanded = await expandAccountProviders(config);
2801
- if (expanded) return;
2802
- const existing = config.provider[PROVIDER_ID2];
2803
- config.provider[PROVIDER_ID2] = {
2804
- ...existing,
2805
- ...await providerConfig(existing)
2806
- };
2807
- },
2808
- provider: {
2809
- id: PROVIDER_ID2,
2810
- models: async (provider) => defaultModelsForProvider(provider.models)
3083
+ function readEventType(ev) {
3084
+ if (!ev || typeof ev !== "object") return void 0;
3085
+ const e = ev;
3086
+ if (typeof e.type === "string") return e.type;
3087
+ const payload = e.payload;
3088
+ if (payload && typeof payload === "object") {
3089
+ const t = payload.type;
3090
+ if (typeof t === "string") return t;
3091
+ }
3092
+ return void 0;
3093
+ }
3094
+ var server = async (input) => {
3095
+ if (input && typeof input === "object" && "client" in input) {
3096
+ setOpencodeClient(input.client);
2811
3097
  }
2812
- });
3098
+ return {
3099
+ config: async (config) => {
3100
+ config.provider ??= {};
3101
+ const expanded = await expandAccountProviders(config);
3102
+ if (expanded) return;
3103
+ const existing = config.provider[PROVIDER_ID2];
3104
+ config.provider[PROVIDER_ID2] = {
3105
+ ...existing,
3106
+ ...await providerConfig(existing)
3107
+ };
3108
+ },
3109
+ event: async ({ event }) => {
3110
+ if (readEventType(event) === "global.disposed") {
3111
+ evictAllSessions("global.disposed");
3112
+ }
3113
+ },
3114
+ provider: {
3115
+ id: PROVIDER_ID2,
3116
+ models: async (provider) => defaultModelsForProvider(provider.models)
3117
+ }
3118
+ };
3119
+ };
2813
3120
  var index_default = {
2814
3121
  id: "@khalilgharbaoui/opencode-claude-code-plugin",
2815
3122
  server