@khalilgharbaoui/opencode-claude-code-plugin 0.51.0 → 0.53.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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@khalilgharbaoui/opencode-claude-code-plugin",
3
- "version": "0.51.0",
3
+ "version": "0.53.0",
4
4
  "description": "Claude Code CLI provider plugin for opencode",
5
5
  "homepage": "https://opencode-claude-code-plugin.dev/",
6
6
  "funding": {
@@ -117,6 +117,8 @@ Defaults below describe normal headless opencode use when the key is absent.
117
117
  | `failoverAccounts` | string[] | unset/derived | Account expansion supplies the resolved account list so a limited account can offer the others. Do not hand-wire it; set `accounts` instead. |
118
118
  | `baseCliPath` | string | unset/derived | The `cliPath` before the per-account wrapper substitution, so a failover can build another account's wrapper on the same binary. Supplied by the config hook. Do not hand-wire it. |
119
119
  | `accountInProcess` | boolean | unset/derived | Set by the config hook on Windows, where there is no per-account wrapper script: the spawn exports `CLAUDE_CONFIG_DIR` itself and strips the `@<account>` marker off `--model`. Do not hand-wire it. |
120
+ | `subagentDispatch` | `"ask"` / `"off"` | `"off"` | **Opt-in: only an explicit `"ask"` raises the form**, so unset and `"off"` are identical and a dispatch is handed to opencode exactly as it is today. With `"ask"`, a proxied `task` or `task_batch` call is held at the drain and the turn ends on opencode's native `question` form asking how the subagents should run; the real `task` calls are released, with the answer applied, on the next step of the SAME opencode turn. Always exactly one question first (`Same as last time` when this conversation has one, `Default`, four curated model/effort combos, `Customise…`), so the common answer is one click. `Customise…` opens a second form with one row per agent type plus one account row when more than one account is offered. That account row also offers `Per type…` when the dispatch has more than one agent type, and a type row with more than one task offers `Per task…`; either one opens a third form, and asking for both gives one third form carrying the per-type account rows followed by the per-task rows. Every row takes a typed answer too: a model id, an effort level, an `@account`, or any combination (`claude-opus-5-5 max @worker`); anything unrecognised keeps that row's default with a NOTICE. **The account is choosable at three widths and the narrowest wins**: the dispatch (`Account` row), the agent type (`Per type…` or `@name` on the type's row) and one task (`@name` on its row in the third form). Every account answer, typed ones included, is checked against the same same-group candidate list. The answer applies to each child's own `claude` spawn (`--model`, `CLAUDE_CODE_EFFORT_LEVEL`, and the account's wrapper and `CLAUDE_CONFIG_DIR`) and beats `forceModel`, `defaultSubagentModel` and the inherited effort, because it is about this dispatch. A child is matched to its task by the dispatching session, the subagent type and the task prompt, verified against `parentID`. A dismissed or unanswered form dispatches with the defaults and writes one `▌ **subagent dispatch:**` note; the dispatch is never lost. Never on compaction turns, never inside a child session (a subagent follows the choice its own parent made for it), and only where opencode's registry has the `question` entry. `Same as last time` is per opencode session and per agent type and is kept in `$XDG_STATE_HOME/opencode-claude-code-plugin/subagent-dispatch.json` (`0600`, capped at 128 sessions and 32 agent types each, pruned at 30 days, merged rather than overwritten between opencode processes, deleted with the session), so it survives an opencode restart; only the model id, effort level and account name are stored. A remembered model this install no longer has, or an account no longer configured or no longer in the group, is dropped from the row and named in one note. Both transports, both opencode majors. |
121
+ | `subagentDispatchCrossGroup` | boolean | `false` | Let `subagentDispatch`'s account row offer accounts outside the dispatching account's own `accountGroups` group. **Off by default and never implied by anything else**: a subagent is handed the task text the main agent writes, which can quote the conversation, and it reads the repository, so offering another group is offering that group the conversation by another route. Does nothing when `accountGroups` is unset, where every account is already one group, or when `subagentDispatch` is off. |
120
122
  | `defaultSubagentModel` | string | unset | Seed-config default for discovered `mode: subagent` agents without a full `provider/model` pin; `forceModel` takes precedence. Keeps the caller's account. Unknown ids warn and keep the inherited model. Not independently read per expanded account. |
121
123
  | `defaultSubagentCacheTtl` | string | unset | Prompt cache TTL (`5m` / `1h`) for discovered `mode: subagent` agents that declare no `cacheTtl`; the agent's own value takes precedence. Unset leaves the CLI's default (1 hour on a subscription). Unknown values warn and change nothing. Headless and interactive spawns, never compaction. |
122
124
  | `fallbackModels` | string[] | unset | Ordered models to try when the model a turn would run on is refused. Default for agents declaring no `fallbackModels`; a per-agent list replaces it rather than extending it. Same account throughout, never a switch. Armed only by the CLI refusing the model (`model_not_found`) or by a usage limit when the `accountFailover` form is not taking the turn, which is the case whenever it is `"off"` (its default) or has no other account to offer; with `"ask"` and another account the switch form wins. Entries must be registered model ids, unknown ones warn and are skipped, the current model is dropped from its own chain, each entry is tried at most once per turn, and an exhausted chain surfaces the original error. Never on compaction or title stubs; both transports. Writes a `▌ **model fallback:**` note that transcript rebuilds strip. Not independently read per expanded account. |
@@ -150,7 +152,7 @@ Defaults below describe normal headless opencode use when the key is absent.
150
152
  | `forkSessions` | boolean | `false` | When a new opencode session turns out to be a fork of one this provider already served, branch the parent's Claude conversation with `claude --resume <parent> --fork-session` instead of re-rendering the whole thread as text into the first message. Measured on CLI 2.1.280 with haiku 4.5 over a ~13k-token thread: 814 cache tokens written and 39,710 read, against 22,355 written and 17,385 read for the replay, so $0.0058 against $0.0467 for that turn; the parent's transcript is byte-identical afterwards. Neither opencode major tells a provider that a session is a fork, so the parent is found by matching this prompt's history against what each sibling session key was last asked to continue. Off by default because a resumed Claude conversation reuses the system prompt recorded on its FIRST request (`--system-prompt-snapshot`, default `on`), so a forked session answers under the parent's appended system prompt rather than this turn's; measured directly, a parent seeded with codename ZEBRA and forked while passing QUAIL answered ZEBRA. Falls back to the replay, unchanged, for: another account, an unknown or released parent session id, a busy parent (live process, proxied call in flight, unanswered plan-mode question), a fork cut mid-conversation, a fork taken mid tool round trip, a different cwd / model / agent / effort / prompt-cache TTL, compaction, an account-failover switch, and a `claude` whose `--help` does not advertise `--fork-session`. Recording costs nothing while the option is off. |
151
153
  | `resumeAfterRestart` | boolean | `true` | After an opencode restart, resume the conversation's Claude session (`--resume`) instead of replaying the thread as text. Persists session id + conversation digest per session key in `$XDG_STATE_HOME/opencode-claude-code-plugin/claude-sessions.json` (0600, 256 entries, 30 days) after each successful turn. Resumes only on the same session key and binary, with the transcript on disk and the history equal to the recorded conversation plus Claude's reply; anything else (edit, revert, compaction, account switch, open tool round trip) replays. Not on compaction. Log: `resuming the claude session from before the restart` (NOTICE) or `not resuming ...` with a reason (INFO). `false` disables reading and writing. |
152
154
  | `resumeAcrossModelChanges` | boolean | `true` | When the operator changes a conversation's model or reasoning effort mid-conversation, continue the same Claude session (`--resume <id> --model <new>`) instead of replaying the thread as text. Both are in the session key, so changing either used to start a fresh Claude conversation (issue #91); neither needs one, because `--model` and `CLAUDE_CODE_EFFORT_LEVEL` are spawn-time and the CLI applies either to a resumed transcript (measured on 2.1.288). The session is MOVED off the old key, so one Claude conversation has exactly one owner. Refused on every term `resumeAfterRestart` is refused on (another account, a transcript that is gone, a conversation that changed) plus two more: a sibling whose transcript may still be written to, and anything that is not the same cwd, request scope, opencode session, agent and prompt-cache TTL. Not on compaction, not on an account-failover switch. It reads the record `resumeAfterRestart` writes, so `resumeAfterRestart: false` turns it off too. Log: `continuing this conversation's claude session under the new model or effort` (NOTICE). |
153
- | `crossAccountResume` | boolean | `true` | On an account-failover switch (and on the switch back when the override expires), continue the same Claude conversation on the other account instead of replaying the thread as text. An account is a `CLAUDE_CONFIG_DIR` and a conversation is one `projects/<encoded cwd>/<id>.jsonl` inside it, so the switch copies that file into the target account's own projects dir (0600, dir 0700) and keeps the session id for the `--resume` both transports already build. Measured on CLI 2.1.288: a copied transcript resumes under another config dir with its context intact, the CLI resolves a session by FILENAME (not the `sessionId` in the records), it appends to the copy it found rather than to the cwd's path, the source comes out byte-identical, and no record holds a user, org or account id. Never overwrites: an older copy already at the target path (what a switch back finds) makes the carry take a fresh session id, and the live copy is the longest one. Refuses on nothing to carry, a non-file at the target path, or a failed copy, and each refusal falls back to the replay. Not on compaction, and inert with one account. Log: `carried this conversation's claude transcript to the other account` or `replaying this conversation as text on the other account` with a reason (both NOTICE). What is NOT verified live is a different Anthropic login answering another's conversation; `false` keeps the replay. |
155
+ | `crossAccountResume` | boolean | `true` | On any account switch (a by-hand pick of another account's model in the same session, the switch form, and the switch back when the override expires), and only between accounts in the same `accountGroups` group, continue the same Claude conversation on the other account instead of replaying the thread as text. An account is a `CLAUDE_CONFIG_DIR` and a conversation is one `projects/<encoded cwd>/<id>.jsonl` inside it, so the switch copies that file into the target account's own projects dir (0600, dir 0700) and keeps the session id for the `--resume` both transports already build. Measured on CLI 2.1.288: a copied transcript resumes under another config dir with its context intact, the CLI resolves a session by FILENAME (not the `sessionId` in the records), it appends to the copy it found rather than to the cwd's path, the source comes out byte-identical, and no record holds a user, org or account id. Never overwrites: an older copy already at the target path (what a switch back finds) makes the carry take a fresh session id, and the live copy is the longest one. Refuses on nothing to carry, a non-file at the target path, or a failed copy, and each refusal falls back to the replay. Not on compaction, and inert with one account. Log: `carried this conversation's claude transcript to the other account` or `replaying this conversation as text on the other account` with a reason (both NOTICE). What is NOT verified live is a different Anthropic login answering another's conversation; `false` keeps the replay. |
154
156
  | `bridgeOpencodeSkills` | boolean | `false` | Stage the user's opencode skills for Claude's native Skill tool as `opencode-skills:<name>`, on the headless and interactive spawns (never compaction). Covers every root opencode reads: project `.opencode/`, `.claude/`, `.agents/` walking up, the opencode config dirs (`skill/` and `skills/`), and global `~/.claude/skills` and `~/.agents/skills` under opencode's own `OPENCODE_DISABLE_EXTERNAL_SKILLS` / `OPENCODE_DISABLE_CLAUDE_CODE_SKILLS` switches. Requires the CLI's `--help` to advertise `--plugin-dir`; otherwise no-op. Bridged skills are also listed in opencode's forwarded system prompt, so a large skill set costs prompt tokens twice, which is why it is off by default; `true` opts the user's skills in. Bundled skill staging ignores this option, but still requires flag support and successful discovery/staging. |
155
157
  | `bridgeSkipNativeSkills` | boolean | `true` | Leave a skill unbridged when the Claude session already loads it: from `<CLAUDE_CONFIG_DIR>/skills`, the project's `.claude/skills`, or an installed plugin's `skills/`. Matched by resolved directory, by byte-identical SKILL.md, or (user/project scope only, since plugin skills are namespaced `<plugin>:<name>`) by name. A name match means `Skill("<name>")` answers from Claude's copy, not opencode's, so it is logged at WARN with both paths. The plugin scan reads `installed_plugins.json` and does not check whether the plugin is enabled. `false` bridges everything and reinstates the duplicates. |
156
158
  | `transport` | `"auto"` / `"headless"` / `"interactive"` | unset (headless) | Explicit choice wins over `interactive` and its env var. `auto` prefers headless and probes required headless flags with free help/argument parsing and no prompt. Only definitive unsupported flags select the PTY; timeout, auth failure, missing binary and unknown output stay headless. Never retries a submitted request. Explicit or automatic PTY selection requires `Bun.Terminal`, otherwise errors. Title stubs do not probe. `/compact` follows the selected transport. On the PTY, `read-only` is `--restricted` plus `dontAsk` (CLI 2.1.263+, refused below rather than weakened) and plan mode parks `ExitPlanMode`'s dialog for the operator. `/claude-code-doctor usage` reports the free headless check unavailable when those flags are absent; it does not run a PTY inference prompt. |
@@ -433,17 +435,18 @@ anything else: this note is recent and a long-lived opencode window may predate
433
435
 
434
436
  **Opt-in.** `{ "accountFailover": "ask" }` and more than one account configured makes a
435
437
  usage-rejected turn end on opencode's `question` form instead of that note: one option
436
- per other configured account, plus `stop`. Picking an account applies it inside the same
438
+ per other configured account in the limited account's `accountGroups` group (every other
439
+ account when no groups are set), plus `stop`. Picking an account applies it inside the same
437
440
  opencode turn, with no new user message, and the task carries on. Leaving the form
438
441
  unanswered waits and costs nothing.
439
442
 
440
443
  It is opt-in rather than on by default because of what was measured against a real
441
- five-hour limit on 2026-10-03. Two of the three findings are not the plugin's to fix:
442
- typing a message instead of picking **dismisses** the form in opencode, and that message
443
- then runs on the still-limited account and raises a second form (the "I had to send two
444
- messages before anything ran" report); and a switch is always a full replay. The third,
445
- every pick being refused as `unrecognised answer` because another plugin appends a
446
- routing tag to tool results, is fixed.
444
+ five-hour limit on 2026-10-03. One finding is not the plugin's to fix: typing a message
445
+ instead of picking **dismisses** the form in opencode, and that message then runs on the
446
+ still-limited account and raises a second form (the "I had to send two messages before
447
+ anything ran" report). The other two are fixed: every pick was refused as `unrecognised
448
+ answer` because another plugin appends a routing tag to tool results, and a switch was
449
+ always a full replay (since 0.47.0 it carries the conversation over, see below).
447
450
 
448
451
  Tell the user what a pick actually does before recommending one:
449
452
 
@@ -576,6 +579,61 @@ the opencode schema requires it. Markdown fallback reads top-level scalar fields
576
579
  | `cacheTtl` | `5m` or `1h`; anything else warns and leaves the CLI's default alone. Exported as `CLAUDE_CODE_PROMPT_CACHE_TTL`, beating a shell export of the same name. Works for any discovered agent mode; compaction skips it. |
577
580
  | `fallbackModels` | Ordered registered bare model ids to try when this agent's model is refused. Both YAML spellings (`[a, b]` or a `- ` block). Replaces the provider-level `fallbackModels` rather than extending it. Keeps the caller's account; an entry carrying `@account` has it stripped. Unknown ids warn and are skipped. |
578
581
 
582
+ ### Ask before each subagent dispatch
583
+
584
+ Opt-in; off by default. Turns a fan-out into one question instead of a static agent file.
585
+
586
+ ```json
587
+ {
588
+ "provider": {
589
+ "claude-code": {
590
+ "options": {
591
+ "proxyTools": ["Bash", "Write", "Edit", "WebFetch", "Task"],
592
+ "subagentDispatch": "ask"
593
+ }
594
+ }
595
+ }
596
+ }
597
+ ```
598
+
599
+ What happens, in order:
600
+
601
+ 1. The model calls `mcp__opencode_proxy__task` or `mcp__opencode_proxy__task_batch`.
602
+ 2. The plugin holds the call and ends the step on opencode's `question` form with ONE
603
+ question: `Same as last time` (when this conversation has one), `Default`, four
604
+ curated model/effort combos, and `Customise…`.
605
+ 3. Answering anything but `Customise…` releases the subagents immediately, with that
606
+ answer applied, on the next step of the SAME opencode turn.
607
+ 4. `Customise…` opens one row per agent type, plus an `Account` row when more than one
608
+ account is offered. A type with more than one task also offers `Per task…`, and the
609
+ account row offers `Per type…` when the dispatch has more than one agent type.
610
+ 5. Either of those opens one third form, carrying the per-type account rows first and
611
+ the per-task rows after them. Asking for neither never builds it.
612
+ 6. Each subagent opencode starts spawns its own `claude` with the chosen `--model`,
613
+ `CLAUDE_CODE_EFFORT_LEVEL` and account wrapper.
614
+
615
+ Rules worth stating to a user:
616
+
617
+ - A dispatch answer beats `forceModel`, `defaultSubagentModel` and the inherited effort.
618
+ - Any row takes a typed answer: a registered model id, an effort level, an `@account`,
619
+ or any combination (`claude-opus-5-5 max @worker`). Unrecognised text keeps that row's
620
+ default with a NOTICE.
621
+ - The account has three widths and the narrowest wins: the dispatch's `Account` row, the
622
+ agent type (`Per type…` or `@name` on the type's row), and one task (`@name` on its row
623
+ in the third form, which is how a single task gets its own account).
624
+ - Dismissing or ignoring the form dispatches with the defaults and writes one
625
+ `▌ **subagent dispatch:**` note. The dispatch is never lost.
626
+ - Every account answer, offered or typed, is checked against the accounts in the
627
+ dispatching account's own `accountGroups` group. `subagentDispatchCrossGroup: true` is
628
+ the explicit, default-off opt-in for the rest; it does nothing when `accountGroups` is
629
+ unset.
630
+ - Never on compaction, never inside a child session, and only where opencode's registry
631
+ has the `question` entry.
632
+ - `Same as last time` is per opencode session and per agent type, kept in
633
+ `$XDG_STATE_HOME/opencode-claude-code-plugin/subagent-dispatch.json`, so it survives an
634
+ opencode restart and is deleted with the session. A remembered model or account that is
635
+ no longer available is dropped and named in one note.
636
+
579
637
  ### Degrade to another model instead of failing
580
638
 
581
639
  ```yaml