@khalilgharbaoui/opencode-claude-code-plugin 0.32.0 → 0.33.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
@@ -1633,12 +1633,12 @@ function setProxyDeadlineGuard(guard) {
1633
1633
  var PROXY_DEADLINE_RECHECK_MS = 6e4;
1634
1634
  var deadlineRecheckMs = PROXY_DEADLINE_RECHECK_MS;
1635
1635
  function armProxyDeadline(opts) {
1636
- let cancelled = false;
1636
+ let cancelled2 = false;
1637
1637
  let extended = false;
1638
1638
  let timer = null;
1639
1639
  const check = async () => {
1640
1640
  timer = null;
1641
- if (cancelled) return;
1641
+ if (cancelled2) return;
1642
1642
  let keep = false;
1643
1643
  const guard = proxyDeadlineGuard;
1644
1644
  if (guard) {
@@ -1648,7 +1648,7 @@ function armProxyDeadline(opts) {
1648
1648
  keep = false;
1649
1649
  }
1650
1650
  }
1651
- if (cancelled) return;
1651
+ if (cancelled2) return;
1652
1652
  if (keep) {
1653
1653
  if (!extended && opts.logExtension) {
1654
1654
  log.warn("proxy call past its deadline, but opencode is still serving it; waiting", {
@@ -1667,7 +1667,7 @@ function armProxyDeadline(opts) {
1667
1667
  timer = setTimeout(() => void check(), opts.deadlineMs);
1668
1668
  return {
1669
1669
  cancel() {
1670
- cancelled = true;
1670
+ cancelled2 = true;
1671
1671
  if (timer) clearTimeout(timer);
1672
1672
  timer = null;
1673
1673
  }
@@ -1685,6 +1685,7 @@ function buildProxyTimeoutError(toolName, ms) {
1685
1685
  }
1686
1686
  var TASK_PROXY_NOTE = "This and task_batch are the ONLY tools that dispatch opencode subagents (including user @-mentions). Claude Code's built-in TaskCreate/TaskUpdate manage a local todo list and cannot dispatch subagents. Do not search config files to verify a subagent type exists: invalid types fail fast with a clear error. Calls block until the subagent finishes. For two or more independent subagents in one response use task_batch, not several task calls: those run one after another. Task calls have no proxy deadline by default: the call waits for the subagent to finish (a positive proxyToolTimeoutMs override adds a deadline).";
1687
1687
  var TASK_BACKGROUND_NOTE = 'Fire-and-collect: set `background: true` and the call returns at once with `<task id="ses_..." state="running">` instead of the subagent\'s answer. Keep working on something that does not overlap it, then end your turn: when the subagent finishes, opencode delivers its result to this conversation on its own as a new message. Do NOT poll, sleep or loop waiting for it. The `id` in that envelope is the task_id: pass it to task_status to read a result the notification did not deliver, or to task_cancel to stop a background subagent you no longer want.';
1688
+ var TASK_BACKGROUND_NOTE_V2 = 'Fire-and-collect: set `background: true` and the call returns at once with `The subagent is working in the background (sessionID: ses_...)` instead of the subagent\'s answer. Keep working on something that does not overlap it, then end your turn: when the subagent finishes, opencode delivers its result to this conversation on its own, as a `<subagent sessionID="..." state="completed">` message. Do NOT poll, sleep or loop waiting for it. That sessionID is the task_id: pass it to task_status to read a result the notification did not deliver, or to task_cancel to stop a background subagent you no longer want.';
1688
1689
  var TASK_BATCH_TOOL_NAME = "task_batch";
1689
1690
  var TASK_BATCH_PROXY_NOTE = "Use this instead of several task calls in one response: Claude Code runs MCP tool calls one at a time, so separate task calls run serially even when emitted together, while one task_batch call fans them out as parallel opencode task calls. Each task takes the same fields as the task tool. Results come back in task order, each labelled. Like task it has no proxy deadline by default (a positive proxyToolTimeoutMs override adds one).";
1690
1691
  var TASK_INPUT_REQUIRED = ["description", "prompt", "subagent_type"];
@@ -1785,7 +1786,7 @@ var BACKGROUND_TASK_TOOL_DEFS = [
1785
1786
  properties: {
1786
1787
  task_id: {
1787
1788
  type: "string",
1788
- description: 'The id from the `<task id="..." state="running">` envelope the background dispatch returned.'
1789
+ description: 'The background subagent\'s own opencode session id: the `id` in the `<task id="..." state="running">` envelope on opencode 1.x, or the `sessionID` the background dispatch reported on opencode 2.'
1789
1790
  }
1790
1791
  },
1791
1792
  required: ["task_id"]
@@ -1799,21 +1800,22 @@ var BACKGROUND_TASK_TOOL_DEFS = [
1799
1800
  properties: {
1800
1801
  task_id: {
1801
1802
  type: "string",
1802
- description: 'The id from the `<task id="..." state="running">` envelope the background dispatch returned.'
1803
+ description: 'The background subagent\'s own opencode session id: the `id` in the `<task id="..." state="running">` envelope on opencode 1.x, or the `sessionID` the background dispatch reported on opencode 2.'
1803
1804
  }
1804
1805
  },
1805
1806
  required: ["task_id"]
1806
1807
  }
1807
1808
  }
1808
1809
  ];
1809
- function applyBackgroundSubagentSupport(tools, supported) {
1810
+ function applyBackgroundSubagentSupport(tools, supported, dialect = "v1") {
1810
1811
  if (!tools.some((t) => t.name === "task")) return tools;
1812
+ const note = dialect === "v2" ? TASK_BACKGROUND_NOTE_V2 : TASK_BACKGROUND_NOTE;
1811
1813
  const adjusted = tools.map((t) => {
1812
1814
  if (t.name !== "task" && t.name !== TASK_BATCH_TOOL_NAME) return t;
1813
1815
  if (supported) {
1814
1816
  return { ...t, description: `${t.description}
1815
1817
 
1816
- ${TASK_BACKGROUND_NOTE}` };
1818
+ ${note}` };
1817
1819
  }
1818
1820
  return { ...t, inputSchema: stripBackgroundFromSchema(t.name, t.inputSchema) };
1819
1821
  });
@@ -2432,7 +2434,9 @@ function resolveDisallowedTools(options) {
2432
2434
  function resolveProxyOpencodeToolDefs(options) {
2433
2435
  const requested = options.requested ?? [];
2434
2436
  if (requested.length === 0) return [];
2435
- const items = options.items;
2437
+ const items = options.modelTools !== void 0 ? options.modelTools.flatMap(
2438
+ (tool) => (tool.type === void 0 || tool.type === "function") && typeof tool.name === "string" ? [{ id: tool.name, description: tool.description, parameters: tool.inputSchema }] : []
2439
+ ) : options.items;
2436
2440
  if (!items) {
2437
2441
  log.warn(
2438
2442
  "proxyOpencodeTools is set but opencode's tool registry did not answer; forwarding nothing this spawn",
@@ -2458,12 +2462,20 @@ function resolveProxyOpencodeToolDefs(options) {
2458
2462
  unknown.push(name);
2459
2463
  continue;
2460
2464
  }
2465
+ if (item.id === "execute" && options.allowCodeExecution === false) {
2466
+ log.warn("Code Mode execute not forwarded: read-only permission preset");
2467
+ continue;
2468
+ }
2461
2469
  if (taken.has(item.id)) {
2462
2470
  collided.push(item.id);
2463
2471
  continue;
2464
2472
  }
2465
2473
  if (seen.has(item.id)) continue;
2466
2474
  seen.add(item.id);
2475
+ if (item.id === "execute" && (!item.parameters || typeof item.parameters !== "object" || Array.isArray(item.parameters) || !Object.keys(item.parameters).length)) {
2476
+ log.warn("Code Mode execute not forwarded: no model-visible input schema");
2477
+ continue;
2478
+ }
2467
2479
  out.push({
2468
2480
  name: item.id,
2469
2481
  description: typeof item.description === "string" ? item.description : "",
@@ -2500,6 +2512,7 @@ function resolveMcpProxyToolDefs(options) {
2500
2512
  if (tool?.type !== void 0 && tool.type !== "function") continue;
2501
2513
  const name = typeof tool?.name === "string" ? tool.name.trim() : "";
2502
2514
  if (!name) continue;
2515
+ if (name === "execute") continue;
2503
2516
  const matchedServer = serversByLengthDesc.find(
2504
2517
  (server2) => name === server2 || name.startsWith(`${server2}_`)
2505
2518
  );
@@ -3132,8 +3145,8 @@ async function abortSession(sessionID) {
3132
3145
  const client = opencodeClient;
3133
3146
  if (!client?.session?.abort) return false;
3134
3147
  try {
3135
- await client.session.abort({ path: { id: sessionID } });
3136
- return true;
3148
+ const res = await client.session.abort({ path: { id: sessionID } });
3149
+ return res.data !== false;
3137
3150
  } catch (err) {
3138
3151
  log.warn("failed to abort opencode session", {
3139
3152
  sessionID,
@@ -3149,21 +3162,22 @@ var TASK_CANCEL_TOOL_NAME = "task_cancel";
3149
3162
  var MAX_LEDGERS = 32;
3150
3163
  var MAX_COLLECTED_PER_SESSION = 64;
3151
3164
  var collected = /* @__PURE__ */ new Map();
3152
- function ledgerFor(sessionKey2) {
3153
- let ids = collected.get(sessionKey2);
3165
+ var cancelled = /* @__PURE__ */ new Map();
3166
+ function ledgerIn(ledgers2, sessionKey2) {
3167
+ let ids = ledgers2.get(sessionKey2);
3154
3168
  if (!ids) {
3155
3169
  ids = /* @__PURE__ */ new Set();
3156
- collected.set(sessionKey2, ids);
3157
- while (collected.size > MAX_LEDGERS) {
3158
- const oldest = collected.keys().next();
3170
+ ledgers2.set(sessionKey2, ids);
3171
+ while (ledgers2.size > MAX_LEDGERS) {
3172
+ const oldest = ledgers2.keys().next();
3159
3173
  if (oldest.done) break;
3160
- collected.delete(oldest.value);
3174
+ ledgers2.delete(oldest.value);
3161
3175
  }
3162
3176
  }
3163
3177
  return ids;
3164
3178
  }
3165
- function markCollected(sessionKey2, taskId) {
3166
- const ids = ledgerFor(sessionKey2);
3179
+ function mark(ledgers2, sessionKey2, taskId) {
3180
+ const ids = ledgerIn(ledgers2, sessionKey2);
3167
3181
  ids.add(taskId);
3168
3182
  while (ids.size > MAX_COLLECTED_PER_SESSION) {
3169
3183
  const oldest = ids.values().next();
@@ -3171,11 +3185,30 @@ function markCollected(sessionKey2, taskId) {
3171
3185
  ids.delete(oldest.value);
3172
3186
  }
3173
3187
  }
3188
+ function markCollected(sessionKey2, taskId) {
3189
+ mark(collected, sessionKey2, taskId);
3190
+ }
3174
3191
  function hasCollectedBackgroundTask(sessionKey2, taskId) {
3175
3192
  return collected.get(sessionKey2)?.has(taskId) === true;
3176
3193
  }
3177
3194
  function clearBackgroundTasks(sessionKey2) {
3178
3195
  collected.delete(sessionKey2);
3196
+ cancelled.delete(sessionKey2);
3197
+ }
3198
+ var lastGate;
3199
+ function recordBackgroundSubagentGate(gate, now = Date.now()) {
3200
+ lastGate = { ...gate, at: now };
3201
+ }
3202
+ function snapshotBackgroundSubagentGate() {
3203
+ return lastGate ? { ...lastGate } : void 0;
3204
+ }
3205
+ function snapshotBackgroundTasks() {
3206
+ const keys = /* @__PURE__ */ new Set([...collected.keys(), ...cancelled.keys()]);
3207
+ return [...keys].map((sessionKey2) => ({
3208
+ sessionKey: sessionKey2,
3209
+ collected: [...collected.get(sessionKey2) ?? []],
3210
+ cancelled: [...cancelled.get(sessionKey2) ?? []]
3211
+ }));
3179
3212
  }
3180
3213
  function readTaskId(input) {
3181
3214
  const raw = input.task_id ?? input.taskId ?? input.sessionID;
@@ -3183,14 +3216,20 @@ function readTaskId(input) {
3183
3216
  const id = raw.trim();
3184
3217
  return id.length > 0 ? id : null;
3185
3218
  }
3186
- var MISSING_TASK_ID = 'task_id is required: pass the id from the `<task id="...">` envelope the background dispatch returned.';
3219
+ var MISSING_TASK_ID = 'task_id is required: pass the background subagent\'s own opencode session id, from the `<task id="...">` envelope on opencode 1.x or the `sessionID` the background dispatch reported on opencode 2.';
3187
3220
  async function guardParent(taskId, callerSessionId) {
3188
3221
  if (!callerSessionId || callerSessionId === "default") {
3189
3222
  return `Cannot check that task_id ${taskId} belongs to this conversation (this turn has no opencode session id), so it is not inspected or cancelled.`;
3190
3223
  }
3191
3224
  const parent = await fetchSessionParentId(taskId);
3192
3225
  if (parent === callerSessionId) return null;
3193
- return `task_id ${taskId} is not a subagent of this conversation, so it cannot be inspected or cancelled from here. Use the id from a \`<task id="...">\` envelope this conversation received.`;
3226
+ return `task_id ${taskId} is not a subagent of this conversation, so it cannot be inspected or cancelled from here. Use the session id from a background dispatch this conversation itself made.`;
3227
+ }
3228
+ function isBackgroundTaskRunning(runState, replies) {
3229
+ if (runState === "busy") return true;
3230
+ if (runState === "idle") return false;
3231
+ const reply = lastAssistantReply(replies);
3232
+ return reply !== void 0 && !reply.completed && reply.error === void 0;
3194
3233
  }
3195
3234
  function lastAssistantReply(replies) {
3196
3235
  for (let index = replies.length - 1; index >= 0; index--) {
@@ -3212,7 +3251,7 @@ async function collectBackgroundTask(input, options) {
3212
3251
  };
3213
3252
  }
3214
3253
  const runState = await fetchSessionRunState(taskId);
3215
- if (runState === "busy") {
3254
+ if (isBackgroundTaskRunning(runState, replies)) {
3216
3255
  return {
3217
3256
  kind: "text",
3218
3257
  text: `<task id="${taskId}" state="running">
@@ -3267,7 +3306,10 @@ async function cancelBackgroundTask(input, options) {
3267
3306
  if (!taskId) return { kind: "error", message: MISSING_TASK_ID };
3268
3307
  const refused = await guardParent(taskId, options.callerSessionId);
3269
3308
  if (refused) return { kind: "error", message: refused };
3270
- const before = await fetchSessionRunState(taskId);
3309
+ const running = isBackgroundTaskRunning(
3310
+ await fetchSessionRunState(taskId),
3311
+ await fetchSessionReplies(taskId) ?? []
3312
+ );
3271
3313
  const aborted = await abortSession(taskId);
3272
3314
  if (!aborted) {
3273
3315
  return {
@@ -3276,15 +3318,16 @@ async function cancelBackgroundTask(input, options) {
3276
3318
  };
3277
3319
  }
3278
3320
  collected.get(options.sessionKey)?.delete(taskId);
3321
+ mark(cancelled, options.sessionKey, taskId);
3279
3322
  log.info("cancelled background subagent", {
3280
3323
  sessionKey: options.sessionKey,
3281
3324
  taskId,
3282
- wasRunning: before === "busy"
3325
+ wasRunning: running
3283
3326
  });
3284
3327
  return {
3285
3328
  kind: "text",
3286
3329
  text: `<task id="${taskId}" state="cancelled">
3287
- ` + (before === "busy" ? "Stopped. No completion notification will arrive for it." : "It was not running when the cancel was sent, so nothing was interrupted. No completion notification will arrive for it.") + "\n</task>"
3330
+ ` + (running ? "Stopped. No completion notification will arrive for it." : "It was not running when the cancel was sent, so nothing was interrupted. No completion notification will arrive for it.") + "\n</task>"
3288
3331
  };
3289
3332
  }
3290
3333
 
@@ -4677,24 +4720,24 @@ function globalConfigDir() {
4677
4720
  const xdg = process.env.XDG_CONFIG_HOME ?? path4.join(os2.homedir(), ".config");
4678
4721
  return path4.join(xdg, "opencode");
4679
4722
  }
4680
- function loadGlobalConfig() {
4723
+ function loadGlobalConfig(hostApi = "v1") {
4681
4724
  const dir = globalConfigDir();
4682
4725
  let merged = {};
4683
4726
  for (const name of FILE_NAMES.slice().reverse()) {
4684
4727
  const file = path4.join(dir, name);
4685
4728
  if (!fileExists(file)) continue;
4686
4729
  const parsed = readAndParse(file);
4687
- if (parsed) merged = deepMerge(merged, parsed);
4730
+ if (parsed) merged = mergeConfigLayer(merged, parsed, hostApi);
4688
4731
  }
4689
4732
  return merged;
4690
4733
  }
4691
- function loadProjectFilesInDir(dir) {
4734
+ function loadProjectFilesInDir(dir, hostApi = "v1") {
4692
4735
  let merged = {};
4693
4736
  for (const name of PROJECT_FILE_NAMES) {
4694
4737
  const file = path4.join(dir, name);
4695
4738
  if (!fileExists(file)) continue;
4696
4739
  const parsed = readAndParse(file);
4697
- if (parsed) merged = deepMerge(merged, parsed);
4740
+ if (parsed) merged = mergeConfigLayer(merged, parsed, hostApi);
4698
4741
  }
4699
4742
  return merged;
4700
4743
  }
@@ -4781,8 +4824,29 @@ function translateServer(name, spec) {
4781
4824
  }
4782
4825
  function extractMcpBlock(config) {
4783
4826
  const mcp = config.mcp;
4784
- if (!mcp || typeof mcp !== "object" || Array.isArray(mcp)) return {};
4785
- return mcp;
4827
+ if (!isPlainObject(mcp)) return {};
4828
+ if ("servers" in mcp && !isPlainObject(mcp.servers)) return {};
4829
+ const native = isPlainObject(mcp.servers) && typeof mcp.servers.type !== "string" && typeof mcp.servers.enabled !== "boolean" && typeof mcp.servers.disabled !== "boolean";
4830
+ const block = native ? mcp.servers : mcp;
4831
+ return Object.fromEntries(
4832
+ Object.entries(block).filter(([name, spec]) => isPlainObject(spec) && (native || name !== "timeout" || typeof spec.type === "string")).map(([name, spec]) => {
4833
+ const server2 = spec;
4834
+ return [name, {
4835
+ ...server2,
4836
+ ...typeof server2.disabled === "boolean" ? { enabled: !server2.disabled } : {}
4837
+ }];
4838
+ })
4839
+ );
4840
+ }
4841
+ function mergeConfigLayer(target, source, hostApi) {
4842
+ const merged = deepMerge(target, source);
4843
+ if (hostApi === "v2" && isPlainObject(source.mcp)) {
4844
+ merged.mcp = {
4845
+ ...isPlainObject(merged.mcp) ? merged.mcp : {},
4846
+ servers: { ...extractMcpBlock(target), ...extractMcpBlock(source) }
4847
+ };
4848
+ }
4849
+ return merged;
4786
4850
  }
4787
4851
  function mergeMcp(target, source) {
4788
4852
  const out = { ...target };
@@ -4800,12 +4864,12 @@ function mergeMcp(target, source) {
4800
4864
  }
4801
4865
  return out;
4802
4866
  }
4803
- function bridgeOpencodeMcp(cwd, runtimeStatus, excludeServers) {
4867
+ function bridgeOpencodeMcp(cwd, runtimeStatus, excludeServers, hostApi = "v1") {
4804
4868
  const {
4805
4869
  servers: merged,
4806
4870
  enabledServerNames: allEnabledServerNames,
4807
4871
  hash
4808
- } = mergeOpencodeMcp(cwd, runtimeStatus);
4872
+ } = mergeOpencodeMcp(cwd, runtimeStatus, hostApi);
4809
4873
  const servers = {};
4810
4874
  const bridgedServerNames = [];
4811
4875
  for (const [name, spec] of Object.entries(merged)) {
@@ -4825,10 +4889,10 @@ function bridgeOpencodeMcp(cwd, runtimeStatus, excludeServers) {
4825
4889
  excludeServers
4826
4890
  });
4827
4891
  }
4828
- function opencodeConfigLayers(cwd) {
4829
- const worktree = detectWorktree(cwd);
4892
+ function opencodeConfigLayers(cwd, hostApi = "v1") {
4893
+ const worktree = hostApi === "v2" ? void 0 : detectWorktree(cwd);
4830
4894
  const layers = [];
4831
- layers.push(loadGlobalConfig());
4895
+ layers.push(loadGlobalConfig(hostApi));
4832
4896
  const explicitConfig = process.env.OPENCODE_CONFIG;
4833
4897
  if (explicitConfig && fileExists(explicitConfig)) {
4834
4898
  const parsed = readAndParse(explicitConfig);
@@ -4850,22 +4914,30 @@ function opencodeConfigLayers(cwd) {
4850
4914
  }
4851
4915
  }
4852
4916
  for (const dir of projectDirs.slice().reverse()) {
4853
- layers.push(loadProjectFilesInDir(dir));
4917
+ layers.push(loadProjectFilesInDir(dir, hostApi));
4854
4918
  }
4855
- for (const dir of dotOpencodeDirs(cwd, worktree)) {
4856
- layers.push(loadProjectFilesInDir(dir));
4919
+ if (hostApi === "v2") {
4920
+ const dirs = walkUp({ start: cwd, targets: [".opencode"], predicate: dirExists }).reverse();
4921
+ const extra = process.env.OPENCODE_CONFIG_DIR;
4922
+ if (extra && dirExists(extra)) layers.push(loadProjectFilesInDir(extra, hostApi));
4923
+ for (const dir of dirs) layers.push(loadProjectFilesInDir(dir, hostApi));
4924
+ } else {
4925
+ for (const dir of dotOpencodeDirs(cwd, worktree)) {
4926
+ layers.push(loadProjectFilesInDir(dir));
4927
+ }
4857
4928
  }
4858
4929
  return layers;
4859
4930
  }
4860
- function loadMergedOpencodeConfig(cwd) {
4931
+ function loadMergedOpencodeConfig(cwd, hostApi = "v1") {
4861
4932
  let merged = {};
4862
- for (const layer of opencodeConfigLayers(cwd)) merged = deepMerge(merged, layer);
4933
+ for (const layer of opencodeConfigLayers(cwd, hostApi)) merged = mergeConfigLayer(merged, layer, hostApi);
4863
4934
  return merged;
4864
4935
  }
4865
- function mergeOpencodeMcp(cwd, runtimeStatus) {
4936
+ function mergeOpencodeMcp(cwd, runtimeStatus, hostApi = "v1") {
4866
4937
  let merged = {};
4867
- for (const layer of opencodeConfigLayers(cwd)) {
4868
- merged = mergeMcp(merged, extractMcpBlock(layer));
4938
+ for (const layer of opencodeConfigLayers(cwd, hostApi)) {
4939
+ const servers = extractMcpBlock(layer);
4940
+ merged = hostApi === "v2" ? { ...merged, ...servers } : mergeMcp(merged, servers);
4869
4941
  }
4870
4942
  if (runtimeStatus) {
4871
4943
  for (const name of Object.keys(merged)) {
@@ -5005,7 +5077,8 @@ function collectStartupDiagnostics(providers, opencodeVersion) {
5005
5077
  const cwd = describeSpawnCwd(firstOption(providers, "cwd"));
5006
5078
  let mcpServers = [];
5007
5079
  try {
5008
- mcpServers = mergeOpencodeMcp(cwd.resolved).enabledServerNames;
5080
+ const hostApi = firstOption(providers, "hostApi") === "v2" || opencodeVersion?.replace(/^v/, "").startsWith("2.") ? "v2" : "v1";
5081
+ mcpServers = mergeOpencodeMcp(cwd.resolved, void 0, hostApi).enabledServerNames;
5009
5082
  } catch (err) {
5010
5083
  log.debug("startup diagnostics could not read MCP config", {
5011
5084
  error: err instanceof Error ? err.message : String(err)
@@ -5219,6 +5292,10 @@ function formatDoctorReport(report) {
5219
5292
  }
5220
5293
  }
5221
5294
  lines.push("");
5295
+ lines.push("**Background subagents**");
5296
+ lines.push("");
5297
+ lines.push(...formatBackgroundSubagents(report.backgroundSubagents));
5298
+ lines.push("");
5222
5299
  lines.push("**Plan usage**");
5223
5300
  lines.push("");
5224
5301
  switch (report.planUsage.status) {
@@ -5251,6 +5328,59 @@ function formatDoctorReport(report) {
5251
5328
  }
5252
5329
  return lines.join("\n");
5253
5330
  }
5331
+ function formatBackgroundSubagents(state) {
5332
+ const lines = [];
5333
+ const gate = state.gate;
5334
+ if (!gate) {
5335
+ lines.push(
5336
+ "Not read yet this process. The gate is resolved the first time a turn plans its proxy tools, so send one message and run this again."
5337
+ );
5338
+ } else {
5339
+ lines.push("| Field | Value |");
5340
+ lines.push("|---|---|");
5341
+ lines.push(`| \`background\` offered to Claude | ${gate.supported ? "yes" : "no"} |`);
5342
+ lines.push(
5343
+ `| \`task_status\` / \`task_cancel\` | ${gate.supported ? "registered" : "not registered"} |`
5344
+ );
5345
+ lines.push(`| opencode API | ${gate.hostApi} |`);
5346
+ lines.push(`| decided from | ${describeBackgroundGateSource(gate)} |`);
5347
+ lines.push(`| read | ${formatAge(Math.max(0, Date.now() - gate.at))} ago |`);
5348
+ if (!gate.supported && gate.hostApi === "v1") {
5349
+ lines.push("");
5350
+ lines.push(
5351
+ "opencode 1.x keeps background subagents behind a flag on its OWN process: set `OPENCODE_EXPERIMENTAL_BACKGROUND_SUBAGENTS=true` (or the blanket `OPENCODE_EXPERIMENTAL`) in opencode's environment and restart it. The plugin never reads that variable itself; it reads whether opencode's advertised `task` schema carries a `background` property, which is how opencode publishes the flag."
5352
+ );
5353
+ }
5354
+ }
5355
+ const active = state.ledgers.filter(
5356
+ (ledger) => ledger.collected.length > 0 || ledger.cancelled.length > 0
5357
+ );
5358
+ lines.push("");
5359
+ if (active.length === 0) {
5360
+ lines.push("No background task has been collected or cancelled by this process.");
5361
+ } else {
5362
+ lines.push("| session | collected | cancelled |");
5363
+ lines.push("|---|---|---|");
5364
+ for (const ledger of active) {
5365
+ lines.push(
5366
+ `| ${describeSessionKey(ledger.sessionKey).session} | ${list(ledger.collected)} | ${list(
5367
+ ledger.cancelled
5368
+ )} |`
5369
+ );
5370
+ }
5371
+ lines.push("");
5372
+ lines.push(
5373
+ "A collected task is one this conversation read back with `task_status`, or was told about by opencode's own completion notification; a result is handed over once."
5374
+ );
5375
+ }
5376
+ return lines;
5377
+ }
5378
+ function describeBackgroundGateSource(gate) {
5379
+ if (gate.hostApi === "v2") {
5380
+ return "opencode 2 offers `background` unconditionally, so the registry is not consulted";
5381
+ }
5382
+ return gate.registryResolved ? "opencode's live `task` schema" : "opencode's live tool registry did not answer, so the answer defaulted to no";
5383
+ }
5254
5384
  async function checkProxyAuth(url, fetchImpl = fetch, timeoutMs = 3e3) {
5255
5385
  let authority;
5256
5386
  try {
@@ -5317,7 +5447,11 @@ async function gatherDoctorReport(options) {
5317
5447
  proxyServers,
5318
5448
  mcpServerErrors: snapshotMcpServerErrors(),
5319
5449
  pluginLoadFailures: snapshotPluginLoadFailures(),
5320
- planUsage
5450
+ planUsage,
5451
+ backgroundSubagents: {
5452
+ gate: snapshotBackgroundSubagentGate(),
5453
+ ledgers: snapshotBackgroundTasks()
5454
+ }
5321
5455
  };
5322
5456
  }
5323
5457
  async function buildDoctorReport(options) {
@@ -5332,7 +5466,8 @@ async function buildDoctorReport(options) {
5332
5466
  proxyServers: report.proxyServers.map((server2) => server2.auth.status),
5333
5467
  mcpServerErrors: report.mcpServerErrors.length,
5334
5468
  pluginLoadFailures: report.pluginLoadFailures.length,
5335
- planUsage: report.planUsage.status
5469
+ planUsage: report.planUsage.status,
5470
+ backgroundSubagents: report.backgroundSubagents.gate?.supported ?? "not read"
5336
5471
  });
5337
5472
  return formatDoctorReport(report);
5338
5473
  } catch (error) {
@@ -6820,7 +6955,13 @@ function resolveClaude(cmd = "claude") {
6820
6955
  throw new Error(`Could not resolve command on PATH: ${cmd}`);
6821
6956
  }
6822
6957
  function encodeCwd(cwd) {
6823
- return path8.resolve(cwd).replace(/[^a-zA-Z0-9]/g, "-");
6958
+ const resolved = path8.resolve(cwd);
6959
+ let real = resolved;
6960
+ try {
6961
+ real = fs6.realpathSync.native(resolved);
6962
+ } catch {
6963
+ }
6964
+ return real.replace(/[^a-zA-Z0-9]/g, "-");
6824
6965
  }
6825
6966
  function interactiveSpawnEnv(opts) {
6826
6967
  return {
@@ -7341,16 +7482,18 @@ function spawnInteractiveProcess(opts) {
7341
7482
  if (!startPromise) startPromise = session.start();
7342
7483
  return startPromise;
7343
7484
  };
7344
- const emitResult = (subtype, isError, result, usage) => {
7485
+ const emitResult = (opts2) => {
7345
7486
  lineEmitter.emit(
7346
7487
  "line",
7347
7488
  JSON.stringify({
7348
7489
  type: "result",
7349
- subtype,
7350
- is_error: isError,
7351
- result,
7490
+ subtype: opts2.subtype,
7491
+ is_error: opts2.isError,
7492
+ stop_reason: opts2.stopReason,
7493
+ terminal_reason: opts2.terminalReason,
7494
+ result: opts2.result,
7352
7495
  session_id: session.sessionId,
7353
- usage: usage ?? {},
7496
+ usage: opts2.usage ?? {},
7354
7497
  total_cost_usd: null,
7355
7498
  duration_ms: 0
7356
7499
  })
@@ -7373,20 +7516,24 @@ function spawnInteractiveProcess(opts) {
7373
7516
  lastCallCacheWriteTokens: lastCallUsage?.cache_creation_input_tokens
7374
7517
  });
7375
7518
  const timedOut = !stopReason;
7376
- emitResult(
7377
- timedOut ? "error_during_execution" : stopReason,
7378
- timedOut,
7379
- timedOut ? "Interactive transport: the turn ended without a terminal stop_reason (turn timeout or claude exit). Output above may be incomplete." : void 0,
7519
+ emitResult({
7520
+ subtype: timedOut ? "error_during_execution" : "success",
7521
+ isError: timedOut,
7522
+ stopReason: timedOut ? null : stopReason,
7523
+ terminalReason: timedOut ? "error_during_execution" : "completed",
7524
+ result: timedOut ? "Interactive transport: the turn ended without a terminal stop_reason (turn timeout or claude exit). Output above may be incomplete." : void 0,
7380
7525
  usage
7381
- );
7526
+ });
7382
7527
  } catch (err) {
7383
7528
  const e = err instanceof Error ? err : new Error(String(err));
7384
7529
  log.error("interactive turn failed", { error: e.message });
7385
- emitResult(
7386
- "error_during_execution",
7387
- true,
7388
- `Interactive transport failed: ${e.message}`
7389
- );
7530
+ emitResult({
7531
+ subtype: "error_during_execution",
7532
+ isError: true,
7533
+ stopReason: null,
7534
+ terminalReason: "error_during_execution",
7535
+ result: `Interactive transport failed: ${e.message}`
7536
+ });
7390
7537
  if (errorHandlers.size > 0) {
7391
7538
  for (const h of errorHandlers) h(e);
7392
7539
  } else {
@@ -7503,6 +7650,13 @@ Structured questions in this environment go through exactly one tool: \`mcp__ope
7503
7650
  - If that tool is not in your visible tool list it is deferred \u2014 load it with ToolSearch (\`select:mcp__opencode_proxy__question\`), then call it by its FULL name.
7504
7651
  - Do NOT call bare \`question\` \u2014 that is not a tool. Always use the full \`mcp__opencode_proxy__question\` name when invoking it.
7505
7652
  - Claude Code's built-in \`AskUserQuestion\` is disabled in this environment; the proxy is the only way to ask structured questions.`;
7653
+ function codeModeProxyHint(active) {
7654
+ return active ? `## opencode Code Mode
7655
+
7656
+ Instructions referring to the execute tool mean \`mcp__opencode_proxy__execute\` in this Claude Code session. Use its full name; if deferred, load it with ToolSearch (\`select:mcp__opencode_proxy__execute\`). Pass the code argument unchanged, using the provided catalog's exact \`search(...)\` and \`tools[...]\` signatures inside that code. The catalog tools are not separate Claude tools. Calls execute in opencode under this session's permissions.` : `## opencode Code Mode unavailable
7657
+
7658
+ The forwarded opencode catalog may mention execute, but this session does not expose an execute proxy. Do not invent an execute tool or claim a catalog call ran. For MCP servers available directly in Claude Code, discover their actual MCP tools with ToolSearch instead. If no matching tool exists, report that limitation; a shell/CLI call is not a test of MCP.`;
7659
+ }
7506
7660
  var CLAUDE_CLI_CONTEXT_NOTE = `## Runtime environment: Claude Code CLI
7507
7661
 
7508
7662
  You are running via the Claude Code CLI (not a direct API call). This affects context management:
@@ -7988,7 +8142,7 @@ function effectiveMcpConfig(config, cwd, proxyConfigPath, runtimeStatus, exclude
7988
8142
  let bridgedHash = null;
7989
8143
  let allEnabledServerNames = [];
7990
8144
  if (config.bridgeOpencodeMcp !== false) {
7991
- const bridged = bridgeOpencodeMcp(cwd, runtimeStatus, excludeServers);
8145
+ const bridged = bridgeOpencodeMcp(cwd, runtimeStatus, excludeServers, config.hostApi);
7992
8146
  if (bridged) {
7993
8147
  if (bridged.path) paths.push(bridged.path);
7994
8148
  bridgedHash = bridged.hash;
@@ -8048,7 +8202,7 @@ function resolvedProxyMcpTools(config, allEnabledServerNames, modelTools, taken)
8048
8202
  });
8049
8203
  if (resolution.defs.length === 0) {
8050
8204
  log.warn(
8051
- "proxyOpencodeMcpTools is on but no MCP tool was found in opencode's tool set; those servers stay on the direct bridge this spawn",
8205
+ "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" : ""),
8052
8206
  { servers: allEnabledServerNames, modelTools: modelTools?.length ?? 0 }
8053
8207
  );
8054
8208
  return null;
@@ -10547,18 +10701,25 @@ var ClaudeCodeLanguageModel = class {
10547
10701
  )
10548
10702
  )
10549
10703
  });
10704
+ const hostDialect = self.config.hostApi === "v2" ? "v2" : "v1";
10550
10705
  backgroundSubagentsSupported = liveTaskSupportsBackground(
10551
10706
  liveToolInfo.taskParameters,
10552
- self.config.hostApi === "v2" ? "v2" : "v1"
10707
+ hostDialect
10553
10708
  );
10554
10709
  enrichedProxy = applyBackgroundSubagentSupport(
10555
10710
  enrichedProxy,
10556
- backgroundSubagentsSupported
10711
+ backgroundSubagentsSupported,
10712
+ hostDialect
10557
10713
  );
10714
+ recordBackgroundSubagentGate({
10715
+ supported: backgroundSubagentsSupported,
10716
+ hostApi: hostDialect,
10717
+ registryResolved: liveToolInfo.resolved
10718
+ });
10558
10719
  log.info("background subagent gate", {
10559
10720
  supported: backgroundSubagentsSupported,
10560
10721
  registryResolved: liveToolInfo.resolved,
10561
- hostApi: self.config.hostApi ?? "v1",
10722
+ hostApi: hostDialect,
10562
10723
  note: backgroundSubagentsSupported ? "task accepts `background`; task_status and task_cancel are registered" : "`background` stripped from the task schema; set OPENCODE_EXPERIMENTAL_BACKGROUND_SUBAGENTS=true in opencode's environment to enable it"
10563
10724
  });
10564
10725
  }
@@ -10579,6 +10740,8 @@ var ClaudeCodeLanguageModel = class {
10579
10740
  const opencodeToolDefs = resolveProxyOpencodeToolDefs({
10580
10741
  requested: self.config.proxyOpencodeTools,
10581
10742
  items: liveToolInfo.items,
10743
+ modelTools: self.config.hostApi === "v2" ? options.tools ?? [] : void 0,
10744
+ allowCodeExecution: self.config.permissionPreset !== "read-only",
10582
10745
  taken: new Set(
10583
10746
  [...enrichedProxy ?? [], ...proxyMcpTools ?? []].map(
10584
10747
  (t) => t.name
@@ -10624,7 +10787,8 @@ var ClaudeCodeLanguageModel = class {
10624
10787
  ...extractSystemMessages(options.prompt),
10625
10788
  ...taskProxyEnabled ? [SUBAGENT_DISPATCH_HINT] : [],
10626
10789
  ...backgroundSubagentsSupported ? [BACKGROUND_SUBAGENT_HINT] : [],
10627
- ...questionProxyActive ? [QUESTION_PROXY_HINT] : []
10790
+ ...questionProxyActive ? [QUESTION_PROXY_HINT] : [],
10791
+ ...self.config.hostApi === "v2" && options.tools?.some((t) => t.name === "execute") ? [codeModeProxyHint(opencodeToolDefs.some((t) => t.name === "execute"))] : []
10628
10792
  ],
10629
10793
  {
10630
10794
  compressEnabled: pluginCompressEnabled,
@@ -11203,7 +11367,7 @@ import {
11203
11367
  existsSync as existsSync3,
11204
11368
  mkdirSync as mkdirSync4,
11205
11369
  readFileSync as readFileSync6,
11206
- realpathSync as realpathSync2,
11370
+ realpathSync as realpathSync3,
11207
11371
  rmSync as rmSync3,
11208
11372
  writeFileSync as writeFileSync5
11209
11373
  } from "fs";
@@ -11242,7 +11406,7 @@ function userIntendsToUseUnscoped() {
11242
11406
  function ourLoadedDir() {
11243
11407
  try {
11244
11408
  const filePath = fileURLToPath3(import.meta.url);
11245
- return realpathSync2(resolve5(filePath, "..", ".."));
11409
+ return realpathSync3(resolve5(filePath, "..", ".."));
11246
11410
  } catch {
11247
11411
  return null;
11248
11412
  }
@@ -11311,7 +11475,7 @@ function cleanupOne(cacheRoot, ourDir) {
11311
11475
  if (!existsSync3(stalePath)) return;
11312
11476
  let realStalePath = stalePath;
11313
11477
  try {
11314
- realStalePath = realpathSync2(stalePath);
11478
+ realStalePath = realpathSync3(stalePath);
11315
11479
  } catch {
11316
11480
  }
11317
11481
  if (ourDir && realStalePath === ourDir) return;
@@ -11384,6 +11548,30 @@ function toV1Session(info) {
11384
11548
  const directory = typeof root === "string" ? typeof session.subpath === "string" && session.subpath.length > 0 ? path9.resolve(root, session.subpath) : root : void 0;
11385
11549
  return { ...info, directory, parentID: session.parentID };
11386
11550
  }
11551
+ function toV1Messages(messages) {
11552
+ if (!Array.isArray(messages)) return [];
11553
+ return messages.flatMap((entry) => {
11554
+ if (!entry || typeof entry !== "object") return [];
11555
+ const message = entry;
11556
+ const parts = Array.isArray(message.content) ? message.content.flatMap((part) => {
11557
+ if (!part || typeof part !== "object") return [];
11558
+ const piece = part;
11559
+ return piece.type === "text" && typeof piece.text === "string" ? [{ type: "text", text: piece.text }] : [];
11560
+ }) : typeof message.text === "string" ? [{ type: "text", text: message.text }] : [];
11561
+ const error = message.error;
11562
+ const info = {
11563
+ role: typeof message.type === "string" ? message.type : "",
11564
+ time: message.time && typeof message.time === "object" ? message.time : {},
11565
+ ...error && typeof error === "object" ? {
11566
+ error: {
11567
+ name: typeof error.type === "string" ? error.type : "error",
11568
+ data: { message: typeof error.message === "string" ? error.message : void 0 }
11569
+ }
11570
+ } : {}
11571
+ };
11572
+ return [{ info, parts }];
11573
+ });
11574
+ }
11387
11575
  function createV1ClientShim(ctx) {
11388
11576
  const client = {};
11389
11577
  const mcpList = ctx.mcp?.list;
@@ -11408,7 +11596,8 @@ function createV1ClientShim(ctx) {
11408
11596
  client.tool = {
11409
11597
  // V1: `{ data: [{ id, description, parameters }] }`. V2 returns the tools
11410
11598
  // themselves, with an Effect schema for input rather than JSON Schema, so
11411
- // `parameters` is empty; only `proxyOpencodeTools` reads it.
11599
+ // `parameters` is empty. V2 proxyOpencodeTools uses the per-turn model
11600
+ // snapshot instead, which also includes synthesized Code Mode execute.
11412
11601
  list: async () => {
11413
11602
  const tools = await toolList.call(ctx.tool);
11414
11603
  const data = (tools ?? []).flatMap((tool) => {
@@ -11436,13 +11625,27 @@ ${agentList}` : subagent.description;
11436
11625
  };
11437
11626
  }
11438
11627
  const sessionGet = ctx.session?.get;
11628
+ const sessionContext = ctx.session?.context;
11629
+ const sessionInterrupt = ctx.session?.interrupt;
11630
+ const session = {};
11439
11631
  if (typeof sessionGet === "function") {
11440
- client.session = {
11441
- get: async (options) => ({
11442
- data: toV1Session(await sessionGet.call(ctx.session, { sessionID: options.path.id }))
11443
- })
11632
+ session.get = async (options) => ({
11633
+ data: toV1Session(await sessionGet.call(ctx.session, { sessionID: options.path.id }))
11634
+ });
11635
+ }
11636
+ if (typeof sessionContext === "function") {
11637
+ session.messages = async (options) => ({
11638
+ data: toV1Messages(await sessionContext.call(ctx.session, { sessionID: options.path.id }))
11639
+ });
11640
+ }
11641
+ if (typeof sessionInterrupt === "function") {
11642
+ session.abort = async (options) => {
11643
+ const result = await sessionInterrupt.call(ctx.session, { sessionID: options.path.id });
11644
+ const interrupted = result?.interrupted;
11645
+ return { data: typeof interrupted === "boolean" ? interrupted : true };
11444
11646
  };
11445
11647
  }
11648
+ if (Object.keys(session).length > 0) client.session = session;
11446
11649
  return client;
11447
11650
  }
11448
11651
 
@@ -11575,9 +11778,11 @@ function plainObject(value) {
11575
11778
  }
11576
11779
  function configuredSeedSettings(config, pluginOptions) {
11577
11780
  const provider = plainObject(plainObject(config.provider)?.[BASE_PROVIDER_ID]);
11781
+ const nativeProvider = plainObject(plainObject(config.providers)?.[BASE_PROVIDER_ID]);
11578
11782
  return {
11579
11783
  ...plainObject(provider?.options) ?? {},
11580
11784
  ...plainObject(provider?.settings) ?? {},
11785
+ ...plainObject(nativeProvider?.settings) ?? {},
11581
11786
  ...pluginOptions ?? {}
11582
11787
  };
11583
11788
  }
@@ -12225,7 +12430,7 @@ var index_default = {
12225
12430
  setup: createV2Setup({
12226
12431
  createProvider: createClaudeCode,
12227
12432
  defaultProxyTools: DEFAULT_PROXY_TOOL_NAMES,
12228
- loadConfig: loadMergedOpencodeConfig,
12433
+ loadConfig: (directory) => loadMergedOpencodeConfig(directory, "v2"),
12229
12434
  buildAgentRegistry: (config) => buildAgentRegistry(config)
12230
12435
  })
12231
12436
  };