@khalilgharbaoui/opencode-claude-code-plugin 0.3.1 → 0.4.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/dist/index.d.ts +39 -1
- package/dist/index.js +331 -58
- package/dist/index.js.map +1 -1
- package/package.json +2 -2
package/dist/index.d.ts
CHANGED
|
@@ -114,6 +114,7 @@ interface ClaudeCodeConfig {
|
|
|
114
114
|
proxyTools?: string[];
|
|
115
115
|
webSearch?: WebSearchRouting;
|
|
116
116
|
hotReloadMcp?: boolean;
|
|
117
|
+
proxyOpencodeMcpTools?: boolean;
|
|
117
118
|
}
|
|
118
119
|
type WebSearchRouting = "claude" | "disabled" | (string & {});
|
|
119
120
|
interface ClaudeCodeProviderSettings {
|
|
@@ -191,6 +192,21 @@ interface ClaudeCodeProviderSettings {
|
|
|
191
192
|
* survives MCP changes until the chat is reset).
|
|
192
193
|
*/
|
|
193
194
|
hotReloadMcp?: boolean;
|
|
195
|
+
/**
|
|
196
|
+
* Route opencode MCP server tools through the in-process `opencode_proxy`
|
|
197
|
+
* MCP server instead of bridging them directly into Claude CLI's
|
|
198
|
+
* `--mcp-config`. With both layers configured for the same MCP server,
|
|
199
|
+
* direct bridging causes each tool invocation to execute twice — once by
|
|
200
|
+
* Claude CLI's own MCP child process and once by opencode. Routing through
|
|
201
|
+
* the proxy keeps a single execution site (opencode) while preserving the
|
|
202
|
+
* tool-call/result surface in opencode's UI and its permission prompts.
|
|
203
|
+
*
|
|
204
|
+
* Defaults to `true`. Set to `false` to restore the prior direct-bridge
|
|
205
|
+
* behavior (Claude CLI executes MCP tools itself; opencode also re-executes
|
|
206
|
+
* — accept the duplication if you need Claude to invoke the tool without
|
|
207
|
+
* an opencode round-trip).
|
|
208
|
+
*/
|
|
209
|
+
proxyOpencodeMcpTools?: boolean;
|
|
194
210
|
}
|
|
195
211
|
type PermissionMode = "acceptEdits" | "auto" | "bypassPermissions" | "default" | "dontAsk" | "plan";
|
|
196
212
|
type ControlRequestBehavior = "allow" | "deny";
|
|
@@ -305,6 +321,16 @@ declare class ClaudeCodeLanguageModel implements LanguageModelV3 {
|
|
|
305
321
|
private effectiveMcpConfig;
|
|
306
322
|
/** Resolve ProxyToolDef[] for the configured proxyTools names. */
|
|
307
323
|
private resolvedProxyTools;
|
|
324
|
+
/**
|
|
325
|
+
* Resolve ProxyToolDef[] for opencode's MCP-bridged tools so they go
|
|
326
|
+
* through the in-process proxy instead of being bridged into Claude CLI's
|
|
327
|
+
* `--mcp-config`. Direct bridging causes double execution because both
|
|
328
|
+
* Claude CLI's own MCP child and opencode hold their own connection to
|
|
329
|
+
* the same server; routing through the proxy keeps a single execution
|
|
330
|
+
* site (opencode). Returns null when the feature is disabled, the SDK
|
|
331
|
+
* client is unavailable, or no MCP servers are configured.
|
|
332
|
+
*/
|
|
333
|
+
private resolvedProxyMcpTools;
|
|
308
334
|
/**
|
|
309
335
|
* Create a proxy MCP server for a single active Claude process/session.
|
|
310
336
|
* The process lifecycle owns the server lifecycle via session-manager.
|
|
@@ -339,6 +365,18 @@ interface BridgedMcp {
|
|
|
339
365
|
path: string;
|
|
340
366
|
/** Stable hash of the merged opencode mcp block (pre-translation). */
|
|
341
367
|
hash: string;
|
|
368
|
+
/**
|
|
369
|
+
* Names of opencode MCP servers that were bridged into Claude CLI's
|
|
370
|
+
* `--mcp-config`. Excludes any servers passed in `excludeServers`.
|
|
371
|
+
*/
|
|
372
|
+
serverNames: string[];
|
|
373
|
+
/**
|
|
374
|
+
* Names of every enabled opencode MCP server after merge + runtime
|
|
375
|
+
* overlay, regardless of whether they ended up bridged or excluded.
|
|
376
|
+
* Callers (e.g. the proxy-tool builder) use this to decide which
|
|
377
|
+
* `<server>_<tool>` IDs in opencode's tool catalog are MCP-origin.
|
|
378
|
+
*/
|
|
379
|
+
allEnabledServerNames: string[];
|
|
342
380
|
}
|
|
343
381
|
/**
|
|
344
382
|
* Per-server runtime status from opencode's `client.mcp.status()`. Used as
|
|
@@ -362,7 +400,7 @@ type RuntimeMcpStatus = Record<string, string>;
|
|
|
362
400
|
* return its path + a stable hash. Returns null when no enabled MCP servers
|
|
363
401
|
* remain after the merge + overlay.
|
|
364
402
|
*/
|
|
365
|
-
declare function bridgeOpencodeMcp(cwd: string, runtimeStatus?: RuntimeMcpStatus): BridgedMcp | null;
|
|
403
|
+
declare function bridgeOpencodeMcp(cwd: string, runtimeStatus?: RuntimeMcpStatus, excludeServers?: ReadonlySet<string>): BridgedMcp | null;
|
|
366
404
|
|
|
367
405
|
declare const defaultModels: Record<string, OpenCodeModel>;
|
|
368
406
|
|
package/dist/index.js
CHANGED
|
@@ -637,7 +637,7 @@ function mergeMcp(target, source) {
|
|
|
637
637
|
}
|
|
638
638
|
return out;
|
|
639
639
|
}
|
|
640
|
-
function bridgeOpencodeMcp(cwd, runtimeStatus) {
|
|
640
|
+
function bridgeOpencodeMcp(cwd, runtimeStatus, excludeServers) {
|
|
641
641
|
const worktree = detectWorktree(cwd);
|
|
642
642
|
let merged = {};
|
|
643
643
|
merged = mergeMcp(merged, extractMcpBlock(loadGlobalConfig()));
|
|
@@ -676,15 +676,37 @@ function bridgeOpencodeMcp(cwd, runtimeStatus) {
|
|
|
676
676
|
merged[name] = { ...base, enabled: status === "connected" };
|
|
677
677
|
}
|
|
678
678
|
}
|
|
679
|
+
const allEnabledServerNames = [];
|
|
680
|
+
for (const [name, spec] of Object.entries(merged)) {
|
|
681
|
+
if (!spec || typeof spec !== "object") continue;
|
|
682
|
+
const enabled = spec.enabled;
|
|
683
|
+
if (enabled === false) continue;
|
|
684
|
+
allEnabledServerNames.push(name);
|
|
685
|
+
}
|
|
679
686
|
const servers = {};
|
|
687
|
+
const bridgedServerNames = [];
|
|
680
688
|
for (const [name, spec] of Object.entries(merged)) {
|
|
681
689
|
if (!spec || typeof spec !== "object") continue;
|
|
690
|
+
if (excludeServers?.has(name)) continue;
|
|
682
691
|
const translated = translateServer(name, spec);
|
|
683
|
-
if (translated)
|
|
692
|
+
if (translated) {
|
|
693
|
+
servers[name] = translated;
|
|
694
|
+
bridgedServerNames.push(name);
|
|
695
|
+
}
|
|
696
|
+
}
|
|
697
|
+
const mergedBody = JSON.stringify({ mcpServers: merged }, null, 2);
|
|
698
|
+
const hash = crypto.createHash("sha256").update(mergedBody).digest("hex").slice(0, 12);
|
|
699
|
+
if (Object.keys(servers).length === 0) {
|
|
700
|
+
const allEnabledServersExcluded = excludeServers && allEnabledServerNames.length > 0 && allEnabledServerNames.every((name) => excludeServers.has(name));
|
|
701
|
+
if (!allEnabledServersExcluded) return null;
|
|
702
|
+
return {
|
|
703
|
+
path: "",
|
|
704
|
+
hash,
|
|
705
|
+
serverNames: [],
|
|
706
|
+
allEnabledServerNames
|
|
707
|
+
};
|
|
684
708
|
}
|
|
685
|
-
if (Object.keys(servers).length === 0) return null;
|
|
686
709
|
const body = JSON.stringify({ mcpServers: servers }, null, 2);
|
|
687
|
-
const hash = crypto.createHash("sha256").update(body).digest("hex").slice(0, 12);
|
|
688
710
|
const outPath = path2.join(
|
|
689
711
|
pluginTmpDir(),
|
|
690
712
|
`mcp-${hash}.json`
|
|
@@ -702,9 +724,15 @@ function bridgeOpencodeMcp(cwd, runtimeStatus) {
|
|
|
702
724
|
log.info("bridged opencode MCP config", {
|
|
703
725
|
target: outPath,
|
|
704
726
|
hash,
|
|
705
|
-
servers:
|
|
727
|
+
servers: bridgedServerNames,
|
|
728
|
+
excluded: excludeServers ? Array.from(excludeServers) : []
|
|
706
729
|
});
|
|
707
|
-
return {
|
|
730
|
+
return {
|
|
731
|
+
path: outPath,
|
|
732
|
+
hash,
|
|
733
|
+
serverNames: bridgedServerNames,
|
|
734
|
+
allEnabledServerNames
|
|
735
|
+
};
|
|
708
736
|
}
|
|
709
737
|
|
|
710
738
|
// src/runtime-status.ts
|
|
@@ -736,6 +764,35 @@ async function getRuntimeMcpStatus() {
|
|
|
736
764
|
return void 0;
|
|
737
765
|
}
|
|
738
766
|
}
|
|
767
|
+
async function fetchOpencodeToolList(provider, model, directory) {
|
|
768
|
+
const client = opencodeClient;
|
|
769
|
+
if (!client?.tool?.list) return void 0;
|
|
770
|
+
try {
|
|
771
|
+
const res = await client.tool.list({
|
|
772
|
+
query: { provider, model, ...directory ? { directory } : {} }
|
|
773
|
+
});
|
|
774
|
+
const data = res.data;
|
|
775
|
+
if (!Array.isArray(data)) return void 0;
|
|
776
|
+
const out = [];
|
|
777
|
+
for (const entry of data) {
|
|
778
|
+
if (!entry || typeof entry !== "object") continue;
|
|
779
|
+
const e = entry;
|
|
780
|
+
const id = typeof e.id === "string" ? e.id : null;
|
|
781
|
+
const description = typeof e.description === "string" ? e.description : "";
|
|
782
|
+
const parameters = e.parameters && typeof e.parameters === "object" ? e.parameters : {};
|
|
783
|
+
if (!id) continue;
|
|
784
|
+
out.push({ id, description, parameters });
|
|
785
|
+
}
|
|
786
|
+
return out;
|
|
787
|
+
} catch (err) {
|
|
788
|
+
log.warn("failed to fetch opencode tool list", {
|
|
789
|
+
provider,
|
|
790
|
+
model,
|
|
791
|
+
error: err instanceof Error ? err.message : String(err)
|
|
792
|
+
});
|
|
793
|
+
return void 0;
|
|
794
|
+
}
|
|
795
|
+
}
|
|
739
796
|
|
|
740
797
|
// src/session-manager.ts
|
|
741
798
|
import { spawn } from "child_process";
|
|
@@ -1262,33 +1319,71 @@ function writeJson(res, body) {
|
|
|
1262
1319
|
|
|
1263
1320
|
// src/proxy-broker.ts
|
|
1264
1321
|
import { EventEmitter as EventEmitter3 } from "events";
|
|
1265
|
-
var
|
|
1322
|
+
var pendingByCallId = /* @__PURE__ */ new Map();
|
|
1323
|
+
var callIdsBySession = /* @__PURE__ */ new Map();
|
|
1266
1324
|
var emitter = new EventEmitter3();
|
|
1325
|
+
var PENDING_PROXY_CALL_TIMEOUT_MS = 10 * 60 * 1e3;
|
|
1267
1326
|
function eventName(sessionKey2) {
|
|
1268
1327
|
return `pending:${sessionKey2}`;
|
|
1269
1328
|
}
|
|
1329
|
+
function indexAdd(sessionKey2, callId) {
|
|
1330
|
+
let s = callIdsBySession.get(sessionKey2);
|
|
1331
|
+
if (!s) {
|
|
1332
|
+
s = /* @__PURE__ */ new Set();
|
|
1333
|
+
callIdsBySession.set(sessionKey2, s);
|
|
1334
|
+
}
|
|
1335
|
+
s.add(callId);
|
|
1336
|
+
}
|
|
1337
|
+
function indexRemove(sessionKey2, callId) {
|
|
1338
|
+
const s = callIdsBySession.get(sessionKey2);
|
|
1339
|
+
if (!s) return;
|
|
1340
|
+
s.delete(callId);
|
|
1341
|
+
if (s.size === 0) callIdsBySession.delete(sessionKey2);
|
|
1342
|
+
}
|
|
1270
1343
|
function onPendingProxyCall(sessionKey2, handler) {
|
|
1271
1344
|
const name = eventName(sessionKey2);
|
|
1272
1345
|
emitter.on(name, handler);
|
|
1273
1346
|
return () => emitter.off(name, handler);
|
|
1274
1347
|
}
|
|
1275
1348
|
function queuePendingProxyCall(sessionKey2, call) {
|
|
1276
|
-
const
|
|
1277
|
-
if (
|
|
1278
|
-
|
|
1279
|
-
|
|
1349
|
+
const previous = pendingByCallId.get(call.id);
|
|
1350
|
+
if (previous) {
|
|
1351
|
+
clearTimeout(previous.timer);
|
|
1352
|
+
previous.reject(
|
|
1353
|
+
new Error(`Replaced pending proxy call ${call.id} with a fresh one`)
|
|
1280
1354
|
);
|
|
1281
|
-
|
|
1282
|
-
|
|
1355
|
+
pendingByCallId.delete(call.id);
|
|
1356
|
+
indexRemove(previous.sessionKey, call.id);
|
|
1357
|
+
}
|
|
1358
|
+
const timer = setTimeout(() => {
|
|
1359
|
+
const current = pendingByCallId.get(call.id);
|
|
1360
|
+
if (!current) return;
|
|
1361
|
+
pendingByCallId.delete(call.id);
|
|
1362
|
+
indexRemove(current.sessionKey, call.id);
|
|
1363
|
+
current.reject(
|
|
1364
|
+
new Error(
|
|
1365
|
+
`Proxy tool call '${call.toolName}' timed out after ${PENDING_PROXY_CALL_TIMEOUT_MS}ms waiting for opencode to resolve the call`
|
|
1366
|
+
)
|
|
1367
|
+
);
|
|
1368
|
+
log.warn("timed out pending proxy call", {
|
|
1369
|
+
sessionKey: current.sessionKey,
|
|
1370
|
+
toolCallId: call.id,
|
|
1371
|
+
toolName: call.toolName,
|
|
1372
|
+
timeoutMs: PENDING_PROXY_CALL_TIMEOUT_MS
|
|
1373
|
+
});
|
|
1374
|
+
}, PENDING_PROXY_CALL_TIMEOUT_MS);
|
|
1283
1375
|
const pending = {
|
|
1284
1376
|
sessionKey: sessionKey2,
|
|
1285
1377
|
toolCallId: call.id,
|
|
1286
1378
|
toolName: call.toolName,
|
|
1287
1379
|
input: call.input,
|
|
1380
|
+
createdAt: Date.now(),
|
|
1381
|
+
timer,
|
|
1288
1382
|
resolve: call.resolve,
|
|
1289
1383
|
reject: call.reject
|
|
1290
1384
|
};
|
|
1291
|
-
|
|
1385
|
+
pendingByCallId.set(call.id, pending);
|
|
1386
|
+
indexAdd(sessionKey2, call.id);
|
|
1292
1387
|
emitter.emit(eventName(sessionKey2), pending);
|
|
1293
1388
|
log.info("queued pending proxy call", {
|
|
1294
1389
|
sessionKey: sessionKey2,
|
|
@@ -1297,21 +1392,55 @@ function queuePendingProxyCall(sessionKey2, call) {
|
|
|
1297
1392
|
});
|
|
1298
1393
|
return pending;
|
|
1299
1394
|
}
|
|
1300
|
-
function
|
|
1301
|
-
|
|
1395
|
+
function getPendingProxyCalls(sessionKey2) {
|
|
1396
|
+
const s = callIdsBySession.get(sessionKey2);
|
|
1397
|
+
if (!s || s.size === 0) return [];
|
|
1398
|
+
const out = [];
|
|
1399
|
+
for (const id of s) {
|
|
1400
|
+
const p = pendingByCallId.get(id);
|
|
1401
|
+
if (p) out.push(p);
|
|
1402
|
+
}
|
|
1403
|
+
return out;
|
|
1302
1404
|
}
|
|
1303
|
-
function
|
|
1304
|
-
const pending =
|
|
1405
|
+
function resolvePendingProxyCallById(toolCallId, result) {
|
|
1406
|
+
const pending = pendingByCallId.get(toolCallId);
|
|
1305
1407
|
if (!pending) return false;
|
|
1306
|
-
|
|
1408
|
+
pendingByCallId.delete(toolCallId);
|
|
1409
|
+
indexRemove(pending.sessionKey, toolCallId);
|
|
1410
|
+
clearTimeout(pending.timer);
|
|
1307
1411
|
pending.resolve(result);
|
|
1308
1412
|
log.info("resolved pending proxy call", {
|
|
1309
|
-
sessionKey:
|
|
1413
|
+
sessionKey: pending.sessionKey,
|
|
1310
1414
|
toolCallId: pending.toolCallId,
|
|
1311
1415
|
toolName: pending.toolName
|
|
1312
1416
|
});
|
|
1313
1417
|
return true;
|
|
1314
1418
|
}
|
|
1419
|
+
function rejectPendingProxyCallById(toolCallId, error) {
|
|
1420
|
+
const pending = pendingByCallId.get(toolCallId);
|
|
1421
|
+
if (!pending) return false;
|
|
1422
|
+
pendingByCallId.delete(toolCallId);
|
|
1423
|
+
indexRemove(pending.sessionKey, toolCallId);
|
|
1424
|
+
clearTimeout(pending.timer);
|
|
1425
|
+
pending.reject(error);
|
|
1426
|
+
log.warn("rejected pending proxy call", {
|
|
1427
|
+
sessionKey: pending.sessionKey,
|
|
1428
|
+
toolCallId: pending.toolCallId,
|
|
1429
|
+
toolName: pending.toolName,
|
|
1430
|
+
error: error.message
|
|
1431
|
+
});
|
|
1432
|
+
return true;
|
|
1433
|
+
}
|
|
1434
|
+
function rejectAllPendingProxyCallsForSession(sessionKey2, error) {
|
|
1435
|
+
const s = callIdsBySession.get(sessionKey2);
|
|
1436
|
+
if (!s) return 0;
|
|
1437
|
+
const ids = [...s];
|
|
1438
|
+
let count = 0;
|
|
1439
|
+
for (const id of ids) {
|
|
1440
|
+
if (rejectPendingProxyCallById(id, error)) count++;
|
|
1441
|
+
}
|
|
1442
|
+
return count;
|
|
1443
|
+
}
|
|
1315
1444
|
|
|
1316
1445
|
// src/claude-code-language-model.ts
|
|
1317
1446
|
import { readFileSync as readFileSync2, writeFileSync as writeFileSync3 } from "fs";
|
|
@@ -1432,18 +1561,20 @@ var ClaudeCodeLanguageModel = class {
|
|
|
1432
1561
|
* provided it overlays opencode's UI-toggled state on top of disk config
|
|
1433
1562
|
* so `/mcps` toggles propagate without a config file write.
|
|
1434
1563
|
*/
|
|
1435
|
-
effectiveMcpConfig(cwd, proxyConfigPath, runtimeStatus) {
|
|
1564
|
+
effectiveMcpConfig(cwd, proxyConfigPath, runtimeStatus, excludeServers) {
|
|
1436
1565
|
const paths = Array.isArray(this.config.mcpConfig) ? this.config.mcpConfig.slice() : this.config.mcpConfig ? [this.config.mcpConfig] : [];
|
|
1437
1566
|
let bridgedHash = null;
|
|
1567
|
+
let allEnabledServerNames = [];
|
|
1438
1568
|
if (this.config.bridgeOpencodeMcp !== false) {
|
|
1439
|
-
const bridged = bridgeOpencodeMcp(cwd, runtimeStatus);
|
|
1569
|
+
const bridged = bridgeOpencodeMcp(cwd, runtimeStatus, excludeServers);
|
|
1440
1570
|
if (bridged) {
|
|
1441
|
-
paths.push(bridged.path);
|
|
1571
|
+
if (bridged.path) paths.push(bridged.path);
|
|
1442
1572
|
bridgedHash = bridged.hash;
|
|
1573
|
+
allEnabledServerNames = bridged.allEnabledServerNames;
|
|
1443
1574
|
}
|
|
1444
1575
|
}
|
|
1445
1576
|
if (proxyConfigPath) paths.push(proxyConfigPath);
|
|
1446
|
-
return { paths, bridgedHash };
|
|
1577
|
+
return { paths, bridgedHash, allEnabledServerNames };
|
|
1447
1578
|
}
|
|
1448
1579
|
/** Resolve ProxyToolDef[] for the configured proxyTools names. */
|
|
1449
1580
|
resolvedProxyTools() {
|
|
@@ -1459,6 +1590,45 @@ var ClaudeCodeLanguageModel = class {
|
|
|
1459
1590
|
}
|
|
1460
1591
|
return picked.length > 0 ? picked : null;
|
|
1461
1592
|
}
|
|
1593
|
+
/**
|
|
1594
|
+
* Resolve ProxyToolDef[] for opencode's MCP-bridged tools so they go
|
|
1595
|
+
* through the in-process proxy instead of being bridged into Claude CLI's
|
|
1596
|
+
* `--mcp-config`. Direct bridging causes double execution because both
|
|
1597
|
+
* Claude CLI's own MCP child and opencode hold their own connection to
|
|
1598
|
+
* the same server; routing through the proxy keeps a single execution
|
|
1599
|
+
* site (opencode). Returns null when the feature is disabled, the SDK
|
|
1600
|
+
* client is unavailable, or no MCP servers are configured.
|
|
1601
|
+
*/
|
|
1602
|
+
async resolvedProxyMcpTools(allEnabledServerNames) {
|
|
1603
|
+
if (this.config.proxyOpencodeMcpTools === false) return null;
|
|
1604
|
+
if (this.config.bridgeOpencodeMcp === false) return null;
|
|
1605
|
+
if (allEnabledServerNames.length === 0) return null;
|
|
1606
|
+
const items = await fetchOpencodeToolList(
|
|
1607
|
+
this.config.provider,
|
|
1608
|
+
this.modelId,
|
|
1609
|
+
this.config.cwd
|
|
1610
|
+
);
|
|
1611
|
+
if (!items || items.length === 0) return null;
|
|
1612
|
+
const serversByLengthDesc = [...allEnabledServerNames].sort(
|
|
1613
|
+
(a, b) => b.length - a.length
|
|
1614
|
+
);
|
|
1615
|
+
const out = [];
|
|
1616
|
+
const seen = /* @__PURE__ */ new Set();
|
|
1617
|
+
for (const item of items) {
|
|
1618
|
+
const matchedServer = serversByLengthDesc.find(
|
|
1619
|
+
(name) => item.id === name || item.id.startsWith(`${name}_`)
|
|
1620
|
+
);
|
|
1621
|
+
if (!matchedServer) continue;
|
|
1622
|
+
if (seen.has(item.id)) continue;
|
|
1623
|
+
seen.add(item.id);
|
|
1624
|
+
out.push({
|
|
1625
|
+
name: item.id,
|
|
1626
|
+
description: item.description ?? "",
|
|
1627
|
+
inputSchema: item.parameters && typeof item.parameters === "object" ? item.parameters : { type: "object", properties: {} }
|
|
1628
|
+
});
|
|
1629
|
+
}
|
|
1630
|
+
return out.length > 0 ? out : null;
|
|
1631
|
+
}
|
|
1462
1632
|
/**
|
|
1463
1633
|
* Create a proxy MCP server for a single active Claude process/session.
|
|
1464
1634
|
* The process lifecycle owns the server lifecycle via session-manager.
|
|
@@ -1740,7 +1910,7 @@ var ClaudeCodeLanguageModel = class {
|
|
|
1740
1910
|
const scope = this.requestScope(options);
|
|
1741
1911
|
const affinity = this.sessionAffinity(options);
|
|
1742
1912
|
const sk = sessionKey(cwd, `${this.modelId}::${scope}::${affinity}`);
|
|
1743
|
-
if (scope === "tools" && this.resolvedProxyTools()) {
|
|
1913
|
+
if (scope === "tools" && (this.resolvedProxyTools() || this.config.proxyOpencodeMcpTools !== false && this.config.bridgeOpencodeMcp !== false)) {
|
|
1744
1914
|
return this.doGenerateViaStream(options);
|
|
1745
1915
|
}
|
|
1746
1916
|
if (scope === "no-tools") {
|
|
@@ -2104,8 +2274,14 @@ ${plan}
|
|
|
2104
2274
|
);
|
|
2105
2275
|
const resolvedProxy = this.resolvedProxyTools();
|
|
2106
2276
|
const self = this;
|
|
2107
|
-
const
|
|
2108
|
-
const
|
|
2277
|
+
const previousPendingProxyCalls = getPendingProxyCalls(sk);
|
|
2278
|
+
const previousPendingProxyMatches = previousPendingProxyCalls.map((call) => ({
|
|
2279
|
+
call,
|
|
2280
|
+
result: this.extractPendingProxyResult(options.prompt, call.toolCallId)
|
|
2281
|
+
}));
|
|
2282
|
+
const hasMatchedPendingResults = previousPendingProxyMatches.some(
|
|
2283
|
+
(m) => m.result !== null
|
|
2284
|
+
);
|
|
2109
2285
|
const runtimeStatus = await getRuntimeMcpStatus();
|
|
2110
2286
|
log.info("doStream starting", {
|
|
2111
2287
|
cwd,
|
|
@@ -2137,8 +2313,18 @@ ${plan}
|
|
|
2137
2313
|
}
|
|
2138
2314
|
}
|
|
2139
2315
|
const setup = async () => {
|
|
2140
|
-
|
|
2141
|
-
|
|
2316
|
+
const discovery = self.effectiveMcpConfig(
|
|
2317
|
+
cwd,
|
|
2318
|
+
void 0,
|
|
2319
|
+
runtimeStatus
|
|
2320
|
+
);
|
|
2321
|
+
const proxyMcpTools = await self.resolvedProxyMcpTools(
|
|
2322
|
+
discovery.allEnabledServerNames
|
|
2323
|
+
);
|
|
2324
|
+
const excludeServers = proxyMcpTools ? new Set(discovery.allEnabledServerNames) : void 0;
|
|
2325
|
+
const combinedProxyTools = resolvedProxy || proxyMcpTools ? [...resolvedProxy ?? [], ...proxyMcpTools ?? []] : null;
|
|
2326
|
+
if (!proxyServer && combinedProxyTools) {
|
|
2327
|
+
proxyServer = await self.ensureProxyServer(combinedProxyTools, sk);
|
|
2142
2328
|
}
|
|
2143
2329
|
const proxyDisallowed = resolvedProxy ? disallowedToolFlags(resolvedProxy) : [];
|
|
2144
2330
|
const extraDisallowed = [];
|
|
@@ -2147,7 +2333,8 @@ ${plan}
|
|
|
2147
2333
|
const mcp = self.effectiveMcpConfig(
|
|
2148
2334
|
cwd,
|
|
2149
2335
|
proxyServer?.configPath(),
|
|
2150
|
-
runtimeStatus
|
|
2336
|
+
runtimeStatus,
|
|
2337
|
+
excludeServers
|
|
2151
2338
|
);
|
|
2152
2339
|
const systemPromptFile = activeProcess ? void 0 : buildAppendedSystemPrompt(cwd);
|
|
2153
2340
|
const cliArgs = buildCliArgs({
|
|
@@ -2224,21 +2411,27 @@ ${plan}
|
|
|
2224
2411
|
const skipResultForIds = /* @__PURE__ */ new Set();
|
|
2225
2412
|
const toolCallsById = /* @__PURE__ */ new Map();
|
|
2226
2413
|
let resultMeta = {};
|
|
2227
|
-
const
|
|
2414
|
+
const drainBuffer = [];
|
|
2415
|
+
let drainTimer = null;
|
|
2416
|
+
const DRAIN_QUIET_MS = 100;
|
|
2417
|
+
const finishWithToolCalls = (calls) => {
|
|
2228
2418
|
if (controllerClosed) return;
|
|
2229
|
-
|
|
2230
|
-
|
|
2231
|
-
|
|
2232
|
-
|
|
2233
|
-
|
|
2234
|
-
|
|
2235
|
-
|
|
2236
|
-
|
|
2237
|
-
|
|
2238
|
-
|
|
2239
|
-
|
|
2240
|
-
|
|
2241
|
-
|
|
2419
|
+
if (calls.length === 0) return;
|
|
2420
|
+
for (const call of calls) {
|
|
2421
|
+
controller.enqueue({
|
|
2422
|
+
type: "tool-input-start",
|
|
2423
|
+
id: call.toolCallId,
|
|
2424
|
+
toolName: call.toolName
|
|
2425
|
+
});
|
|
2426
|
+
controller.enqueue({
|
|
2427
|
+
type: "tool-call",
|
|
2428
|
+
toolCallId: call.toolCallId,
|
|
2429
|
+
toolName: call.toolName,
|
|
2430
|
+
input: JSON.stringify(call.input),
|
|
2431
|
+
providerExecuted: false
|
|
2432
|
+
});
|
|
2433
|
+
skipResultForIds.add(call.toolCallId);
|
|
2434
|
+
}
|
|
2242
2435
|
controller.enqueue({
|
|
2243
2436
|
type: "finish",
|
|
2244
2437
|
finishReason: toFinishReason("tool-calls"),
|
|
@@ -2254,6 +2447,21 @@ ${plan}
|
|
|
2254
2447
|
} catch {
|
|
2255
2448
|
}
|
|
2256
2449
|
};
|
|
2450
|
+
const drainNow = () => {
|
|
2451
|
+
if (drainTimer) {
|
|
2452
|
+
clearTimeout(drainTimer);
|
|
2453
|
+
drainTimer = null;
|
|
2454
|
+
}
|
|
2455
|
+
if (drainBuffer.length === 0) return;
|
|
2456
|
+
if (controllerClosed) return;
|
|
2457
|
+
const batch = drainBuffer.splice(0, drainBuffer.length);
|
|
2458
|
+
log.info("draining pending proxy calls into stream finish", {
|
|
2459
|
+
sessionKey: sk,
|
|
2460
|
+
count: batch.length,
|
|
2461
|
+
toolCallIds: batch.map((c) => c.toolCallId)
|
|
2462
|
+
});
|
|
2463
|
+
finishWithToolCalls(batch);
|
|
2464
|
+
};
|
|
2257
2465
|
let gotPartialEvents = false;
|
|
2258
2466
|
const lineHandler = (line) => {
|
|
2259
2467
|
if (!line.trim()) return;
|
|
@@ -2686,6 +2894,15 @@ ${plan}
|
|
|
2686
2894
|
const closeHandler = () => {
|
|
2687
2895
|
log.debug("readline closed");
|
|
2688
2896
|
if (controllerClosed) return;
|
|
2897
|
+
if (drainBuffer.length > 0 || getPendingProxyCalls(sk).length > 0) {
|
|
2898
|
+
rejectAllPendingProxyCallsForSession(
|
|
2899
|
+
sk,
|
|
2900
|
+
new Error(
|
|
2901
|
+
"Claude CLI subprocess closed before pending tool calls were resolved"
|
|
2902
|
+
)
|
|
2903
|
+
);
|
|
2904
|
+
drainBuffer.length = 0;
|
|
2905
|
+
}
|
|
2689
2906
|
controllerClosed = true;
|
|
2690
2907
|
cleanupTurn();
|
|
2691
2908
|
endTextBlock();
|
|
@@ -2707,6 +2924,10 @@ ${plan}
|
|
|
2707
2924
|
if (cleanedUp) return;
|
|
2708
2925
|
cleanedUp = true;
|
|
2709
2926
|
clearFallbackTimer();
|
|
2927
|
+
if (drainTimer) {
|
|
2928
|
+
clearTimeout(drainTimer);
|
|
2929
|
+
drainTimer = null;
|
|
2930
|
+
}
|
|
2710
2931
|
lineEmitter.off("line", lineHandler);
|
|
2711
2932
|
lineEmitter.off("close", closeHandler);
|
|
2712
2933
|
pendingProxyUnsubscribe?.();
|
|
@@ -2716,6 +2937,15 @@ ${plan}
|
|
|
2716
2937
|
const procErrorHandler = (err) => {
|
|
2717
2938
|
log.error("process error", { error: err.message });
|
|
2718
2939
|
if (controllerClosed) return;
|
|
2940
|
+
if (drainBuffer.length > 0 || getPendingProxyCalls(sk).length > 0) {
|
|
2941
|
+
rejectAllPendingProxyCallsForSession(
|
|
2942
|
+
sk,
|
|
2943
|
+
new Error(
|
|
2944
|
+
`Claude CLI subprocess error: ${err.message}`
|
|
2945
|
+
)
|
|
2946
|
+
);
|
|
2947
|
+
drainBuffer.length = 0;
|
|
2948
|
+
}
|
|
2719
2949
|
controllerClosed = true;
|
|
2720
2950
|
cleanupTurn();
|
|
2721
2951
|
controller.enqueue({ type: "error", error: err });
|
|
@@ -2727,12 +2957,31 @@ ${plan}
|
|
|
2727
2957
|
lineEmitter.on("line", lineHandler);
|
|
2728
2958
|
lineEmitter.on("close", closeHandler);
|
|
2729
2959
|
pendingProxyUnsubscribe = onPendingProxyCall(sk, (call) => {
|
|
2960
|
+
if (controllerClosed) {
|
|
2961
|
+
log.warn(
|
|
2962
|
+
"pending proxy call arrived after stream close; rejecting",
|
|
2963
|
+
{
|
|
2964
|
+
sessionKey: sk,
|
|
2965
|
+
toolCallId: call.toolCallId,
|
|
2966
|
+
toolName: call.toolName
|
|
2967
|
+
}
|
|
2968
|
+
);
|
|
2969
|
+
rejectPendingProxyCallById(
|
|
2970
|
+
call.toolCallId,
|
|
2971
|
+
new Error(
|
|
2972
|
+
`Pending proxy call '${call.toolName}' arrived after the stream was already closed`
|
|
2973
|
+
)
|
|
2974
|
+
);
|
|
2975
|
+
return;
|
|
2976
|
+
}
|
|
2730
2977
|
log.info("received pending proxy call for session", {
|
|
2731
2978
|
sessionKey: sk,
|
|
2732
2979
|
toolCallId: call.toolCallId,
|
|
2733
2980
|
toolName: call.toolName
|
|
2734
2981
|
});
|
|
2735
|
-
|
|
2982
|
+
drainBuffer.push(call);
|
|
2983
|
+
if (drainTimer) clearTimeout(drainTimer);
|
|
2984
|
+
drainTimer = setTimeout(drainNow, DRAIN_QUIET_MS);
|
|
2736
2985
|
});
|
|
2737
2986
|
proc.on("error", procErrorHandler);
|
|
2738
2987
|
if (options.abortSignal) {
|
|
@@ -2758,21 +3007,44 @@ ${plan}
|
|
|
2758
3007
|
startResultFallback(5e3);
|
|
2759
3008
|
});
|
|
2760
3009
|
}
|
|
2761
|
-
if (
|
|
2762
|
-
|
|
2763
|
-
|
|
2764
|
-
|
|
2765
|
-
|
|
2766
|
-
|
|
2767
|
-
|
|
2768
|
-
|
|
2769
|
-
|
|
2770
|
-
|
|
2771
|
-
|
|
2772
|
-
|
|
3010
|
+
if (hasMatchedPendingResults) {
|
|
3011
|
+
for (const { call, result } of previousPendingProxyMatches) {
|
|
3012
|
+
if (result) {
|
|
3013
|
+
log.info("resolving pending proxy call from tool result prompt", {
|
|
3014
|
+
sessionKey: sk,
|
|
3015
|
+
toolCallId: call.toolCallId,
|
|
3016
|
+
toolName: call.toolName
|
|
3017
|
+
});
|
|
3018
|
+
resolvePendingProxyCallById(call.toolCallId, result);
|
|
3019
|
+
} else {
|
|
3020
|
+
log.warn(
|
|
3021
|
+
"pending proxy call had no matching tool-result; rejecting as orphan",
|
|
3022
|
+
{
|
|
3023
|
+
sessionKey: sk,
|
|
3024
|
+
toolCallId: call.toolCallId,
|
|
3025
|
+
toolName: call.toolName
|
|
3026
|
+
}
|
|
3027
|
+
);
|
|
3028
|
+
rejectPendingProxyCallById(
|
|
3029
|
+
call.toolCallId,
|
|
3030
|
+
new Error(
|
|
3031
|
+
`Pending proxy call '${call.toolName}' (${call.toolCallId}) was not matched in tool-result turn; rejecting as orphaned`
|
|
3032
|
+
)
|
|
3033
|
+
);
|
|
3034
|
+
}
|
|
2773
3035
|
}
|
|
2774
3036
|
return;
|
|
2775
3037
|
}
|
|
3038
|
+
if (previousPendingProxyCalls.length > 0) {
|
|
3039
|
+
for (const call of previousPendingProxyCalls) {
|
|
3040
|
+
rejectPendingProxyCallById(
|
|
3041
|
+
call.toolCallId,
|
|
3042
|
+
new Error(
|
|
3043
|
+
`Pending proxy call '${call.toolName}' (${call.toolCallId}) was orphaned by a new user turn; rejecting`
|
|
3044
|
+
)
|
|
3045
|
+
);
|
|
3046
|
+
}
|
|
3047
|
+
}
|
|
2776
3048
|
proc.stdin?.write(userMsg + "\n");
|
|
2777
3049
|
log.debug("sent user message", { textLength: userMsg.length });
|
|
2778
3050
|
};
|
|
@@ -3225,7 +3497,8 @@ function createClaudeCode(settings = {}) {
|
|
|
3225
3497
|
controlRequestDenyMessage: settings.controlRequestDenyMessage,
|
|
3226
3498
|
proxyTools,
|
|
3227
3499
|
webSearch: settings.webSearch,
|
|
3228
|
-
hotReloadMcp: settings.hotReloadMcp ?? true
|
|
3500
|
+
hotReloadMcp: settings.hotReloadMcp ?? true,
|
|
3501
|
+
proxyOpencodeMcpTools: settings.proxyOpencodeMcpTools ?? true
|
|
3229
3502
|
});
|
|
3230
3503
|
};
|
|
3231
3504
|
const provider = function(modelId) {
|