@khalilgharbaoui/opencode-claude-code-plugin 0.49.0 → 0.51.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.49.0",
3
+ "version": "0.51.0",
4
4
  "description": "Claude Code CLI provider plugin for opencode",
5
5
  "homepage": "https://opencode-claude-code-plugin.dev/",
6
6
  "funding": {
@@ -113,6 +113,7 @@ Defaults below describe normal headless opencode use when the key is absent.
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
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. |
116
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. |
117
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. |
118
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. |
@@ -132,6 +133,7 @@ Defaults below describe normal headless opencode use when the key is absent.
132
133
  | `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. |
133
134
  | `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`. |
134
135
  | `bridgeOpencodeMcp` | boolean | `true` | Discover/translate disk MCP config plus runtime enabled status. False stops this bridge, not explicit `mcpConfig`, the built-in-tool proxy, or Claude's own MCP settings. Only bridge trusted servers. |
136
+ | `bridgeMcpOauthTokens` | boolean | `false` | Give a bridged **remote** MCP server the OAuth bearer token opencode already holds for it, so the Claude CLI can use a server the user authenticated through opencode's own flow instead of reporting it `needs-auth` for the whole session (`MCP server "<name>" is needs-auth in Claude Code`). **Off by default and never a silent recommendation**: it copies a credential out of opencode's own store (`$XDG_DATA_HOME/opencode/mcp-auth.json`, usually `~/.local/share/opencode/`) into the `0600` bridged config the plugin writes for a second program, which then lets a model drive calls with it. Say that before suggesting it. opencode **1 only**: opencode 2 keeps MCP credentials in its database behind a typed plugin API and has no such file, so the option finds nothing there. Purely additive when on: a token is matched by opencode's own key for it (the server's **name**) and only when the stored `serverUrl` still equals the configured URL, a server with no usable token is bridged unchanged rather than dropped, an `Authorization` header the user wrote wins in any casing, and a local (stdio) server is never touched. An expired token (30 s margin) is not used and WARNs once per server per process; the plugin never runs an OAuth flow and never refreshes. A rotation forces a respawn through `hotReloadMcp` because the token's **expiry** is folded into the bridge hash (never anything derived from the token). No token ever reaches a log line, the doctor report or `/claude-code-doctor bundle`. |
135
137
  | `mcpConfig` | string or string[] | unset | Extra `--mcp-config` paths or inline JSON passed alongside the bridged config. |
136
138
  | `strictMcpConfig` | boolean | `false` | Headless `--strict-mcp-config`: use only explicitly supplied MCP configs, ignoring other MCP sources, not all settings/credentials/hooks. The interactive wrapper adds it whenever it passes MCP paths, independently of this option. |
137
139
  | `hotReloadMcp` | boolean | `true` | With bridging on, compare merged MCP config/status at turn start and respawn on drift, so a server enabled, disabled or finished connecting since the spawn reaches the model. Keeps the session via `--resume` on both transports. Acts only at a safe boundary: never during compaction, and never while a proxied call is pending, a turn is in flight or a plan-mode approval is outstanding. Logs the joined and left server names at INFO. One respawn per conversation per `CLAUDE_CODE_MCP_HOT_RELOAD_COOLDOWN_MS` (default 60000) so a flapping server cannot respawn every turn. Does not reload arbitrary provider options or watch explicit `mcpConfig` contents. |
@@ -472,10 +474,43 @@ Tell the user what a pick actually does before recommending one:
472
474
  fix; a switch made from that form lasts until opencode restarts. With the form off and
473
475
  another account configured the note ends with "Or pick a model from the
474
476
  `"<account>"` account and resend." instead of offering the form.
475
- - Not available on the interactive transport or on compaction turns.
477
+ - Works on both transports; not available on compaction turns.
476
478
 
477
479
  `{ "accountFailover": "off" }`, which is also the default, keeps the note.
478
480
 
481
+ ### Switching account by hand, and keeping two accounts apart
482
+
483
+ Picking a model under another account's provider in the **same** opencode session
484
+ continues the conversation on that account rather than starting over: the Claude
485
+ transcript is copied into the other account's `CLAUDE_CONFIG_DIR` and resumed with
486
+ `--resume`. Both transports, either direction, and it composes with a model or effort
487
+ change in the same step. It refuses (and replays as text, saying which in one NOTICE)
488
+ when the thread is no longer the conversation that account was last asked to continue,
489
+ when the transcript is gone or still being written, or with
490
+ `"crossAccountResume": false`.
491
+
492
+ When two accounts must NOT see each other's work, name them into groups:
493
+
494
+ ```json
495
+ {
496
+ "provider": {
497
+ "claude-code": {
498
+ "options": {
499
+ "accounts": ["work", "hobby"],
500
+ "accountGroups": { "work": "work" }
501
+ }
502
+ }
503
+ }
504
+ }
505
+ ```
506
+
507
+ `work` is alone in the group `work`; `hobby` and `default` share the implicit `default`
508
+ group. Moving a conversation from `work` to `hobby` now sends `hobby` nothing: no
509
+ transcript copy and no text replay, one `▌ **account group:**` note, and the
510
+ conversation left intact on `work` to switch back to. Recommend one group name per
511
+ real-world boundary (employer, client, personal), and name only the accounts that have
512
+ to stay apart, since everything unnamed is already together.
513
+
479
514
  ### Subagents on one model, on the caller's account
480
515
 
481
516
  opencode's agent config cannot say "inherit the account, change the model", because the
@@ -1114,6 +1149,7 @@ Prefer the doctor: it needs no logging change and no restart.
1114
1149
  | `proxy call still waiting` in the log, or a `task` that looks stuck | Expected: `task`/`task_batch` carry no default deadline, and the line is a status report | `/claude-code-doctor` lists pending calls with tool, age and deadline. Tell a working subagent from a wedged one there before proposing any timeout change; see the note under "Proxy tool names" |
1115
1150
  | An MCP server's tool returns nothing useful, and `plugin.log` carries `MCP server asked for operator input and was declined` | That server raised an MCP **elicitation**, a mid-call request for the operator to type or choose something. The Claude Code session the plugin drives is headless, so there is nobody to ask: the plugin answers `{"action":"decline"}` under the CLI's documented schema and warns once per server. The tool call still returns and the turn continues | Not configurable, and deliberately so: accepting would mean inventing the answer the server asked for. Run that server's flow in Claude Code directly, where its own dialog can take the input, or configure the server so that path does not elicit |
1116
1151
  | An MCP server's tools are simply absent | Claude Code could not connect that server | Read the once-per-process WARN at session start, and the doctor's **MCP config entries Claude Code skipped** section for one the CLI refused outright. `mcpServers` in the ready block is disk discovery, not live connectivity; fix the server where it is configured |
1152
+ | `MCP server "<name>" is needs-auth in Claude Code`, for a remote server that works in opencode | Whether the user authenticated it through opencode's own OAuth flow. opencode holds that token; the bridged config carries no credential, so the CLI cannot connect and the server is dead for the whole session | On opencode **1**, `bridgeMcpOauthTokens: true` hands the CLI the token opencode already has. Tell the user what it does before suggesting it: it copies a credential out of opencode's own store into the bridged `0600` config. On opencode **2** there is no such file and the option finds nothing, so the fix is configuring that server for Claude Code directly |
1117
1153
  | An MCP server opencode has configured is missing on the FIRST turn of a fresh `opencode run`, but present in the TUI | Not a config fault, and mostly fixed. The bridge reads opencode's live MCP status when the turn plans its spawn. On opencode 2 a server still connecting is reported `pending`, which the bridge no longer reads as disabled, and the turn waits up to `mcpConnectWaitMs` (3 s) for the host to decide; opencode 1 cannot reach this state, because its own status call blocks until every server resolves. If the server is slower than the budget it is still bridged, and if it genuinely joins later `hotReloadMcp` moves the conversation onto a process that has it on the next turn, logging `opencode MCP servers changed, respawning claude` with the joined names | Nothing, usually. For a very slow server raise `mcpConnectWaitMs`. Check `plugin.log` for `waited for opencode MCP servers to finish connecting` and for the respawn line; if neither appears and the server is still absent, the status the host reported was a real refusal (`failed`, `needs_auth`), which is opencode's to fix |
1118
1154
  | `permissionPreset` set but nothing about the session looks restricted | The option never reached that provider, or the name is not one the plugin knows (only `read-only` exists) | Read the `permissionPreset` row in `/claude-code-doctor`, or `permissionPresets` in the ready block, for the provider the conversation is on: `none` means it is not configured there (each account is its own provider id), `applied: false` with a name means an unrecognised name applied nothing, and `overrides` lists what an applied preset replaced |
1119
1155
  | `permissionPreset: "read-only"` set, but reads are unconfined or something still prompts | `--restricted` needs CLI 2.1.258 and `--permission-prompts none` needs 2.1.263; below those the preset falls back to `--disallowedTools` plus the plugin's own deny and WARNs naming what is lost | `claude --version`. Below 2.1.258 the working-directory confinement on reads is gone; below 2.1.263 the denial happens in the plugin instead of the CLI. The preset still holds, with one layer fewer |
@@ -1143,6 +1179,7 @@ Prefer the doctor: it needs no logging change and no restart.
1143
1179
  | `Provider did not return a tool result` on a tool row (opencode 2), or a row that never leaves pending (opencode 1) | An older plugin lost the result of a tool Claude Code ran itself when the step had to end first: Claude Code answers a batch of tool calls in one message and withholds its own results while one of them is proxied, and a refused tool's refusal arrives a frame ahead of the call's registration | Upgrade. The model always had the real output; only opencode's row was unanswered. The row now carries a result, and a deferred one says plainly that Claude Code kept the output in its own context |
1144
1180
  | A line at the top of a reply that answers the PREVIOUS message | A reused `claude` process kept talking after the plugin closed the turn (most often after a proxied tool call, or when a background command it started reported back and the CLI answered on its own), and the next turn replays it | Working as designed, and it is led by `▌ **between turns:**` so it is distinguishable; older plugins replayed it as bare text. It is stripped from any transcript rebuilt for the CLI. A default `proxyTools` prevents the background-command route entirely |
1145
1181
  | Claude "forgot" the earlier part of a long conversation | Claude Code compacted its own context | Look for the `▌ **context compacted:**` note in the transcript |
1182
+ | 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 |
1146
1183
  | 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 |
1147
1184
  | Wanting the per-turn cost in the chat | Not shown by default | Set `turnStats: true` and restart opencode |
1148
1185
  | 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 |