@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/README.md +1 -1
- package/dist/index.js +288 -28
- package/dist/index.js.map +1 -1
- package/package.json +2 -2
- package/skills/claude-code-plugin/SKILL.md +72 -16
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@khalilgharbaoui/opencode-claude-code-plugin",
|
|
3
|
-
"version": "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"` | `"
|
|
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
|
|
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
|
|
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
|
-
|
|
298
|
-
|
|
299
|
-
|
|
300
|
-
|
|
301
|
-
|
|
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
|
|
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" }
|
|
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`
|
|
414
|
-
|
|
415
|
-
|
|
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
|
|
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.
|