@khalilgharbaoui/opencode-claude-code-plugin 0.16.0 → 0.18.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 +18 -3
- package/dist/index.d.ts +14 -0
- package/dist/index.js +238 -65
- package/dist/index.js.map +1 -1
- package/package.json +4 -3
- package/skills/claude-code-plugin/SKILL.md +441 -0
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
|
|
@@ -552,6 +553,20 @@ Notes:
|
|
|
552
553
|
|
|
553
554
|
Fully restart opencode after upgrading to load the command and runtime changes. Other providers do not gain Claude's native side-question behavior from this command.
|
|
554
555
|
|
|
556
|
+
## Configuration skill
|
|
557
|
+
|
|
558
|
+
The package includes a `claude-code-plugin` skill so your agent can configure it without asking you to navigate all its options. Ask, for example:
|
|
559
|
+
|
|
560
|
+
```text
|
|
561
|
+
Use the claude-code-plugin skill to configure a work account and idle worker cleanup.
|
|
562
|
+
```
|
|
563
|
+
|
|
564
|
+
It covers accounts, models and agent effort, proxy tools, permissions, MCP/skill bridging, timeouts, logging, upgrades and troubleshooting. It directs the agent to preserve JSONC comments, change only requested settings, validate the result, protect credentials and ask before paid probes or broader permissions.
|
|
565
|
+
|
|
566
|
+
The plugin registers the bundled directory with opencode's `skills.paths`, making it available to other providers too on supporting opencode versions. For ordinary headless Claude turns it also loads through Claude's native Skill tool as `opencode-skills:claude-code-plugin`, even when `bridgeOpencodeSkills` is off. This requires CLI `--plugin-dir` support; interactive transport, compaction and direct `doGenerate` calls do not load the native bridge.
|
|
567
|
+
|
|
568
|
+
No separate skill installation or copying is needed. It ships with each package version, so upgrading updates the reference. Fully restart opencode to load it. `test-configure-skill.ts` checks coverage of provider/logging options, model ids, proxy tools and environment variables; maintainers must update behavior and default guidance in the same change as the implementation.
|
|
569
|
+
|
|
555
570
|
## Skill bridge
|
|
556
571
|
|
|
557
572
|
opencode and Claude Code use the same on-disk skill format, a `<name>/SKILL.md` whose frontmatter carries `name` and `description`, but they read from different directories. opencode looks in `.opencode/skills/` and `~/.config/opencode/skills/`; the Claude CLI looks in `~/.claude/skills/` and its own plugins. So opencode advertises your skills in the system prompt it forwards, the model calls `Skill("browser-automation")`, and Claude answers `Unknown skill`.
|
|
@@ -567,7 +582,7 @@ Claude can invoke them with the Skill tool or as `/opencode-skills:<name>`. `--p
|
|
|
567
582
|
|
|
568
583
|
Discovery order, first match wins: `.opencode/skills/` walking up from the working directory, then `~/.opencode/skills/`, then `$OPENCODE_CONFIG_DIR/skills/`, then `~/.config/opencode/skills/`. A project skill shadows a global one of the same name. If the skill set is unchanged the staged directory is reused between spawns.
|
|
569
584
|
|
|
570
|
-
|
|
585
|
+
Bridging **your own skills is off by default** here, unlike on the fork it came from: every bridged skill is also listed in the system prompt opencode already forwards, so a large skill set is paid for twice on every turn. Turn it on when you see `Unknown skill`. The bundled configuration skill is loaded independently of this opt-in. The native bridge is not used for compaction or interactive transport, or on a Claude CLI without `--plugin-dir` (the plugin probes `claude --help` and logs a notice).
|
|
571
586
|
|
|
572
587
|
This bridge was written by [@broskees](https://github.com/broskees) (Joseph Roberts) on his fork and absorbed here with credit; see [Credits](#credits).
|
|
573
588
|
|
|
@@ -965,7 +980,7 @@ This plugin absorbs work from its forks directly, cherry-picked with the origina
|
|
|
965
980
|
| [@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
981
|
| [@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
982
|
| [@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` |
|
|
983
|
+
| [@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
984
|
| [@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
985
|
| [@CollieIsCute](https://github.com/CollieIsCute) (Collie Tsai) | The plan-mode approval bridge. | `8c5b583` |
|
|
971
986
|
| [@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
|
@@ -78,6 +78,10 @@ type OpenCodeConfig = {
|
|
|
78
78
|
models?: Record<string, unknown>;
|
|
79
79
|
}>;
|
|
80
80
|
agent?: Record<string, Record<string, unknown>>;
|
|
81
|
+
skills?: {
|
|
82
|
+
paths?: string[];
|
|
83
|
+
urls?: string[];
|
|
84
|
+
};
|
|
81
85
|
};
|
|
82
86
|
/**
|
|
83
87
|
* Bus events surface to plugins. Shape mirrors what opencode core publishes
|
|
@@ -630,6 +634,16 @@ declare class ClaudeCodeLanguageModel implements LanguageModelV3 {
|
|
|
630
634
|
*/
|
|
631
635
|
private ensureProxyServer;
|
|
632
636
|
private extractPendingProxyResult;
|
|
637
|
+
/**
|
|
638
|
+
* The result opencode produced for a pending proxy call, if the prompt
|
|
639
|
+
* carries it. For `task_batch` that means every child's result gathered
|
|
640
|
+
* back onto the parent: opencode runs the children in one step and hands
|
|
641
|
+
* all their results to the next call together, so a partial set is not
|
|
642
|
+
* expected. If it ever happens the batch still resolves, with the gap
|
|
643
|
+
* named in the text, because leaving the parent pending would send this
|
|
644
|
+
* turn down the fresh-envelope path and reject the call as orphaned.
|
|
645
|
+
*/
|
|
646
|
+
private extractPendingProxyResultForCall;
|
|
633
647
|
/**
|
|
634
648
|
* Resolve the session affinity token for this LLM call. Delegates to the
|
|
635
649
|
* exported `resolveSessionAffinity` helper so the logic is unit-testable.
|
package/dist/index.js
CHANGED
|
@@ -447,10 +447,13 @@ function detectCliSupportsFlag(cliPath, flag) {
|
|
|
447
447
|
if (cached) return cached;
|
|
448
448
|
const promise = (async () => {
|
|
449
449
|
try {
|
|
450
|
-
const
|
|
450
|
+
const execution = execFileAsync(cliPath, ["--help"], {
|
|
451
451
|
timeout: 5e3,
|
|
452
|
+
killSignal: "SIGKILL",
|
|
452
453
|
maxBuffer: 4 * 1024 * 1024
|
|
453
454
|
});
|
|
455
|
+
execution.child.stdin?.end();
|
|
456
|
+
const { stdout } = await execution;
|
|
454
457
|
return stdout.includes(flag);
|
|
455
458
|
} catch (err) {
|
|
456
459
|
log.warn("failed to probe claude cli flag support", {
|
|
@@ -523,6 +526,8 @@ var PROXY_DEFAULT_TIMEOUT_MS = 10 * 60 * 1e3;
|
|
|
523
526
|
var PROXY_PER_TOOL_DEFAULT_TIMEOUT_MS = {
|
|
524
527
|
task: 60 * 60 * 1e3,
|
|
525
528
|
// 60 min
|
|
529
|
+
task_batch: 60 * 60 * 1e3,
|
|
530
|
+
// 60 min, same reasoning: it IS task calls
|
|
526
531
|
question: 30 * 60 * 1e3
|
|
527
532
|
// 30 min
|
|
528
533
|
};
|
|
@@ -562,14 +567,58 @@ function resolveProxyClientCeilingMs(overrides) {
|
|
|
562
567
|
function buildProxyTimeoutError(toolName, ms) {
|
|
563
568
|
const key = toolName.toLowerCase();
|
|
564
569
|
const base = `Proxy tool '${toolName}' timed out after ${ms}ms waiting for opencode to resolve the call`;
|
|
565
|
-
if (key === "task") {
|
|
570
|
+
if (key === "task" || key === TASK_BATCH_TOOL_NAME) {
|
|
566
571
|
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."
|
|
572
|
+
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
573
|
);
|
|
569
574
|
}
|
|
570
575
|
return new Error(base);
|
|
571
576
|
}
|
|
572
|
-
var TASK_PROXY_NOTE = "This
|
|
577
|
+
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).";
|
|
578
|
+
var TASK_BATCH_TOOL_NAME = "task_batch";
|
|
579
|
+
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).";
|
|
580
|
+
var TASK_INPUT_REQUIRED = ["description", "prompt", "subagent_type"];
|
|
581
|
+
function taskBatchInputError(input) {
|
|
582
|
+
const tasks = input?.tasks;
|
|
583
|
+
if (!Array.isArray(tasks) || tasks.length < 2) {
|
|
584
|
+
return "task_batch requires a `tasks` array with at least two items; use `task` for one subagent";
|
|
585
|
+
}
|
|
586
|
+
for (const [index, task] of tasks.entries()) {
|
|
587
|
+
if (task === null || typeof task !== "object" || Array.isArray(task)) {
|
|
588
|
+
return `task_batch tasks[${index}] must be an object`;
|
|
589
|
+
}
|
|
590
|
+
const item = task;
|
|
591
|
+
for (const field of TASK_INPUT_REQUIRED) {
|
|
592
|
+
if (typeof item[field] !== "string") {
|
|
593
|
+
return `task_batch tasks[${index}].${field} must be a string`;
|
|
594
|
+
}
|
|
595
|
+
}
|
|
596
|
+
}
|
|
597
|
+
return null;
|
|
598
|
+
}
|
|
599
|
+
function taskBatchTasks(input) {
|
|
600
|
+
if (taskBatchInputError(input)) return [];
|
|
601
|
+
return input.tasks;
|
|
602
|
+
}
|
|
603
|
+
function taskBatchChildToolCallId(parentToolCallId, index) {
|
|
604
|
+
return `${parentToolCallId}_task_${index}`;
|
|
605
|
+
}
|
|
606
|
+
function formatTaskBatchResults(children) {
|
|
607
|
+
const total = children.length;
|
|
608
|
+
const sections = children.map(({ task, result }, index) => {
|
|
609
|
+
const label = typeof task.description === "string" ? task.description : `task ${index + 1}`;
|
|
610
|
+
const agent = typeof task.subagent_type === "string" ? ` (${task.subagent_type})` : "";
|
|
611
|
+
const header = `## task ${index + 1} of ${total}: ${label}${agent}`;
|
|
612
|
+
if (!result) return `${header}
|
|
613
|
+
[missing] opencode returned no result for this task in the batch`;
|
|
614
|
+
if (result.kind === "error") return `${header}
|
|
615
|
+
[error] ${result.message}`;
|
|
616
|
+
return `${header}
|
|
617
|
+
${result.isError ? "[error] " : ""}${result.text}`;
|
|
618
|
+
});
|
|
619
|
+
const failed = children.some(({ result }) => !result || result.kind === "error" || result.isError);
|
|
620
|
+
return { kind: "text", text: sections.join("\n\n"), ...failed ? { isError: true } : {} };
|
|
621
|
+
}
|
|
573
622
|
var AGENT_TYPES_HEADING = "Available agent types";
|
|
574
623
|
var AGENT_BLURB_LIMIT = 140;
|
|
575
624
|
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 +646,7 @@ function overlayTaskProxyDescription(tools, liveDescription) {
|
|
|
597
646
|
const agentTypes = extractAgentTypeList(liveDescription);
|
|
598
647
|
if (!agentTypes) return tools;
|
|
599
648
|
return tools.map(
|
|
600
|
-
(t) => t.name === "task" ? { ...t, description: `${agentTypes}
|
|
649
|
+
(t) => t.name === "task" || t.name === TASK_BATCH_TOOL_NAME ? { ...t, description: `${agentTypes}
|
|
601
650
|
|
|
602
651
|
${t.description}` } : t
|
|
603
652
|
);
|
|
@@ -615,6 +664,32 @@ function filterQuestionProxyByOpencodeSupport(tools, opencodeHasQuestion) {
|
|
|
615
664
|
if (opencodeHasQuestion) return tools;
|
|
616
665
|
return tools.filter((t) => t.name !== "question");
|
|
617
666
|
}
|
|
667
|
+
var TASK_INPUT_PROPERTIES = {
|
|
668
|
+
description: {
|
|
669
|
+
type: "string",
|
|
670
|
+
description: "A short (3-5 words) description of the task"
|
|
671
|
+
},
|
|
672
|
+
prompt: {
|
|
673
|
+
type: "string",
|
|
674
|
+
description: "The task for the agent to perform"
|
|
675
|
+
},
|
|
676
|
+
subagent_type: {
|
|
677
|
+
type: "string",
|
|
678
|
+
description: "The type of specialized agent to use for this task"
|
|
679
|
+
},
|
|
680
|
+
task_id: {
|
|
681
|
+
type: "string",
|
|
682
|
+
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."
|
|
683
|
+
},
|
|
684
|
+
command: {
|
|
685
|
+
type: "string",
|
|
686
|
+
description: "The command that triggered this task"
|
|
687
|
+
},
|
|
688
|
+
background: {
|
|
689
|
+
type: "boolean",
|
|
690
|
+
description: "Run the task in the background when supported by opencode"
|
|
691
|
+
}
|
|
692
|
+
};
|
|
618
693
|
var DEFAULT_PROXY_TOOLS = [
|
|
619
694
|
{
|
|
620
695
|
name: "bash",
|
|
@@ -708,35 +783,30 @@ var DEFAULT_PROXY_TOOLS = [
|
|
|
708
783
|
{
|
|
709
784
|
name: "task",
|
|
710
785
|
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,
|
|
786
|
+
inputSchema: {
|
|
787
|
+
type: "object",
|
|
788
|
+
properties: TASK_INPUT_PROPERTIES,
|
|
789
|
+
required: TASK_INPUT_REQUIRED
|
|
790
|
+
}
|
|
791
|
+
},
|
|
792
|
+
{
|
|
793
|
+
name: TASK_BATCH_TOOL_NAME,
|
|
794
|
+
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
795
|
inputSchema: {
|
|
712
796
|
type: "object",
|
|
713
797
|
properties: {
|
|
714
|
-
|
|
715
|
-
type: "
|
|
716
|
-
|
|
717
|
-
|
|
718
|
-
|
|
719
|
-
|
|
720
|
-
|
|
721
|
-
|
|
722
|
-
|
|
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"
|
|
798
|
+
tasks: {
|
|
799
|
+
type: "array",
|
|
800
|
+
minItems: 2,
|
|
801
|
+
description: "Independent subagent tasks to run concurrently",
|
|
802
|
+
items: {
|
|
803
|
+
type: "object",
|
|
804
|
+
properties: TASK_INPUT_PROPERTIES,
|
|
805
|
+
required: TASK_INPUT_REQUIRED
|
|
806
|
+
}
|
|
737
807
|
}
|
|
738
808
|
},
|
|
739
|
-
required: ["
|
|
809
|
+
required: ["tasks"]
|
|
740
810
|
}
|
|
741
811
|
},
|
|
742
812
|
{
|
|
@@ -922,6 +992,13 @@ async function createProxyMcpServer(tools = DEFAULT_PROXY_TOOLS, timeoutOverride
|
|
|
922
992
|
});
|
|
923
993
|
return;
|
|
924
994
|
}
|
|
995
|
+
if (toolName === TASK_BATCH_TOOL_NAME) {
|
|
996
|
+
const problem = taskBatchInputError(input);
|
|
997
|
+
if (problem) {
|
|
998
|
+
writeToolCallResult(res, requestId, { kind: "error", message: problem });
|
|
999
|
+
return;
|
|
1000
|
+
}
|
|
1001
|
+
}
|
|
925
1002
|
const interceptor = interceptors?.get(toolName);
|
|
926
1003
|
if (interceptor) {
|
|
927
1004
|
let intercepted;
|
|
@@ -1126,6 +1203,7 @@ function disallowedToolFlags(tools) {
|
|
|
1126
1203
|
grep: ["Grep"],
|
|
1127
1204
|
webfetch: ["WebFetch"],
|
|
1128
1205
|
task: ["Agent"],
|
|
1206
|
+
task_batch: ["Agent"],
|
|
1129
1207
|
// `question` disables Claude Code's built-in `AskUserQuestion` so the
|
|
1130
1208
|
// structured-questions path flows through opencode's native `question`
|
|
1131
1209
|
// tool instead — same UI/permission/audit benefits as the other
|
|
@@ -3227,7 +3305,17 @@ import * as crypto2 from "crypto";
|
|
|
3227
3305
|
import * as fs3 from "fs";
|
|
3228
3306
|
import * as os2 from "os";
|
|
3229
3307
|
import * as path4 from "path";
|
|
3308
|
+
import { fileURLToPath } from "url";
|
|
3230
3309
|
var SKILL_PLUGIN_NAME = "opencode-skills";
|
|
3310
|
+
function bundledSkillsDir() {
|
|
3311
|
+
try {
|
|
3312
|
+
const here = fileURLToPath(import.meta.url);
|
|
3313
|
+
const dir = path4.resolve(path4.dirname(here), "..", "skills");
|
|
3314
|
+
return dirExists(dir) ? dir : null;
|
|
3315
|
+
} catch {
|
|
3316
|
+
return null;
|
|
3317
|
+
}
|
|
3318
|
+
}
|
|
3231
3319
|
function dirExists(p) {
|
|
3232
3320
|
try {
|
|
3233
3321
|
return fs3.statSync(p).isDirectory();
|
|
@@ -3266,28 +3354,46 @@ function skillRoots(cwd) {
|
|
|
3266
3354
|
if (xdg) push(path4.join(xdg, "opencode", "skills"));
|
|
3267
3355
|
return roots;
|
|
3268
3356
|
}
|
|
3357
|
+
function collectSkills(root, claimed, found) {
|
|
3358
|
+
let entries;
|
|
3359
|
+
try {
|
|
3360
|
+
entries = fs3.readdirSync(root, { withFileTypes: true });
|
|
3361
|
+
} catch {
|
|
3362
|
+
return;
|
|
3363
|
+
}
|
|
3364
|
+
for (const entry of entries) {
|
|
3365
|
+
if (!entry.isDirectory() && !entry.isSymbolicLink()) continue;
|
|
3366
|
+
const name = entry.name;
|
|
3367
|
+
if (name.startsWith(".")) continue;
|
|
3368
|
+
if (claimed.has(name)) continue;
|
|
3369
|
+
const dir = path4.join(root, name);
|
|
3370
|
+
if (!fileExists(path4.join(dir, "SKILL.md"))) continue;
|
|
3371
|
+
claimed.add(name);
|
|
3372
|
+
found.push({ name, dir });
|
|
3373
|
+
}
|
|
3374
|
+
}
|
|
3375
|
+
var byName = (a, b) => a.name.localeCompare(b.name);
|
|
3269
3376
|
function discoverOpencodeSkills(cwd) {
|
|
3270
3377
|
const found = [];
|
|
3271
3378
|
const claimed = /* @__PURE__ */ new Set();
|
|
3272
|
-
for (const root of skillRoots(cwd))
|
|
3273
|
-
|
|
3274
|
-
|
|
3275
|
-
|
|
3276
|
-
|
|
3277
|
-
|
|
3278
|
-
|
|
3279
|
-
|
|
3280
|
-
|
|
3281
|
-
|
|
3282
|
-
|
|
3283
|
-
|
|
3284
|
-
|
|
3285
|
-
|
|
3286
|
-
|
|
3287
|
-
|
|
3288
|
-
|
|
3289
|
-
|
|
3290
|
-
return found.sort((a, b) => a.name.localeCompare(b.name));
|
|
3379
|
+
for (const root of skillRoots(cwd)) collectSkills(root, claimed, found);
|
|
3380
|
+
return found.sort(byName);
|
|
3381
|
+
}
|
|
3382
|
+
function discoverBundledSkills() {
|
|
3383
|
+
const root = bundledSkillsDir();
|
|
3384
|
+
if (!root) return [];
|
|
3385
|
+
const found = [];
|
|
3386
|
+
collectSkills(root, /* @__PURE__ */ new Set(), found);
|
|
3387
|
+
return found.sort(byName);
|
|
3388
|
+
}
|
|
3389
|
+
function registerBundledSkillPath(config) {
|
|
3390
|
+
const dir = bundledSkillsDir();
|
|
3391
|
+
if (!dir) return false;
|
|
3392
|
+
config.skills ??= {};
|
|
3393
|
+
const paths = config.skills.paths ??= [];
|
|
3394
|
+
if (paths.some((p) => path4.resolve(p) === dir)) return false;
|
|
3395
|
+
paths.push(dir);
|
|
3396
|
+
return true;
|
|
3291
3397
|
}
|
|
3292
3398
|
function linkSkill(source, target) {
|
|
3293
3399
|
try {
|
|
@@ -3333,8 +3439,10 @@ function buildSkillPluginDir(skills) {
|
|
|
3333
3439
|
return root;
|
|
3334
3440
|
}
|
|
3335
3441
|
async function resolveSkillPluginDirs(opts) {
|
|
3336
|
-
|
|
3337
|
-
const
|
|
3442
|
+
const user = opts.enabled ? discoverOpencodeSkills(opts.cwd) : [];
|
|
3443
|
+
const claimed = new Set(user.map((s) => s.name));
|
|
3444
|
+
const bundled = discoverBundledSkills().filter((s) => !claimed.has(s.name));
|
|
3445
|
+
const skills = [...user, ...bundled].sort(byName);
|
|
3338
3446
|
if (skills.length === 0) return [];
|
|
3339
3447
|
const supported = await detectCliSupportsFlag(opts.cliPath, "--plugin-dir");
|
|
3340
3448
|
if (!supported) {
|
|
@@ -3349,6 +3457,7 @@ async function resolveSkillPluginDirs(opts) {
|
|
|
3349
3457
|
log.info("bridged opencode skills into claude", {
|
|
3350
3458
|
count: skills.length,
|
|
3351
3459
|
names: skills.map((s) => s.name),
|
|
3460
|
+
bundled: bundled.map((s) => s.name),
|
|
3352
3461
|
pluginDir: dir
|
|
3353
3462
|
});
|
|
3354
3463
|
return [dir];
|
|
@@ -4599,8 +4708,9 @@ blocker. The user can interrupt or abort at any time; turn endings should
|
|
|
4599
4708
|
mark meaningful checkpoints, not every completed substep.`;
|
|
4600
4709
|
var SUBAGENT_DISPATCH_HINT = `## opencode subagents
|
|
4601
4710
|
|
|
4602
|
-
Subagent dispatch in this environment goes through exactly
|
|
4711
|
+
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
4712
|
|
|
4713
|
+
- 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
4714
|
- 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
4715
|
- 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
4716
|
- 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 +4914,24 @@ var ClaudeCodeLanguageModel = class {
|
|
|
4804
4914
|
DEFAULT_PROXY_TOOLS.map((t) => [t.name.toLowerCase(), t])
|
|
4805
4915
|
);
|
|
4806
4916
|
const picked = [];
|
|
4917
|
+
const seen = /* @__PURE__ */ new Set();
|
|
4807
4918
|
const unknown = [];
|
|
4919
|
+
const pick = (def) => {
|
|
4920
|
+
if (seen.has(def.name)) return;
|
|
4921
|
+
seen.add(def.name);
|
|
4922
|
+
picked.push(def);
|
|
4923
|
+
};
|
|
4808
4924
|
for (const n of names) {
|
|
4809
4925
|
const def = defsByName.get(String(n).toLowerCase());
|
|
4810
|
-
if (def)
|
|
4811
|
-
|
|
4926
|
+
if (!def) {
|
|
4927
|
+
unknown.push(String(n));
|
|
4928
|
+
continue;
|
|
4929
|
+
}
|
|
4930
|
+
pick(def);
|
|
4931
|
+
if (def.name === "task") {
|
|
4932
|
+
const batch = defsByName.get(TASK_BATCH_TOOL_NAME);
|
|
4933
|
+
if (batch) pick(batch);
|
|
4934
|
+
}
|
|
4812
4935
|
}
|
|
4813
4936
|
if (unknown.length > 0) {
|
|
4814
4937
|
const known = [...defsByName.keys()].join(", ");
|
|
@@ -4998,6 +5121,41 @@ var ClaudeCodeLanguageModel = class {
|
|
|
4998
5121
|
}
|
|
4999
5122
|
return null;
|
|
5000
5123
|
}
|
|
5124
|
+
/**
|
|
5125
|
+
* The result opencode produced for a pending proxy call, if the prompt
|
|
5126
|
+
* carries it. For `task_batch` that means every child's result gathered
|
|
5127
|
+
* back onto the parent: opencode runs the children in one step and hands
|
|
5128
|
+
* all their results to the next call together, so a partial set is not
|
|
5129
|
+
* expected. If it ever happens the batch still resolves, with the gap
|
|
5130
|
+
* named in the text, because leaving the parent pending would send this
|
|
5131
|
+
* turn down the fresh-envelope path and reject the call as orphaned.
|
|
5132
|
+
*/
|
|
5133
|
+
extractPendingProxyResultForCall(prompt, call) {
|
|
5134
|
+
if (call.toolName !== TASK_BATCH_TOOL_NAME) {
|
|
5135
|
+
return this.extractPendingProxyResult(prompt, call.toolCallId);
|
|
5136
|
+
}
|
|
5137
|
+
const tasks = taskBatchTasks(call.input);
|
|
5138
|
+
if (tasks.length === 0) {
|
|
5139
|
+
return { kind: "error", message: "task_batch input is not a list of task objects" };
|
|
5140
|
+
}
|
|
5141
|
+
const children = tasks.map((task, index) => ({
|
|
5142
|
+
task,
|
|
5143
|
+
result: this.extractPendingProxyResult(
|
|
5144
|
+
prompt,
|
|
5145
|
+
taskBatchChildToolCallId(call.toolCallId, index)
|
|
5146
|
+
)
|
|
5147
|
+
}));
|
|
5148
|
+
const answered = children.filter((child) => child.result !== null).length;
|
|
5149
|
+
if (answered === 0) return null;
|
|
5150
|
+
if (answered < children.length) {
|
|
5151
|
+
log.warn("task_batch resolving with child results missing", {
|
|
5152
|
+
toolCallId: call.toolCallId,
|
|
5153
|
+
answered,
|
|
5154
|
+
total: children.length
|
|
5155
|
+
});
|
|
5156
|
+
}
|
|
5157
|
+
return formatTaskBatchResults(children);
|
|
5158
|
+
}
|
|
5001
5159
|
/**
|
|
5002
5160
|
* Resolve the session affinity token for this LLM call. Delegates to the
|
|
5003
5161
|
* exported `resolveSessionAffinity` helper so the logic is unit-testable.
|
|
@@ -5809,7 +5967,7 @@ ${plan}
|
|
|
5809
5967
|
const self = this;
|
|
5810
5968
|
const previousPendingProxyMatches = previousPendingProxyCalls.map((call) => ({
|
|
5811
5969
|
call,
|
|
5812
|
-
result: this.
|
|
5970
|
+
result: this.extractPendingProxyResultForCall(options.prompt, call)
|
|
5813
5971
|
}));
|
|
5814
5972
|
const hasMatchedPendingResults = previousPendingProxyMatches.some(
|
|
5815
5973
|
(m) => m.result !== null
|
|
@@ -6271,20 +6429,34 @@ ${plan}
|
|
|
6271
6429
|
const finishWithToolCalls = (calls) => {
|
|
6272
6430
|
if (controllerClosed) return;
|
|
6273
6431
|
if (calls.length === 0) return;
|
|
6274
|
-
|
|
6432
|
+
const enqueueToolCall = (toolCallId, toolName, input) => {
|
|
6275
6433
|
controller.enqueue({
|
|
6276
6434
|
type: "tool-input-start",
|
|
6277
|
-
id:
|
|
6278
|
-
toolName
|
|
6435
|
+
id: toolCallId,
|
|
6436
|
+
toolName
|
|
6279
6437
|
});
|
|
6280
6438
|
controller.enqueue({
|
|
6281
6439
|
type: "tool-call",
|
|
6282
|
-
toolCallId
|
|
6283
|
-
toolName
|
|
6284
|
-
input: JSON.stringify(
|
|
6440
|
+
toolCallId,
|
|
6441
|
+
toolName,
|
|
6442
|
+
input: JSON.stringify(input),
|
|
6285
6443
|
providerExecuted: false
|
|
6286
6444
|
});
|
|
6287
|
-
skipResultForIds.add(
|
|
6445
|
+
skipResultForIds.add(toolCallId);
|
|
6446
|
+
};
|
|
6447
|
+
for (const call of calls) {
|
|
6448
|
+
if (call.toolName === TASK_BATCH_TOOL_NAME) {
|
|
6449
|
+
for (const [index, task] of taskBatchTasks(call.input).entries()) {
|
|
6450
|
+
enqueueToolCall(
|
|
6451
|
+
taskBatchChildToolCallId(call.toolCallId, index),
|
|
6452
|
+
"task",
|
|
6453
|
+
task
|
|
6454
|
+
);
|
|
6455
|
+
}
|
|
6456
|
+
skipResultForIds.add(call.toolCallId);
|
|
6457
|
+
} else {
|
|
6458
|
+
enqueueToolCall(call.toolCallId, call.toolName, call.input);
|
|
6459
|
+
}
|
|
6288
6460
|
markPendingProxyCallEmitted(call.toolCallId);
|
|
6289
6461
|
}
|
|
6290
6462
|
controller.enqueue({
|
|
@@ -7532,7 +7704,7 @@ import {
|
|
|
7532
7704
|
} from "fs";
|
|
7533
7705
|
import { homedir as homedir6 } from "os";
|
|
7534
7706
|
import { join as join8, resolve as resolve4 } from "path";
|
|
7535
|
-
import { fileURLToPath } from "url";
|
|
7707
|
+
import { fileURLToPath as fileURLToPath2 } from "url";
|
|
7536
7708
|
var STALE_PACKAGE_NAME = "opencode-claude-code-plugin";
|
|
7537
7709
|
var SUSPECT_DESCRIPTION_TOKEN = "Claude Code";
|
|
7538
7710
|
var alreadyRan = false;
|
|
@@ -7564,7 +7736,7 @@ function userIntendsToUseUnscoped() {
|
|
|
7564
7736
|
}
|
|
7565
7737
|
function ourLoadedDir() {
|
|
7566
7738
|
try {
|
|
7567
|
-
const filePath =
|
|
7739
|
+
const filePath = fileURLToPath2(import.meta.url);
|
|
7568
7740
|
return realpathSync(resolve4(filePath, "..", ".."));
|
|
7569
7741
|
} catch {
|
|
7570
7742
|
return null;
|
|
@@ -7638,12 +7810,12 @@ import { execFile as execFile2 } from "child_process";
|
|
|
7638
7810
|
import * as fs6 from "fs";
|
|
7639
7811
|
import * as path8 from "path";
|
|
7640
7812
|
import { promisify as promisify2 } from "util";
|
|
7641
|
-
import { fileURLToPath as
|
|
7813
|
+
import { fileURLToPath as fileURLToPath3 } from "url";
|
|
7642
7814
|
var cachedPluginVersion;
|
|
7643
7815
|
function pluginVersion() {
|
|
7644
7816
|
if (cachedPluginVersion) return cachedPluginVersion;
|
|
7645
7817
|
try {
|
|
7646
|
-
const here = path8.dirname(
|
|
7818
|
+
const here = path8.dirname(fileURLToPath3(import.meta.url));
|
|
7647
7819
|
const raw = fs6.readFileSync(path8.join(here, "..", "package.json"), "utf8");
|
|
7648
7820
|
const version = JSON.parse(raw).version;
|
|
7649
7821
|
cachedPluginVersion = typeof version === "string" ? version : "unknown";
|
|
@@ -8039,6 +8211,7 @@ var server = async (input) => {
|
|
|
8039
8211
|
return {
|
|
8040
8212
|
config: async (config) => {
|
|
8041
8213
|
if (registerSideQuestionCommand(config)) ownsSideQuestionCommand = true;
|
|
8214
|
+
registerBundledSkillPath(config);
|
|
8042
8215
|
config.provider ??= {};
|
|
8043
8216
|
await buildAgentRegistry(config);
|
|
8044
8217
|
const expanded = await expandAccountProviders(config);
|