@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 +4 -1
- package/dist/index.d.ts +9 -1
- package/dist/index.js +206 -15
- package/dist/index.js.map +1 -1
- package/package.json +2 -2
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
|
-
|
|
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, {
|
|
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
|
-
{
|
|
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, {
|
|
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, {
|
|
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,
|