@khalilgharbaoui/opencode-claude-code-plugin 0.11.0 → 0.11.2

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
@@ -281,6 +281,20 @@ By default, the plugin proxies `Bash`, `Edit`, `Write`, `WebFetch`, and `Task`.
281
281
  - **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.
282
282
  - **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.
283
283
 
284
+ **Steering models to it.** Headless Claude Code CLIs expose no `Agent`/`Task`
285
+ dispatch tool of their own (verified on 2.1.211), while they *do* expose
286
+ `TaskCreate` — a todo tool. So "use a subagent" requests get mis-resolved:
287
+ a todo appears, nothing runs, and the model may still narrate a successful
288
+ dispatch. Two spawn-time countermeasures prevent that. The plugin injects
289
+ opencode's live agent-type list into the `task` proxy description (so the model
290
+ picks a real `subagent_type` instead of guessing a Claude Code name like
291
+ `general-purpose`, and doesn't grep configs to check a subagent exists), and
292
+ appends a system-prompt note naming
293
+ `mcp__opencode_proxy__task` as the only dispatch path — with the ToolSearch
294
+ recovery step for harnesses that defer MCP tool schemas. Both apply per Claude
295
+ process at spawn, and provider options are read once at opencode startup, so
296
+ `proxyTools` changes need a full opencode restart.
297
+
284
298
  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.
285
299
 
286
300
  Without `"Task"` in `proxyTools`, Claude's built-in `Agent` tool stays enabled and Claude orchestrates subagents internally with no opencode child-session visibility. To opt out of all proxying, including Task, use an explicit empty list:
@@ -544,8 +558,8 @@ grep "plugin ready" ~/.local/share/opencode-claude-code/plugin.log
544
558
 
545
559
  ```json
546
560
  {
547
- "plugin": "0.10.0",
548
- "opencode": "unknown",
561
+ "plugin": "0.11.1",
562
+ "opencode": "1.18.5",
549
563
  "cwd": { "resolved": "/Users/you/code/app", "source": "process" },
550
564
  "providers": ["claude-code-default", "claude-code-work"],
551
565
  "accounts": ["default", "work"],
@@ -568,8 +582,9 @@ Reading it:
568
582
  flags like `--thinking-display`.
569
583
  - **`mcpServers`** is the on-disk merge, before opencode's runtime toggles
570
584
  are applied (those aren't settled yet at startup).
571
- - **`opencode`** reads `unknown` on current opencode: as of 1.17.18 it does
572
- not expose its own version to plugins.
585
+ - **`opencode`** is read from the running opencode binary (`--version`), since
586
+ opencode still does not hand its version to plugins. It reads `unknown` when
587
+ opencode is run from source rather than as the packaged binary.
573
588
 
574
589
  ### Default behavior (no config, no env)
575
590
 
package/dist/index.d.ts CHANGED
@@ -505,6 +505,17 @@ declare class ClaudeCodeLanguageModel implements LanguageModelV3 {
505
505
  * client is unavailable, or no MCP servers are configured.
506
506
  */
507
507
  private resolvedProxyMcpTools;
508
+ /**
509
+ * Live description of opencode's `task` tool for the current
510
+ * provider/model, exactly as opencode's registry renders it for native
511
+ * models — including the "Available agent types" list (built from the
512
+ * default agent's permissions). Overlaid onto the static `task` proxy
513
+ * def so Claude sees the same subagent catalog native opencode models
514
+ * see, instead of hunting through config files. Returns undefined when
515
+ * the SDK client is unavailable (direct AI-SDK use, tests) so the
516
+ * static def stands.
517
+ */
518
+ private fetchLiveTaskDescription;
508
519
  /**
509
520
  * Create a proxy MCP server for a single active Claude process/session.
510
521
  * The process lifecycle owns the server lifecycle via session-manager.
package/dist/index.js CHANGED
@@ -2117,6 +2117,37 @@ function buildProxyTimeoutError(toolName, ms) {
2117
2117
  }
2118
2118
  return new Error(base);
2119
2119
  }
2120
+ 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).";
2121
+ var AGENT_TYPES_HEADING = "Available agent types";
2122
+ var AGENT_BLURB_LIMIT = 140;
2123
+ function extractAgentTypeList(liveDescription) {
2124
+ const live = liveDescription?.trim();
2125
+ if (!live) return void 0;
2126
+ const start = live.indexOf(AGENT_TYPES_HEADING);
2127
+ if (start === -1) return void 0;
2128
+ const entries = [];
2129
+ for (const raw of live.slice(start).split("\n")) {
2130
+ const match = /^-\s*([^:]+):\s*(.+)$/.exec(raw.trim());
2131
+ if (!match) continue;
2132
+ const name = match[1].trim();
2133
+ const blurb = match[2].trim();
2134
+ entries.push(
2135
+ `- ${name}: ${blurb.length > AGENT_BLURB_LIMIT ? `${blurb.slice(0, AGENT_BLURB_LIMIT).trimEnd()}\u2026` : blurb}`
2136
+ );
2137
+ }
2138
+ if (entries.length === 0) return void 0;
2139
+ return `Valid subagent_type values, from opencode's live registry \u2014 anything else fails:
2140
+ ${entries.join("\n")}`;
2141
+ }
2142
+ function overlayTaskProxyDescription(tools, liveDescription) {
2143
+ const agentTypes = extractAgentTypeList(liveDescription);
2144
+ if (!agentTypes) return tools;
2145
+ return tools.map(
2146
+ (t) => t.name === "task" ? { ...t, description: `${agentTypes}
2147
+
2148
+ ${t.description}` } : t
2149
+ );
2150
+ }
2120
2151
  var DEFAULT_PROXY_TOOLS = [
2121
2152
  {
2122
2153
  name: "bash",
@@ -2209,7 +2240,7 @@ var DEFAULT_PROXY_TOOLS = [
2209
2240
  },
2210
2241
  {
2211
2242
  name: "task",
2212
- 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). 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).",
2243
+ 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,
2213
2244
  inputSchema: {
2214
2245
  type: "object",
2215
2246
  properties: {
@@ -2884,6 +2915,14 @@ than pausing for user confirmation between subtasks. End the turn only
2884
2915
  when the task is done, you need clarification on intent, or you hit a real
2885
2916
  blocker. The user can interrupt or abort at any time; turn endings should
2886
2917
  mark meaningful checkpoints, not every completed substep.`;
2918
+ var SUBAGENT_DISPATCH_HINT = `## opencode subagents
2919
+
2920
+ Subagent dispatch in this environment goes through exactly one tool: \`mcp__opencode_proxy__task\`.
2921
+
2922
+ - 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>"\`.
2923
+ - 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.
2924
+ - 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.
2925
+ - Do not verify a subagent's existence by searching config files \u2014 the tool's description lists the available agent types, and invalid types fail fast with a clear error.`;
2887
2926
  var CLAUDE_CLI_CONTEXT_NOTE = `## Runtime environment: Claude Code CLI
2888
2927
 
2889
2928
  You are running via the Claude Code CLI (not a direct API call). This affects context management:
@@ -3058,6 +3097,24 @@ var ClaudeCodeLanguageModel = class {
3058
3097
  }
3059
3098
  return out.length > 0 ? out : null;
3060
3099
  }
3100
+ /**
3101
+ * Live description of opencode's `task` tool for the current
3102
+ * provider/model, exactly as opencode's registry renders it for native
3103
+ * models — including the "Available agent types" list (built from the
3104
+ * default agent's permissions). Overlaid onto the static `task` proxy
3105
+ * def so Claude sees the same subagent catalog native opencode models
3106
+ * see, instead of hunting through config files. Returns undefined when
3107
+ * the SDK client is unavailable (direct AI-SDK use, tests) so the
3108
+ * static def stands.
3109
+ */
3110
+ async fetchLiveTaskDescription() {
3111
+ const items = await fetchOpencodeToolList(
3112
+ this.config.provider,
3113
+ this.modelId,
3114
+ this.config.cwd
3115
+ );
3116
+ return items?.find((item) => item.id === "task")?.description || void 0;
3117
+ }
3061
3118
  /**
3062
3119
  * Create a proxy MCP server for a single active Claude process/session.
3063
3120
  * The process lifecycle owns the server lifecycle via session-manager.
@@ -3929,7 +3986,23 @@ ${plan}
3929
3986
  discovery.allEnabledServerNames
3930
3987
  );
3931
3988
  const excludeServers = proxyMcpTools ? new Set(discovery.allEnabledServerNames) : void 0;
3932
- const combinedProxyTools = resolvedProxy || proxyMcpTools ? [...resolvedProxy ?? [], ...proxyMcpTools ?? []] : null;
3989
+ const taskProxyEnabled = resolvedProxy?.some((t) => t.name === "task") ?? false;
3990
+ let enrichedProxy = resolvedProxy;
3991
+ if (resolvedProxy && taskProxyEnabled) {
3992
+ const liveTaskDescription = await self.fetchLiveTaskDescription();
3993
+ enrichedProxy = overlayTaskProxyDescription(
3994
+ resolvedProxy,
3995
+ liveTaskDescription
3996
+ );
3997
+ log.info("task proxy description overlay", {
3998
+ applied: Boolean(liveTaskDescription),
3999
+ liveDescriptionLength: liveTaskDescription?.length ?? 0,
4000
+ listsAgentTypes: Boolean(
4001
+ liveTaskDescription?.includes("Available agent types")
4002
+ )
4003
+ });
4004
+ }
4005
+ const combinedProxyTools = enrichedProxy || proxyMcpTools ? [...enrichedProxy ?? [], ...proxyMcpTools ?? []] : null;
3933
4006
  if (!proxyServer && combinedProxyTools) {
3934
4007
  proxyServer = await self.ensureProxyServer(combinedProxyTools, sk);
3935
4008
  }
@@ -3946,7 +4019,10 @@ ${plan}
3946
4019
  const systemPromptFile = activeProcess ? void 0 : buildAppendedSystemPrompt(
3947
4020
  cwd,
3948
4021
  self.config.multiStepContinuation !== false,
3949
- extractSystemMessages(options.prompt)
4022
+ [
4023
+ ...extractSystemMessages(options.prompt),
4024
+ ...taskProxyEnabled ? [SUBAGENT_DISPATCH_HINT] : []
4025
+ ]
3950
4026
  );
3951
4027
  cliArgs = buildCliArgs({
3952
4028
  sessionKey: sk,
@@ -5537,8 +5613,10 @@ function cleanupOne(cacheRoot, ourDir) {
5537
5613
  }
5538
5614
 
5539
5615
  // src/startup-diagnostics.ts
5616
+ import { execFile as execFile2 } from "child_process";
5540
5617
  import * as fs5 from "fs";
5541
5618
  import * as path6 from "path";
5619
+ import { promisify as promisify2 } from "util";
5542
5620
  import { fileURLToPath as fileURLToPath2 } from "url";
5543
5621
  var cachedPluginVersion;
5544
5622
  function pluginVersion() {
@@ -5564,6 +5642,29 @@ function pickOpencodeVersion(input) {
5564
5642
  if (typeof direct === "string" && direct.length > 0) return direct;
5565
5643
  return void 0;
5566
5644
  }
5645
+ var execFileAsync2 = promisify2(execFile2);
5646
+ var opencodeVersionProbe;
5647
+ function detectOpencodeVersion(execPath = process.execPath) {
5648
+ if (opencodeVersionProbe) return opencodeVersionProbe;
5649
+ opencodeVersionProbe = (async () => {
5650
+ if (!path6.basename(execPath).toLowerCase().includes("opencode")) {
5651
+ log.debug("skipping opencode version probe: execPath is not opencode", { execPath });
5652
+ return void 0;
5653
+ }
5654
+ try {
5655
+ const { stdout } = await execFileAsync2(execPath, ["--version"], { timeout: 5e3 });
5656
+ const match = /\d+\.\d+\.\d+\S*/.exec(stdout.trim());
5657
+ return match ? match[0] : void 0;
5658
+ } catch (err) {
5659
+ log.debug("opencode version probe failed", {
5660
+ execPath,
5661
+ error: err instanceof Error ? err.message : String(err)
5662
+ });
5663
+ return void 0;
5664
+ }
5665
+ })();
5666
+ return opencodeVersionProbe;
5667
+ }
5567
5668
  function describeSpawnCwd(configured, live = process.cwd(), captured = getOpencodeProjectDirectory()) {
5568
5669
  if (typeof configured === "string" && configured.length > 0) {
5569
5670
  return { resolved: configured, source: "configured" };
@@ -5619,10 +5720,8 @@ function logStartupDiagnostics(providers, opencodeVersion) {
5619
5720
  logged = true;
5620
5721
  void (async () => {
5621
5722
  try {
5622
- const { claudeCliPath, ...rest } = collectStartupDiagnostics(
5623
- providers,
5624
- opencodeVersion
5625
- );
5723
+ const version = opencodeVersion ?? process.env.OPENCODE_VERSION ?? await detectOpencodeVersion();
5724
+ const { claudeCliPath, ...rest } = collectStartupDiagnostics(providers, version);
5626
5725
  const cli = await detectCliVersion(claudeCliPath);
5627
5726
  const diagnostics = {
5628
5727
  ...rest,