@khalilgharbaoui/opencode-claude-code-plugin 0.1.6 → 0.2.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
@@ -170,6 +170,7 @@ The account model IDs are internally suffixed, for example `claude-sonnet-4-6@wo
170
170
  | `bridgeOpencodeMcp` | boolean | `true` | Auto-translate your opencode `mcp` block into Claude's `--mcp-config`. See [MCP bridge](#mcp-bridge). |
171
171
  | `mcpConfig` | string \| string[] | – | Extra `--mcp-config` paths/JSON passed alongside the bridged config. |
172
172
  | `strictMcpConfig` | boolean | `false` | Pass `--strict-mcp-config` so Claude loads **only** the configured servers and ignores `~/.claude/settings.json`. |
173
+ | `webSearch` | `"claude"` \| `"disabled"` \| `<tool>` | `"claude"` | Routing for Claude's built-in `WebSearch`. See [WebSearch routing](#websearch-routing). |
173
174
 
174
175
  ### Overriding model metadata
175
176
 
@@ -227,6 +228,27 @@ To turn off proxying entirely:
227
228
  ### What you give up
228
229
 
229
230
  - A small per-call latency hop through `127.0.0.1:<random>/mcp`.
231
+
232
+ ---
233
+
234
+ ## WebSearch routing
235
+
236
+ Claude Code ships a built-in `WebSearch` tool. The `webSearch` option controls who actually executes those calls:
237
+
238
+ | `webSearch` value | Behavior | When to use |
239
+ |---|---|---|
240
+ | `"claude"` (default) | Claude CLI runs WebSearch internally via Anthropic. Zero setup, no extra cost, no API key. | Most users. |
241
+ | `"<opencode-tool-name>"` (e.g. `"websearch_web_search_exa"`) | Forward to that opencode-side tool with `executed:false`. Requires the corresponding MCP server to be configured in opencode (e.g. [exa-mcp-server](https://github.com/exa-labs/exa-mcp-server)). | You want a specific search backend (Exa, Tavily, Brave) and have the MCP wired up in opencode. |
242
+ | `"disabled"` | `WebSearch` is added to `--disallowedTools` so the model can't call it. | Compliance/security scenarios where outbound search isn't allowed. |
243
+
244
+ ```json
245
+ "options": { "webSearch": "websearch_web_search_exa" }
246
+ ```
247
+
248
+ **Trade-offs**
249
+
250
+ - Claude-side execution: free with your Claude usage, no API key, but no opencode visibility into queries/results, no caching/rate-limit hooks.
251
+ - opencode-side execution: choose any backend, queries flow through opencode's audit/policy/cache, but costs money (search APIs are paid) and adds a network hop.
230
252
  - Some Claude-specific tool features stay on the built-in side (notably `MultiEdit` — see the note above).
231
253
 
232
254
  ---
package/dist/index.d.ts CHANGED
@@ -70,12 +70,35 @@ type OpenCodeConfig = {
70
70
  models?: Record<string, unknown>;
71
71
  }>;
72
72
  };
73
+ /**
74
+ * Bus events surface to plugins. Shape mirrors what opencode core publishes
75
+ * via `GlobalBus.emit("event", { directory, payload: { type, properties } })`
76
+ * but kept loose since opencode adds events over time and this plugin only
77
+ * reacts to a small subset (currently just `global.disposed`).
78
+ */
79
+ type OpenCodeEvent = {
80
+ type?: string;
81
+ payload?: {
82
+ type?: string;
83
+ properties?: Record<string, unknown>;
84
+ };
85
+ [key: string]: unknown;
86
+ };
73
87
  type OpenCodeHooks = {
74
88
  config?: (input: OpenCodeConfig) => Promise<void>;
75
89
  provider?: {
76
90
  id: string;
77
91
  models?: (provider: OpenCodeProvider) => Promise<Record<string, OpenCodeModel>>;
78
92
  };
93
+ /**
94
+ * Called for every bus event opencode publishes. We use this to react to
95
+ * `global.disposed` (fired when opencode invalidates its config — e.g.
96
+ * after a UI MCP toggle or `updateGlobal`) and evict cached claude
97
+ * subprocesses so the next turn picks up the fresh config.
98
+ */
99
+ event?: (input: {
100
+ event: OpenCodeEvent;
101
+ }) => Promise<void>;
79
102
  };
80
103
  type OpenCodePlugin = (input: unknown, options?: Record<string, unknown>) => Promise<OpenCodeHooks>;
81
104
 
@@ -95,7 +118,10 @@ interface ClaudeCodeConfig {
95
118
  controlRequestToolBehaviors?: Record<string, ControlRequestBehavior>;
96
119
  controlRequestDenyMessage?: string;
97
120
  proxyTools?: string[];
121
+ webSearch?: WebSearchRouting;
122
+ hotReloadMcp?: boolean;
98
123
  }
124
+ type WebSearchRouting = "claude" | "disabled" | (string & {});
99
125
  interface ClaudeCodeProviderSettings {
100
126
  cliPath?: string;
101
127
  cwd?: string;
@@ -147,6 +173,30 @@ interface ClaudeCodeProviderSettings {
147
173
  * Supported: `bash`, `write`, `edit`, `webfetch`. Leave empty or unset to disable proxying.
148
174
  */
149
175
  proxyTools?: string[];
176
+ /**
177
+ * Routing for Claude's built-in `WebSearch` tool.
178
+ *
179
+ * - `"claude"` (default): Claude CLI runs WebSearch internally via
180
+ * Anthropic's web search. No MCP setup required, no extra cost.
181
+ * - `"<opencode-tool-name>"` (e.g. `"websearch_web_search_exa"`): forward
182
+ * the call to that opencode-side tool with `executed:false`. Requires
183
+ * the corresponding MCP server to be configured in opencode.
184
+ * - `"disabled"`: prevent the model from calling WebSearch entirely
185
+ * (passes `WebSearch` via `--disallowedTools`).
186
+ */
187
+ webSearch?: WebSearchRouting;
188
+ /**
189
+ * Detect mid-session opencode MCP config changes and respawn the
190
+ * underlying claude process so newly enabled / disabled MCPs become
191
+ * visible to the model without restarting opencode or starting a new
192
+ * chat. Eviction happens at the start of the next user turn (never mid
193
+ * tool-call) and `--session-id` is preserved so the conversation
194
+ * continues seamlessly. Defaults to `true`.
195
+ *
196
+ * Set to `false` to keep the previous behavior (cached subprocess
197
+ * survives MCP changes until the chat is reset).
198
+ */
199
+ hotReloadMcp?: boolean;
150
200
  }
151
201
  type PermissionMode = "acceptEdits" | "auto" | "bypassPermissions" | "default" | "dontAsk" | "plan";
152
202
  type ControlRequestBehavior = "allow" | "deny";
@@ -248,9 +298,14 @@ declare class ClaudeCodeLanguageModel implements LanguageModelV3 {
248
298
  private toFinishReason;
249
299
  private requestScope;
250
300
  /**
251
- * Build the combined `--mcp-config` list: user-configured paths plus the
252
- * auto-bridged opencode MCP config (when enabled and present) and the
253
- * proxy MCP scratch file (when proxyTools are enabled).
301
+ * Build the combined `--mcp-config` list and return both the list and the
302
+ * hash of the bridged opencode MCP block (or null when bridging is off /
303
+ * yields nothing). The hash is used to detect mid-session config changes
304
+ * and respawn the underlying claude process.
305
+ *
306
+ * `runtimeStatus` is a snapshot of opencode's `client.mcp.status()`. When
307
+ * provided it overlays opencode's UI-toggled state on top of disk config
308
+ * so `/mcps` toggles propagate without a config file write.
254
309
  */
255
310
  private effectiveMcpConfig;
256
311
  /** Resolve ProxyToolDef[] for the configured proxyTools names. */
@@ -284,15 +339,35 @@ declare class ClaudeCodeLanguageModel implements LanguageModelV3 {
284
339
  doStream(options: LanguageModelV3CallOptions): Promise<Awaited<ReturnType<LanguageModelV3["doStream"]>>>;
285
340
  }
286
341
 
342
+ interface BridgedMcp {
343
+ /** Path to the temp file containing the translated `--mcp-config`. */
344
+ path: string;
345
+ /** Stable hash of the merged opencode mcp block (pre-translation). */
346
+ hash: string;
347
+ }
287
348
  /**
288
- * Read opencode config file(s), translate their `mcp` block to Claude CLI
289
- * format, write a scratch file, and return its path. Later files override
290
- * earlier files per server-name (matching opencode's own merge semantics).
349
+ * Per-server runtime status from opencode's `client.mcp.status()`. Used as
350
+ * an overlay on top of the on-disk merged config so opencode's UI-toggled
351
+ * state which lives only in-memory; `connect()`/`disconnect()` never
352
+ * touch disk — propagates to the bridged claude subprocess.
353
+ *
354
+ * Treatment per server:
355
+ * - "connected" → force `enabled: true` (mirror opencode)
356
+ * - any other status → force `enabled: false` (don't ship a server
357
+ * opencode can't run; user fixes it in opencode first)
358
+ * - missing entry → leave disk value
291
359
  *
292
- * Returns null when no opencode config with MCP servers is found — callers
293
- * should treat that as "nothing to bridge" and carry on.
360
+ * Omit the overlay and the bridge falls back to disk-only.
361
+ */
362
+ type RuntimeMcpStatus = Record<string, string>;
363
+ /**
364
+ * Read opencode config layers, deep-merge their `mcp` blocks per opencode's
365
+ * own semantics, optionally apply an opencode runtime-status overlay, then
366
+ * translate each server to Claude CLI format, write a scratch file, and
367
+ * return its path + a stable hash. Returns null when no enabled MCP servers
368
+ * remain after the merge + overlay.
294
369
  */
295
- declare function bridgeOpencodeMcp(cwd: string): string | null;
370
+ declare function bridgeOpencodeMcp(cwd: string, runtimeStatus?: RuntimeMcpStatus): BridgedMcp | null;
296
371
 
297
372
  declare const defaultModels: Record<string, OpenCodeModel>;
298
373