@khalilgharbaoui/opencode-claude-code-plugin 0.25.0 → 0.26.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
@@ -80,6 +80,32 @@ In your `opencode.json`, point at the local build with a `file://` URL:
80
80
 
81
81
  CI installs and builds on **Node 24** (`.github/workflows/publish.yml`), which is the only version this package is built against. `package.json` declares no `engines` range, so older Node versions are untested rather than deliberately unsupported. opencode itself may run under Bun; the [interactive transport](#interactive-transport-experimental) requires that.
82
82
 
83
+ ### opencode 2
84
+
85
+ The same package runs on opencode 1.x and 2.x, and nothing changes for 1.x. The same config works too: opencode 2's native key is `plugins`, but it still reads the 1.x `plugin` key shown above, so an existing install needs no edit. A config used only by opencode 2 can spell it natively:
86
+
87
+ ```json
88
+ {
89
+ "plugins": ["@khalilgharbaoui/opencode-claude-code-plugin"]
90
+ }
91
+ ```
92
+
93
+ Your existing `provider.claude-code.options` block keeps working, because opencode 2 still reads 1.x config files. Its native spelling is `provider.claude-code.settings`, and plugin-level settings such as `accounts` may also go in the plugin entry itself: `{"package": "@khalilgharbaoui/opencode-claude-code-plugin", "options": {"accounts": ["work"]}}`.
94
+
95
+ For a local checkout, point opencode 2 at the **`dist` directory**, not the repository root. It loads `<dir>/server` or `<dir>/index` from a directory and never reads `package.json#main`:
96
+
97
+ ```json
98
+ {
99
+ "plugins": ["/absolute/path/to/opencode-claude-code-plugin/dist"]
100
+ }
101
+ ```
102
+
103
+ Verified live on opencode **2.0.11** with Claude Code 2.1.280: chat turns, proxied tools running through opencode 2's own `shell`, `edit`, `write`, `webfetch` and `subagent` tools and their permission rules, Claude's own tools rendered in the transcript, subagent dispatch with the agent list, reasoning variants, compaction, account providers, `/claude-code-doctor`, `/btw`, and the bundled configuration skill. Differences from 1.x:
104
+
105
+ - **`/btw` is answered after the running turn**, not inside it. opencode 2's plugin API has no session-status route, which is what 1.x uses to write the answer into a turn that is still running. The aside is queued, so it can never swallow the turn's own continuation.
106
+ - **No todo panel.** opencode 2 has no `todowrite` tool, so Claude's task list is not mirrored into one.
107
+ - **Account failover and the plan-mode form** use opencode 2's `question` tool, which takes the same input as 1.x. Both are covered by offline tests only on 2.x, since neither can be triggered on demand.
108
+
83
109
  ---
84
110
 
85
111
  ## Models
package/dist/index.d.ts CHANGED
@@ -1,5 +1,188 @@
1
1
  import { LanguageModelV3, LanguageModelV3CallOptions } from '@ai-sdk/provider';
2
2
 
3
+ /**
4
+ * Hand-written structural mirror of the slice of opencode 2.x's plugin API
5
+ * this package touches. Same approach as `opencode-types.ts` for V1: no
6
+ * runtime or type dependency on `@opencode/plugin`, whose promise API pulls in
7
+ * an Effect release candidate we have no other use for.
8
+ *
9
+ * Read off the shipped `.d.ts` of `@opencode/plugin@2.0.11`, `@opencode/schema`
10
+ * and `@opencode/client` at the same version, not off the website, whose
11
+ * provider docs describe a metadata-only surface and omit the `aisdk` domain
12
+ * entirely. Every field here is one we read or write; everything else is left
13
+ * out on purpose so drift in an unused corner cannot break the build.
14
+ */
15
+
16
+ interface V2Registration {
17
+ readonly dispose: () => Promise<void>;
18
+ }
19
+ /** `@opencode/schema/provider` `Provider.Info`, the fields we set. */
20
+ interface V2ProviderInfo {
21
+ id: string;
22
+ name: string;
23
+ activation: "auto" | "enabled" | "disabled";
24
+ package: string;
25
+ settings?: Record<string, unknown>;
26
+ headers?: Record<string, string>;
27
+ body?: Record<string, unknown>;
28
+ }
29
+ interface V2ModelVariant {
30
+ id: string;
31
+ settings?: Record<string, unknown>;
32
+ headers?: Record<string, string>;
33
+ body?: Record<string, unknown>;
34
+ }
35
+ interface V2ModelCost {
36
+ input: number;
37
+ output: number;
38
+ cache: {
39
+ read: number;
40
+ write: number;
41
+ };
42
+ }
43
+ /** `@opencode/schema/model` `Model.Info`, every required field plus the optional ones we fill. */
44
+ interface V2ModelInfo {
45
+ id: string;
46
+ modelID: string;
47
+ providerID: string;
48
+ name: string;
49
+ family?: string;
50
+ capabilities: {
51
+ tools: boolean;
52
+ input: string[];
53
+ output: string[];
54
+ };
55
+ variants: V2ModelVariant[];
56
+ time: {
57
+ released: number;
58
+ };
59
+ cost: V2ModelCost[];
60
+ status: "alpha" | "beta" | "deprecated" | "active";
61
+ enabled: boolean;
62
+ limit: {
63
+ context: number;
64
+ input?: number;
65
+ output: number;
66
+ };
67
+ settings?: Record<string, unknown>;
68
+ headers?: Record<string, string>;
69
+ package?: string;
70
+ }
71
+ interface V2ProviderRecord {
72
+ readonly provider: V2ProviderInfo;
73
+ readonly models: ReadonlyMap<string, V2ModelInfo>;
74
+ }
75
+ interface V2ProviderEditor {
76
+ list(): readonly V2ProviderRecord[];
77
+ get(providerID: string): V2ProviderRecord | undefined;
78
+ add(input: {
79
+ info: V2ProviderInfo;
80
+ models: readonly V2ModelInfo[];
81
+ }): void;
82
+ update(providerID: string, update: (provider: V2ProviderInfo) => void): void;
83
+ remove(providerID: string): void;
84
+ readonly models: {
85
+ set(providerID: string, models: readonly V2ModelInfo[]): void;
86
+ };
87
+ }
88
+ interface V2AISDKSdkEvent {
89
+ readonly model: V2ModelInfo;
90
+ readonly package: string;
91
+ readonly options: Record<string, unknown>;
92
+ sdk?: unknown;
93
+ }
94
+ interface V2AISDKLanguageEvent {
95
+ readonly model: V2ModelInfo;
96
+ readonly sdk: unknown;
97
+ readonly options: Record<string, unknown>;
98
+ language?: LanguageModelV3;
99
+ }
100
+ type V2RequestKind = "primary" | "compaction" | "title" | "generate";
101
+ interface V2ModelRequestEvent {
102
+ readonly sessionID: string;
103
+ readonly agent: string;
104
+ readonly model: {
105
+ id: string;
106
+ providerID: string;
107
+ variant?: string;
108
+ };
109
+ readonly kind: V2RequestKind;
110
+ baseURL?: string;
111
+ headers: Record<string, string>;
112
+ }
113
+ interface V2ModelHookOptions {
114
+ readonly providerID?: string;
115
+ }
116
+ /** `@opencode/schema/prompt-input` `Prompt`: we only rewrite `text`. */
117
+ type V2Prompt = {
118
+ text?: string;
119
+ } & Record<string, unknown>;
120
+ type V2Delivery = "steer" | "queue";
121
+ interface V2CommandInvocation {
122
+ readonly sessionID: string;
123
+ readonly prompt: V2Prompt;
124
+ readonly delivery: V2Delivery;
125
+ }
126
+ interface V2CommandDefinition {
127
+ readonly name: string;
128
+ readonly description?: string;
129
+ readonly execute: (input: V2CommandInvocation) => Promise<void>;
130
+ }
131
+ /** `session.deleted` is the one event this plugin acts on. */
132
+ interface V2Event {
133
+ readonly type?: string;
134
+ readonly data?: {
135
+ readonly sessionID?: string;
136
+ } & Record<string, unknown>;
137
+ }
138
+ interface V2Context {
139
+ readonly app: {
140
+ readonly version: string;
141
+ };
142
+ readonly location: {
143
+ readonly directory: string;
144
+ };
145
+ readonly options: Record<string, unknown>;
146
+ readonly provider: {
147
+ transform(callback: (editor: V2ProviderEditor) => void): Promise<V2Registration>;
148
+ };
149
+ readonly aisdk: {
150
+ hook(name: "sdk", callback: (event: V2AISDKSdkEvent) => Promise<void> | void, options?: V2ModelHookOptions): Promise<V2Registration>;
151
+ hook(name: "language", callback: (event: V2AISDKLanguageEvent) => Promise<void> | void, options?: V2ModelHookOptions): Promise<V2Registration>;
152
+ };
153
+ readonly session: {
154
+ hook(name: "model.request", callback: (event: V2ModelRequestEvent) => Promise<void> | void, options?: V2ModelHookOptions): Promise<V2Registration>;
155
+ prompt?(input: V2Prompt & {
156
+ sessionID: string;
157
+ delivery?: V2Delivery;
158
+ }): Promise<unknown>;
159
+ };
160
+ readonly command?: {
161
+ transform(callback: (editor: {
162
+ add(definition: V2CommandDefinition): void;
163
+ }) => void): Promise<V2Registration>;
164
+ };
165
+ readonly event?: {
166
+ subscribe(options?: {
167
+ signal?: AbortSignal;
168
+ }): AsyncIterable<V2Event>;
169
+ };
170
+ readonly skill?: {
171
+ transform(callback: (editor: {
172
+ get(id: string): unknown;
173
+ add(skill: {
174
+ id: string;
175
+ name: string;
176
+ description?: string;
177
+ path: string;
178
+ content: string;
179
+ }): void;
180
+ }) => void): Promise<V2Registration>;
181
+ };
182
+ }
183
+ /** What `setup` may return: a cleanup run when opencode unloads the plugin. */
184
+ type V2Cleanup = () => void | Promise<void>;
185
+
3
186
  type ModelID = string;
4
187
  type ProviderID = string;
5
188
  type OpenCodeModel = {
@@ -158,6 +341,14 @@ type LogMode = "silent" | "debug";
158
341
  interface ClaudeCodeConfig {
159
342
  provider: string;
160
343
  cliPath: string;
344
+ /**
345
+ * Which opencode major this model was created by, which decides the tool
346
+ * vocabulary its stream uses (src/host-tools.ts). Set only by the V2
347
+ * entrypoint's `sdk` hook; absent means opencode 1.x. Per model rather than
348
+ * per process on purpose: opencode 1.18 also calls a dual export's `setup`,
349
+ * so nothing process-wide may decide it.
350
+ */
351
+ hostApi?: "v1" | "v2";
161
352
  /** Drive interactive claude (subscription) instead of headless --print. */
162
353
  interactive?: boolean;
163
354
  /** Deprecated/no-op with interactive: Claude Code's TUI requires manual confirmation for bypassPermissions. */
@@ -258,6 +449,8 @@ type WebSearchRouting = "claude" | "disabled" | (string & {});
258
449
  type AccountFailoverMode = "ask" | "off";
259
450
  interface ClaudeCodeProviderSettings {
260
451
  cliPath?: string;
452
+ /** Internal: set by the opencode 2 entrypoint. See `ClaudeCodeConfig.hostApi`. */
453
+ hostApi?: "v1" | "v2";
261
454
  /** Drive interactive claude (subscription) instead of headless --print. */
262
455
  interactive?: boolean;
263
456
  /** Deprecated/no-op with interactive: Claude Code's TUI requires manual confirmation for bypassPermissions. */
@@ -724,6 +917,16 @@ declare class ClaudeCodeLanguageModel implements LanguageModelV3 {
724
917
  get provider(): string;
725
918
  private toUsage;
726
919
  private toFinishReason;
920
+ /**
921
+ * Whether this call only names the session, which gets the synthetic stub
922
+ * rather than a `claude` spawn. opencode 1.x sends a title request with no
923
+ * tools, and that is the whole test there. opencode 2 sends its tool set
924
+ * along with it (measured on 2.0.11: `scope: "tools"`, agent `title`), so
925
+ * every new V2 session paid for a second `claude` process just to title
926
+ * itself; for a V2 model the request kind, carried as the `title` agent,
927
+ * decides instead.
928
+ */
929
+ private isTitleRequest;
727
930
  private requestScope;
728
931
  /**
729
932
  * Build the combined `--mcp-config` list and return both the list and the
@@ -842,7 +1045,14 @@ declare class ClaudeCodeLanguageModel implements LanguageModelV3 {
842
1045
  private synthesizeTitle;
843
1046
  private doGenerateViaStream;
844
1047
  doGenerate(options: LanguageModelV3CallOptions): Promise<Awaited<ReturnType<LanguageModelV3["doGenerate"]>>>;
1048
+ private doGenerateForHost;
1049
+ /**
1050
+ * Tool parts leave in opencode 1.x's vocabulary; on opencode 2.x they are
1051
+ * renamed at this one edge (src/host-tools.ts). On V1 the stream is
1052
+ * returned untouched.
1053
+ */
845
1054
  doStream(options: LanguageModelV3CallOptions): Promise<Awaited<ReturnType<LanguageModelV3["doStream"]>>>;
1055
+ private doStreamForHost;
846
1056
  }
847
1057
 
848
1058
  interface BridgedMcp {
@@ -959,6 +1169,20 @@ declare function configModelsForProvider(providerModels: OpenCodeProvider["model
959
1169
  * diagnostics never report another provider's options.
960
1170
  */
961
1171
  declare function claudeCodeProviders(providers: Record<string, DiagnosticsProviderEntry> | undefined): Record<string, DiagnosticsProviderEntry>;
1172
+ /**
1173
+ * Record what every known agent asked for, so `resolveAgentModel` and
1174
+ * `resolveAgentEffort` can answer at spawn time without the language model
1175
+ * needing to see opencode's config.
1176
+ *
1177
+ * Runs BEFORE `expandAccountProviders`, which deletes the seed provider entry
1178
+ * once it has expanded it: `defaultSubagentModel` has to be read while it is
1179
+ * still there.
1180
+ *
1181
+ * Purely observational. It defines no agents and changes no agent's config;
1182
+ * an agent this plugin never heard of is simply absent from the registry,
1183
+ * which is what keeps opencode's built-ins out of the override path.
1184
+ */
1185
+ declare function buildAgentRegistry(config: OpenCodeConfig): Promise<void>;
962
1186
  /**
963
1187
  * The opencode session id a `session.deleted` bus event names, or undefined
964
1188
  * for any other event. opencode publishes `{ type, properties: { info } }`
@@ -968,6 +1192,7 @@ declare function extractDeletedSessionId(event: OpenCodeEvent | undefined): stri
968
1192
  declare const _default: {
969
1193
  id: string;
970
1194
  server: OpenCodePlugin;
1195
+ setup: (ctx: V2Context) => Promise<V2Cleanup>;
971
1196
  };
972
1197
 
973
- export { type AgentRecord, type ClaudeCodeConfig, ClaudeCodeLanguageModel, type ClaudeCodeProvider, type ClaudeCodeProviderSettings, type ClaudeStreamMessage, DEFAULT_PROXY_TOOL_NAMES, type OpenCodeHooks, type OpenCodeModel, type OpenCodePlugin, _resetPlanModeWarningForTests, bridgeOpencodeMcp, claudeCodeProviders, configModelsForProvider, createClaudeCode, _default as default, defaultModels, extractDeletedSessionId, getAgentRegistry, getDefaultSubagentModel, registerDoctorCommand, registerSideQuestionCommand, resolveAgentModel, warnIfPlanModeCannotExit };
1198
+ export { type AgentRecord, type ClaudeCodeConfig, ClaudeCodeLanguageModel, type ClaudeCodeProvider, type ClaudeCodeProviderSettings, type ClaudeStreamMessage, DEFAULT_PROXY_TOOL_NAMES, type OpenCodeHooks, type OpenCodeModel, type OpenCodePlugin, _resetPlanModeWarningForTests, bridgeOpencodeMcp, buildAgentRegistry, claudeCodeProviders, configModelsForProvider, createClaudeCode, _default as default, defaultModels, extractDeletedSessionId, getAgentRegistry, getDefaultSubagentModel, registerDoctorCommand, registerSideQuestionCommand, resolveAgentModel, warnIfPlanModeCannotExit };