@khalilgharbaoui/opencode-claude-code-plugin 0.14.1 → 0.15.1

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
@@ -176,6 +176,66 @@ CLAUDE_CONFIG_DIR="$HOME/.claude-work" claude auth login
176
176
 
177
177
  The account model IDs are internally suffixed, for example `claude-sonnet-4-6@work`, so long-lived Claude subprocess sessions do not collide across accounts. The generated wrapper strips the suffix before calling `claude --model`.
178
178
 
179
+ ### Subagents: your account, their model
180
+
181
+ opencode's agent config cannot express "inherit the account, choose the model". A subagent that omits `model` inherits the invoking agent's whole model string; one that pins `model` inherits neither half, so pinning Opus also pins whichever account was written into it. This plugin closes that gap, because it is the piece that knows the account is the *provider* while the model is only a `--model` flag.
182
+
183
+ Write an agent markdown file. Nothing goes in `opencode.json`.
184
+
185
+ ```markdown
186
+ ---
187
+ description: Designs and builds UI work
188
+ mode: subagent
189
+ ---
190
+ You are a designer...
191
+ ```
192
+
193
+ `@designer` now runs on **the account of the session that invoked it**, on whatever model you point it at. Which model comes from one of two places.
194
+
195
+ Per agent, in the agent's own file:
196
+
197
+ ```yaml
198
+ forceModel: claude-haiku-4-5
199
+ ```
200
+
201
+ Or once, for every subagent that pins nothing, in the provider options:
202
+
203
+ ```json
204
+ { "provider": { "claude-code": { "options": { "defaultSubagentModel": "claude-opus-5" } } } }
205
+ ```
206
+
207
+ The rules, in order:
208
+
209
+ | The agent | Runs on |
210
+ | --- | --- |
211
+ | `forceModel: <id>` | the caller's account, that model |
212
+ | `mode: subagent`, no model, `defaultSubagentModel` set | the caller's account, that model |
213
+ | `mode: subagent`, no model, no default set | untouched, inherits the caller's model |
214
+ | `model: <provider>/<id>` | exactly that, account and all (untouched) |
215
+ | anything opencode ships (`explore`, `general`, `compaction`) | untouched |
216
+
217
+ **`defaultSubagentModel` is unset by default and nothing is overridden without it.** That is deliberate: this feature rewrites what the model picker said would run, so an existing setup that upgrades the plugin has to behave exactly as it did before. Built-ins are excluded for the same reason, since forcing Opus onto a cheap exploration agent would be an expensive surprise nobody asked for. An unknown model id is refused and the original kept, rather than spawning the CLI with a `--model` it will reject.
218
+
219
+ Two things worth knowing. The overridden model is part of the Claude session key, so a subagent forced to Opus never shares a `claude` process with a Fable parent in the same directory. And opencode still prices the turn against the model *it* routed, so a cost readout attributes the work to the caller's model, not the one that actually ran.
220
+
221
+ ### The effort an agent runs at
222
+
223
+ The same file can state its own thinking budget:
224
+
225
+ ```yaml
226
+ reasoningEffort: high
227
+ ```
228
+
229
+ That beats whatever effort the call arrived with. It has to, because opencode resolves one effort for a session and a subagent inherits it, which is wrong in the expensive direction: a caller who picked `max` for their own turn otherwise hands `max` to every worker it dispatches, and a mechanical lane burns a weekly cap at the costliest setting available. Model and effort together are what a turn costs, so both belong with the agent rather than with whoever happened to dispatch it.
230
+
231
+ An agent that declares nothing keeps the inherited effort, so this changes nothing until a file asks for it. An unrecognised level is refused and the inherited one kept, since the CLI rejects a level it does not know. Compaction is exempt: its summary always gets the full budget.
232
+
233
+ To force an **account** rather than a model, pin the full string. Both halves are needed, because the provider selects the account's config dir and the `@account` marker is what the model was registered under for that provider:
234
+
235
+ ```yaml
236
+ model: claude-code-appical/claude-opus-5@appical
237
+ ```
238
+
179
239
  ### Options reference
180
240
 
181
241
  ```json
@@ -439,6 +499,10 @@ Every proxied tool call has a deadline: if opencode hasn't resolved it (run the
439
499
 
440
500
  The `task` and `question` defaults are deliberately generous. Subagents routinely run 20–40 min, and a question can sit on a slow operator; under the old flat 10-minute ceiling the proxy fired mid-call, Claude believed its dispatch had failed, and the subagent's eventual result was dropped (the parent turn had already ended on the timeout error). If a `task` call *does* time out, the error tells Claude not to "schedule a wake-up" — that is a Claude Code affordance which cannot fire in this headless/proxy context, so deferring silently loses the work.
441
501
 
502
+ Starting with 0.15.0, clients advertising SSE receive immediate headers and keepalive comments every 15 seconds while a proxy call runs. This prevents long unanswered HTTP requests from being abandoned before the configured tool deadline; JSON-only clients retain their existing response format. Keepalives do not extend the tool deadline.
503
+
504
+ If Claude nevertheless abandons the HTTP call, the plugin preserves narration emitted while opencode was running the tool, renders it on return, and delivers the late completion as a plain-text continuation naming the original call. It tells Claude not to run the tool again. A silent post-tool continuation gets one resumed-process retry, preserving the original model, account, effort, and proxy configuration; a second failure ends with an error rather than an indefinite hang. Buffered narration is capped at 500 lines and 2 MiB, with a warning if output was dropped.
505
+
442
506
  ```json
443
507
  "options": {
444
508
  "proxyTools": ["Bash", "Edit", "Write", "WebFetch", "Task"],
@@ -448,6 +512,25 @@ The `task` and `question` defaults are deliberately generous. Subagents routinel
448
512
 
449
513
  ---
450
514
 
515
+ ## Side questions with /btw
516
+
517
+ After a normal Claude Code turn, use:
518
+
519
+ ```text
520
+ /btw Why did you choose that approach?
521
+ ```
522
+
523
+ The plugin registers the command without replacing an existing user-defined `btw` command. It calls Claude Code's native `side_question` control protocol on the current process, using the same model and account. The answer renders in the opencode conversation, but neither the question nor answer is sent as a normal Claude user turn or included in plugin-generated history and compaction transcripts.
524
+
525
+ - Requires Claude Code CLI **2.1.258 or newer**, the oldest verified version.
526
+ - Requires an existing, idle **headless** session with the same model and effort. Send a normal message first if the process has not started or was evicted. Interactive transport is not supported.
527
+ - This is not a concurrent TUI overlay: opencode may queue the command while a turn runs, and the plugin refuses it while a tool or another aside is outstanding.
528
+ - Each aside sees the main conversation, not previous aside exchanges. Include the relevant detail explicitly when asking a follow-up.
529
+ - The control response has no token/cost usage fields. Aside usage is not reported in opencode's counters; this does not mean the request is free.
530
+ - A request times out after two minutes. Abort and timeout cancel that side request without killing the main session.
531
+
532
+ 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.
533
+
451
534
  ## WebSearch routing
452
535
 
453
536
  Claude Code ships a built-in `WebSearch` tool. The `webSearch` option controls who actually executes those calls:
@@ -618,16 +701,19 @@ The plugin forwards Claude's thinking blocks (`thinking_delta` stream events) to
618
701
 
619
702
  What you see is a **summary** of the model's thinking, not the raw chain-of-thought. Anthropic [stopped exposing raw thinking on the Claude 4 family](https://platform.claude.com/docs/en/build-with-claude/extended-thinking#summarized-thinking) and ships a server-generated digest instead. For Claude Opus 4.7 specifically, [thinking content is omitted from responses by default](https://platform.claude.com/docs/en/about-claude/models/whats-new-claude-4-7#thinking-content-omitted-by-default); the plugin opts back in by passing `--thinking-display summarized` on every spawn. Claude Code CLI 2.1.142+ is required for that flag to take effect; older CLIs skip it silently.
620
703
 
621
- ### Reasoning effort variants
704
+ ### Reasoning effort
705
+
706
+ Each model exposes `low` / `medium` / `high` / `xhigh` / `max` variants, and an agent can set `reasoningEffort` in its own frontmatter (`minimal` is also accepted and maps to the CLI's `low`). The plugin hands the level to the CLI as `CLAUDE_CODE_EFFORT_LEVEL` at spawn, which Claude Code treats as the session-wide override: it beats the `effortLevel` in that account's `settings.json` and a shell export of the same variable. Effort is fixed for the life of a `claude` process, so it is part of the session key. Changing effort retires the previous effort's process and remembered transcript ID before replaying the conversation into a fresh process. Switching back cannot resume stale context; same-effort streaming turns still reuse their process. This reset is scoped to the same directory, model, provider/account, agent, and conversation. If the previous effort still has pending work (including tool results, plan approval, recovery, or `/btw`), the switch is rejected: finish that work at its original effort first. Title, compaction, and `/btw` calls do not trigger effort resets.
622
707
 
623
- Each model exposes `low` / `medium` / `high` / `xhigh` / `max` variants. Picking one injects the corresponding Claude CLI thinking keyword (e.g. `(ultrathink)` for `max`) into the user message. Compaction calls skip this injection so the full output budget goes to the summary.
708
+ Earlier versions injected a thinking keyword such as `(ultrathink)` into the user message instead. Claude Code stopped recognising every keyword except `ultrathink`, so that path is gone and nothing is appended to your messages any more. Compaction skips request and agent effort overrides, but still inherits a shell-level `CLAUDE_CODE_EFFORT_LEVEL` when set.
624
709
 
625
710
  ### Env-var overrides
626
711
 
627
- The plugin respects the standard Claude Code thinking env vars. If you set them in your shell, they pass through to the spawned process untouched.
712
+ The plugin respects the standard Claude Code thinking env vars. If you set them in your shell, they pass through to the spawned process untouched, with the one exception in the first row.
628
713
 
629
714
  | Env var | Effect |
630
715
  |---|---|
716
+ | `CLAUDE_CODE_EFFORT_LEVEL=<level>` | Session effort override. Passes through when no effort was requested; a variant or an agent's `reasoningEffort` replaces it for that spawn. |
631
717
  | `CLAUDE_CODE_DISABLE_THINKING=1` | Disable thinking entirely. |
632
718
  | `CLAUDE_CODE_DISABLE_ADAPTIVE_THINKING=1` | Disable adaptive thinking only. |
633
719
  | `CLAUDE_CODE_SHOW_THINKING_SUMMARIES=0` | Suppress summaries (the plugin sets this to `1` by default when unset). |
package/dist/index.d.ts CHANGED
@@ -62,6 +62,14 @@ type OpenCodeProvider = {
62
62
  models: Record<string, OpenCodeModel>;
63
63
  };
64
64
  type OpenCodeConfig = {
65
+ command?: Record<string, {
66
+ template: string;
67
+ description?: string;
68
+ agent?: string;
69
+ model?: string;
70
+ variant?: string;
71
+ subtask?: boolean;
72
+ }>;
65
73
  provider?: Record<string, {
66
74
  name?: string;
67
75
  npm?: string;
@@ -69,6 +77,7 @@ type OpenCodeConfig = {
69
77
  options?: Record<string, unknown>;
70
78
  models?: Record<string, unknown>;
71
79
  }>;
80
+ agent?: Record<string, Record<string, unknown>>;
72
81
  };
73
82
  /**
74
83
  * Bus events surface to plugins. Shape mirrors what opencode core publishes
@@ -227,6 +236,12 @@ interface ClaudeCodeProviderSettings {
227
236
  account?: string;
228
237
  configDir?: string;
229
238
  accounts?: string[];
239
+ /**
240
+ * Model that subagents run on when their own definition pins nothing.
241
+ * Unset means no implicit override at all, so an agent keeps inheriting the
242
+ * caller's model exactly as opencode intends. See `src/agent-models.ts`.
243
+ */
244
+ defaultSubagentModel?: string;
230
245
  skipPermissions?: boolean;
231
246
  permissionMode?: PermissionMode;
232
247
  mcpConfig?: string | string[];
@@ -665,6 +680,38 @@ type RuntimeMcpStatus = Record<string, string>;
665
680
  */
666
681
  declare function bridgeOpencodeMcp(cwd: string, runtimeStatus?: RuntimeMcpStatus, excludeServers?: ReadonlySet<string>): BridgedMcp | null;
667
682
 
683
+ type AgentRecord = {
684
+ mode?: string;
685
+ /** A fully-qualified `provider/model` the agent pinned for itself. */
686
+ model?: string;
687
+ /** Model NAME this agent wants, on whatever account the caller is using. */
688
+ forceModel?: string;
689
+ /** Thinking budget this agent wants, whatever the caller's picker says. */
690
+ reasoningEffort?: string;
691
+ };
692
+ declare function getAgentRegistry(): Record<string, AgentRecord>;
693
+ declare function getDefaultSubagentModel(): string | undefined;
694
+ /**
695
+ * The model a request should actually spawn with.
696
+ *
697
+ * Order, first match wins:
698
+ * 1. The agent declared `forceModel`.
699
+ * 2. The agent is a discovered subagent and `defaultSubagentModel` is set.
700
+ * 3. Anything else: the id opencode asked for, untouched.
701
+ *
702
+ * An agent that pinned a full `provider/model` is out of scope entirely:
703
+ * opencode already routed the call to that provider, and second-guessing it
704
+ * here would silently undo a choice the user made explicitly.
705
+ *
706
+ * Fails closed. An id that is not in the model registry is refused and the
707
+ * original kept, because the alternative is spawning the CLI with a `--model`
708
+ * it will reject, on a turn someone is waiting for.
709
+ */
710
+ declare function resolveAgentModel(agent: string | undefined, modelId: string, overrides?: {
711
+ records?: Record<string, AgentRecord>;
712
+ defaultSubagentModel?: string;
713
+ }): string;
714
+
668
715
  declare const defaultModels: Record<string, OpenCodeModel>;
669
716
 
670
717
  interface ClaudeCodeProvider {
@@ -673,6 +720,7 @@ interface ClaudeCodeProvider {
673
720
  languageModel(modelId: string): LanguageModelV3;
674
721
  }
675
722
  declare const DEFAULT_PROXY_TOOL_NAMES: string[];
723
+ declare function registerSideQuestionCommand(config: OpenCodeConfig): void;
676
724
  declare function createClaudeCode(settings?: ClaudeCodeProviderSettings): ClaudeCodeProvider;
677
725
  /**
678
726
  * Build models in OpenCode's config schema format (flat properties like
@@ -691,4 +739,4 @@ declare const _default: {
691
739
  server: OpenCodePlugin;
692
740
  };
693
741
 
694
- export { type ClaudeCodeConfig, ClaudeCodeLanguageModel, type ClaudeCodeProvider, type ClaudeCodeProviderSettings, type ClaudeStreamMessage, DEFAULT_PROXY_TOOL_NAMES, type OpenCodeHooks, type OpenCodeModel, type OpenCodePlugin, bridgeOpencodeMcp, claudeCodeProviders, configModelsForProvider, createClaudeCode, _default as default, defaultModels };
742
+ export { type AgentRecord, type ClaudeCodeConfig, ClaudeCodeLanguageModel, type ClaudeCodeProvider, type ClaudeCodeProviderSettings, type ClaudeStreamMessage, DEFAULT_PROXY_TOOL_NAMES, type OpenCodeHooks, type OpenCodeModel, type OpenCodePlugin, bridgeOpencodeMcp, claudeCodeProviders, configModelsForProvider, createClaudeCode, _default as default, defaultModels, getAgentRegistry, getDefaultSubagentModel, registerSideQuestionCommand, resolveAgentModel };