@khalilgharbaoui/opencode-claude-code-plugin 0.54.1 → 0.55.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.54.1",
3
+ "version": "0.55.0",
4
4
  "description": "Claude Code CLI provider plugin for opencode",
5
5
  "homepage": "https://opencode-claude-code-plugin.dev/",
6
6
  "funding": {
@@ -112,8 +112,8 @@ Defaults below describe normal headless opencode use when the key is absent.
112
112
  |---|---|---|---|
113
113
  | `cliPath` | string | `"claude"` | Executable, not a shell command with flags. Use an absolute path for a non-PATH install. The opencode config hook supplies this default; only direct `createClaudeCode()` use falls back to `CLAUDE_CLI_PATH`. Account providers wrap it; never select a generated wrapper yourself. |
114
114
  | `accounts` | string[] | unset | Unset keeps provider `claude-code`. Any array, including `[]`, expands to `claude-code-default` plus normalized, deduplicated names. Non-default accounts use `~/.claude-<name>`; default uses the CLI's normal environment/auth. |
115
- | `accountFailover` | `"ask"` / `"off"` | `"off"` | **Opt-in: only an explicit `"ask"` opens the switch form**, so unset and `"off"` behave identically. `"ask"` ends a usage-limited turn on opencode's native `question` form listing the other configured accounts, and continues the task on the pick inside the same opencode turn. Only ever fires with more than one account configured. The pick is sticky for the LIMITED account until the limit's reset time (or until opencode restarts when the CLI reported none), so it covers every session on that account and subagents follow their parent; child sessions are never shown the form. Leaving it unanswered waits and costs nothing. `stop`, a dismissal, or text that is not one of the offered accounts ends the turn the way a limited turn ends without the form. Triggered only by a rejected `rate_limit_event`, one of the two known account-limit error texts, or one of the five account-level failure kinds the CLI names on its own error reply (`authentication_failed`, `oauth_org_not_allowed`, `account_on_hold`, `verification_required`, `billing_error`); never by a generic failure. Never on compaction turns; both transports (the TUI's limit reply carries the same `error: "rate_limit"` kind). A switch carries the Claude conversation to the target account's config dir and resumes it there (`crossAccountResume`, default on) and only replays the thread as text when that copy is refused; either way, MCP servers configured only in the limited account's Claude profile are gone. With the default `"off"`, a limited turn ends on one `▌ **usage limit:**` note instead (see "When an account runs out of usage"). |
116
- | `accountGroups` | object | – | **Off unless set**, and unset means every account behaves as one group, so nothing changes on an install that does not set it. A map from an account name in `accounts` to a group name, e.g. `{"work": "work"}`; every account not named, `default` included, is in one implicit group. With it set the plugin only moves a conversation between accounts in the SAME group, which covers all four ways one can move: the by-hand switch (picking a model under another account's provider in the same opencode session), the `accountFailover` form, the override that form sets, and `crossAccountResume`. A switch ACROSS groups sends the other account nothing at all, neither the Claude transcript nor a text replay of the thread (the replay is the same history by another route), starts fresh with only the current user message, and writes one `▌ **account group:**` note naming both accounts and both groups. The source account keeps the conversation and its transcript, so switching back continues it. The switch form offers only same-group accounts, and with none left behaves as a single-account install; `describeOtherAccounts` in the usage-limit and account-block notes is filtered the same way. An account name that is not configured is ignored with one WARN, as is a non-object value or a non-string group. Compaction is exempt and a child session follows its parent. Shown resolved (never echoed) in the startup block and `/claude-code-doctor` as `accountGroups`, names only. Log: `starting fresh on this account: it is in another account group` (NOTICE, reason `another-account-group`). It guards what the plugin moves on its own; it is not a permission system. |
115
+ | `accountFailover` | `"ask"` / `"auto"` / `"off"` | `"off"` | **Opt-in: only an explicit `"ask"` opens the switch form**, so unset and `"off"` behave identically. `"auto"`, also opt-in, makes the same switch without a form, moving the same request to the next eligible account in configured order inside the same turn (see "Automatic failover" below). `"ask"` ends a usage-limited turn on opencode's native `question` form listing the other configured accounts, and continues the task on the pick inside the same opencode turn. Only ever fires with more than one account configured. The pick is sticky for the LIMITED account until the limit's reset time (or until opencode restarts when the CLI reported none), so it covers every session on that account and subagents follow their parent; child sessions are never shown the form. Leaving it unanswered waits and costs nothing. `stop`, a dismissal, or text that is not one of the offered accounts ends the turn the way a limited turn ends without the form. Triggered only by a rejected `rate_limit_event`, one of the two known account-limit error texts, or one of the five account-level failure kinds the CLI names on its own error reply (`authentication_failed`, `oauth_org_not_allowed`, `account_on_hold`, `verification_required`, `billing_error`); never by a generic failure. Never on compaction turns; both transports (the TUI's limit reply carries the same `error: "rate_limit"` kind). A switch carries the Claude conversation to the target account's config dir and resumes it there (`crossAccountResume`, default on) and only replays the thread as text when that copy is refused; either way, MCP servers configured only in the limited account's Claude profile are gone. With the default `"off"`, a limited turn ends on one `▌ **usage limit:**` note instead (see "When an account runs out of usage"). |
116
+ | `accountGroups` | object | – | **Off unless set**, and unset means every account behaves as one group, so nothing changes on an install that does not set it. A map from an account name in `accounts` to a group name, e.g. `{"work": "work"}`; every account not named, `default` included, is in one implicit group. With it set the plugin only moves a conversation between accounts in the SAME group, which covers every way one can move: the by-hand switch (picking a model under another account's provider in the same opencode session), the `accountFailover` form, the `"auto"` switch, the override either one sets, and `crossAccountResume`. A switch ACROSS groups sends the other account nothing at all, neither the Claude transcript nor a text replay of the thread (the replay is the same history by another route), starts fresh with only the current user message, and writes one `▌ **account group:**` note naming both accounts and both groups. The source account keeps the conversation and its transcript, so switching back continues it. The switch form offers only same-group accounts, the `"auto"` switch moves only to one, and with none left either behaves as a single-account install; `describeOtherAccounts` in the usage-limit and account-block notes is filtered the same way. An account name that is not configured is ignored with one WARN, as is a non-object value or a non-string group. Compaction is exempt and a child session follows its parent (under `"auto"` a child can move itself, which moves the parent with it, within the group). Shown resolved (never echoed) in the startup block and `/claude-code-doctor` as `accountGroups`, names only. Log: `starting fresh on this account: it is in another account group` (NOTICE, reason `another-account-group`). It guards what the plugin moves on its own; it is not a permission system. |
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. |
@@ -227,6 +227,7 @@ their secret values. Arbitrary MCP `{env:NAME}` placeholders are outside this li
227
227
  | `ANTHROPIC_AUTH_TOKEN` | CLI auth-token input; same strip/warning rule. Never display it. |
228
228
  | `DISABLE_AUTOUPDATER` | Set to `1` on every spawned `claude`, and only when the user has not set it. Keeps the CLI from updating mid-session, which would invalidate the cached version that gates `--thinking-display summarized`, `--plugin-dir` and fast mode. Not a provider option: a user-set value (including `0`, meaning keep updating) is never overwritten, which is the intended escape hatch. Tell a user who wants CLI autoupdates to export `DISABLE_AUTOUPDATER=0`, not to look for a config key. |
229
229
  | `CLAUDE_CODE_DISABLE_NONESSENTIAL_TRAFFIC` | Set to `1` on every spawned `claude` under the same never-overwrite rule. Suppresses non-essential CLI network traffic and independently blocks auto-update. An empty string counts as user-set and is left alone; the CLI reads it as off. |
230
+ | `MCP_TIMEOUT` | Set to `120000` on every spawned `claude` (both transports), only when the user has not set it. Claude Code's 30 s default let a subagent give up on the plugin's in-process proxy while opencode was busy, and run with no tools ("no shell tool"). A subagent that reports no tools, or a slow first reply in a very long session, points here. |
230
231
  | `OPENCODE_CONFIG` | Explicit config file, also read by the disk MCP bridge before project layers. |
231
232
  | `OPENCODE_CONFIG_DIR` | Additional `.opencode`-style config/skill root. The plugin's direct agent-file fallback does not use it; agents must reach the config hook or a supported agent directory. |
232
233
  | `OPENCODE_WORKTREE` | Overrides the disk MCP bridge's project walk-up boundary. opencode 1.x layering only: the opencode 2 layering walks to the filesystem root and applies no worktree boundary at all. |
@@ -494,6 +495,60 @@ Tell the user what a pick actually does before recommending one:
494
495
 
495
496
  `{ "accountFailover": "off" }`, which is also the default, keeps the note.
496
497
 
498
+ ### Automatic failover
499
+
500
+ **Opt-in.** `{ "accountFailover": "auto" }` and more than one account configured makes
501
+ the switch a pick would make, without the form: the failed attempt is dropped and the
502
+ **same request** runs on the next eligible account **inside the same opencode turn**.
503
+ The user sees one turn with the other account's answer under one note:
504
+
505
+ ```text
506
+ ▌ **account failover:** "work" is out of usage, so this turn moved to "personal"
507
+ automatically and stays there until 2026-09-20T18:00:00.000Z. The conversation
508
+ continues where it stopped.
509
+ ```
510
+
511
+ What to tell a user who asks for it, or asks why billing moved:
512
+
513
+ - **Triggers** are exactly the form's, and only on a turn that **failed**
514
+ (`is_error: true`): a usage limit (rejected `rate_limit_event`, the CLI's
515
+ `rate_limit` reply, the two known limit texts) or one of the five account-block kinds.
516
+ A served turn with a rejected event, a generic error, a server error or a timeout
517
+ never switches, and healthy turns never rotate between accounts.
518
+ - **Order** is configured order: `default` first, then `accounts` as written. It skips
519
+ the account that just failed, every account already tried for this request, every
520
+ account outside the limited account's `accountGroups` group, and every account known
521
+ to be out: one with an unexpired override moving its own work elsewhere, or one whose
522
+ remembered limit has a reset time still ahead. Both expire on their own. The choice
523
+ is made when the failure arrives, so a limit another session learned meanwhile
524
+ counts. An account whose runtime cannot be prepared (config dir or wrapper not
525
+ writable) is skipped and the walk moves on; an account already tried is never run
526
+ again.
527
+ - **Bounded.** Each account is tried at most once per request. With none left the turn
528
+ finishes as an error: the `▌ **usage limit:**` note naming the last account tried and
529
+ offering no other (or the `▌ **claude account:**` note for a block), below one
530
+ `▌ **account failover:**` line per switch made on the way. A `doGenerate` caller gets
531
+ the error finish (`usage_limit` or the block's kind), never an answer, and no
532
+ usage-limit or failover line; its text is at most the CLI's own error text plus,
533
+ for a block, the `▌ **claude account:**` note with the login command.
534
+ - **Sticky** like a pick: the override is keyed on the account the conversation was on,
535
+ so every session on it, subagents included, follows until the limit's reset time, or
536
+ until opencode restarts when none was reported (always the case for a block).
537
+ - **Billing** moves to the target account for the rest of the turn and every later turn
538
+ on the limited account until the override expires. Opting in is the consent.
539
+ - **The conversation** is carried (`crossAccountResume`) and resumed, or replayed into a
540
+ fresh session when the carry is refused, exactly as for a pick; the note's last
541
+ sentence says which. Work the failed account did in that turn is not re-run by the
542
+ plugin, but a replay only has what opencode has.
543
+ - **Scope.** Both transports, `doGenerate` callers (no failover note in the returned text), and
544
+ child sessions, which switch themselves because nothing is asked; the parent follows
545
+ through the same override. Never compaction (it fails outright) and never a title
546
+ request (no spawn). An available account beats the `fallbackModels` chain, as the form
547
+ does. Stopping the turn, or cancelling the stream it returned, stops the attempt in
548
+ flight and starts no further account.
549
+ - Verified with a scripted `claude` only; the switch path itself is the one measured
550
+ live for the form.
551
+
497
552
  ### Switching account by hand, and keeping two accounts apart
498
553
 
499
554
  Picking a model under another account's provider in the **same** opencode session
@@ -664,10 +719,10 @@ Per-agent replaces provider-level; it never merges. Unset means no chain, which
664
719
  the default. Two triggers only, never a generic error: the CLI refusing the model
665
720
  (assistant `error: "model_not_found"`, or a failed result whose text is *"There's an
666
721
  issue with the selected model"*, measured on CLI 2.1.280, where the result `subtype`
667
- is misleadingly `success`), and a usage limit **only when the `accountFailover` form is
668
- not taking the turn**, meaning it is `"off"` (its default) or has no other account to
669
- offer. With `"ask"` and another account configured the switch form wins and the chain
670
- stays out of it: model is a capability choice, account is a billing choice.
722
+ is misleadingly `success`), and a usage limit **only when no `accountFailover` switch is
723
+ taking the turn**, meaning it is `"off"` (its default) or has no other account to
724
+ offer. With `"ask"` or `"auto"` and another eligible account configured the switch wins
725
+ and the chain stays out of it: model is a capability choice, account is a billing choice.
671
726
  An expired login, a billing hold and every other error kind are excluded because they
672
727
  fail the same way on the next model.
673
728
 
@@ -1283,6 +1338,9 @@ Prefer the doctor: it needs no logging change and no restart.
1283
1338
  | Claude "forgot" the earlier part of a long conversation | Claude Code compacted its own context | Look for the `▌ **context compacted:**` note in the transcript |
1284
1339
  | On a turn that replayed the thread as text, Claude knows what tools the previous reply ran but not what it concluded | Plugin 0.49.0 or older kept each replayed message's first 2,000 characters, and Claude Code runs its own tools without ending a step, so a reply's tool calls and their output (one result alone is capped at 10,000 characters) sit in the same message ahead of the prose and the answer was cut | Upgrade. Each replayed message is clipped in the middle now, keeping the answer it ends on, and the newest reply gets a larger budget. Better still, avoid the replay: it only happens when the Claude session cannot be resumed, and the `▌` note or the plugin log names the reason |
1285
1340
  | Claude forgot the whole conversation at once | Claude Code cleared it (`/clear` sent as a message, or a plan-mode exit that clears context) | Look for the `▌ **claude code reset:**` note. The plugin does not replay history there on purpose; a new opencode session gets a clean slate |
1341
+ | After switching opencode agents (plan to build, build to plan) Claude ignores what was said under the other agent or answers an earlier message | Plugin older than 0.55.0: each agent runs its own `claude` process, and a reused one was sent only the newest message | Upgrade. The reused (or `--resume`d) process is now told what it missed, replayed as text ahead of the current message under `<missed_conversation>`; the log says `replaying the messages this claude session missed`. The processes stay separate on purpose (a Claude session keeps the system prompt of its first request) |
1342
+ | `prompt is too long` on the first turn after a restart or after a lost Claude session, and `/compact` fails the same way | Plugin older than 0.55.0: the text replay of the thread had per-message clips but no total | Upgrade. The replay is capped at 180,000 characters, newest first, with a `[N earlier messages omitted ...]` line when it had to drop |
1343
+ | opencode 2: every turn fails with `ENOENT ... posix_spawn '<claude>'` though the binary exists, only in a subdirectory of a project | Plugin older than 0.55.0 appended the session's project-relative `subpath` to its own absolute directory | Upgrade; drop a `cwd` set only to work around it |
1286
1344
  | Wanting the per-turn cost in the chat | Not shown by default | Set `turnStats: true` and restart opencode |
1287
1345
  | On the interactive transport, every turn ends with a `▌ **claude code error:**` note naming `end_turn` (or `stop_sequence` / `max_tokens`), and `turnStats` never prints | Plugin older than 0.33.0 put the stop reason in the synthesized `result`'s `subtype`, and any non-`success` subtype finishes the turn as an error, which also suppresses the stats footer | Upgrade and relaunch. A turn that reaches a terminal stop reason now synthesizes the shape a headless turn emits (`subtype: "success"`, the stop reason in a top-level `stop_reason`), so it finishes as an ordinary reply. A turn that reaches NO terminal stop reason is still an error on purpose, so truncation stays visible |
1288
1346
  | On the interactive transport with a working directory under `/tmp` (or any symlinked path), the turn hangs until the 30-minute turn timeout and then reports no terminal stop reason | Plugin older than 0.33.0 named the transcript directory from `path.resolve`, which does not follow symlinks, so it tailed a file Claude Code never writes. On macOS `/tmp` is a symlink to `/private/tmp` | Upgrade and relaunch. The directory is now named from the cwd's resolved real path, which is what the CLI uses (`/tmp/scratch` is `~/.claude/projects/-private-tmp-scratch`). Check the `jsonlPath` in the `prepared interactive claude session` log line against the directory that actually exists under `<CLAUDE_CONFIG_DIR>/projects/` |