@khalilgharbaoui/opencode-claude-code-plugin 0.37.0 → 0.38.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.37.0",
3
+ "version": "0.38.0",
4
4
  "description": "Claude Code CLI provider plugin for opencode",
5
5
  "homepage": "https://opencode-claude-code-plugin.dev/",
6
6
  "funding": {
@@ -26,7 +26,7 @@
26
26
  "build": "tsup",
27
27
  "dev": "tsup --watch",
28
28
  "typecheck": "tsc --noEmit",
29
- "test": "OPENCODE_CLAUDE_CODE_LOG_FILE=0 tsx --test test-bridge.ts test-broker.ts test-proxy-mcp.ts test-proxy-task.ts test-auto-continue.ts test-has-new-user-content.ts test-get-claude-user-message.ts test-logger.ts test-cli-args.ts test-permission-presets.ts test-session-manager.ts test-compaction-model.ts test-tool-mapping.ts test-cwd-resolution.ts test-todo-ledger.ts test-session-affinity.ts test-config-models.ts test-ask-user-question.ts test-claude-session-wrapper.ts test-spawn-env.ts test-respawn.ts test-startup-diagnostics.ts test-subagent-hint.ts test-exit-plan-mode-question.ts test-compress-tool.ts test-agent-models.ts test-side-question.ts test-btw-command.ts test-effort-sessions.ts test-tool-block-index.ts test-context-usage.ts test-skill-bridge.ts test-turn-stats.ts test-cli-events.ts test-cli-events-stream.ts test-result-fallback.ts test-doctor.ts test-configure-skill.ts test-unattended-replay.ts test-process-lifecycle.ts test-account-failover.ts test-host-tools.ts test-v2-entrypoint.ts test-v2-client.ts test-tmp-dir.ts test-cleanup-stale.ts test-account-wrapper.ts test-runtime-status-sessions.ts test-index-hooks.ts test-silent-turn.ts test-mcp-tool-result-name.ts test-model-fallback.ts test-do-generate.ts test-interactive-usage.ts test-background-subagents.ts test-interactive-result.ts test-cli-probe-cache.ts test-mcp-late-connect.ts test-prologue-abort.ts test-diagnostic-bundle.ts test-ids.ts test-session-fork.ts test-deferred-cli-tool-result.ts test-stale-build.ts",
29
+ "test": "OPENCODE_CLAUDE_CODE_LOG_FILE=0 tsx --test test-bridge.ts test-broker.ts test-proxy-mcp.ts test-proxy-task.ts test-auto-continue.ts test-has-new-user-content.ts test-get-claude-user-message.ts test-logger.ts test-cli-args.ts test-permission-presets.ts test-session-manager.ts test-compaction-model.ts test-tool-mapping.ts test-cwd-resolution.ts test-todo-ledger.ts test-session-affinity.ts test-config-models.ts test-ask-user-question.ts test-claude-session-wrapper.ts test-spawn-env.ts test-respawn.ts test-startup-diagnostics.ts test-subagent-hint.ts test-exit-plan-mode-question.ts test-compress-tool.ts test-agent-models.ts test-side-question.ts test-btw-command.ts test-effort-sessions.ts test-tool-block-index.ts test-context-usage.ts test-skill-bridge.ts test-turn-stats.ts test-cli-events.ts test-cli-events-stream.ts test-result-fallback.ts test-doctor.ts test-configure-skill.ts test-unattended-replay.ts test-process-lifecycle.ts test-account-failover.ts test-host-tools.ts test-v2-entrypoint.ts test-v2-client.ts test-tmp-dir.ts test-cleanup-stale.ts test-account-wrapper.ts test-runtime-status-sessions.ts test-index-hooks.ts test-silent-turn.ts test-mcp-tool-result-name.ts test-model-fallback.ts test-do-generate.ts test-interactive-usage.ts test-background-subagents.ts test-interactive-result.ts test-cli-probe-cache.ts test-mcp-late-connect.ts test-prologue-abort.ts test-diagnostic-bundle.ts test-ids.ts test-session-fork.ts test-deferred-cli-tool-result.ts test-stale-build.ts test-tui-log-sink.ts",
30
30
  "generate:log-messages": "tsx scripts/generate-log-messages.ts"
31
31
  },
32
32
  "dependencies": {
@@ -112,12 +112,12 @@ 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"` | `"ask"` | When the account a conversation runs on is out of usage, end the turn on opencode's native `question` form listing the other configured accounts, and continue the task on the pick inside the same opencode turn. Only ever fires with more than one account configured, so a single-account install is unaffected by the default. 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 as the rate-limit error does. 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 or the interactive transport. A switch cannot resume the Claude session (transcripts live under the account's own config dir), so the conversation is replayed into a fresh one: it costs input tokens on the new account, and MCP servers configured only in the limited account's Claude profile are gone. `"off"` keeps the plain rate-limit error. |
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 or the interactive transport. A switch cannot resume the Claude session (transcripts live under the account's own config dir), so the conversation is replayed into a fresh one: it costs input tokens on the new account, and 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
116
  | `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. |
117
117
  | `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. |
118
118
  | `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. |
119
119
  | `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 spawns only (not compaction, not the interactive transport). |
120
- | `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 `accountFailover` has no other account to offer; with 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, title stubs or the interactive transport. Writes a `▌ **model fallback:**` note that transcript rebuilds strip. Not independently read per expanded account. |
120
+ | `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, title stubs or the interactive transport. Writes a `▌ **model fallback:**` note that transcript rebuilds strip. Not independently read per expanded account. |
121
121
  | `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. |
122
122
  | `skipPermissions` | boolean | `true` | Pass `--dangerously-skip-permissions` to headless Claude, 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. |
123
123
  | `permissionMode` | `acceptEdits` / `auto` / `bypassPermissions` / `default` / `dontAsk` / `plan` | unset | Headless `--permission-mode`, not version-gated: verify the installed CLI supports the value. `plan` is enforced: it overrides `skipPermissions: true` and the plugin drops `--dangerously-skip-permissions` for it, so claude cannot edit or run commands. Every other value governs prompting and still passes the skip flag, so `plan` is the only one that makes a run read-only. Nothing releases plan mode mid-session (no headless `ExitPlanMode`), so leaving it means a config change and an opencode restart; the plugin warns once at startup. Not forwarded by the current interactive spawn path. |
@@ -151,7 +151,7 @@ Defaults below describe normal headless opencode use when the key is absent.
151
151
  | `interactiveBypass` | boolean | `false` | Deprecated no-op. The TUI asks for a manual safety confirmation on `bypassPermissions`, so the plugin never passes it. |
152
152
  | `interactiveAllowTools` | string[] | `["Bash", "Edit", "Write", "Read", "WebFetch"]` | With `interactive`: replaces the built-in pre-allow list. MCP wildcards from discovered bridge names plus `mcp__opencode_proxy__*` are added even with `[]`. Not a capability denylist; review permissions before enabling. |
153
153
  | `interactiveSystemPrompt` | boolean | `true` | With `interactive`: append the plugin's own prompt. opencode's forwarded system prompt is deliberately not sent on this transport (it can trip Claude's third-party usage gate). `false` is for diagnostics only. |
154
- | `logging` | object | see below | File and TUI logging policy. |
154
+ | `logging` | object | see below | File logging plus how much reaches the operator. |
155
155
  | `name` | string | unset | Low-level `createClaudeCode()` provider identity fallback after `providerID`, not the opencode display-name setting. Display name lives at `provider.<id>.name`; account expansion supplies its own label. Leave this option unset. |
156
156
  | `providerID` | string | derived | Config hook writes the actual provider id (`claude-code` or `claude-code-work`). Do not override manually. |
157
157
  | `hostApi` | `"v1"` \| `"v2"` | derived | Which opencode major created the model, which decides the tool names its stream uses (`bash` on 1.x, `shell` on 2.x). Set only by the opencode 2 entrypoint. Do not set it: forcing `"v2"` under opencode 1.x makes every proxied tool call fail as an unavailable tool. |
@@ -164,9 +164,25 @@ Defaults below describe normal headless opencode use when the key is absent.
164
164
  |---|---|---|---|
165
165
  | `file` | boolean | `false` | Persist entries that pass `level`. Logs can contain prompts/tool data/CLI arguments; enable temporarily with consent, not as a credential dump. |
166
166
  | `dir` | path | `~/.local/share/opencode-claude-code/` | Where `plugin.log` goes. |
167
- | `mode` | `"silent"` / `"debug"` | `"silent"` | After level filtering: silent routes lower levels only to the file if enabled; WARN/ERROR go to stderr/TUI too. Debug echoes all emitted levels to stderr, but does not lower the threshold. |
167
+ | `mode` | `"silent"` / `"debug"` | `"silent"` | After level filtering: silent routes lower levels only to the file if enabled; WARN/ERROR are surfaced to the operator too. Debug surfaces all emitted levels, but does not lower the threshold. |
168
168
  | `level` | `debug` / `info` / `notice` / `warn` / `error` | `"info"` | Minimum level emitted anywhere. |
169
169
 
170
+ Where a surfaced line goes depends on whether a full-screen TUI owns the terminal, and
171
+ the plugin detects that itself (`isTuiHost`, true when the plugin is running off the main
172
+ thread, which is how opencode 1.x's TUI runs a plugin). Outside a TUI (`opencode run`,
173
+ `opencode serve`, tests, and opencode 2, which runs plugins in a separate `serve --stdio`
174
+ process) it is stderr, prefixed `[opencode-claude-code] WARN:`, unchanged. Inside the TUI
175
+ **nothing is written to stderr at any level**, because stderr there is the terminal the
176
+ TUI is drawing on and a raw line sits on top of the interface until a redraw. Instead the
177
+ line goes to opencode's own log (`client.app.log`, service `opencode-claude-code`, so it
178
+ lands in `~/.local/share/opencode/log/`), and WARN/ERROR also raise a toast titled
179
+ `claude-code`. The toast carries the message only, never the JSON data; the same message
180
+ text toasts at most once per process, and a burst raises at most three toasts plus one
181
+ line naming how many were held back. Nothing is held back from either log. So when a user
182
+ reports "the plugin printed garbage over my opencode UI", the answer is to upgrade, not to
183
+ turn logging off; and when they ask where a WARN went in the TUI, point them at opencode's
184
+ own log or the toast rather than at stderr.
185
+
170
186
  ## Environment variables
171
187
 
172
188
  Set variables in the environment that launches opencode, then fully restart it.
@@ -292,13 +308,49 @@ suffix and sets the config dir. Existing `CLAUDE.md`, `settings.json`, `skills/`
292
308
  missing; existing targets stay untouched. This shares capabilities/settings, not an
293
309
  isolation boundary. Auth/session files are not part of the shared list.
294
310
 
311
+ ### When an account runs out of usage
312
+
313
+ By default (`accountFailover` unset or `"off"`) a usage-limited turn ends with one
314
+ `▌ **usage limit:**` note and nothing else, on every limited turn rather than once per
315
+ process:
316
+
317
+ ```text
318
+ ▌ **usage limit:** the Claude account "work" is out of usage in the 5-hour window, which
319
+ resets at 2026-09-20 20:00. Pick a model from the "personal" or "default" account and
320
+ resend your message, or wait for the window to reset.
321
+ ```
322
+
323
+ - The reset time is **local** to the operator, to the minute, and is omitted when the
324
+ CLI reported none. The window name comes from the CLI's `rateLimitType`.
325
+ - With no other account configured the advice is "Wait for the window to reset, or
326
+ enable extra usage on the account." instead.
327
+ - It replaces the CLI's own error text for that turn: the raw sentence names no account,
328
+ and the `▌ **rate limit:**` paragraph the plugin used to write is log-only now, so a
329
+ limited turn carries exactly one block.
330
+ - Written on a child session too (a subagent that died on a limit is what the parent's
331
+ `task` result should say), but never on a compaction turn, whose text becomes the
332
+ stored summary. How the turn finishes is unchanged.
333
+ - `permission_denials`, `turnStats` and the finish reason are untouched: the note
334
+ replaces text, not control flow.
335
+
336
+ When a user says the limit message is unhelpful, check the plugin version before
337
+ anything else: this note is recent and a long-lived opencode window may predate it.
338
+
295
339
  ### Account failover
296
340
 
297
- With more than one account configured, `accountFailover` is `"ask"` by default. When a
298
- turn is rejected for usage, the turn ends on opencode's `question` form instead of an
299
- error: one option per other configured account, plus `stop`. Picking an account applies
300
- it inside the same opencode turn, with no new user message, and the task carries on.
301
- Leaving the form unanswered waits and costs nothing.
341
+ **Opt-in.** `{ "accountFailover": "ask" }` and more than one account configured makes a
342
+ usage-rejected turn end on opencode's `question` form instead of that note: one option
343
+ per other configured account, plus `stop`. Picking an account applies it inside the same
344
+ opencode turn, with no new user message, and the task carries on. Leaving the form
345
+ unanswered waits and costs nothing.
346
+
347
+ It is opt-in rather than on by default because of what was measured against a real
348
+ five-hour limit on 2026-10-03. Two of the three findings are not the plugin's to fix:
349
+ typing a message instead of picking **dismisses** the form in opencode, and that message
350
+ then runs on the still-limited account and raises a second form (the "I had to send two
351
+ messages before anything ran" report); and a switch is always a full replay. The third,
352
+ every pick being refused as `unrecognised answer` because another plugin appends a
353
+ routing tag to tool results, is fixed.
302
354
 
303
355
  Tell the user what a pick actually does before recommending one:
304
356
 
@@ -313,7 +365,7 @@ Tell the user what a pick actually does before recommending one:
313
365
  - MCP servers configured only in the limited account's Claude profile will be **missing**
314
366
  on the target account.
315
367
  - `stop`, dismissing the form, or answering with anything that is not one of the offered
316
- accounts ends the turn exactly as the rate-limit error does today. The limit is
368
+ accounts ends the turn the way a limited turn ends without the form. The limit is
317
369
  unchanged either way; failover moves the work, it does not create usage.
318
370
  - Only a rejected `rate_limit_event`, one of the two known account-limit error texts, or
319
371
  an account-level failure the CLI reports on its own error reply opens the form. The
@@ -324,10 +376,12 @@ Tell the user what a pick actually does before recommending one:
324
376
  command to fix it: `claude auth login` for the default account, or
325
377
  `CLAUDE_CONFIG_DIR=<that account's config dir> claude auth login` for a named one. When
326
378
  a user reports "Failed to authenticate: OAuth session expired", that command is the
327
- fix; a switch made from that form lasts until opencode restarts.
379
+ fix; a switch made from that form lasts until opencode restarts. With the form off and
380
+ another account configured the note ends with "Or pick a model from the
381
+ `"<account>"` account and resend." instead of offering the form.
328
382
  - Not available on the interactive transport or on compaction turns.
329
383
 
330
- `{ "accountFailover": "off" }` keeps the plain rate-limit error.
384
+ `{ "accountFailover": "off" }`, which is also the default, keeps the note.
331
385
 
332
386
  ### Subagents on one model, on the caller's account
333
387
 
@@ -410,9 +464,10 @@ Per-agent replaces provider-level; it never merges. Unset means no chain, which
410
464
  the default. Two triggers only, never a generic error: the CLI refusing the model
411
465
  (assistant `error: "model_not_found"`, or a failed result whose text is *"There's an
412
466
  issue with the selected model"*, measured on CLI 2.1.280, where the result `subtype`
413
- is misleadingly `success`), and a usage limit **only when `accountFailover` has no
414
- other account to offer**. With another account configured the switch form wins and
415
- the chain stays out of it: model is a capability choice, account is a billing choice.
467
+ is misleadingly `success`), and a usage limit **only when the `accountFailover` form is
468
+ not taking the turn**, meaning it is `"off"` (its default) or has no other account to
469
+ offer. With `"ask"` and another account configured the switch form wins and the chain
470
+ stays out of it: model is a capability choice, account is a billing choice.
416
471
  An expired login, a billing hold and every other error kind are excluded because they
417
472
  fail the same way on the next model.
418
473
 
@@ -836,7 +891,8 @@ user's own Claude Code hooks (`hook_response` with `outcome: "error"` or a non-z
836
891
  `exit_code`), not opencode's. A failing `SessionStart` hook is otherwise invisible: the
837
892
  CLI drops its contribution and the turn succeeds, so the context it was meant to add is
838
893
  missing from every turn on that process. Read it whenever a hook's effect is absent. The
839
- first failure is also a WARN in the terminal. Only the hook's `stderr` is shown, capped
894
+ first failure is also a plugin WARN, which in the TUI means a toast plus a line in
895
+ opencode's own log rather than terminal text. Only the hook's `stderr` is shown, capped
840
896
  at 200 characters, because its stdout is spliced into the model's context. The plugin
841
897
  never passes `--include-hook-events`, so only `SessionStart` (and `Setup`) hooks are
842
898
  reported at all; `cancelled` is not a failure, since an abort produces it.