@khalilgharbaoui/opencode-claude-code-plugin 0.16.0 → 0.17.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/README.md CHANGED
@@ -357,7 +357,7 @@ By default, the plugin proxies `Bash`, `Edit`, `Write`, `WebFetch`, and `Task`.
357
357
  | `"Edit"` | `Edit`, `MultiEdit` | `mcp__opencode_proxy__edit` |
358
358
  | `"Write"` | `Write` | `mcp__opencode_proxy__write` |
359
359
  | `"WebFetch"` | `WebFetch` | `mcp__opencode_proxy__webfetch` |
360
- | `"Task"` | `Agent` | `mcp__opencode_proxy__task` |
360
+ | `"Task"` | `Agent` | `mcp__opencode_proxy__task`, `mcp__opencode_proxy__task_batch` |
361
361
  | `"Question"` | `AskUserQuestion` | `mcp__opencode_proxy__question` |
362
362
  | `"Compress"` | none | `mcp__opencode_proxy__compress` |
363
363
 
@@ -369,6 +369,7 @@ By default, the plugin proxies `Bash`, `Edit`, `Write`, `WebFetch`, and `Task`.
369
369
  - **Resume:** pass the child session ID back as `task_id` to continue that subagent session. Omit it to create a fresh child.
370
370
  - **Nested tasks:** current opencode defaults `subagent_depth` to `1`, so a first-level child cannot launch another child. Increase top-level `subagent_depth` to permit deeper nesting, and explicitly grant `permission.task` on every subagent that should delegate; opencode otherwise adds a task deny to spawned subagent sessions.
371
371
  - **Background:** `background: true` returns after starting the child and lets opencode notify the parent when it finishes. Current opencode requires `OPENCODE_EXPERIMENTAL_BACKGROUND_SUBAGENTS=true` in the environment of the opencode process. Foreground is the default.
372
+ - **Several at once:** `mcp__opencode_proxy__task_batch` takes a `tasks` array of ordinary task inputs and runs them concurrently. It exists because Claude Code sends MCP requests one at a time: when the model emits two `task` calls in one response, the second only leaves the CLI after the first has returned (measured live, 2026-09-06), so "launch two subagents" was always serial. The plugin turns one `task_batch` call into N opencode `task` calls inside a single tool boundary, which opencode executes in parallel, then hands the model every result together, labelled in task order. Same permissions, same 60-minute deadline, same `subagent_type` list. Enabled whenever `Task` is proxied. Designed and first implemented by [@broskees](https://github.com/broskees) on his fork.
372
373
 
373
374
  **Steering models to it.** Headless Claude Code CLIs expose no `Agent`/`Task`
374
375
  dispatch tool of their own (verified on 2.1.211), while they *do* expose
@@ -965,7 +966,7 @@ This plugin absorbs work from its forks directly, cherry-picked with the origina
965
966
  | [@galvani](https://github.com/galvani) (Jan Kozak) | Per-session working directory for `opencode serve`, so one server spawns each project's `claude` in the right place. Also found the stale `toolCallMap` re-emission three months before it was fixed here. | `9e02ce4`, `2238ed0` |
966
967
  | [@HeikoAtGitHub](https://github.com/HeikoAtGitHub) | Stopped sending `AGENTS.md` to the model twice (opencode already forwards it). Independently diagnosed the 5-minute proxy wall. | `25260a4`, `42f426d` |
967
968
  | [@bernardofortes](https://github.com/bernardofortes) (Bernardo Fortes) | `idleProcessTimeoutMs`, idle eviction of retained `claude` workers. | `a5f723a` |
968
- | [@broskees](https://github.com/broskees) (Joseph Roberts) | Task proxy default-on (PR #18), the abort `interrupt` so Esc really stops the CLI, the skill bridge, and the undici 300 s diagnosis of the proxy wall. | PR #18, `68ed142` |
969
+ | [@broskees](https://github.com/broskees) (Joseph Roberts) | Task proxy default-on (PR #18), the abort `interrupt` so Esc really stops the CLI, the skill bridge, `task_batch` for concurrent subagents (and the measurement that the CLI serialises MCP calls), and the undici 300 s diagnosis of the proxy wall. | PR #18, `68ed142` |
969
970
  | [@jknlsn](https://github.com/jknlsn) (Jake Nelson) | Per-tool proxy timeouts, subagent dispatch steering, the question proxy, the start watchdog respawn. | `84f3db9`, `94980a6`, `47501d0`, `ffefc24` |
970
971
  | [@CollieIsCute](https://github.com/CollieIsCute) (Collie Tsai) | The plan-mode approval bridge. | `8c5b583` |
971
972
  | [@flupkede](https://github.com/flupkede) | The compress proxy tool design and the AI-SDK v4 image-part fix. | `4ac319f`, `60a6e9a` |
package/dist/index.d.ts CHANGED
@@ -630,6 +630,16 @@ declare class ClaudeCodeLanguageModel implements LanguageModelV3 {
630
630
  */
631
631
  private ensureProxyServer;
632
632
  private extractPendingProxyResult;
633
+ /**
634
+ * The result opencode produced for a pending proxy call, if the prompt
635
+ * carries it. For `task_batch` that means every child's result gathered
636
+ * back onto the parent: opencode runs the children in one step and hands
637
+ * all their results to the next call together, so a partial set is not
638
+ * expected. If it ever happens the batch still resolves, with the gap
639
+ * named in the text, because leaving the parent pending would send this
640
+ * turn down the fresh-envelope path and reject the call as orphaned.
641
+ */
642
+ private extractPendingProxyResultForCall;
633
643
  /**
634
644
  * Resolve the session affinity token for this LLM call. Delegates to the
635
645
  * exported `resolveSessionAffinity` helper so the logic is unit-testable.
package/dist/index.js CHANGED
@@ -523,6 +523,8 @@ var PROXY_DEFAULT_TIMEOUT_MS = 10 * 60 * 1e3;
523
523
  var PROXY_PER_TOOL_DEFAULT_TIMEOUT_MS = {
524
524
  task: 60 * 60 * 1e3,
525
525
  // 60 min
526
+ task_batch: 60 * 60 * 1e3,
527
+ // 60 min, same reasoning: it IS task calls
526
528
  question: 30 * 60 * 1e3
527
529
  // 30 min
528
530
  };
@@ -562,14 +564,58 @@ function resolveProxyClientCeilingMs(overrides) {
562
564
  function buildProxyTimeoutError(toolName, ms) {
563
565
  const key = toolName.toLowerCase();
564
566
  const base = `Proxy tool '${toolName}' timed out after ${ms}ms waiting for opencode to resolve the call`;
565
- if (key === "task") {
567
+ if (key === "task" || key === TASK_BATCH_TOOL_NAME) {
566
568
  return new Error(
567
- base + " (the subagent). The subagent may still be running but its result is no longer reachable in this session. Do not declare the dispatch failed, and do not 'schedule a wake-up' or defer -- that mechanism does not apply here. If the result is required, re-dispatch or verify it directly now."
569
+ base + (key === "task" ? " (the subagent)." : " (the subagents).") + " The subagent may still be running but its result is no longer reachable in this session. Do not declare the dispatch failed, and do not 'schedule a wake-up' or defer -- that mechanism does not apply here. If the result is required, re-dispatch or verify it directly now."
568
570
  );
569
571
  }
570
572
  return new Error(base);
571
573
  }
572
- var TASK_PROXY_NOTE = "This is the ONLY tool that dispatches 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 \u2014 invalid types fail fast with a clear error. Foreground calls block until the subagent finishes; set `background` to request opencode's background execution mode. Task calls get a 60-minute proxy deadline by default (configurable via proxyToolTimeoutMs).";
574
+ 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. Foreground calls block until the subagent finishes; set `background` to request opencode's background execution mode. For two or more independent subagents in one response use task_batch, not several task calls: those run one after another. Task calls get a 60-minute proxy deadline by default (configurable via proxyToolTimeoutMs).";
575
+ var TASK_BATCH_TOOL_NAME = "task_batch";
576
+ 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. Same 60-minute proxy deadline as task (configurable via proxyToolTimeoutMs).";
577
+ var TASK_INPUT_REQUIRED = ["description", "prompt", "subagent_type"];
578
+ function taskBatchInputError(input) {
579
+ const tasks = input?.tasks;
580
+ if (!Array.isArray(tasks) || tasks.length < 2) {
581
+ return "task_batch requires a `tasks` array with at least two items; use `task` for one subagent";
582
+ }
583
+ for (const [index, task] of tasks.entries()) {
584
+ if (task === null || typeof task !== "object" || Array.isArray(task)) {
585
+ return `task_batch tasks[${index}] must be an object`;
586
+ }
587
+ const item = task;
588
+ for (const field of TASK_INPUT_REQUIRED) {
589
+ if (typeof item[field] !== "string") {
590
+ return `task_batch tasks[${index}].${field} must be a string`;
591
+ }
592
+ }
593
+ }
594
+ return null;
595
+ }
596
+ function taskBatchTasks(input) {
597
+ if (taskBatchInputError(input)) return [];
598
+ return input.tasks;
599
+ }
600
+ function taskBatchChildToolCallId(parentToolCallId, index) {
601
+ return `${parentToolCallId}_task_${index}`;
602
+ }
603
+ function formatTaskBatchResults(children) {
604
+ const total = children.length;
605
+ const sections = children.map(({ task, result }, index) => {
606
+ const label = typeof task.description === "string" ? task.description : `task ${index + 1}`;
607
+ const agent = typeof task.subagent_type === "string" ? ` (${task.subagent_type})` : "";
608
+ const header = `## task ${index + 1} of ${total}: ${label}${agent}`;
609
+ if (!result) return `${header}
610
+ [missing] opencode returned no result for this task in the batch`;
611
+ if (result.kind === "error") return `${header}
612
+ [error] ${result.message}`;
613
+ return `${header}
614
+ ${result.isError ? "[error] " : ""}${result.text}`;
615
+ });
616
+ const failed = children.some(({ result }) => !result || result.kind === "error" || result.isError);
617
+ return { kind: "text", text: sections.join("\n\n"), ...failed ? { isError: true } : {} };
618
+ }
573
619
  var AGENT_TYPES_HEADING = "Available agent types";
574
620
  var AGENT_BLURB_LIMIT = 140;
575
621
  var QUESTION_PROXY_NOTE = "This routes structured questions through opencode's native `question` tool, which renders a TUI form with the options you provide and blocks until the operator answers. Claude Code's built-in AskUserQuestion is disabled in this environment; this proxy is the ONLY way to ask the operator for a decision or clarification. Answers come back as arrays of selected labels (set `multiple: true` to allow more than one). If the operator dismisses the form the call returns an error \u2014 treat that as 'no answer' and stop, do not guess. Question calls get a 30-minute proxy deadline by default (configurable via proxyToolTimeoutMs); for long-AFK scenarios prefer fewer, high-signal questions.";
@@ -597,7 +643,7 @@ function overlayTaskProxyDescription(tools, liveDescription) {
597
643
  const agentTypes = extractAgentTypeList(liveDescription);
598
644
  if (!agentTypes) return tools;
599
645
  return tools.map(
600
- (t) => t.name === "task" ? { ...t, description: `${agentTypes}
646
+ (t) => t.name === "task" || t.name === TASK_BATCH_TOOL_NAME ? { ...t, description: `${agentTypes}
601
647
 
602
648
  ${t.description}` } : t
603
649
  );
@@ -615,6 +661,32 @@ function filterQuestionProxyByOpencodeSupport(tools, opencodeHasQuestion) {
615
661
  if (opencodeHasQuestion) return tools;
616
662
  return tools.filter((t) => t.name !== "question");
617
663
  }
664
+ var TASK_INPUT_PROPERTIES = {
665
+ description: {
666
+ type: "string",
667
+ description: "A short (3-5 words) description of the task"
668
+ },
669
+ prompt: {
670
+ type: "string",
671
+ description: "The task for the agent to perform"
672
+ },
673
+ subagent_type: {
674
+ type: "string",
675
+ description: "The type of specialized agent to use for this task"
676
+ },
677
+ task_id: {
678
+ type: "string",
679
+ description: "Set this only if you mean to resume a previous task: pass the prior task_id to continue the same subagent session instead of creating a fresh one."
680
+ },
681
+ command: {
682
+ type: "string",
683
+ description: "The command that triggered this task"
684
+ },
685
+ background: {
686
+ type: "boolean",
687
+ description: "Run the task in the background when supported by opencode"
688
+ }
689
+ };
618
690
  var DEFAULT_PROXY_TOOLS = [
619
691
  {
620
692
  name: "bash",
@@ -708,35 +780,30 @@ var DEFAULT_PROXY_TOOLS = [
708
780
  {
709
781
  name: "task",
710
782
  description: "Launch an opencode subagent to handle a complex multi-step task autonomously. Routed through opencode's task tool so subagent orchestration, permission, and lifecycle are handled by opencode. Use `subagent_type` to pick which configured subagent runs (e.g. `build`, `general`, `explore`, or any custom subagent declared in opencode.json). " + TASK_PROXY_NOTE,
783
+ inputSchema: {
784
+ type: "object",
785
+ properties: TASK_INPUT_PROPERTIES,
786
+ required: TASK_INPUT_REQUIRED
787
+ }
788
+ },
789
+ {
790
+ name: TASK_BATCH_TOOL_NAME,
791
+ description: "Launch two or more independent opencode subagents at the same time and get all their results back together. Put one ordinary task input in `tasks` for each subagent. " + TASK_BATCH_PROXY_NOTE,
711
792
  inputSchema: {
712
793
  type: "object",
713
794
  properties: {
714
- description: {
715
- type: "string",
716
- description: "A short (3-5 words) description of the task"
717
- },
718
- prompt: {
719
- type: "string",
720
- description: "The task for the agent to perform"
721
- },
722
- subagent_type: {
723
- type: "string",
724
- description: "The type of specialized agent to use for this task"
725
- },
726
- task_id: {
727
- type: "string",
728
- description: "Set this only if you mean to resume a previous task \u2014 pass the prior task_id to continue the same subagent session instead of creating a fresh one."
729
- },
730
- command: {
731
- type: "string",
732
- description: "The command that triggered this task"
733
- },
734
- background: {
735
- type: "boolean",
736
- description: "Run the task in the background when supported by opencode"
795
+ tasks: {
796
+ type: "array",
797
+ minItems: 2,
798
+ description: "Independent subagent tasks to run concurrently",
799
+ items: {
800
+ type: "object",
801
+ properties: TASK_INPUT_PROPERTIES,
802
+ required: TASK_INPUT_REQUIRED
803
+ }
737
804
  }
738
805
  },
739
- required: ["description", "prompt", "subagent_type"]
806
+ required: ["tasks"]
740
807
  }
741
808
  },
742
809
  {
@@ -922,6 +989,13 @@ async function createProxyMcpServer(tools = DEFAULT_PROXY_TOOLS, timeoutOverride
922
989
  });
923
990
  return;
924
991
  }
992
+ if (toolName === TASK_BATCH_TOOL_NAME) {
993
+ const problem = taskBatchInputError(input);
994
+ if (problem) {
995
+ writeToolCallResult(res, requestId, { kind: "error", message: problem });
996
+ return;
997
+ }
998
+ }
925
999
  const interceptor = interceptors?.get(toolName);
926
1000
  if (interceptor) {
927
1001
  let intercepted;
@@ -1126,6 +1200,7 @@ function disallowedToolFlags(tools) {
1126
1200
  grep: ["Grep"],
1127
1201
  webfetch: ["WebFetch"],
1128
1202
  task: ["Agent"],
1203
+ task_batch: ["Agent"],
1129
1204
  // `question` disables Claude Code's built-in `AskUserQuestion` so the
1130
1205
  // structured-questions path flows through opencode's native `question`
1131
1206
  // tool instead — same UI/permission/audit benefits as the other
@@ -4599,8 +4674,9 @@ blocker. The user can interrupt or abort at any time; turn endings should
4599
4674
  mark meaningful checkpoints, not every completed substep.`;
4600
4675
  var SUBAGENT_DISPATCH_HINT = `## opencode subagents
4601
4676
 
4602
- Subagent dispatch in this environment goes through exactly one tool: \`mcp__opencode_proxy__task\`.
4677
+ Subagent dispatch in this environment goes through exactly two tools: \`mcp__opencode_proxy__task\` for one subagent and \`mcp__opencode_proxy__task_batch\` for two or more at once.
4603
4678
 
4679
+ - Two or more independent subagents in one response: make ONE \`mcp__opencode_proxy__task_batch\` call with a \`tasks\` array (each item is a normal task input). Claude Code runs MCP calls one at a time, so several \`mcp__opencode_proxy__task\` calls in the same response run serially; \`task_batch\` runs them concurrently in opencode and returns every result together, labelled in order.
4604
4680
  - When the user mentions \`@<agent>\` or an instruction says "call the task tool with subagent: <name>", call \`mcp__opencode_proxy__task\` with \`subagent_type: "<name>"\`.
4605
4681
  - If that tool is not in your visible tool list it is deferred \u2014 load it with ToolSearch (\`select:mcp__opencode_proxy__task\`), then call it.
4606
4682
  - Claude Code's built-in TaskCreate/TaskUpdate/TaskList manage a local todo list. They cannot dispatch subagents; creating a task there runs nothing. Never report a subagent as dispatched unless \`mcp__opencode_proxy__task\` returned its result.
@@ -4804,11 +4880,24 @@ var ClaudeCodeLanguageModel = class {
4804
4880
  DEFAULT_PROXY_TOOLS.map((t) => [t.name.toLowerCase(), t])
4805
4881
  );
4806
4882
  const picked = [];
4883
+ const seen = /* @__PURE__ */ new Set();
4807
4884
  const unknown = [];
4885
+ const pick = (def) => {
4886
+ if (seen.has(def.name)) return;
4887
+ seen.add(def.name);
4888
+ picked.push(def);
4889
+ };
4808
4890
  for (const n of names) {
4809
4891
  const def = defsByName.get(String(n).toLowerCase());
4810
- if (def) picked.push(def);
4811
- else unknown.push(String(n));
4892
+ if (!def) {
4893
+ unknown.push(String(n));
4894
+ continue;
4895
+ }
4896
+ pick(def);
4897
+ if (def.name === "task") {
4898
+ const batch = defsByName.get(TASK_BATCH_TOOL_NAME);
4899
+ if (batch) pick(batch);
4900
+ }
4812
4901
  }
4813
4902
  if (unknown.length > 0) {
4814
4903
  const known = [...defsByName.keys()].join(", ");
@@ -4998,6 +5087,41 @@ var ClaudeCodeLanguageModel = class {
4998
5087
  }
4999
5088
  return null;
5000
5089
  }
5090
+ /**
5091
+ * The result opencode produced for a pending proxy call, if the prompt
5092
+ * carries it. For `task_batch` that means every child's result gathered
5093
+ * back onto the parent: opencode runs the children in one step and hands
5094
+ * all their results to the next call together, so a partial set is not
5095
+ * expected. If it ever happens the batch still resolves, with the gap
5096
+ * named in the text, because leaving the parent pending would send this
5097
+ * turn down the fresh-envelope path and reject the call as orphaned.
5098
+ */
5099
+ extractPendingProxyResultForCall(prompt, call) {
5100
+ if (call.toolName !== TASK_BATCH_TOOL_NAME) {
5101
+ return this.extractPendingProxyResult(prompt, call.toolCallId);
5102
+ }
5103
+ const tasks = taskBatchTasks(call.input);
5104
+ if (tasks.length === 0) {
5105
+ return { kind: "error", message: "task_batch input is not a list of task objects" };
5106
+ }
5107
+ const children = tasks.map((task, index) => ({
5108
+ task,
5109
+ result: this.extractPendingProxyResult(
5110
+ prompt,
5111
+ taskBatchChildToolCallId(call.toolCallId, index)
5112
+ )
5113
+ }));
5114
+ const answered = children.filter((child) => child.result !== null).length;
5115
+ if (answered === 0) return null;
5116
+ if (answered < children.length) {
5117
+ log.warn("task_batch resolving with child results missing", {
5118
+ toolCallId: call.toolCallId,
5119
+ answered,
5120
+ total: children.length
5121
+ });
5122
+ }
5123
+ return formatTaskBatchResults(children);
5124
+ }
5001
5125
  /**
5002
5126
  * Resolve the session affinity token for this LLM call. Delegates to the
5003
5127
  * exported `resolveSessionAffinity` helper so the logic is unit-testable.
@@ -5809,7 +5933,7 @@ ${plan}
5809
5933
  const self = this;
5810
5934
  const previousPendingProxyMatches = previousPendingProxyCalls.map((call) => ({
5811
5935
  call,
5812
- result: this.extractPendingProxyResult(options.prompt, call.toolCallId)
5936
+ result: this.extractPendingProxyResultForCall(options.prompt, call)
5813
5937
  }));
5814
5938
  const hasMatchedPendingResults = previousPendingProxyMatches.some(
5815
5939
  (m) => m.result !== null
@@ -6271,20 +6395,34 @@ ${plan}
6271
6395
  const finishWithToolCalls = (calls) => {
6272
6396
  if (controllerClosed) return;
6273
6397
  if (calls.length === 0) return;
6274
- for (const call of calls) {
6398
+ const enqueueToolCall = (toolCallId, toolName, input) => {
6275
6399
  controller.enqueue({
6276
6400
  type: "tool-input-start",
6277
- id: call.toolCallId,
6278
- toolName: call.toolName
6401
+ id: toolCallId,
6402
+ toolName
6279
6403
  });
6280
6404
  controller.enqueue({
6281
6405
  type: "tool-call",
6282
- toolCallId: call.toolCallId,
6283
- toolName: call.toolName,
6284
- input: JSON.stringify(call.input),
6406
+ toolCallId,
6407
+ toolName,
6408
+ input: JSON.stringify(input),
6285
6409
  providerExecuted: false
6286
6410
  });
6287
- skipResultForIds.add(call.toolCallId);
6411
+ skipResultForIds.add(toolCallId);
6412
+ };
6413
+ for (const call of calls) {
6414
+ if (call.toolName === TASK_BATCH_TOOL_NAME) {
6415
+ for (const [index, task] of taskBatchTasks(call.input).entries()) {
6416
+ enqueueToolCall(
6417
+ taskBatchChildToolCallId(call.toolCallId, index),
6418
+ "task",
6419
+ task
6420
+ );
6421
+ }
6422
+ skipResultForIds.add(call.toolCallId);
6423
+ } else {
6424
+ enqueueToolCall(call.toolCallId, call.toolName, call.input);
6425
+ }
6288
6426
  markPendingProxyCallEmitted(call.toolCallId);
6289
6427
  }
6290
6428
  controller.enqueue({