@khalilgharbaoui/opencode-claude-code-plugin 0.33.0 → 0.33.2
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 +34 -11
- package/dist/index.d.ts +2 -1
- package/dist/index.js +80 -26
- package/dist/index.js.map +1 -1
- package/package.json +1 -1
- package/skills/claude-code-plugin/SKILL.md +170 -83
package/README.md
CHANGED
|
@@ -21,7 +21,7 @@ Three ways to reach Claude from opencode. They differ in who authenticates, who
|
|
|
21
21
|
| **Authentication** | An Anthropic Platform API key, held in opencode's own auth store. | Whatever the official `claude` CLI already holds: a subscription login, an API key, Bedrock, or Vertex. The plugin never reads, stores, or replays a token of its own, and there is no subscription token here to lift. | The Claude OAuth session, used outside the official client. Meridian runs a local proxy that maps Anthropic-style HTTP onto the Claude Agent SDK and your Claude session; `opencode-claude-auth` reads the OAuth tokens out of the macOS Keychain or `~/.claude/.credentials.json` and refreshes them against Anthropic's OAuth endpoint itself. |
|
|
22
22
|
| **What is billed, and to whom** | Pay as you go on the Platform account that owns the key. | Whatever the CLI's own authentication bills. Headless `--print` is the Agent SDK path; an API key found anywhere the CLI looks switches the same turn onto Console pay-as-you-go instead. `apiKeySource` on the CLI's `system` init event is the field that says which, and the plugin warns once per process when a key is in effect. On a subscription, headless and interactive turns both draw from the plan's ordinary usage limits. See [which login bills what](#which-login-bills-what). | The subscription the reused session belongs to. Meridian's own FAQ: "Usage limits follow your Max subscription, not Anthropic API billing tiers." |
|
|
23
23
|
| **Terms-of-service status** | The ordinary API route. Nothing unusual about it. | Sanctioned: the official client does the authenticating, and driving `claude` is what `claude` is for. | Disallowed. Anthropic disallowed reusing subscription authentication for third-party Claude use in February 2026, and each project says so in its own words: Meridian's wrapper "makes no claims regarding compliance with Anthropic's Terms of Service"; `opencode-claude-auth` calls itself "a community workaround" and notes that the terms say subscription tokens "should only be used with official Anthropic clients"; `opencode-claude-plan` quotes Consumer Terms 3.7 and asks you to accept that your account "could be suspended or terminated". |
|
|
24
|
-
| **Model list and fast mode** | Whatever opencode's own provider registers. |
|
|
24
|
+
| **Model list and fast mode** | Whatever opencode's own provider registers. | 18 ids auto-registered, Haiku 4.5 through Opus 5.5 plus Fable and Mythos, each carrying a `(N×)` list-price suffix, and any other id `claude --model` accepts passes straight through. Three `-fast` Opus ids are this plugin's own markers and opt a headless session into fast mode through `--settings` (CLI 2.1.220+). See [Models](#models). | `opencode-claude-auth`'s README lists 14 model ids. Meridian's lists none, because model metadata comes from opencode's own `anthropic` provider. Neither README mentions fast mode. |
|
|
25
25
|
| **Which tools run where, under whose permissions** | All of them are opencode's, behind opencode's permission prompts and audit log. | Your choice, per tool. `Bash`, `Edit`, `Write`, `WebFetch` and `Task` are proxied by default: Claude calls an in-process MCP tool and **opencode** executes it, under its own permissions and audit log. Anything neither proxied nor named in `extraDisallowedTools` runs inside Claude Code under `--dangerously-skip-permissions`. See [Selective tool proxy](#selective-tool-proxy) and [Read-only mode](#read-only-mode). | All of them are opencode's, because the model call is an ordinary provider call. This is the one row where the third column matches the native provider and this plugin has to work for the same result. |
|
|
26
26
|
| **Reasoning and effort** | opencode's own reasoning controls. | Five picker variants per model, `low` through `max`, handed to the CLI as `CLAUDE_CODE_EFFORT_LEVEL` at spawn. Effort is fixed for the life of a `claude` process, so it is part of the session key, and an agent's own `reasoningEffort` beats the effort a call arrived with. Thinking is Anthropic's summarized digest, not raw chain-of-thought. See [Extended thinking](#extended-thinking). | Meridian's SDK-features file exposes a `thinking` key. Neither README documents per-model effort variants. |
|
|
27
27
|
| **Context window** | Whatever the model exposes. | The registered limits: 200k context / 64k output on the 4.5 generation, 1M / 128k on 4.6 and later, all at standard pricing with no above-200K tier. Claude Code may also compact or clear its own context mid-conversation, which the plugin can announce but not prevent. | Not stated in either README. |
|
|
@@ -102,7 +102,7 @@ The same package runs on opencode 1.x and 2.x, and nothing changes for 1.x. The
|
|
|
102
102
|
}
|
|
103
103
|
```
|
|
104
104
|
|
|
105
|
-
Your existing `provider.claude-code.options` block keeps working, because opencode 2 still reads 1.x config files.
|
|
105
|
+
Your existing `provider.claude-code.options` block keeps working, because opencode 2 still reads 1.x config files. opencode 2's own spelling is `providers.claude-code.settings` (note the plural `providers`). All four places the plugin reads its settings from, lowest precedence first: `provider.claude-code.options`, `provider.claude-code.settings`, `providers.claude-code.settings`, and the plugin entry's own `options`, which wins over all of them. Plugin-level settings such as `accounts` usually go in that last one: `{"package": "@khalilgharbaoui/opencode-claude-code-plugin", "options": {"accounts": ["work"]}}`.
|
|
106
106
|
|
|
107
107
|
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`:
|
|
108
108
|
|
|
@@ -470,7 +470,7 @@ reaches the CLI, so there is nothing there to fall back from.
|
|
|
470
470
|
| `strictMcpConfig` | boolean | `false` | Pass `--strict-mcp-config` so Claude loads **only** the configured servers and ignores `~/.claude/settings.json`. |
|
|
471
471
|
| `hotReloadMcp` | boolean | `true` | With MCP bridging on, compare the merged MCP config and runtime status at the start of each turn and respawn the `claude` process when they drifted, so a server you just enabled or disabled becomes visible without restarting opencode or opening a new chat. Eviction waits for pending proxy calls, never happening mid tool-call, and the session id is preserved for `--resume`. Set `false` to keep a cached subprocess until the chat is reset. It does not reload other provider options and does not watch the contents of files named in `mcpConfig`. |
|
|
472
472
|
| `proxyOpencodeMcpTools` | boolean | `false` | Route opencode's MCP-backed tools through the in-process `opencode_proxy` server instead of bridging them straight into Claude's `--mcp-config`, so each call executes once, inside opencode, with its permission prompt and its tool row. **The default changed from `true` to `false` in this release, and no behaviour changed with it:** at `true` it used to route nothing at all, because discovery read opencode's tool registry, which contains built-ins and plugin-declared tools and has never contained an MCP tool. Discovery now reads the model tool set opencode passes the provider, which is where MCP tools actually are, so the option works, and turning it on is the operator's decision rather than a silent migration of traffic that the direct bridge is handling today. Two caveats before enabling it: pair it with `strictMcpConfig: true`, because a server also registered in Claude Code's own config is reached directly and bypasses the proxy entirely; and a routed call runs in opencode with the calling agent's permissions, the same trade [`proxyOpencodeTools`](#options-reference) makes. Servers whose tools are not found stay on the direct bridge, and a warning says so, so do not treat this as an exactly-once guarantee for write-capable tools. |
|
|
473
|
-
| `proxyOpencodeTools` | string[] | `[]` | Forward named opencode tools through the proxy
|
|
473
|
+
| `proxyOpencodeTools` | string[] | `[]` | Forward explicitly named opencode tools through the proxy (for example, a plugin's `compress` or V2 Code Mode `execute`). V1 uses registry ids; V2 uses the current model tool snapshot, including its real JSON Schema and agent visibility, not the registry's empty schemas. A forwarded tool runs inside opencode with the calling agent's permissions. A name already held by a proxy def is dropped with a warning. The read-only preset refuses `execute`. See [Forwarding opencode's own tools](#forwarding-opencode-s-own-tools) and [V2 Code Mode](#v2-code-mode). |
|
|
474
474
|
| `stripContextReminders` | boolean | `false` | Remove opencode-dcp's `<dcp-system-reminder>` blocks from message text when no `compress` tool is proxied, so an order the model cannot follow stops being re-sent with every message that carries it. Inert as soon as `compress` is reachable. See [Trimming unsatisfiable context reminders](#trimming-unsatisfiable-context-reminders). |
|
|
475
475
|
| `webSearch` | `"claude"` \| `"disabled"` \| `<tool>` | `"claude"` | Routing for Claude's built-in `WebSearch`. See [WebSearch routing](#websearch-routing). |
|
|
476
476
|
| `multiStepContinuation` | boolean | `true` | Append a system-prompt hint nudging Claude to chain tool calls within one turn instead of pausing between subtasks. Each opencode turn boundary requires the user to manually press "continue", so for multi-step tasks this reduces friction. Set `false` to disable. |
|
|
@@ -1063,16 +1063,39 @@ Claude Code ships a built-in `WebSearch` tool. The `webSearch` option controls w
|
|
|
1063
1063
|
|
|
1064
1064
|
## MCP bridge
|
|
1065
1065
|
|
|
1066
|
-
If `bridgeOpencodeMcp` is true (the default), the plugin reads your opencode config's
|
|
1066
|
+
If `bridgeOpencodeMcp` is true (the default), the plugin reads your opencode config's MCP servers, translates them into Claude's MCP schema, writes a private temp file, and passes it to `claude --mcp-config`. It accepts V1 `mcp.<server>` and V2 `mcp.servers.<server>`; V2's `servers` container and timeout defaults are not servers. `disabled: true` is supported alongside legacy `enabled: false`. Live runtime status takes precedence when available.
|
|
1067
1067
|
|
|
1068
|
-
### Discovery
|
|
1068
|
+
### Discovery and precedence
|
|
1069
1069
|
|
|
1070
|
-
|
|
1071
|
-
2. `OPENCODE_CONFIG_DIR` env var
|
|
1072
|
-
3. Walk up from the current `cwd` looking for `opencode.jsonc`, `opencode.json`, `config.json`, or a `.opencode/` directory
|
|
1073
|
-
4. Global `$XDG_CONFIG_HOME/opencode` or `~/.config/opencode`
|
|
1070
|
+
The disk bridge reads global config, then `OPENCODE_CONFIG`, then project direct files and `.opencode` files. V1 keeps its existing repo-boundary discovery, `.opencode` ordering and per-server deep merges.
|
|
1074
1071
|
|
|
1075
|
-
|
|
1072
|
+
On V2, project discovery walks to the filesystem root (including ancestors above the repo). Direct files are applied parent-first, then `.opencode` files parent-first: the closest file wins within each group, and all `.opencode` files override direct files. Each higher-precedence server entry **replaces the entire server object**, so repeat its type, URL/command and other required fields in an override. Both `.json` and `.jsonc` are read, with `.jsonc` winning within a directory. Runtime toggles continue to participate in the hot-reload hash.
|
|
1073
|
+
|
|
1074
|
+
### V2 Code Mode
|
|
1075
|
+
|
|
1076
|
+
V2 normally exposes MCP tools through Code Mode's `execute` and its catalog, rather than as individual server-prefixed model tools. `proxyOpencodeMcpTools` matches individual tools only; on a Code Mode-only snapshot it warns and falls back to the direct Claude MCP bridge. This fallback does **not** execute tools under opencode's permission policy.
|
|
1077
|
+
|
|
1078
|
+
To explicitly opt into Code Mode through opencode instead, use these provider settings (headless transport):
|
|
1079
|
+
|
|
1080
|
+
```json
|
|
1081
|
+
{
|
|
1082
|
+
"providers": {
|
|
1083
|
+
"claude-code": {
|
|
1084
|
+
"settings": {
|
|
1085
|
+
"proxyOpencodeTools": ["execute"],
|
|
1086
|
+
"bridgeOpencodeMcp": false,
|
|
1087
|
+
"strictMcpConfig": true
|
|
1088
|
+
}
|
|
1089
|
+
}
|
|
1090
|
+
}
|
|
1091
|
+
}
|
|
1092
|
+
```
|
|
1093
|
+
|
|
1094
|
+
Preserve other entries in `proxyOpencodeTools`. `execute` is a code runner that can call **all tools in the session's Code Mode catalog**, not just MCP; opting in must be deliberate. It runs in opencode with the calling agent's permissions, and the plugin refuses it under `permissionPreset: "read-only"`. The plugin preserves the model-visible schema and tells Claude to call `mcp__opencode_proxy__execute` (discoverable via `ToolSearch`), using the original `search(...)` and `tools[...]` catalog signatures inside its code argument.
|
|
1095
|
+
|
|
1096
|
+
`bridgeOpencodeMcp: false` prevents a second direct MCP connection, while `strictMcpConfig: true` excludes Claude's own MCP sources. Do not add the same servers through explicit `mcpConfig` if you want Code Mode-only routing. OpenCode still owns the MCP connections; disabling the disk bridge does not disable its catalog.
|
|
1097
|
+
|
|
1098
|
+
For individual MCP proxies instead, set `codemode: false` on the relevant V2 MCP servers, then use `proxyOpencodeMcpTools: true` with `strictMcpConfig: true`. Neither approach guarantees exactly-once side effects across retries. Fully restart all opencode server/GUI processes after changing provider settings or plugin code; a new chat alone is insufficient. Real Claude smoke tests consume usage and run configured hooks, so request approval first.
|
|
1076
1099
|
|
|
1077
1100
|
### Translation
|
|
1078
1101
|
|
|
@@ -1636,7 +1659,7 @@ This plugin absorbs work from its forks directly, cherry-picked with the origina
|
|
|
1636
1659
|
| [@willmcginnis](https://github.com/willmcginnis) | The proxy endpoint authentication (PR #28, GHSA-3mxm-w7gf-3c5x). | PR #28 |
|
|
1637
1660
|
| [@nic-lan](https://github.com/nic-lan) | The issue #29 diagnosis of subagent output lost across the CLI resume boundary, and the fix for unattended output replaying as one text block per delta (PR #35). | #29, PR #35 |
|
|
1638
1661
|
| [@acastro2](https://github.com/acastro2) (Alexandre Castro) | Found and fixed CLI tool results being emitted under a different name than their call, which made opencode 2 abort every turn that used a Claude-side MCP server (PR #46). | PR #46 |
|
|
1639
|
-
| [@bangnh1](https://github.com/bangnh1) | Independently found and diagnosed the turn-summed usage that tripped auto-compaction after a single prompt, measured on opencode 2 (PR #62; the fix landed as PR #63). | PR #62 |
|
|
1662
|
+
| [@bangnh1](https://github.com/bangnh1) | Independently found and diagnosed the turn-summed usage that tripped auto-compaction after a single prompt, measured on opencode 2 (PR #62; the fix landed as PR #63), and fixed opencode 2's MCP config layout (`mcp.servers`, `disabled`, `providers.<id>.settings`) with opt-in Code Mode `execute` proxying (PR #67). | PR #62, PR #67 |
|
|
1640
1663
|
| [@JWebCoder](https://github.com/JWebCoder) (joao moura) | Diagnosed that auto-continue never fires on current CLIs (PR #15). | PR #15 |
|
|
1641
1664
|
|
|
1642
1665
|
Commit hashes are on the contributors' forks where the work was cherry-picked; `git log --author` on this repo shows the preserved authorship.
|
package/dist/index.d.ts
CHANGED
|
@@ -1230,6 +1230,7 @@ declare class ClaudeCodeLanguageModel implements LanguageModelV3 {
|
|
|
1230
1230
|
private doStreamForHost;
|
|
1231
1231
|
}
|
|
1232
1232
|
|
|
1233
|
+
type McpHostApi = "v1" | "v2";
|
|
1233
1234
|
interface BridgedMcp {
|
|
1234
1235
|
/** Path to the temp file containing the translated `--mcp-config`. */
|
|
1235
1236
|
path: string;
|
|
@@ -1270,7 +1271,7 @@ type RuntimeMcpStatus = Record<string, string>;
|
|
|
1270
1271
|
* return its path + a stable hash. Returns null when no enabled MCP servers
|
|
1271
1272
|
* remain after the merge + overlay.
|
|
1272
1273
|
*/
|
|
1273
|
-
declare function bridgeOpencodeMcp(cwd: string, runtimeStatus?: RuntimeMcpStatus, excludeServers?: ReadonlySet<string
|
|
1274
|
+
declare function bridgeOpencodeMcp(cwd: string, runtimeStatus?: RuntimeMcpStatus, excludeServers?: ReadonlySet<string>, hostApi?: McpHostApi): BridgedMcp | null;
|
|
1274
1275
|
|
|
1275
1276
|
type AgentRecord = {
|
|
1276
1277
|
mode?: string;
|
package/dist/index.js
CHANGED
|
@@ -2434,7 +2434,9 @@ function resolveDisallowedTools(options) {
|
|
|
2434
2434
|
function resolveProxyOpencodeToolDefs(options) {
|
|
2435
2435
|
const requested = options.requested ?? [];
|
|
2436
2436
|
if (requested.length === 0) return [];
|
|
2437
|
-
const items = options.
|
|
2437
|
+
const items = options.modelTools !== void 0 ? options.modelTools.flatMap(
|
|
2438
|
+
(tool) => (tool.type === void 0 || tool.type === "function") && typeof tool.name === "string" ? [{ id: tool.name, description: tool.description, parameters: tool.inputSchema }] : []
|
|
2439
|
+
) : options.items;
|
|
2438
2440
|
if (!items) {
|
|
2439
2441
|
log.warn(
|
|
2440
2442
|
"proxyOpencodeTools is set but opencode's tool registry did not answer; forwarding nothing this spawn",
|
|
@@ -2460,12 +2462,20 @@ function resolveProxyOpencodeToolDefs(options) {
|
|
|
2460
2462
|
unknown.push(name);
|
|
2461
2463
|
continue;
|
|
2462
2464
|
}
|
|
2465
|
+
if (item.id === "execute" && options.allowCodeExecution === false) {
|
|
2466
|
+
log.warn("Code Mode execute not forwarded: read-only permission preset");
|
|
2467
|
+
continue;
|
|
2468
|
+
}
|
|
2463
2469
|
if (taken.has(item.id)) {
|
|
2464
2470
|
collided.push(item.id);
|
|
2465
2471
|
continue;
|
|
2466
2472
|
}
|
|
2467
2473
|
if (seen.has(item.id)) continue;
|
|
2468
2474
|
seen.add(item.id);
|
|
2475
|
+
if (item.id === "execute" && (!item.parameters || typeof item.parameters !== "object" || Array.isArray(item.parameters) || !Object.keys(item.parameters).length)) {
|
|
2476
|
+
log.warn("Code Mode execute not forwarded: no model-visible input schema");
|
|
2477
|
+
continue;
|
|
2478
|
+
}
|
|
2469
2479
|
out.push({
|
|
2470
2480
|
name: item.id,
|
|
2471
2481
|
description: typeof item.description === "string" ? item.description : "",
|
|
@@ -2502,6 +2512,7 @@ function resolveMcpProxyToolDefs(options) {
|
|
|
2502
2512
|
if (tool?.type !== void 0 && tool.type !== "function") continue;
|
|
2503
2513
|
const name = typeof tool?.name === "string" ? tool.name.trim() : "";
|
|
2504
2514
|
if (!name) continue;
|
|
2515
|
+
if (name === "execute") continue;
|
|
2505
2516
|
const matchedServer = serversByLengthDesc.find(
|
|
2506
2517
|
(server2) => name === server2 || name.startsWith(`${server2}_`)
|
|
2507
2518
|
);
|
|
@@ -4709,24 +4720,24 @@ function globalConfigDir() {
|
|
|
4709
4720
|
const xdg = process.env.XDG_CONFIG_HOME ?? path4.join(os2.homedir(), ".config");
|
|
4710
4721
|
return path4.join(xdg, "opencode");
|
|
4711
4722
|
}
|
|
4712
|
-
function loadGlobalConfig() {
|
|
4723
|
+
function loadGlobalConfig(hostApi = "v1") {
|
|
4713
4724
|
const dir = globalConfigDir();
|
|
4714
4725
|
let merged = {};
|
|
4715
4726
|
for (const name of FILE_NAMES.slice().reverse()) {
|
|
4716
4727
|
const file = path4.join(dir, name);
|
|
4717
4728
|
if (!fileExists(file)) continue;
|
|
4718
4729
|
const parsed = readAndParse(file);
|
|
4719
|
-
if (parsed) merged =
|
|
4730
|
+
if (parsed) merged = mergeConfigLayer(merged, parsed, hostApi);
|
|
4720
4731
|
}
|
|
4721
4732
|
return merged;
|
|
4722
4733
|
}
|
|
4723
|
-
function loadProjectFilesInDir(dir) {
|
|
4734
|
+
function loadProjectFilesInDir(dir, hostApi = "v1") {
|
|
4724
4735
|
let merged = {};
|
|
4725
4736
|
for (const name of PROJECT_FILE_NAMES) {
|
|
4726
4737
|
const file = path4.join(dir, name);
|
|
4727
4738
|
if (!fileExists(file)) continue;
|
|
4728
4739
|
const parsed = readAndParse(file);
|
|
4729
|
-
if (parsed) merged =
|
|
4740
|
+
if (parsed) merged = mergeConfigLayer(merged, parsed, hostApi);
|
|
4730
4741
|
}
|
|
4731
4742
|
return merged;
|
|
4732
4743
|
}
|
|
@@ -4813,8 +4824,29 @@ function translateServer(name, spec) {
|
|
|
4813
4824
|
}
|
|
4814
4825
|
function extractMcpBlock(config) {
|
|
4815
4826
|
const mcp = config.mcp;
|
|
4816
|
-
if (!
|
|
4817
|
-
return
|
|
4827
|
+
if (!isPlainObject(mcp)) return {};
|
|
4828
|
+
if ("servers" in mcp && !isPlainObject(mcp.servers)) return {};
|
|
4829
|
+
const native = isPlainObject(mcp.servers) && typeof mcp.servers.type !== "string" && typeof mcp.servers.enabled !== "boolean" && typeof mcp.servers.disabled !== "boolean";
|
|
4830
|
+
const block = native ? mcp.servers : mcp;
|
|
4831
|
+
return Object.fromEntries(
|
|
4832
|
+
Object.entries(block).filter(([name, spec]) => isPlainObject(spec) && (native || name !== "timeout" || typeof spec.type === "string")).map(([name, spec]) => {
|
|
4833
|
+
const server2 = spec;
|
|
4834
|
+
return [name, {
|
|
4835
|
+
...server2,
|
|
4836
|
+
...typeof server2.disabled === "boolean" ? { enabled: !server2.disabled } : {}
|
|
4837
|
+
}];
|
|
4838
|
+
})
|
|
4839
|
+
);
|
|
4840
|
+
}
|
|
4841
|
+
function mergeConfigLayer(target, source, hostApi) {
|
|
4842
|
+
const merged = deepMerge(target, source);
|
|
4843
|
+
if (hostApi === "v2" && isPlainObject(source.mcp)) {
|
|
4844
|
+
merged.mcp = {
|
|
4845
|
+
...isPlainObject(merged.mcp) ? merged.mcp : {},
|
|
4846
|
+
servers: { ...extractMcpBlock(target), ...extractMcpBlock(source) }
|
|
4847
|
+
};
|
|
4848
|
+
}
|
|
4849
|
+
return merged;
|
|
4818
4850
|
}
|
|
4819
4851
|
function mergeMcp(target, source) {
|
|
4820
4852
|
const out = { ...target };
|
|
@@ -4832,12 +4864,12 @@ function mergeMcp(target, source) {
|
|
|
4832
4864
|
}
|
|
4833
4865
|
return out;
|
|
4834
4866
|
}
|
|
4835
|
-
function bridgeOpencodeMcp(cwd, runtimeStatus, excludeServers) {
|
|
4867
|
+
function bridgeOpencodeMcp(cwd, runtimeStatus, excludeServers, hostApi = "v1") {
|
|
4836
4868
|
const {
|
|
4837
4869
|
servers: merged,
|
|
4838
4870
|
enabledServerNames: allEnabledServerNames,
|
|
4839
4871
|
hash
|
|
4840
|
-
} = mergeOpencodeMcp(cwd, runtimeStatus);
|
|
4872
|
+
} = mergeOpencodeMcp(cwd, runtimeStatus, hostApi);
|
|
4841
4873
|
const servers = {};
|
|
4842
4874
|
const bridgedServerNames = [];
|
|
4843
4875
|
for (const [name, spec] of Object.entries(merged)) {
|
|
@@ -4857,10 +4889,10 @@ function bridgeOpencodeMcp(cwd, runtimeStatus, excludeServers) {
|
|
|
4857
4889
|
excludeServers
|
|
4858
4890
|
});
|
|
4859
4891
|
}
|
|
4860
|
-
function opencodeConfigLayers(cwd) {
|
|
4861
|
-
const worktree = detectWorktree(cwd);
|
|
4892
|
+
function opencodeConfigLayers(cwd, hostApi = "v1") {
|
|
4893
|
+
const worktree = hostApi === "v2" ? void 0 : detectWorktree(cwd);
|
|
4862
4894
|
const layers = [];
|
|
4863
|
-
layers.push(loadGlobalConfig());
|
|
4895
|
+
layers.push(loadGlobalConfig(hostApi));
|
|
4864
4896
|
const explicitConfig = process.env.OPENCODE_CONFIG;
|
|
4865
4897
|
if (explicitConfig && fileExists(explicitConfig)) {
|
|
4866
4898
|
const parsed = readAndParse(explicitConfig);
|
|
@@ -4882,22 +4914,30 @@ function opencodeConfigLayers(cwd) {
|
|
|
4882
4914
|
}
|
|
4883
4915
|
}
|
|
4884
4916
|
for (const dir of projectDirs.slice().reverse()) {
|
|
4885
|
-
layers.push(loadProjectFilesInDir(dir));
|
|
4917
|
+
layers.push(loadProjectFilesInDir(dir, hostApi));
|
|
4886
4918
|
}
|
|
4887
|
-
|
|
4888
|
-
|
|
4919
|
+
if (hostApi === "v2") {
|
|
4920
|
+
const dirs = walkUp({ start: cwd, targets: [".opencode"], predicate: dirExists }).reverse();
|
|
4921
|
+
const extra = process.env.OPENCODE_CONFIG_DIR;
|
|
4922
|
+
if (extra && dirExists(extra)) layers.push(loadProjectFilesInDir(extra, hostApi));
|
|
4923
|
+
for (const dir of dirs) layers.push(loadProjectFilesInDir(dir, hostApi));
|
|
4924
|
+
} else {
|
|
4925
|
+
for (const dir of dotOpencodeDirs(cwd, worktree)) {
|
|
4926
|
+
layers.push(loadProjectFilesInDir(dir));
|
|
4927
|
+
}
|
|
4889
4928
|
}
|
|
4890
4929
|
return layers;
|
|
4891
4930
|
}
|
|
4892
|
-
function loadMergedOpencodeConfig(cwd) {
|
|
4931
|
+
function loadMergedOpencodeConfig(cwd, hostApi = "v1") {
|
|
4893
4932
|
let merged = {};
|
|
4894
|
-
for (const layer of opencodeConfigLayers(cwd)) merged =
|
|
4933
|
+
for (const layer of opencodeConfigLayers(cwd, hostApi)) merged = mergeConfigLayer(merged, layer, hostApi);
|
|
4895
4934
|
return merged;
|
|
4896
4935
|
}
|
|
4897
|
-
function mergeOpencodeMcp(cwd, runtimeStatus) {
|
|
4936
|
+
function mergeOpencodeMcp(cwd, runtimeStatus, hostApi = "v1") {
|
|
4898
4937
|
let merged = {};
|
|
4899
|
-
for (const layer of opencodeConfigLayers(cwd)) {
|
|
4900
|
-
|
|
4938
|
+
for (const layer of opencodeConfigLayers(cwd, hostApi)) {
|
|
4939
|
+
const servers = extractMcpBlock(layer);
|
|
4940
|
+
merged = hostApi === "v2" ? { ...merged, ...servers } : mergeMcp(merged, servers);
|
|
4901
4941
|
}
|
|
4902
4942
|
if (runtimeStatus) {
|
|
4903
4943
|
for (const name of Object.keys(merged)) {
|
|
@@ -5037,7 +5077,8 @@ function collectStartupDiagnostics(providers, opencodeVersion) {
|
|
|
5037
5077
|
const cwd = describeSpawnCwd(firstOption(providers, "cwd"));
|
|
5038
5078
|
let mcpServers = [];
|
|
5039
5079
|
try {
|
|
5040
|
-
|
|
5080
|
+
const hostApi = firstOption(providers, "hostApi") === "v2" || opencodeVersion?.replace(/^v/, "").startsWith("2.") ? "v2" : "v1";
|
|
5081
|
+
mcpServers = mergeOpencodeMcp(cwd.resolved, void 0, hostApi).enabledServerNames;
|
|
5041
5082
|
} catch (err) {
|
|
5042
5083
|
log.debug("startup diagnostics could not read MCP config", {
|
|
5043
5084
|
error: err instanceof Error ? err.message : String(err)
|
|
@@ -7609,6 +7650,13 @@ Structured questions in this environment go through exactly one tool: \`mcp__ope
|
|
|
7609
7650
|
- If that tool is not in your visible tool list it is deferred \u2014 load it with ToolSearch (\`select:mcp__opencode_proxy__question\`), then call it by its FULL name.
|
|
7610
7651
|
- Do NOT call bare \`question\` \u2014 that is not a tool. Always use the full \`mcp__opencode_proxy__question\` name when invoking it.
|
|
7611
7652
|
- Claude Code's built-in \`AskUserQuestion\` is disabled in this environment; the proxy is the only way to ask structured questions.`;
|
|
7653
|
+
function codeModeProxyHint(active) {
|
|
7654
|
+
return active ? `## opencode Code Mode
|
|
7655
|
+
|
|
7656
|
+
Instructions referring to the execute tool mean \`mcp__opencode_proxy__execute\` in this Claude Code session. Use its full name; if deferred, load it with ToolSearch (\`select:mcp__opencode_proxy__execute\`). Pass the code argument unchanged, using the provided catalog's exact \`search(...)\` and \`tools[...]\` signatures inside that code. The catalog tools are not separate Claude tools. Calls execute in opencode under this session's permissions.` : `## opencode Code Mode unavailable
|
|
7657
|
+
|
|
7658
|
+
The forwarded opencode catalog may mention execute, but this session does not expose an execute proxy. Do not invent an execute tool or claim a catalog call ran. For MCP servers available directly in Claude Code, discover their actual MCP tools with ToolSearch instead. If no matching tool exists, report that limitation; a shell/CLI call is not a test of MCP.`;
|
|
7659
|
+
}
|
|
7612
7660
|
var CLAUDE_CLI_CONTEXT_NOTE = `## Runtime environment: Claude Code CLI
|
|
7613
7661
|
|
|
7614
7662
|
You are running via the Claude Code CLI (not a direct API call). This affects context management:
|
|
@@ -8094,7 +8142,7 @@ function effectiveMcpConfig(config, cwd, proxyConfigPath, runtimeStatus, exclude
|
|
|
8094
8142
|
let bridgedHash = null;
|
|
8095
8143
|
let allEnabledServerNames = [];
|
|
8096
8144
|
if (config.bridgeOpencodeMcp !== false) {
|
|
8097
|
-
const bridged = bridgeOpencodeMcp(cwd, runtimeStatus, excludeServers);
|
|
8145
|
+
const bridged = bridgeOpencodeMcp(cwd, runtimeStatus, excludeServers, config.hostApi);
|
|
8098
8146
|
if (bridged) {
|
|
8099
8147
|
if (bridged.path) paths.push(bridged.path);
|
|
8100
8148
|
bridgedHash = bridged.hash;
|
|
@@ -8154,7 +8202,7 @@ function resolvedProxyMcpTools(config, allEnabledServerNames, modelTools, taken)
|
|
|
8154
8202
|
});
|
|
8155
8203
|
if (resolution.defs.length === 0) {
|
|
8156
8204
|
log.warn(
|
|
8157
|
-
"proxyOpencodeMcpTools is on but no MCP tool was found in opencode's tool set; those servers stay on the direct bridge this spawn",
|
|
8205
|
+
"proxyOpencodeMcpTools is on but no MCP tool was found in opencode's tool set; those servers stay on the direct bridge this spawn" + (config.hostApi === "v2" && modelTools?.some((tool) => tool.name === "execute") ? ". For V2 Code Mode, explicitly allowlist execute in proxyOpencodeTools and use bridgeOpencodeMcp: false with strictMcpConfig: true" : ""),
|
|
8158
8206
|
{ servers: allEnabledServerNames, modelTools: modelTools?.length ?? 0 }
|
|
8159
8207
|
);
|
|
8160
8208
|
return null;
|
|
@@ -10692,6 +10740,8 @@ var ClaudeCodeLanguageModel = class {
|
|
|
10692
10740
|
const opencodeToolDefs = resolveProxyOpencodeToolDefs({
|
|
10693
10741
|
requested: self.config.proxyOpencodeTools,
|
|
10694
10742
|
items: liveToolInfo.items,
|
|
10743
|
+
modelTools: self.config.hostApi === "v2" ? options.tools ?? [] : void 0,
|
|
10744
|
+
allowCodeExecution: self.config.permissionPreset !== "read-only",
|
|
10695
10745
|
taken: new Set(
|
|
10696
10746
|
[...enrichedProxy ?? [], ...proxyMcpTools ?? []].map(
|
|
10697
10747
|
(t) => t.name
|
|
@@ -10737,7 +10787,8 @@ var ClaudeCodeLanguageModel = class {
|
|
|
10737
10787
|
...extractSystemMessages(options.prompt),
|
|
10738
10788
|
...taskProxyEnabled ? [SUBAGENT_DISPATCH_HINT] : [],
|
|
10739
10789
|
...backgroundSubagentsSupported ? [BACKGROUND_SUBAGENT_HINT] : [],
|
|
10740
|
-
...questionProxyActive ? [QUESTION_PROXY_HINT] : []
|
|
10790
|
+
...questionProxyActive ? [QUESTION_PROXY_HINT] : [],
|
|
10791
|
+
...self.config.hostApi === "v2" && options.tools?.some((t) => t.name === "execute") ? [codeModeProxyHint(opencodeToolDefs.some((t) => t.name === "execute"))] : []
|
|
10741
10792
|
],
|
|
10742
10793
|
{
|
|
10743
10794
|
compressEnabled: pluginCompressEnabled,
|
|
@@ -11545,7 +11596,8 @@ function createV1ClientShim(ctx) {
|
|
|
11545
11596
|
client.tool = {
|
|
11546
11597
|
// V1: `{ data: [{ id, description, parameters }] }`. V2 returns the tools
|
|
11547
11598
|
// themselves, with an Effect schema for input rather than JSON Schema, so
|
|
11548
|
-
// `parameters` is empty
|
|
11599
|
+
// `parameters` is empty. V2 proxyOpencodeTools uses the per-turn model
|
|
11600
|
+
// snapshot instead, which also includes synthesized Code Mode execute.
|
|
11549
11601
|
list: async () => {
|
|
11550
11602
|
const tools = await toolList.call(ctx.tool);
|
|
11551
11603
|
const data = (tools ?? []).flatMap((tool) => {
|
|
@@ -11726,9 +11778,11 @@ function plainObject(value) {
|
|
|
11726
11778
|
}
|
|
11727
11779
|
function configuredSeedSettings(config, pluginOptions) {
|
|
11728
11780
|
const provider = plainObject(plainObject(config.provider)?.[BASE_PROVIDER_ID]);
|
|
11781
|
+
const nativeProvider = plainObject(plainObject(config.providers)?.[BASE_PROVIDER_ID]);
|
|
11729
11782
|
return {
|
|
11730
11783
|
...plainObject(provider?.options) ?? {},
|
|
11731
11784
|
...plainObject(provider?.settings) ?? {},
|
|
11785
|
+
...plainObject(nativeProvider?.settings) ?? {},
|
|
11732
11786
|
...pluginOptions ?? {}
|
|
11733
11787
|
};
|
|
11734
11788
|
}
|
|
@@ -12376,7 +12430,7 @@ var index_default = {
|
|
|
12376
12430
|
setup: createV2Setup({
|
|
12377
12431
|
createProvider: createClaudeCode,
|
|
12378
12432
|
defaultProxyTools: DEFAULT_PROXY_TOOL_NAMES,
|
|
12379
|
-
loadConfig: loadMergedOpencodeConfig,
|
|
12433
|
+
loadConfig: (directory) => loadMergedOpencodeConfig(directory, "v2"),
|
|
12380
12434
|
buildAgentRegistry: (config) => buildAgentRegistry(config)
|
|
12381
12435
|
})
|
|
12382
12436
|
};
|