@khalilgharbaoui/opencode-claude-code-plugin 0.54.2 → 0.56.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 +9 -1
- package/dist/index.d.ts +159 -10
- package/dist/index.js +2177 -603
- package/dist/index.js.map +1 -1
- package/dist/tui.js +206 -8
- package/dist/tui.js.map +1 -1
- package/package.json +1 -1
- package/skills/claude-code-plugin/SKILL.md +105 -9
|
@@ -9,9 +9,10 @@ This plugin is `@khalilgharbaoui/opencode-claude-code-plugin`. It registers one
|
|
|
9
9
|
`claude-code*` providers and routes inference through the `claude` CLI, not opencode's
|
|
10
10
|
native Anthropic provider. Headless `--print` is the default, which is the Agent SDK
|
|
11
11
|
path. Never promise a user what subscription headless usage draws from: Anthropic's
|
|
12
|
-
policy page is the only authority and it changes (
|
|
13
|
-
|
|
14
|
-
|
|
12
|
+
policy page is the only authority and it changes (read 2026-10-16: subscription usage
|
|
13
|
+
limits by default, and since 2026-10-07 Max and Team plans also carry monthly API credits,
|
|
14
|
+
Max 20x $200, spendable through `claude -p` by giving the CLI an API key from the linked
|
|
15
|
+
Console organization; Pro has none). API-key/cloud billing depends on the CLI's
|
|
15
16
|
authentication. Confirm the user's intended account and billing method.
|
|
16
17
|
|
|
17
18
|
## How this differs from the alternatives
|
|
@@ -112,8 +113,8 @@ Defaults below describe normal headless opencode use when the key is absent.
|
|
|
112
113
|
|---|---|---|---|
|
|
113
114
|
| `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
115
|
| `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
|
|
116
|
+
| `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"). |
|
|
117
|
+
| `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
118
|
| `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
119
|
| `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
120
|
| `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. |
|
|
@@ -121,6 +122,11 @@ Defaults below describe normal headless opencode use when the key is absent.
|
|
|
121
122
|
| `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. |
|
|
122
123
|
| `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. |
|
|
123
124
|
| `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. |
|
|
125
|
+
| `defaultSubagentMaxTurns` | number | unset | `--max-turns` for discovered `mode: subagent` agents that declare no `maxTurns`; the agent's own value wins. Counted by the CLI per user message, so safe on a reused process. CLI 2.1.280+; headless only (print-only flag, the interactive transport WARNs once). Part of the session key. |
|
|
126
|
+
| `defaultSubagentMaxBudgetUsd` | number | unset | `--max-budget-usd` for discovered `mode: subagent` agents that declare no `maxBudgetUsd`. Counted by the CLI over the whole process's life and reset on `--resume`, hence subagents only; a main agent's `maxBudgetUsd` is refused with a WARN. Probe-gated on `claude --help`; headless only. |
|
|
127
|
+
| `maxBudgetUsd` | number | unset | Dollar cap per opencode conversation (its turns plus every descendant subagent), from the CLI's `total_cost_usd` (cumulative per Claude session on headless, so the ledger takes deltas). Unset: no ledger at all. Warn notes at `budgetWarnAt`; at the cap the next fresh turn ends before any spawn with a `▌ **budget:**` note and `{unified:"error", raw:"budget_exceeded"}`. Not on a turn answering a parked proxy call, not on compaction; a child gets an `error` part instead of the note; `doGenerate` gets the finish only. In memory; raising it needs an opencode relaunch. |
|
|
128
|
+
| `budgetWarnAt` | number[] | `[50, 80]` | Percent thresholds of `maxBudgetUsd`, one note each per conversation (only the highest when a turn jumps several). `[]` drops the notes, keeps the stop. Invalid values WARN and keep the default. |
|
|
129
|
+
| `maxBackgroundSubagents` | number | unset | Ceiling on background subagents running at once (the doctor's running count, process-wide). A background `task`, or a `task_batch` with background items, that would exceed it is refused whole with an `isError` tool result naming the cap and the count; nothing reaches opencode. Foreground is never capped. An unreadable count (2 s budget) refuses. |
|
|
124
130
|
| `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. |
|
|
125
131
|
| `cwd` | string | automatic | Pin an absolute existing directory. Otherwise: session directory from SDK, usable `process.cwd()`, captured project directory, final `process.cwd()` fallback. Startup diagnostics cannot show the per-call session tier. |
|
|
126
132
|
| `skipPermissions` | boolean | `true` | Pass `--dangerously-skip-permissions` to Claude on both transports (the TUI also gets Claude Code's own `skipDangerousModePermissionPrompt` so it does not stop on that mode's confirmation; CLI 2.1.263+), even with proxies enabled. Proxied calls still use opencode permissions, but unproxied CLI tools do not. `false` removes the bypass flag; it does not by itself create human approval prompts. Ignored when `permissionMode` is `"plan"`, which always drops the flag. |
|
|
@@ -131,6 +137,7 @@ Defaults below describe normal headless opencode use when the key is absent.
|
|
|
131
137
|
| `controlRequestDenyMessage` | string | built-in text | Override ordinary deny text. `AskUserQuestion` always uses its own stop-and-wait message. |
|
|
132
138
|
| `proxyTools` | string[] | `["Bash", "Edit", "Write", "WebFetch", "Task"]` | Case-insensitive replacement list, not additive and not a capability allowlist. Known entries expose `mcp__opencode_proxy__<name>`; omitted/unknown tools are not disabled. `Task` also brings `task_batch`; `[]` disables this list, not MCP proxying. See the proxy table for exceptions. |
|
|
133
139
|
| `extraDisallowedTools` | string[] | unset | Claude built-ins to switch off outright with `--disallowedTools`, for tools that have no proxy (`["NotebookEdit"]`). Removes the capability rather than routing it. |
|
|
140
|
+
| `mapReadRules` | boolean | `true` | Carry opencode's own `read` permission **deny** rules onto the Read the CLI runs itself, as `--disallowedTools "Read(<pattern>)"`. `Read`, `Glob` and `Grep` are CLI-native by default, so without this a file opencode would refuse to read is read by Claude with no prompt (measured 2026-10-16: "read .env with your Read tool" returned a planted secret). Only `deny` is carried: an `ask` cannot be honoured on a spawn that has nobody to ask, and **opencode's own `*.env` default is an `ask`, not a deny**, so a default install maps nothing. Write `permission: { read: { "*.env": "deny" } }` to close that, or add `Read` to `proxyTools` so the read runs in opencode and its prompt applies. `glob`, `grep`, `list` and `external_directory` rules are not mapped (Claude's deny beats its allow, so "outside the working directory" cannot be said without denying the working directory); nor are agent-level `permission.read` overrides. `/claude-code-doctor` lists what was mapped, what was not, and the asks it cannot enforce. Compaction spawns get no rules. Set `false` to send nothing. |
|
|
134
141
|
| `proxyToolTimeoutMs` | object of proxy tool name to ms | unset | Optional wall-clock backstop per tool, in ms, case-insensitive keys. A proxied call normally ends on an event the plugin listens for, not on a timer: opencode's result, an abort (the CLI is interrupted), the next user message (calls the previous turn left pending are rejected as orphaned), the `claude` process exiting, the chat being deleted, or opencode exiting. Fallback 10 min (including dynamic MCP tools); `task` and `task_batch` have no deadline, so a subagent runs to completion and a chat parked in one holds its worker until one of those events; `question` 30 min. Set both task keys to cover both. A positive value replaces the default, `0` removes that tool's deadline, negative or non-numeric values are ignored, and values above 2147483647 are clamped. Bash `input.timeout` raises the resolved deadline (and restores one after `bash: 0`); executor ceilings still apply. The generated MCP client timeout is the largest effective deadline, or the CLI's maximum while any tool has none. `compress` is intercepted without a deadline. |
|
|
135
142
|
| `planModeQuestion` | boolean | `false` | Bridge `ExitPlanMode` approval to opencode's `question` and return a real CLI tool result. Requires a live question registry entry; otherwise keeps text fallback. Cannot fire on the headless transport: CLI 2.1.258 does not offer `ExitPlanMode` under `--print`, measured directly and through a full plugin probe, so the text path is what runs. On the interactive transport it fires and is verified live (1.18.34): the form's answer is pressed into the TUI's approval dialog. Headless prose yes/no is not a plan-mode unlock; interactive typed yes is. |
|
|
136
143
|
| `webSearch` | `"claude"` / `"disabled"` / `"<opencode tool name>"` | `"claude"` | Default: CLI search with the query rendered as text. Custom target forwards a tool call to an existing opencode tool accepting `query`; this is mapping, not the authenticated proxy replacement, so do not assume CLI search is suppressed. `"disabled"` disallows headless `WebSearch`. |
|
|
@@ -153,6 +160,8 @@ Defaults below describe normal headless opencode use when the key is absent.
|
|
|
153
160
|
| `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. |
|
|
154
161
|
| `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). |
|
|
155
162
|
| `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. |
|
|
163
|
+
| `usageWarnThresholds` | number[] | `[50, 75, 90, 95]` | Percent levels of a usage window at which a SERVED turn ends with one `▌ **usage:**` note (its own text part, stripped from rebuilt transcripts): account, window, utilization, reset in local time. Read from every headless `rate_limit_event` (not only a rejected one), whose `utilization` is a FRACTION (`0.9` is 90%) and is ABSENT on an `allowed` event with headroom, which is unknown, never zero, and crosses nothing. Readings persist per account per window in `$XDG_STATE_HOME/opencode-claude-code-plugin/account-usage.json` (`0600`, names, numbers and times only), and so does the dedup: each threshold is said once per account per window across reused processes, sessions and restarts, again only after the window's reset. A crossing in a `/compact` turn or a child session is not said there and is said by the next ordinary turn on the account. Never on `doGenerate`, never turns a served turn into a failure. `[]` turns the notes off but keeps the readings. The single-account name is `default`. Interactive transport: no event, so only a limit reply is recorded (`limited`). Log: `claude account usage crossed a warning threshold` (NOTICE). |
|
|
164
|
+
| `usageSwitchAt` | number / `false` | `90` | Percent at which work leaves an account BEFORE the wall, when another `failoverCandidates` account (same `accountGroups` group) exists and is not itself at that level per the store. `accountFailover: "auto"`: sets the existing account override (keyed on the account, until the window's reset; in memory, lost on restart) so the NEXT turn spawns on the other account, carried like a limit switch; the note says `New turns move to the "<account>" account until HH:MM`. `"ask"` / `"off"`: nothing moves and no form (the form only ends a FAILED turn); the note names the other account to pick a model from. The turn that crossed finishes as served. Also a threshold in its own right, so the switch is said exactly once per window. `false` or `0` never. Log: `claude account is near its usage limit; new turns move to another account until the window resets` (WARN). |
|
|
156
165
|
| `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. |
|
|
157
166
|
| `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. |
|
|
158
167
|
| `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. |
|
|
@@ -397,6 +406,30 @@ comes from `organizationType` plus `organizationRateLimitTier` in
|
|
|
397
406
|
ever displayed or logged. Unreadable or unrecognised means no suffix. There is no
|
|
398
407
|
option for it.
|
|
399
408
|
|
|
409
|
+
### Usage before the limit
|
|
410
|
+
|
|
411
|
+
Every headless `rate_limit_event` is kept as a reading per account per window, so the
|
|
412
|
+
operator hears about a window filling up before it runs out (`usageWarnThresholds`,
|
|
413
|
+
`usageSwitchAt` in the table above):
|
|
414
|
+
|
|
415
|
+
```text
|
|
416
|
+
▌ **usage:** the Claude account "work" has used 76% of the 5-hour window, which resets
|
|
417
|
+
at 2026-10-17 18:00.
|
|
418
|
+
```
|
|
419
|
+
|
|
420
|
+
- One note per served turn whose account has an unannounced crossing, each threshold
|
|
421
|
+
once per account per window, persisted, so neither a reused process nor a restart
|
|
422
|
+
repeats it. Not on compaction, `doGenerate` or a child session.
|
|
423
|
+
- At `usageSwitchAt` with `"auto"` and an eligible other account, new turns move there
|
|
424
|
+
until the reset and the note says so; with `"ask"`/`"off"` the note names the account
|
|
425
|
+
to pick a model from. No form after a served turn.
|
|
426
|
+
- The CLI reports only when its view changes, so a reused process can be silent for
|
|
427
|
+
several turns; the last reading stands until a newer one or its reset.
|
|
428
|
+
- Where to look: the TUI sidebar's `Accounts` lines (`work · 5h 76% · resets 18:00`,
|
|
429
|
+
`ok` for a reading without a number, `limited` for a refusal) and the doctor's
|
|
430
|
+
`Account usage` table, which also shows which thresholds were already noted. "No
|
|
431
|
+
reading yet" there with a recent turn usually means the interactive transport.
|
|
432
|
+
|
|
400
433
|
### When an account runs out of usage
|
|
401
434
|
|
|
402
435
|
By default (`accountFailover` unset or `"off"`) a usage-limited turn ends with one
|
|
@@ -495,6 +528,60 @@ Tell the user what a pick actually does before recommending one:
|
|
|
495
528
|
|
|
496
529
|
`{ "accountFailover": "off" }`, which is also the default, keeps the note.
|
|
497
530
|
|
|
531
|
+
### Automatic failover
|
|
532
|
+
|
|
533
|
+
**Opt-in.** `{ "accountFailover": "auto" }` and more than one account configured makes
|
|
534
|
+
the switch a pick would make, without the form: the failed attempt is dropped and the
|
|
535
|
+
**same request** runs on the next eligible account **inside the same opencode turn**.
|
|
536
|
+
The user sees one turn with the other account's answer under one note:
|
|
537
|
+
|
|
538
|
+
```text
|
|
539
|
+
▌ **account failover:** "work" is out of usage, so this turn moved to "personal"
|
|
540
|
+
automatically and stays there until 2026-09-20T18:00:00.000Z. The conversation
|
|
541
|
+
continues where it stopped.
|
|
542
|
+
```
|
|
543
|
+
|
|
544
|
+
What to tell a user who asks for it, or asks why billing moved:
|
|
545
|
+
|
|
546
|
+
- **Triggers** are exactly the form's, and only on a turn that **failed**
|
|
547
|
+
(`is_error: true`): a usage limit (rejected `rate_limit_event`, the CLI's
|
|
548
|
+
`rate_limit` reply, the two known limit texts) or one of the five account-block kinds.
|
|
549
|
+
A served turn with a rejected event, a generic error, a server error or a timeout
|
|
550
|
+
never switches, and healthy turns never rotate between accounts.
|
|
551
|
+
- **Order** is configured order: `default` first, then `accounts` as written. It skips
|
|
552
|
+
the account that just failed, every account already tried for this request, every
|
|
553
|
+
account outside the limited account's `accountGroups` group, and every account known
|
|
554
|
+
to be out: one with an unexpired override moving its own work elsewhere, or one whose
|
|
555
|
+
remembered limit has a reset time still ahead. Both expire on their own. The choice
|
|
556
|
+
is made when the failure arrives, so a limit another session learned meanwhile
|
|
557
|
+
counts. An account whose runtime cannot be prepared (config dir or wrapper not
|
|
558
|
+
writable) is skipped and the walk moves on; an account already tried is never run
|
|
559
|
+
again.
|
|
560
|
+
- **Bounded.** Each account is tried at most once per request. With none left the turn
|
|
561
|
+
finishes as an error: the `▌ **usage limit:**` note naming the last account tried and
|
|
562
|
+
offering no other (or the `▌ **claude account:**` note for a block), below one
|
|
563
|
+
`▌ **account failover:**` line per switch made on the way. A `doGenerate` caller gets
|
|
564
|
+
the error finish (`usage_limit` or the block's kind), never an answer, and no
|
|
565
|
+
usage-limit or failover line; its text is at most the CLI's own error text plus,
|
|
566
|
+
for a block, the `▌ **claude account:**` note with the login command.
|
|
567
|
+
- **Sticky** like a pick: the override is keyed on the account the conversation was on,
|
|
568
|
+
so every session on it, subagents included, follows until the limit's reset time, or
|
|
569
|
+
until opencode restarts when none was reported (always the case for a block).
|
|
570
|
+
- **Billing** moves to the target account for the rest of the turn and every later turn
|
|
571
|
+
on the limited account until the override expires. Opting in is the consent.
|
|
572
|
+
- **The conversation** is carried (`crossAccountResume`) and resumed, or replayed into a
|
|
573
|
+
fresh session when the carry is refused, exactly as for a pick; the note's last
|
|
574
|
+
sentence says which. Work the failed account did in that turn is not re-run by the
|
|
575
|
+
plugin, but a replay only has what opencode has.
|
|
576
|
+
- **Scope.** Both transports, `doGenerate` callers (no failover note in the returned text), and
|
|
577
|
+
child sessions, which switch themselves because nothing is asked; the parent follows
|
|
578
|
+
through the same override. Never compaction (it fails outright) and never a title
|
|
579
|
+
request (no spawn). An available account beats the `fallbackModels` chain, as the form
|
|
580
|
+
does. Stopping the turn, or cancelling the stream it returned, stops the attempt in
|
|
581
|
+
flight and starts no further account.
|
|
582
|
+
- Verified with a scripted `claude` only; the switch path itself is the one measured
|
|
583
|
+
live for the form.
|
|
584
|
+
|
|
498
585
|
### Switching account by hand, and keeping two accounts apart
|
|
499
586
|
|
|
500
587
|
Picking a model under another account's provider in the **same** opencode session
|
|
@@ -591,6 +678,8 @@ the opencode schema requires it. Markdown fallback reads top-level scalar fields
|
|
|
591
678
|
| `forceModel` | Registered bare model id, preserving the caller's account even if an account suffix is supplied. Works for any discovered agent mode. |
|
|
592
679
|
| `reasoningEffort` | `minimal`, `low`, `medium`, `high`, `xhigh`, `max`; invalid declarations warn and keep inherited effort. `minimal` maps to CLI `low`. Compaction skips this override. |
|
|
593
680
|
| `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. |
|
|
681
|
+
| `maxTurns` | Positive whole number, passed as `--max-turns` (per user message, CLI 2.1.280+). Any agent mode; headless only; part of the session key. |
|
|
682
|
+
| `maxBudgetUsd` | Positive dollars, passed as `--max-budget-usd` (per process life, reset on resume). Discovered `mode: subagent` agents only; on a main agent it is refused with a WARN. Headless only. |
|
|
594
683
|
| `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. |
|
|
595
684
|
|
|
596
685
|
### Ask before each subagent dispatch
|
|
@@ -665,10 +754,10 @@ Per-agent replaces provider-level; it never merges. Unset means no chain, which
|
|
|
665
754
|
the default. Two triggers only, never a generic error: the CLI refusing the model
|
|
666
755
|
(assistant `error: "model_not_found"`, or a failed result whose text is *"There's an
|
|
667
756
|
issue with the selected model"*, measured on CLI 2.1.280, where the result `subtype`
|
|
668
|
-
is misleadingly `success`), and a usage limit **only when
|
|
669
|
-
|
|
670
|
-
offer. With `"ask"` and another account configured the switch
|
|
671
|
-
stays out of it: model is a capability choice, account is a billing choice.
|
|
757
|
+
is misleadingly `success`), and a usage limit **only when no `accountFailover` switch is
|
|
758
|
+
taking the turn**, meaning it is `"off"` (its default) or has no other account to
|
|
759
|
+
offer. With `"ask"` or `"auto"` and another eligible account configured the switch wins
|
|
760
|
+
and the chain stays out of it: model is a capability choice, account is a billing choice.
|
|
672
761
|
An expired login, a billing hold and every other error kind are excluded because they
|
|
673
762
|
fail the same way on the next model.
|
|
674
763
|
|
|
@@ -1284,6 +1373,13 @@ Prefer the doctor: it needs no logging change and no restart.
|
|
|
1284
1373
|
| Claude "forgot" the earlier part of a long conversation | Claude Code compacted its own context | Look for the `▌ **context compacted:**` note in the transcript |
|
|
1285
1374
|
| 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 |
|
|
1286
1375
|
| 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 |
|
|
1376
|
+
| 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) |
|
|
1377
|
+
| After an `/undo`, an edited message or a regenerated reply, Claude still acts on the turns that were removed | A `claude` session cannot have messages taken back out of its context, so the reused process still held them | Upgrade. The Claude session is dropped and the thread as it stands now is replayed into a fresh one, under a `▌ **conversation rewound:**` note; the log says `replaying the conversation as text: the thread was edited or undone` with `reason: history-rewound`. Not recognised when nothing of the old thread survives in place (an undo back to the first message, an edit of it), because that is also the shape of an opencode compaction, which must keep its process |
|
|
1378
|
+
| `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 |
|
|
1379
|
+
| 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 |
|
|
1380
|
+
| Claude read a `.env` you thought opencode's `read` rules protected | opencode's own default for `*.env` is `ask`, not deny, and an `ask` cannot be carried onto a spawn with nobody to ask, so `mapReadRules` emits nothing for it. Check `/claude-code-doctor`'s read-rules section | Write `permission: { read: { "*.env": "deny", "*.env.*": "deny" } }` in opencode.json, which `mapReadRules` then carries onto Claude's own Read, or add `Read` to `proxyTools` so opencode's prompt governs it |
|
|
1381
|
+
| Claude read a file an opencode `read` **deny** rule covers | A deny is carried across by default. The doctor's read-rules section names an unmapped pattern (one with `..`) and the allows a deny now shadows; a `*` mid-path is narrower on Claude's side than in opencode | Reword the pattern (a trailing `/*` and a slash-free `*.env` both map exactly), proxy `Read`, or use `permissionPreset: "read-only"` |
|
|
1382
|
+
| A Claude Code hook on `Bash`, `Edit` or `Write` no longer fires | The tool is proxied, so Claude sees it as `mcp__opencode_proxy__<tool>` and the matcher misses | Name the proxied tool in the matcher (`mcp__opencode_proxy__bash`); hooks on CLI-native tools (`Read`) still fire |
|
|
1287
1383
|
| Wanting the per-turn cost in the chat | Not shown by default | Set `turnStats: true` and restart opencode |
|
|
1288
1384
|
| 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 |
|
|
1289
1385
|
| 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/` |
|