@khalilgharbaoui/opencode-claude-code-plugin 0.4.21 → 0.4.23

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
@@ -213,8 +213,11 @@ By default, when Claude Code's CLI uses `Bash`, `Edit`, `Write`, etc., it execut
213
213
  | `"Edit"` | `Edit`, `MultiEdit` | `mcp__opencode_proxy__edit` |
214
214
  | `"Write"` | `Write` | `mcp__opencode_proxy__write` |
215
215
  | `"WebFetch"` | `WebFetch` | `mcp__opencode_proxy__webfetch` |
216
+ | `"Task"` | `Agent` | `mcp__opencode_proxy__task` |
216
217
 
217
- Only those four values are actually proxied; anything else you put in `proxyTools` is ignored. Proxying `Edit` also disables `MultiEdit` opencode has no batched-edit equivalent, so Claude is forced to fan out into single `Edit` calls that each flow through the permission UI.
218
+ The `Task` proxy is the way to let Claude orchestrate opencode's configured subagents (`build`, `general`, custom subagents defined in `opencode.json`) instead of Claude CLI's internal-only general-purpose / Explore / Plan options. With `"Task"` in `proxyTools` and `permission.task: allow` granted to the calling agent, a Claude session can invoke `task(subagent_type="build", prompt="...")` and the subagent runs natively under opencode (with its own permission UI, lifecycle, model assignment, and Tab visibility). Without `"Task"`, Claude's built-in `Agent` tool stays enabled and Claude orchestrates subagents internally with no opencode visibility.
219
+
220
+ Only those five values are actually proxied; anything else you put in `proxyTools` is ignored. Proxying `Edit` also disables `MultiEdit` — opencode has no batched-edit equivalent, so Claude is forced to fan out into single `Edit` calls that each flow through the permission UI.
218
221
 
219
222
  To turn off proxying entirely:
220
223
 
package/dist/index.d.ts CHANGED
@@ -229,7 +229,15 @@ interface ClaudeCodeProviderSettings {
229
229
  * opencode's tool executor (with its native permission UI) and returns
230
230
  * the result.
231
231
  *
232
- * Supported: `bash`, `write`, `edit`, `webfetch`. Leave empty or unset to disable proxying.
232
+ * Supported: `bash`, `write`, `edit`, `webfetch`, `task`. Leave empty or unset to disable proxying.
233
+ *
234
+ * `task` proxies Claude CLI's `Agent` (subagent dispatch) tool through
235
+ * opencode's `task` tool, so subagent calls run under opencode's
236
+ * configured subagent set (build/general/custom) with opencode's
237
+ * permission and lifecycle handling, instead of Claude CLI's
238
+ * internal-only general-purpose / Explore / Plan options. The calling
239
+ * agent must have `permission.task: allow` for the target subagent
240
+ * (see opencode's agent docs).
233
241
  */
234
242
  proxyTools?: string[];
235
243
  /**
package/dist/index.js CHANGED
@@ -124,6 +124,91 @@ var log = {
124
124
  }
125
125
  };
126
126
 
127
+ // src/todo-ledger.ts
128
+ var ledgers = /* @__PURE__ */ new Map();
129
+ var PENDING_CREATE_TTL_MS = 6e4;
130
+ var TASK_CREATED_PATTERN = /Task\s*#?\s*(\d+)\s+created/i;
131
+ var VALID_STATUSES = /* @__PURE__ */ new Set(["pending", "in_progress", "completed"]);
132
+ function getOrCreate(sessionId) {
133
+ let ledger = ledgers.get(sessionId);
134
+ if (!ledger) {
135
+ ledger = { todos: /* @__PURE__ */ new Map(), pendingCreates: /* @__PURE__ */ new Map() };
136
+ ledgers.set(sessionId, ledger);
137
+ }
138
+ return ledger;
139
+ }
140
+ function prunePending(ledger) {
141
+ const cutoff = Date.now() - PENDING_CREATE_TTL_MS;
142
+ for (const [id, pending] of ledger.pendingCreates) {
143
+ if (pending.createdAt < cutoff) ledger.pendingCreates.delete(id);
144
+ }
145
+ }
146
+ function materialize(ledger) {
147
+ return Array.from(ledger.todos.values());
148
+ }
149
+ function resolveSubject(input) {
150
+ const subject = typeof input?.subject === "string" ? input.subject.trim() : "";
151
+ if (subject) return subject;
152
+ const description = typeof input?.description === "string" ? input.description.trim() : "";
153
+ if (description) return description;
154
+ return "(no subject)";
155
+ }
156
+ function applyTaskCreateToolUse(sessionId, toolUseId, input) {
157
+ if (!sessionId || !toolUseId) return;
158
+ const ledger = getOrCreate(sessionId);
159
+ prunePending(ledger);
160
+ ledger.pendingCreates.set(toolUseId, {
161
+ subject: resolveSubject(input),
162
+ createdAt: Date.now()
163
+ });
164
+ }
165
+ function applyTaskCreateToolResult(sessionId, toolUseId, resultText) {
166
+ if (!sessionId || !toolUseId) return null;
167
+ const ledger = ledgers.get(sessionId);
168
+ if (!ledger) return null;
169
+ const pending = ledger.pendingCreates.get(toolUseId);
170
+ if (!pending) return null;
171
+ ledger.pendingCreates.delete(toolUseId);
172
+ const match = typeof resultText === "string" ? resultText.match(TASK_CREATED_PATTERN) : null;
173
+ if (!match) {
174
+ log.debug("TaskCreate result did not match expected format", { sessionId, toolUseId, resultText });
175
+ return null;
176
+ }
177
+ const claudeId = match[1];
178
+ if (ledger.todos.has(claudeId)) {
179
+ log.debug("TaskCreate result for already-known claude id; overwriting", { sessionId, claudeId });
180
+ }
181
+ ledger.todos.set(claudeId, { id: claudeId, content: pending.subject, status: "pending" });
182
+ return materialize(ledger);
183
+ }
184
+ function applyTaskUpdate(sessionId, input) {
185
+ if (!sessionId) return null;
186
+ const taskId = typeof input?.taskId === "string" ? input.taskId : null;
187
+ if (!taskId) return null;
188
+ const ledger = ledgers.get(sessionId);
189
+ if (!ledger) return null;
190
+ const entry = ledger.todos.get(taskId);
191
+ if (!entry) {
192
+ log.debug("TaskUpdate for unknown task id", { sessionId, taskId });
193
+ return null;
194
+ }
195
+ if (input?.status === "deleted") {
196
+ ledger.todos.delete(taskId);
197
+ return materialize(ledger);
198
+ }
199
+ if (typeof input?.status === "string" && VALID_STATUSES.has(input.status)) {
200
+ entry.status = input.status;
201
+ }
202
+ if (typeof input?.subject === "string" && input.subject.trim().length > 0) {
203
+ entry.content = input.subject.trim();
204
+ }
205
+ return materialize(ledger);
206
+ }
207
+ function clearLedger(sessionId) {
208
+ if (!sessionId) return;
209
+ ledgers.delete(sessionId);
210
+ }
211
+
127
212
  // src/tool-mapping.ts
128
213
  function mapToolInput(name, input) {
129
214
  if (!input) return input;
@@ -199,17 +284,42 @@ var CLAUDE_INTERNAL_TOOLS = /* @__PURE__ */ new Set([
199
284
  "ToolSearch",
200
285
  "Agent",
201
286
  "AskFollowupQuestion",
202
- "TaskCreate",
203
- "TaskUpdate",
204
287
  "TaskList",
205
288
  "TaskGet",
206
289
  "TaskStop"
207
290
  ]);
291
+ function emitTodoWrite(todos) {
292
+ return {
293
+ name: "todowrite",
294
+ input: {
295
+ todos: todos.map((todo) => ({
296
+ id: todo.id,
297
+ content: todo.content,
298
+ status: todo.status,
299
+ priority: "medium"
300
+ }))
301
+ },
302
+ executed: false
303
+ };
304
+ }
208
305
  function mapTool(name, input, opts) {
209
306
  if (CLAUDE_INTERNAL_TOOLS.has(name)) {
210
307
  log.debug("skipping Claude CLI internal tool", { name });
211
308
  return { name, input, executed: true, skip: true };
212
309
  }
310
+ if (name === "TaskCreate") {
311
+ if (opts?.sessionId && opts?.toolUseId) {
312
+ applyTaskCreateToolUse(opts.sessionId, opts.toolUseId, input);
313
+ }
314
+ return { name, input, executed: true, skip: true };
315
+ }
316
+ if (name === "TaskUpdate") {
317
+ if (opts?.sessionId) {
318
+ const list = applyTaskUpdate(opts.sessionId, input);
319
+ if (list !== null) return emitTodoWrite(list);
320
+ }
321
+ return { name, input, executed: true, skip: true };
322
+ }
213
323
  if (name === "EnterPlanMode") return { name: "plan_enter", input: {}, executed: false };
214
324
  if (name === "ExitPlanMode") return { name: "plan_exit", input, executed: false };
215
325
  if (name === "TodoWrite") {
@@ -1181,6 +1291,8 @@ function setClaudeSessionId(key, sessionId) {
1181
1291
  claudeSessions.set(key, sessionId);
1182
1292
  }
1183
1293
  function deleteClaudeSessionId(key) {
1294
+ const claudeSessionId = claudeSessions.get(key);
1295
+ if (claudeSessionId) clearLedger(claudeSessionId);
1184
1296
  claudeSessions.delete(key);
1185
1297
  }
1186
1298
  function spawnClaudeProcess(cliPath, cliArgs, cwd, sessionKey2, proxyServer, mcpHash, systemPromptFile) {
@@ -1406,6 +1518,36 @@ var DEFAULT_PROXY_TOOLS = [
1406
1518
  },
1407
1519
  required: ["url"]
1408
1520
  }
1521
+ },
1522
+ {
1523
+ name: "task",
1524
+ 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). The call blocks until the subagent finishes; the 10-minute proxy timeout applies.",
1525
+ inputSchema: {
1526
+ type: "object",
1527
+ properties: {
1528
+ description: {
1529
+ type: "string",
1530
+ description: "A short (3-5 words) description of the task"
1531
+ },
1532
+ prompt: {
1533
+ type: "string",
1534
+ description: "The task for the agent to perform"
1535
+ },
1536
+ subagent_type: {
1537
+ type: "string",
1538
+ description: "The type of specialized agent to use for this task"
1539
+ },
1540
+ task_id: {
1541
+ type: "string",
1542
+ 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."
1543
+ },
1544
+ command: {
1545
+ type: "string",
1546
+ description: "The command that triggered this task"
1547
+ }
1548
+ },
1549
+ required: ["description", "prompt", "subagent_type"]
1550
+ }
1409
1551
  }
1410
1552
  ];
1411
1553
  async function createProxyMcpServer(tools = DEFAULT_PROXY_TOOLS) {
@@ -1642,7 +1784,8 @@ function disallowedToolFlags(tools) {
1642
1784
  edit: ["Edit", "MultiEdit"],
1643
1785
  glob: ["Glob"],
1644
1786
  grep: ["Grep"],
1645
- webfetch: ["WebFetch"]
1787
+ webfetch: ["WebFetch"],
1788
+ task: ["Agent"]
1646
1789
  };
1647
1790
  const out = [];
1648
1791
  const seen = /* @__PURE__ */ new Set();
@@ -2672,7 +2815,11 @@ ${plan}
2672
2815
  input: mappedInput,
2673
2816
  executed,
2674
2817
  skip
2675
- } = mapTool(tc.name, tc.args, { webSearch: this.config.webSearch });
2818
+ } = mapTool(tc.name, tc.args, {
2819
+ webSearch: this.config.webSearch,
2820
+ sessionId: getClaudeSessionId(sk),
2821
+ toolUseId: tc.id
2822
+ });
2676
2823
  if (skip) continue;
2677
2824
  content.push({
2678
2825
  type: "tool-call",
@@ -3115,7 +3262,11 @@ ${plan}
3115
3262
  const { name: mappedName, skip, executed } = mapTool(
3116
3263
  block.name,
3117
3264
  void 0,
3118
- { webSearch: self.config.webSearch }
3265
+ {
3266
+ webSearch: self.config.webSearch,
3267
+ sessionId: getClaudeSessionId(sk),
3268
+ toolUseId: block.id
3269
+ }
3119
3270
  );
3120
3271
  if (!skip) {
3121
3272
  controller.enqueue({
@@ -3249,7 +3400,11 @@ ${plan}
3249
3400
  input: mappedInput,
3250
3401
  executed,
3251
3402
  skip
3252
- } = mapTool(tc.name, parsedInput, { webSearch: self.config.webSearch });
3403
+ } = mapTool(tc.name, parsedInput, {
3404
+ webSearch: self.config.webSearch,
3405
+ sessionId: getClaudeSessionId(sk),
3406
+ toolUseId: tc.id
3407
+ });
3253
3408
  if (!skip) {
3254
3409
  toolCallsById.set(tc.id, {
3255
3410
  id: tc.id,
@@ -3415,7 +3570,11 @@ ${plan}
3415
3570
  input: mappedInput,
3416
3571
  executed,
3417
3572
  skip
3418
- } = mapTool(block.name, parsedInput, { webSearch: self.config.webSearch });
3573
+ } = mapTool(block.name, parsedInput, {
3574
+ webSearch: self.config.webSearch,
3575
+ sessionId: getClaudeSessionId(sk),
3576
+ toolUseId: block.id
3577
+ });
3419
3578
  if (!skip) {
3420
3579
  if (!executed) skipResultForIds.add(block.id);
3421
3580
  controller.enqueue({
@@ -3456,16 +3615,48 @@ ${plan}
3456
3615
  });
3457
3616
  continue;
3458
3617
  }
3618
+ let resultText = "";
3619
+ if (typeof block.content === "string") {
3620
+ resultText = block.content;
3621
+ } else if (Array.isArray(block.content)) {
3622
+ resultText = block.content.filter(
3623
+ (c) => c.type === "text" && typeof c.text === "string"
3624
+ ).map((c) => c.text).join("\n");
3625
+ }
3626
+ const claudeSessionId = getClaudeSessionId(sk);
3627
+ if (claudeSessionId) {
3628
+ const list = applyTaskCreateToolResult(
3629
+ claudeSessionId,
3630
+ block.tool_use_id,
3631
+ resultText
3632
+ );
3633
+ if (list) {
3634
+ const synthId = `todowrite_${block.tool_use_id}`;
3635
+ controller.enqueue({
3636
+ type: "tool-input-start",
3637
+ id: synthId,
3638
+ toolName: "todowrite",
3639
+ providerExecuted: false
3640
+ });
3641
+ controller.enqueue({
3642
+ type: "tool-call",
3643
+ toolCallId: synthId,
3644
+ toolName: "todowrite",
3645
+ input: JSON.stringify({
3646
+ todos: list.map((t) => ({
3647
+ id: t.id,
3648
+ content: t.content,
3649
+ status: t.status,
3650
+ priority: "medium"
3651
+ }))
3652
+ }),
3653
+ providerExecuted: false
3654
+ });
3655
+ noteToolActivity();
3656
+ }
3657
+ }
3459
3658
  const toolCall = toolCallsById.get(block.tool_use_id);
3460
3659
  if (toolCall) {
3461
- let resultText = "";
3462
- if (typeof block.content === "string") {
3463
- resultText = block.content;
3464
- } else if (Array.isArray(block.content)) {
3465
- resultText = block.content.filter(
3466
- (c) => c.type === "text" && typeof c.text === "string"
3467
- ).map((c) => c.text).join("\n");
3468
- }
3469
3660
  controller.enqueue({
3470
3661
  type: "tool-result",
3471
3662
  toolCallId: block.tool_use_id,