@khalilgharbaoui/opencode-claude-code-plugin 0.53.2 → 0.53.3

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.d.ts CHANGED
@@ -1364,7 +1364,7 @@ declare class ClaudeCodeLanguageModel implements LanguageModelV3 {
1364
1364
  private effectiveMcpConfig;
1365
1365
  /** Resolve ProxyToolDef[] for the configured proxyTools names. */
1366
1366
  private resolvedProxyTools;
1367
- /** Resolve ProxyToolDef[] for opencode's MCP-backed tools. */
1367
+ /** Resolve ProxyToolDef[] for opencode's MCP-backed tools, and route the rest. */
1368
1368
  private resolvedProxyMcpTools;
1369
1369
  /** One `client.tool.list()` fetch, shaped for this turn's gates. */
1370
1370
  private fetchLiveToolInfo;
package/dist/index.js CHANGED
@@ -4679,7 +4679,11 @@ function resolveProxyOpencodeToolDefs(options) {
4679
4679
  return out;
4680
4680
  }
4681
4681
  function resolveMcpProxyToolDefs(options) {
4682
- const empty = { defs: [], coveredServers: /* @__PURE__ */ new Set() };
4682
+ const empty = {
4683
+ defs: [],
4684
+ coveredServers: /* @__PURE__ */ new Set(),
4685
+ matchedServers: /* @__PURE__ */ new Set()
4686
+ };
4683
4687
  const serverNames = options.serverNames ?? [];
4684
4688
  if (serverNames.length === 0) return empty;
4685
4689
  const tools = options.tools;
@@ -4688,6 +4692,7 @@ function resolveMcpProxyToolDefs(options) {
4688
4692
  const taken = options.taken ?? /* @__PURE__ */ new Set();
4689
4693
  const defs = [];
4690
4694
  const coveredServers = /* @__PURE__ */ new Set();
4695
+ const matchedServers = /* @__PURE__ */ new Set();
4691
4696
  const seen = /* @__PURE__ */ new Set();
4692
4697
  const collided = [];
4693
4698
  for (const tool of tools) {
@@ -4699,6 +4704,7 @@ function resolveMcpProxyToolDefs(options) {
4699
4704
  (server2) => name === server2 || name.startsWith(`${server2}_`)
4700
4705
  );
4701
4706
  if (!matchedServer) continue;
4707
+ matchedServers.add(matchedServer);
4702
4708
  if (seen.has(name)) continue;
4703
4709
  if (taken.has(name)) {
4704
4710
  collided.push(name);
@@ -4718,7 +4724,7 @@ function resolveMcpProxyToolDefs(options) {
4718
4724
  { collided }
4719
4725
  );
4720
4726
  }
4721
- return { defs, coveredServers };
4727
+ return { defs, coveredServers, matchedServers };
4722
4728
  }
4723
4729
  function readBody(req) {
4724
4730
  return new Promise((resolve6, reject) => {
@@ -5707,7 +5713,7 @@ async function fetchSessionRunState(sessionID, directory) {
5707
5713
  async function settleSessionRunState(sessionID, options = {}) {
5708
5714
  const pollMs = options.pollMs ?? 150;
5709
5715
  const windowMs = options.windowMs ?? 1500;
5710
- const started = Date.now();
5716
+ const started2 = Date.now();
5711
5717
  let last = "unknown";
5712
5718
  let directory = options.directory;
5713
5719
  for (; ; ) {
@@ -5715,7 +5721,7 @@ async function settleSessionRunState(sessionID, options = {}) {
5715
5721
  const state = await fetchSessionRunState(sessionID, directory ?? "");
5716
5722
  if (state === "busy") return "busy";
5717
5723
  if (state !== "unknown") last = state;
5718
- if (Date.now() - started >= windowMs) return last;
5724
+ if (Date.now() - started2 >= windowMs) return last;
5719
5725
  await new Promise((resolve6) => setTimeout(resolve6, pollMs));
5720
5726
  }
5721
5727
  }
@@ -5765,9 +5771,9 @@ async function waitForPendingMcpServers(first, budgetMs, signal) {
5765
5771
  let current = first;
5766
5772
  const waitedFor = pendingMcpServers(current);
5767
5773
  if (waitedFor.length === 0) return current;
5768
- const started = Date.now();
5774
+ const started2 = Date.now();
5769
5775
  for (; ; ) {
5770
- if (Date.now() - started >= budgetMs) break;
5776
+ if (Date.now() - started2 >= budgetMs) break;
5771
5777
  if (signal?.aborted) break;
5772
5778
  await new Promise((resolve6) => setTimeout(resolve6, MCP_CONNECT_POLL_MS));
5773
5779
  if (signal?.aborted) break;
@@ -5781,7 +5787,7 @@ async function waitForPendingMcpServers(first, budgetMs, signal) {
5781
5787
  waitedFor,
5782
5788
  stillPending,
5783
5789
  budgetMs,
5784
- elapsedMs: Date.now() - started,
5790
+ elapsedMs: Date.now() - started2,
5785
5791
  aborted: signal?.aborted === true
5786
5792
  });
5787
5793
  return current;
@@ -5909,6 +5915,56 @@ function clearBackgroundTasks(sessionKey2) {
5909
5915
  collected.delete(sessionKey2);
5910
5916
  cancelled.delete(sessionKey2);
5911
5917
  }
5918
+ var MAX_STARTED = 128;
5919
+ var started = /* @__PURE__ */ new Map();
5920
+ function backgroundTaskIdsIn(text) {
5921
+ const ids = /* @__PURE__ */ new Set();
5922
+ for (const match of text.matchAll(/<task id="([^"\s]+)" state="running">/g)) {
5923
+ ids.add(match[1]);
5924
+ }
5925
+ for (const match of text.matchAll(/in the background \(sessionID: ([^)\s]+)\)/g)) {
5926
+ ids.add(match[1]);
5927
+ }
5928
+ return [...ids];
5929
+ }
5930
+ function noteBackgroundDispatchResult(sessionKey2, result, now = Date.now()) {
5931
+ if (result.kind !== "text" || result.isError === true) return;
5932
+ for (const taskId of backgroundTaskIdsIn(result.text)) {
5933
+ started.delete(taskId);
5934
+ started.set(taskId, { sessionKey: sessionKey2, at: now });
5935
+ while (started.size > MAX_STARTED) {
5936
+ const oldest = started.keys().next();
5937
+ if (oldest.done) break;
5938
+ started.delete(oldest.value);
5939
+ }
5940
+ }
5941
+ }
5942
+ async function countRunningBackgroundTasks() {
5943
+ const entries = [...started.entries()];
5944
+ const cancelledIds = /* @__PURE__ */ new Set();
5945
+ for (const ids of cancelled.values()) for (const id of ids) cancelledIds.add(id);
5946
+ const states = await Promise.all(
5947
+ entries.map(async ([taskId]) => {
5948
+ if (cancelledIds.has(taskId)) return "stopped";
5949
+ try {
5950
+ const runState = await fetchSessionRunState(taskId);
5951
+ if (runState !== "unknown") {
5952
+ return isBackgroundTaskRunning(runState, []) ? "running" : "stopped";
5953
+ }
5954
+ const replies = await fetchSessionReplies(taskId);
5955
+ if (replies === void 0) return "unreadable";
5956
+ return isBackgroundTaskRunning(runState, replies) ? "running" : "stopped";
5957
+ } catch {
5958
+ return "unreadable";
5959
+ }
5960
+ })
5961
+ );
5962
+ return {
5963
+ running: states.filter((state) => state === "running").length,
5964
+ started: entries.length,
5965
+ unreadable: states.filter((state) => state === "unreadable").length
5966
+ };
5967
+ }
5912
5968
  var lastGate;
5913
5969
  function recordBackgroundSubagentGate(gate, now = Date.now()) {
5914
5970
  lastGate = { ...gate, at: now };
@@ -7635,21 +7691,21 @@ async function sessionStatus(client, sessionID) {
7635
7691
  async function waitForSessionIdle(client, sessionID, options = {}) {
7636
7692
  const pollMs = options.pollMs ?? IDLE_POLL_MS;
7637
7693
  const timeoutMs = options.timeoutMs ?? IDLE_WAIT_MAX_MS;
7638
- const started = Date.now();
7694
+ const started2 = Date.now();
7639
7695
  for (; ; ) {
7640
7696
  if (options.stop?.()) return true;
7641
7697
  if (await sessionStatus(client, sessionID) !== "busy") return true;
7642
- if (Date.now() - started >= timeoutMs) return false;
7698
+ if (Date.now() - started2 >= timeoutMs) return false;
7643
7699
  await new Promise((resolve6) => setTimeout(resolve6, pollMs));
7644
7700
  }
7645
7701
  }
7646
7702
  async function deliverAsideInline(client, sessionID, text, options = {}) {
7647
7703
  const pollMs = options.inlinePollMs ?? INLINE_POLL_MS;
7648
7704
  const timeoutMs = options.inlineWaitMs ?? INLINE_WAIT_MAX_MS;
7649
- const started = Date.now();
7705
+ const started2 = Date.now();
7650
7706
  for (; ; ) {
7651
7707
  if (emitAsideInline(sessionID, text)) return true;
7652
- if (Date.now() - started >= timeoutMs) return false;
7708
+ if (Date.now() - started2 >= timeoutMs) return false;
7653
7709
  if (await sessionStatus(client, sessionID) !== "busy") return false;
7654
7710
  await new Promise((resolve6) => setTimeout(resolve6, pollMs));
7655
7711
  }
@@ -7658,12 +7714,12 @@ async function waitForAsideProcess(client, sessionID, options = {}) {
7658
7714
  const pollMs = options.pollMs ?? IDLE_POLL_MS;
7659
7715
  const settleMs = options.settleMs ?? BUSY_SETTLE_MS;
7660
7716
  const spawnWaitMs = options.spawnWaitMs ?? SPAWN_WAIT_MAX_MS;
7661
- const started = Date.now();
7717
+ const started2 = Date.now();
7662
7718
  for (; ; ) {
7663
7719
  const active = findActiveProcessBySessionId(sessionID);
7664
7720
  if (active) return active;
7665
7721
  const busy = await sessionStatus(client, sessionID) === "busy";
7666
- const waitedMs = Date.now() - started;
7722
+ const waitedMs = Date.now() - started2;
7667
7723
  if (!busy && waitedMs >= settleMs) {
7668
7724
  log.info("btw: no live claude process for session, leaving it to the turn", { sessionID, waitedMs });
7669
7725
  return void 0;
@@ -7678,12 +7734,12 @@ async function waitForAsideProcess(client, sessionID, options = {}) {
7678
7734
  async function settleSessionBusy(client, sessionID, active, options = {}) {
7679
7735
  const pollMs = options.pollMs ?? IDLE_POLL_MS;
7680
7736
  const settleMs = options.settleMs ?? BUSY_SETTLE_MS;
7681
- const started = Date.now();
7737
+ const started2 = Date.now();
7682
7738
  for (; ; ) {
7683
7739
  const status = await sessionStatus(client, sessionID);
7684
7740
  if (status === "busy") return true;
7685
7741
  if (status === "unknown") return isProcessBusy(active);
7686
- if (Date.now() - started >= settleMs) return false;
7742
+ if (Date.now() - started2 >= settleMs) return false;
7687
7743
  await new Promise((resolve6) => setTimeout(resolve6, pollMs));
7688
7744
  }
7689
7745
  }
@@ -7787,7 +7843,7 @@ async function handleBtwCommand(client, input, options = {}) {
7787
7843
  );
7788
7844
  }
7789
7845
  if (!busy) return;
7790
- const started = Date.now();
7846
+ const started2 = Date.now();
7791
7847
  const outcome = await Promise.race([
7792
7848
  inlineDelivered,
7793
7849
  waitForSessionIdle(client, input.sessionID, { ...options, stop: () => inlineDone }).then(
@@ -7797,14 +7853,14 @@ async function handleBtwCommand(client, input, options = {}) {
7797
7853
  if (outcome === "inline") {
7798
7854
  log.info("btw: answered inside the running turn, dropping the /btw message", {
7799
7855
  sessionID: input.sessionID,
7800
- waitedMs: Date.now() - started
7856
+ waitedMs: Date.now() - started2
7801
7857
  });
7802
7858
  throw new BtwHandledError(BTW_INLINE_HANDLED_MESSAGE);
7803
7859
  }
7804
7860
  log.info("btw: turn over, releasing the /btw message", {
7805
7861
  sessionID: input.sessionID,
7806
7862
  idle: outcome === "idle",
7807
- waitedMs: Date.now() - started
7863
+ waitedMs: Date.now() - started2
7808
7864
  });
7809
7865
  if (outcome === "timeout") {
7810
7866
  showToast(client, { title: "btw", message: BTW_TURN_TOO_LONG_MESSAGE, variant: "warning", duration: 8e3 });
@@ -7933,6 +7989,7 @@ var PLUGIN_LOG_MESSAGES = /* @__PURE__ */ new Set([
7933
7989
  "proxy-mcp interceptor failed",
7934
7990
  "proxy-mcp rejected a request",
7935
7991
  "proxy-mcp tool call timed out",
7992
+ "proxyOpencodeMcpTools cannot route V2 Code Mode's MCP tools, which reach the model through execute; for Code Mode, explicitly allowlist execute in proxyOpencodeTools and use bridgeOpencodeMcp: false with strictMcpConfig: true",
7936
7993
  "proxyOpencodeTools entry dropped: a proxy tool already holds that name, and it keeps it",
7937
7994
  "proxyOpencodeTools is set but opencode's tool registry did not answer; forwarding nothing this spawn",
7938
7995
  "refusing the pid-named scratch directory",
@@ -9168,6 +9225,10 @@ function formatDoctorReport(report) {
9168
9225
  }
9169
9226
  function formatBackgroundSubagents(state) {
9170
9227
  const lines = [];
9228
+ if (state.running) {
9229
+ lines.push(...formatRunningCount(state.running));
9230
+ lines.push("");
9231
+ }
9171
9232
  const gate = state.gate;
9172
9233
  if (!gate) {
9173
9234
  lines.push(
@@ -9213,6 +9274,19 @@ function formatBackgroundSubagents(state) {
9213
9274
  }
9214
9275
  return lines;
9215
9276
  }
9277
+ function formatRunningCount(count) {
9278
+ if (count.started === 0) {
9279
+ return ["running now: 0 (none started by this process)"];
9280
+ }
9281
+ const lines = [`running now: ${count.running} (of ${count.started} started by this process)`];
9282
+ if (count.unreadable > 0) {
9283
+ lines.push("");
9284
+ lines.push(
9285
+ `${count.unreadable} could not be read from opencode (deleted, or no session route on this build) and are not counted as running.`
9286
+ );
9287
+ }
9288
+ return lines;
9289
+ }
9216
9290
  function describeBackgroundGateSource(gate) {
9217
9291
  if (gate.hostApi === "v2") {
9218
9292
  return "opencode 2 offers `background` unconditionally, so the registry is not consulted";
@@ -9297,7 +9371,8 @@ async function gatherDoctorReport(options) {
9297
9371
  planUsage,
9298
9372
  backgroundSubagents: {
9299
9373
  gate: snapshotBackgroundSubagentGate(),
9300
- ledgers: snapshotBackgroundTasks()
9374
+ ledgers: snapshotBackgroundTasks(),
9375
+ running: await countRunningBackgroundTasks().catch(() => void 0)
9301
9376
  }
9302
9377
  };
9303
9378
  }
@@ -11968,12 +12043,12 @@ function spawnInteractiveProcess(opts) {
11968
12043
  }
11969
12044
  }
11970
12045
  closeProxyServer();
11971
- const started = startPromise !== null;
12046
+ const started2 = startPromise !== null;
11972
12047
  try {
11973
12048
  session.dispose();
11974
12049
  } catch {
11975
12050
  }
11976
- if (!started && proc.exitCode === null && proc.signalCode === null) {
12051
+ if (!started2 && proc.exitCode === null && proc.signalCode === null) {
11977
12052
  proc.signalCode = "SIGTERM";
11978
12053
  proc.emit("exit", null, "SIGTERM");
11979
12054
  proc.emit("close", null, "SIGTERM");
@@ -12737,7 +12812,13 @@ function resolvedProxyTools(config) {
12737
12812
  }
12738
12813
  return picked.length > 0 ? picked : null;
12739
12814
  }
12740
- function resolvedProxyMcpTools(config, allEnabledServerNames, modelTools, taken) {
12815
+ function decideMcpServerRoute(input) {
12816
+ if (input.covered) return "proxy";
12817
+ if (input.inToolSet) return "bridge";
12818
+ if (input.toolSetKnown && input.status === "connected") return "withhold";
12819
+ return "bridge";
12820
+ }
12821
+ function resolvedProxyMcpTools(config, allEnabledServerNames, modelTools, taken, runtimeStatus) {
12741
12822
  if (config.proxyOpencodeMcpTools !== true) return null;
12742
12823
  if (config.bridgeOpencodeMcp === false) return null;
12743
12824
  if (allEnabledServerNames.length === 0) return null;
@@ -12746,18 +12827,47 @@ function resolvedProxyMcpTools(config, allEnabledServerNames, modelTools, taken)
12746
12827
  tools: modelTools,
12747
12828
  taken
12748
12829
  });
12749
- if (resolution.defs.length === 0) {
12830
+ const codeMode = config.hostApi === "v2" && (modelTools?.some((tool) => tool.name === "execute") ?? false);
12831
+ const toolSetKnown = (modelTools?.length ?? 0) > 0 && !codeMode;
12832
+ const withheld = [];
12833
+ const bridged = [];
12834
+ for (const server2 of allEnabledServerNames) {
12835
+ const route = decideMcpServerRoute({
12836
+ covered: resolution.coveredServers.has(server2),
12837
+ inToolSet: resolution.matchedServers.has(server2),
12838
+ status: runtimeStatus?.[server2],
12839
+ toolSetKnown
12840
+ });
12841
+ if (route === "withhold") withheld.push(server2);
12842
+ else if (route === "bridge") bridged.push(server2);
12843
+ }
12844
+ if (codeMode && resolution.defs.length === 0) {
12750
12845
  log.warn(
12751
- "proxyOpencodeMcpTools is on but no MCP tool was found in opencode's tool set; those servers stay on the direct bridge this spawn" + (config.hostApi === "v2" && modelTools?.some((tool) => tool.name === "execute") ? ". For V2 Code Mode, explicitly allowlist execute in proxyOpencodeTools and use bridgeOpencodeMcp: false with strictMcpConfig: true" : ""),
12752
- { servers: allEnabledServerNames, modelTools: modelTools?.length ?? 0 }
12846
+ "proxyOpencodeMcpTools cannot route V2 Code Mode's MCP tools, which reach the model through execute; for Code Mode, explicitly allowlist execute in proxyOpencodeTools and use bridgeOpencodeMcp: false with strictMcpConfig: true",
12847
+ { servers: allEnabledServerNames }
12753
12848
  );
12754
- return null;
12755
12849
  }
12756
- log.debug("routing opencode MCP tools through the proxy", {
12757
- servers: [...resolution.coveredServers],
12758
- tools: resolution.defs.map((def) => def.name)
12759
- });
12760
- return resolution;
12850
+ if (withheld.length > 0 || bridged.length > 0) {
12851
+ log.info("proxyOpencodeMcpTools: MCP servers outside this agent's tool set", {
12852
+ proxied: [...resolution.coveredServers],
12853
+ withheld,
12854
+ bridged,
12855
+ modelTools: modelTools?.length ?? 0,
12856
+ statusKnown: runtimeStatus !== void 0
12857
+ });
12858
+ } else {
12859
+ log.debug("routing opencode MCP tools through the proxy", {
12860
+ servers: [...resolution.coveredServers],
12861
+ tools: resolution.defs.map((def) => def.name)
12862
+ });
12863
+ }
12864
+ const exclude = /* @__PURE__ */ new Set([...resolution.coveredServers, ...withheld]);
12865
+ return {
12866
+ resolution,
12867
+ withheld,
12868
+ bridged,
12869
+ excludeServers: exclude.size > 0 ? exclude : void 0
12870
+ };
12761
12871
  }
12762
12872
  async function fetchLiveToolInfo(config, modelId) {
12763
12873
  const items = await fetchOpencodeToolList(
@@ -14684,13 +14794,14 @@ var ClaudeCodeLanguageModel = class {
14684
14794
  resolvedProxyTools() {
14685
14795
  return resolvedProxyTools(this.config);
14686
14796
  }
14687
- /** Resolve ProxyToolDef[] for opencode's MCP-backed tools. */
14688
- resolvedProxyMcpTools(allEnabledServerNames, modelTools, taken) {
14797
+ /** Resolve ProxyToolDef[] for opencode's MCP-backed tools, and route the rest. */
14798
+ resolvedProxyMcpTools(allEnabledServerNames, modelTools, taken, runtimeStatus) {
14689
14799
  return resolvedProxyMcpTools(
14690
14800
  this.config,
14691
14801
  allEnabledServerNames,
14692
14802
  modelTools,
14693
- taken
14803
+ taken,
14804
+ runtimeStatus
14694
14805
  );
14695
14806
  }
14696
14807
  /** One `client.tool.list()` fetch, shaped for this turn's gates. */
@@ -15999,13 +16110,14 @@ var ClaudeCodeLanguageModel = class {
15999
16110
  void 0,
16000
16111
  runtimeStatus
16001
16112
  );
16002
- const mcpResolution = self.resolvedProxyMcpTools(
16113
+ const mcpRouting = self.resolvedProxyMcpTools(
16003
16114
  discovery.allEnabledServerNames,
16004
16115
  options.tools,
16005
- new Set((resolvedProxy ?? []).map((def) => def.name))
16116
+ new Set((resolvedProxy ?? []).map((def) => def.name)),
16117
+ runtimeStatus
16006
16118
  );
16007
- const proxyMcpTools = mcpResolution?.defs ?? null;
16008
- const excludeServers = mcpResolution ? mcpResolution.coveredServers : void 0;
16119
+ const proxyMcpTools = mcpRouting && mcpRouting.resolution.defs.length > 0 ? mcpRouting.resolution.defs : null;
16120
+ const excludeServers = mcpRouting?.excludeServers;
16009
16121
  const taskProxyEnabled = resolvedProxy?.some((t) => t.name === "task") ?? false;
16010
16122
  const questionProxyEnabled = resolvedProxy?.some((t) => t.name === "question") ?? false;
16011
16123
  const opencodeToolsRequested = (self.config.proxyOpencodeTools?.length ?? 0) > 0;
@@ -16861,6 +16973,9 @@ var ClaudeCodeLanguageModel = class {
16861
16973
  recoveryRequired: channelClosed || state.unattendedTurnEnded
16862
16974
  });
16863
16975
  }
16976
+ if (call.toolName === "task" || call.toolName === TASK_BATCH_TOOL_NAME) {
16977
+ noteBackgroundDispatchResult(sk, result);
16978
+ }
16864
16979
  resolvePendingProxyCallById(call.toolCallId, result);
16865
16980
  } else {
16866
16981
  log.info(