@khalilgharbaoui/opencode-claude-code-plugin 0.64.0 → 0.65.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 CHANGED
@@ -18,8 +18,6 @@
18
18
  [![License](https://img.shields.io/github/license/khalilgharbaoui/opencode-claude-code-plugin?style=flat-square&label=license&labelColor=15181E&color=FFC46B)](./LICENSE)
19
19
  [![Buy me a coffee](https://img.shields.io/badge/support-buy%20me%20a%20coffee-FFC46B?style=flat-square&labelColor=15181E&logo=buymeacoffee&logoColor=FFC46B)](https://www.buymeacoffee.com/khalilgharbaoui)
20
20
 
21
- <a href="https://www.buymeacoffee.com/khalilgharbaoui"><img src="site/public/buy-me-a-coffee.png" alt="Buy me a coffee" width="214" height="60"></a>
22
-
23
21
  **Claude Code is the provider.** This opencode plugin runs Anthropic's Claude models through the official **Claude Code CLI** (`claude`) as a subprocess instead of calling the HTTP API. opencode inherits whatever that CLI is logged in as (a Claude subscription, an API key, Bedrock or Vertex), gets Claude's own tools, MCP servers and skills, and keeps running the tools that touch your machine itself, behind its own permission prompts. One package serves opencode 1.x and 2.x.
24
22
 
25
23
  - **Your CLI's login, untouched.** The plugin never reads, stores or replays a token. Lifting the OAuth session out of the official client is what proxy-style plugins do, and Anthropic disallowed that for third-party Claude use in February 2026. This plugin structurally cannot do it.
@@ -46,9 +44,13 @@
46
44
  }
47
45
  ```
48
46
 
49
- `~/.config/opencode/opencode.json` on opencode 1.x. opencode 2 reads the same key; its native spelling is `plugins`.
47
+ `~/.config/opencode/opencode.json`, on opencode 1.x and 2.x alike. opencode 2's native key is `plugins`, and `opencode plugin add @khalilgharbaoui/opencode-claude-code-plugin` writes that entry and installs the package for you.
48
+
49
+ 3. **Quit opencode fully and relaunch.** Plugins load once, at process start. The model picker now has a **Claude Code** provider (opencode 1.x labels it with its id, `claude-code`) with entries such as `Claude Sonnet 5.5 (2×)` and `Claude Opus 5 (5×)`; the suffix is each model's list price relative to Haiku. A model's full id is `claude-code/<model>`, so one line checks it from a terminal:
50
50
 
51
- 3. **Quit opencode fully and relaunch.** Plugins load once, at process start. The model picker now has a **Claude Code (Default)** provider with entries such as `Claude Sonnet 5.5 (2×)` and `Claude Opus 5 (5×)`; the suffix is each model's list price relative to Haiku.
51
+ ```bash
52
+ opencode run -m claude-code/claude-haiku-5-5 "reply with ok"
53
+ ```
52
54
 
53
55
  Nothing in the picker? [Troubleshooting](./docs/troubleshooting/symptoms.md) is keyed on the first thing you see. `/claude-code-doctor` in any session prints what the plugin thinks is happening, without calling a model.
54
56
 
@@ -70,12 +72,14 @@ Nothing in the picker? [Troubleshooting](./docs/troubleshooting/symptoms.md) is
70
72
  | | opencode's native `anthropic` provider | This plugin | Proxy and token-reuse plugins |
71
73
  |---|---|---|---|
72
74
  | **Authentication** | A Platform API key in opencode's auth store. | Whatever the official `claude` CLI holds. No token is read, stored or replayed. | The Claude OAuth session, used outside the official client. |
73
- | **Terms-of-service status** | The ordinary API route. | Sanctioned: the official client does the authenticating. | Disallowed. Anthropic disallowed reusing subscription authentication for third-party Claude use in February 2026, and each of those projects carries its own disclaimer. |
75
+ | **Terms-of-service status** | The ordinary API route. | Uses the official client: Claude Code does the authenticating, the same as running `claude -p` yourself. Anthropic's [legal page](https://code.claude.com/docs/en/legal-and-compliance) allows signing in to the unmodified binary; it has not approved or endorsed this plugin. | Disallowed. Anthropic disallowed reusing subscription authentication for third-party Claude use in February 2026, and each of those projects carries its own disclaimer. |
74
76
  | **Who runs Bash, Edit, Write** | opencode. | opencode, by default; `Read`, `Grep`, `Glob` run in Claude Code. | opencode. |
75
77
  | **What it costs you** | Baseline. | A `claude` child per conversation under a cap of 16; Claude Code may compact its own context behind opencode's back. | One more moving part, plus the account risk above. |
76
78
 
77
79
  The [full comparison](./docs/comparison.md) has nine rows, names the projects in the third column, and quotes their own READMEs.
78
80
 
81
+ On opencode 2.2.3 or later with one Claude login, also look at opencode's own **Claude Agent** provider: it drives your own `claude` too, runs every tool in opencode, and leaves your `~/.claude` setup out. The comparison has [a section on it](./docs/comparison.md#opencodes-built-in-claude-agent-provider), including what this plugin still adds (opencode 1.x, several accounts, your Claude Code setup, fast mode, `/btw`, the sidebar).
82
+
79
83
  ## Built from measurements
80
84
 
81
85
  A dozen test files drive a fake `claude` through real turns: the stream parser, the proxy broker, both watchdogs, abort, respawn, account failover and the model fallback chain are exercised end to end. Every rule in [`AGENTS.md`](./AGENTS.md) names the probe, the version and the number that produced it, and the evidence lives in [`docs/agents-history.md`](./docs/agents-history.md). Two runtime dependencies. The [site](https://opencode-claude-code-plugin.dev/) reports the live numbers: it rebuilds itself after every release, on every push that moves a figure, and every six hours.
package/dist/index.d.ts CHANGED
@@ -113,6 +113,81 @@ interface V2ModelRequestEvent {
113
113
  interface V2ModelHookOptions {
114
114
  readonly providerID?: string;
115
115
  }
116
+ /**
117
+ * `@opencode/plugin` 2.2.2 `SessionRetry`, the `session.hook("retry")` event:
118
+ * opencode asks before it runs a failed turn again (h #g277).
119
+ */
120
+ interface V2SessionRetryEvent {
121
+ readonly sessionID: string;
122
+ readonly model: {
123
+ readonly id: string;
124
+ readonly providerID: string;
125
+ };
126
+ readonly error: {
127
+ readonly type?: string;
128
+ readonly message?: string;
129
+ };
130
+ readonly attempt: number;
131
+ decision: {
132
+ retry: false;
133
+ } | {
134
+ retry: true;
135
+ delay: number;
136
+ };
137
+ }
138
+ /** One way to connect an integration (`IntegrationMethod`); only `type` is always there. */
139
+ interface V2IntegrationMethod {
140
+ readonly type: string;
141
+ readonly id?: string;
142
+ readonly label?: string;
143
+ }
144
+ /** `@opencode/plugin` 2.2.2 `IntegrationEditor`, the calls we make. */
145
+ interface V2IntegrationEditor {
146
+ get(id: string): {
147
+ id: string;
148
+ name: string;
149
+ } | undefined;
150
+ update(id: string, update: (integration: {
151
+ id: string;
152
+ name: string;
153
+ }) => void): void;
154
+ readonly method: {
155
+ list(integrationID: string): readonly V2IntegrationMethod[];
156
+ update(input: {
157
+ integrationID: string;
158
+ method: {
159
+ id: string;
160
+ type: "external";
161
+ label: string;
162
+ };
163
+ }): void;
164
+ remove(integrationID: string, method: V2IntegrationMethod): void;
165
+ };
166
+ }
167
+ /** `@opencode/client` `ConnectionInfo`, passed back to opencode untouched. */
168
+ interface V2ConnectionInfo {
169
+ readonly type: string;
170
+ readonly id?: string;
171
+ readonly label?: string;
172
+ }
173
+ /** `@opencode/schema/connection` `Connection.Status`. */
174
+ interface V2ConnectionStatus {
175
+ status: "needs_auth";
176
+ message: string;
177
+ url?: string;
178
+ }
179
+ /** `@opencode/plugin` 2.2.2 `IntegrationDomain`, the slice `/connect` needs. */
180
+ interface V2IntegrationDomain {
181
+ transform(callback: (editor: V2IntegrationEditor) => void): Promise<V2Registration>;
182
+ readonly connection: {
183
+ active(integrationID: string): Promise<V2ConnectionInfo | undefined>;
184
+ status(input: {
185
+ integrationID: string;
186
+ connection: V2ConnectionInfo;
187
+ status: V2ConnectionStatus | undefined;
188
+ }): Promise<void>;
189
+ };
190
+ }
116
191
  /** `@opencode/schema/prompt-input` `Prompt`: we only rewrite `text`. */
117
192
  type V2Prompt = {
118
193
  text?: string;
@@ -158,6 +233,8 @@ interface V2Context {
158
233
  readonly options: Record<string, unknown>;
159
234
  readonly provider: {
160
235
  transform(callback: (editor: V2ProviderEditor) => void): Promise<V2Registration>;
236
+ /** Re-runs the transforms. In `@opencode/plugin` 2.2.5; optional because older hosts may lack it. */
237
+ reload?(): Promise<void>;
161
238
  };
162
239
  readonly aisdk: {
163
240
  hook(name: "sdk", callback: (event: V2AISDKSdkEvent) => Promise<void> | void, options?: V2ModelHookOptions): Promise<V2Registration>;
@@ -165,6 +242,7 @@ interface V2Context {
165
242
  };
166
243
  readonly session: {
167
244
  hook(name: "model.request", callback: (event: V2ModelRequestEvent) => Promise<void> | void, options?: V2ModelHookOptions): Promise<V2Registration>;
245
+ hook(name: "retry", callback: (event: V2SessionRetryEvent) => Promise<void> | void, options?: V2ModelHookOptions): Promise<V2Registration>;
168
246
  prompt?(input: V2Prompt & {
169
247
  sessionID: string;
170
248
  delivery?: V2Delivery;
@@ -180,6 +258,8 @@ interface V2Context {
180
258
  signal?: AbortSignal;
181
259
  }): AsyncIterable<V2Event>;
182
260
  };
261
+ /** `/connect`'s entry for each provider (h #g277). Optional: early 2.0 releases lack parts of it. */
262
+ readonly integration?: V2IntegrationDomain;
183
263
  /**
184
264
  * Absent before 2.0.31's measured shape: the definition is plain JSON
185
265
  * Schema, which `Rpc.PortableDefinition` accepts, and the handlers object is
@@ -444,7 +524,11 @@ interface ClaudeCodeConfig {
444
524
  controlRequestDenyMessage?: string;
445
525
  proxyTools?: string[];
446
526
  proxyOpencodeTools?: string[];
527
+ /** See `ClaudeCodeProviderSettings.alwaysLoadProxyTools`. Default bash, edit, write. */
528
+ alwaysLoadProxyTools?: boolean | string[];
447
529
  stripContextReminders?: boolean;
530
+ /** See `ClaudeCodeProviderSettings.verbatimPrompts`. Default true. */
531
+ verbatimPrompts?: boolean;
448
532
  extraDisallowedTools?: string[];
449
533
  /** See `ClaudeCodeProviderSettings.mapReadRules`. Default true. */
450
534
  mapReadRules?: boolean;
@@ -481,6 +565,8 @@ interface ClaudeCodeConfig {
481
565
  resumeAfterRestart?: boolean;
482
566
  /** Carry a conversation's Claude session over when its model or effort changes. Default true. */
483
567
  resumeAcrossModelChanges?: boolean;
568
+ /** Apply a model or effort change to the live `claude` process. Default true. */
569
+ liveModelSwitch?: boolean;
484
570
  /** Carry a conversation's Claude transcript over on an account switch. Default true. */
485
571
  crossAccountResume?: boolean;
486
572
  /** See `ClaudeCodeProviderSettings.maxBudgetUsd`. Resolved; unset is off. */
@@ -536,6 +622,26 @@ type AccountFailoverMode = "ask" | "off" | "auto";
536
622
  type SubagentDispatchMode = "ask" | "off";
537
623
  interface ClaudeCodeProviderSettings {
538
624
  cliPath?: string;
625
+ /**
626
+ * Register the model catalog as the installed Claude Code describes it.
627
+ * Default `true`.
628
+ *
629
+ * A background probe asks the CLI, once per CLI version, which models it
630
+ * knows, the context window it runs each in and the effort levels it
631
+ * accepts, and keeps the answer in `cli-models.json` under
632
+ * `$XDG_STATE_HOME`. Providers are registered from that file: windows and
633
+ * effort variants follow the CLI, and a model the CLI's own picker offers
634
+ * that the catalog lacks is added at an unknown (zero) price. Costs and the
635
+ * `(N×)` labels stay the catalog's, because the CLI carries no prices.
636
+ *
637
+ * Free: each probe is a `--bare` process with no credential, sent
638
+ * `initialize` and `get_context_usage` and never a message (measured: no
639
+ * API request, h #g279). Registration never waits for it, so a fresh answer
640
+ * applies at once on opencode 2 (a provider reload) and from the next
641
+ * start on opencode 1. `false` registers the built-in catalog as it ships
642
+ * and runs no probe.
643
+ */
644
+ cliModelCatalog?: boolean;
539
645
  /** Internal: set by the opencode 2 entrypoint. See `ClaudeCodeConfig.hostApi`. */
540
646
  hostApi?: "v1" | "v2";
541
647
  /** Wins over the legacy interactive option and its environment fallback. */
@@ -839,6 +945,39 @@ interface ClaudeCodeProviderSettings {
839
945
  * reminder is then something the model can actually act on.
840
946
  */
841
947
  stripContextReminders?: boolean;
948
+ /**
949
+ * Which proxy tools Claude Code loads into every request instead of
950
+ * deferring them behind its `ToolSearch` tool. Default `["bash", "edit",
951
+ * "write"]`.
952
+ *
953
+ * Claude Code defers every MCP tool, the proxy's included, so the first
954
+ * proxied call in a `claude` process costs one extra API request to find
955
+ * the tool. A listed tool is served with `_meta["anthropic/alwaysLoad"]`,
956
+ * which skips that request: measured on Claude Code 2.1.314, a turn that
957
+ * ran a proxied `bash` took 2 API calls instead of 3, with 32% fewer prompt
958
+ * tokens and 41% lower cost, while a turn that called no tool carried 531
959
+ * more prompt tokens. `true` loads every tool the proxy serves (the MCP
960
+ * tools `proxyOpencodeMcpTools` adds included), `false` defers them all as
961
+ * before. Names are the proxy's lowercase tool names. An older Claude Code
962
+ * ignores the field.
963
+ */
964
+ alwaysLoadProxyTools?: boolean | string[];
965
+ /**
966
+ * Deliver the text of every message as written. Default true.
967
+ *
968
+ * Claude Code reads the last text block of a headless user message the way
969
+ * it reads typing: a leading `/` runs one of its commands and an `@path`
970
+ * attaches that file. Measured on Claude Code 2.1.314, a message `/model
971
+ * claude-sonnet-5-5` switched the running process to Sonnet without a
972
+ * model call, and a message starting with a path such as `/etc/hosts` lost
973
+ * the environment, deferred-tool, agent and skill context of its turn. On,
974
+ * each envelope carries `client_composed: true`, the field the Agent SDK's
975
+ * `verbatimPrompts` sets, and the text reaches Claude unchanged; hooks,
976
+ * CLAUDE.md and the skill listing still arrive. The one thing lost is the
977
+ * `ultrathink` keyword's reminder: use an effort variant instead. Headless
978
+ * transport only; an older Claude Code ignores the field.
979
+ */
980
+ verbatimPrompts?: boolean;
842
981
  /**
843
982
  * Extra Claude Code built-ins to switch off with `--disallowedTools`,
844
983
  * on top of the ones implied by `proxyTools`.
@@ -1079,6 +1218,29 @@ interface ClaudeCodeProviderSettings {
1079
1218
  * false` turns this off too.
1080
1219
  */
1081
1220
  resumeAcrossModelChanges?: boolean;
1221
+ /**
1222
+ * Apply a model or reasoning effort change to the conversation's running
1223
+ * `claude` process, with the CLI's own `set_model` and `apply_flag_settings`
1224
+ * control requests, instead of stopping it and starting another with
1225
+ * `--resume`. Default `true`.
1226
+ *
1227
+ * Measured on Claude Code 2.1.288 and 2.1.314 (h #g278): the conversation is
1228
+ * kept, the next request goes out under the new model or effort, and the
1229
+ * prompt cache reads and writes are the same as the respawn's. What it saves
1230
+ * is the respawn itself, 4.6 to 6.6 seconds to `system`/`init` per change on
1231
+ * a real install, against about 25 ms for an effort change and 0.5 to 0.7 s
1232
+ * for a model change. To make an effort change possible, a headless spawn
1233
+ * carries its effort as `--effort` and drops CLAUDE_CODE_EFFORT_LEVEL: the
1234
+ * variable beats a live change silently.
1235
+ *
1236
+ * Only between two models on the same account and in the same fast mode,
1237
+ * between two explicit efforts, on an idle child (a change that lands
1238
+ * mid-turn still finishes that turn on the old settings, h #g271), and on
1239
+ * the headless transport: the interactive one has no control channel and
1240
+ * keeps respawning. Any refusal or timeout falls back to the respawn. Needs
1241
+ * `resumeAcrossModelChanges`; `false` here restores the respawn exactly.
1242
+ */
1243
+ liveModelSwitch?: boolean;
1082
1244
  /**
1083
1245
  * Continue the same Claude conversation when it moves to another account,
1084
1246
  * instead of replaying the whole thread as text into a fresh session there.