@khalilgharbaoui/opencode-claude-code-plugin 0.3.0 → 0.4.1
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 +1 -1
- package/dist/index.d.ts +39 -1
- package/dist/index.js +284 -87
- package/dist/index.js.map +1 -1
- package/package.json +1 -1
package/dist/index.js
CHANGED
|
@@ -19,7 +19,7 @@ var log = {
|
|
|
19
19
|
console.error(fmt("NOTICE", msg, data));
|
|
20
20
|
},
|
|
21
21
|
warn(msg, data) {
|
|
22
|
-
|
|
22
|
+
console.error(fmt("WARN", msg, data));
|
|
23
23
|
},
|
|
24
24
|
error(msg, data) {
|
|
25
25
|
console.error(fmt("ERROR", msg, data));
|
|
@@ -381,22 +381,49 @@ Now continuing with the current message:
|
|
|
381
381
|
}
|
|
382
382
|
|
|
383
383
|
// src/mcp-bridge.ts
|
|
384
|
+
import * as fs2 from "fs";
|
|
385
|
+
import * as path2 from "path";
|
|
386
|
+
import * as os2 from "os";
|
|
387
|
+
import * as crypto from "crypto";
|
|
388
|
+
|
|
389
|
+
// src/tmp.ts
|
|
384
390
|
import * as fs from "fs";
|
|
385
|
-
import * as path from "path";
|
|
386
391
|
import * as os from "os";
|
|
387
|
-
import * as
|
|
392
|
+
import * as path from "path";
|
|
393
|
+
var PLUGIN_TMP_DIR = path.join(
|
|
394
|
+
os.tmpdir(),
|
|
395
|
+
`opencode-claude-code-${process.pid}`
|
|
396
|
+
);
|
|
397
|
+
var registered = false;
|
|
398
|
+
function pluginTmpDir() {
|
|
399
|
+
if (!fs.existsSync(PLUGIN_TMP_DIR)) {
|
|
400
|
+
fs.mkdirSync(PLUGIN_TMP_DIR, { recursive: true });
|
|
401
|
+
}
|
|
402
|
+
if (!registered) {
|
|
403
|
+
registered = true;
|
|
404
|
+
process.on("exit", () => {
|
|
405
|
+
try {
|
|
406
|
+
fs.rmSync(PLUGIN_TMP_DIR, { recursive: true, force: true });
|
|
407
|
+
} catch {
|
|
408
|
+
}
|
|
409
|
+
});
|
|
410
|
+
}
|
|
411
|
+
return PLUGIN_TMP_DIR;
|
|
412
|
+
}
|
|
413
|
+
|
|
414
|
+
// src/mcp-bridge.ts
|
|
388
415
|
var FILE_NAMES = ["opencode.jsonc", "opencode.json", "config.json"];
|
|
389
416
|
var PROJECT_FILE_NAMES = ["opencode.json", "opencode.jsonc"];
|
|
390
417
|
function fileExists(p) {
|
|
391
418
|
try {
|
|
392
|
-
return
|
|
419
|
+
return fs2.statSync(p).isFile();
|
|
393
420
|
} catch {
|
|
394
421
|
return false;
|
|
395
422
|
}
|
|
396
423
|
}
|
|
397
424
|
function dirExists(p) {
|
|
398
425
|
try {
|
|
399
|
-
return
|
|
426
|
+
return fs2.statSync(p).isDirectory();
|
|
400
427
|
} catch {
|
|
401
428
|
return false;
|
|
402
429
|
}
|
|
@@ -442,7 +469,7 @@ function stripJsonComments(text) {
|
|
|
442
469
|
}
|
|
443
470
|
function readAndParse(file) {
|
|
444
471
|
try {
|
|
445
|
-
const raw =
|
|
472
|
+
const raw = fs2.readFileSync(file, "utf8");
|
|
446
473
|
return JSON.parse(stripJsonComments(raw));
|
|
447
474
|
} catch (e) {
|
|
448
475
|
log.warn("failed to parse opencode config", {
|
|
@@ -470,14 +497,14 @@ function deepMerge(target, source) {
|
|
|
470
497
|
}
|
|
471
498
|
function walkUp(opts) {
|
|
472
499
|
const out = [];
|
|
473
|
-
let current =
|
|
500
|
+
let current = path2.resolve(opts.start);
|
|
474
501
|
while (true) {
|
|
475
502
|
for (const target of opts.targets) {
|
|
476
|
-
const candidate =
|
|
503
|
+
const candidate = path2.join(current, target);
|
|
477
504
|
if (opts.predicate(candidate)) out.push(candidate);
|
|
478
505
|
}
|
|
479
|
-
if (opts.stop && current ===
|
|
480
|
-
const parent =
|
|
506
|
+
if (opts.stop && current === path2.resolve(opts.stop)) break;
|
|
507
|
+
const parent = path2.dirname(current);
|
|
481
508
|
if (parent === current) break;
|
|
482
509
|
current = parent;
|
|
483
510
|
}
|
|
@@ -485,28 +512,28 @@ function walkUp(opts) {
|
|
|
485
512
|
}
|
|
486
513
|
function detectWorktree(cwd) {
|
|
487
514
|
const override = process.env.OPENCODE_WORKTREE;
|
|
488
|
-
if (override) return
|
|
489
|
-
let current =
|
|
515
|
+
if (override) return path2.resolve(override);
|
|
516
|
+
let current = path2.resolve(cwd);
|
|
490
517
|
while (true) {
|
|
491
|
-
const gitPath =
|
|
518
|
+
const gitPath = path2.join(current, ".git");
|
|
492
519
|
try {
|
|
493
|
-
if (
|
|
520
|
+
if (fs2.existsSync(gitPath)) return current;
|
|
494
521
|
} catch {
|
|
495
522
|
}
|
|
496
|
-
const parent =
|
|
523
|
+
const parent = path2.dirname(current);
|
|
497
524
|
if (parent === current) return void 0;
|
|
498
525
|
current = parent;
|
|
499
526
|
}
|
|
500
527
|
}
|
|
501
528
|
function globalConfigDir() {
|
|
502
|
-
const xdg = process.env.XDG_CONFIG_HOME ??
|
|
503
|
-
return
|
|
529
|
+
const xdg = process.env.XDG_CONFIG_HOME ?? path2.join(os2.homedir(), ".config");
|
|
530
|
+
return path2.join(xdg, "opencode");
|
|
504
531
|
}
|
|
505
532
|
function loadGlobalConfig() {
|
|
506
533
|
const dir = globalConfigDir();
|
|
507
534
|
let merged = {};
|
|
508
535
|
for (const name of FILE_NAMES.slice().reverse()) {
|
|
509
|
-
const file =
|
|
536
|
+
const file = path2.join(dir, name);
|
|
510
537
|
if (!fileExists(file)) continue;
|
|
511
538
|
const parsed = readAndParse(file);
|
|
512
539
|
if (parsed) merged = deepMerge(merged, parsed);
|
|
@@ -516,7 +543,7 @@ function loadGlobalConfig() {
|
|
|
516
543
|
function loadProjectFilesInDir(dir) {
|
|
517
544
|
let merged = {};
|
|
518
545
|
for (const name of PROJECT_FILE_NAMES) {
|
|
519
|
-
const file =
|
|
546
|
+
const file = path2.join(dir, name);
|
|
520
547
|
if (!fileExists(file)) continue;
|
|
521
548
|
const parsed = readAndParse(file);
|
|
522
549
|
if (parsed) merged = deepMerge(merged, parsed);
|
|
@@ -527,7 +554,7 @@ function dotOpencodeDirs(cwd, worktree) {
|
|
|
527
554
|
const dirs = [];
|
|
528
555
|
const seen = /* @__PURE__ */ new Set();
|
|
529
556
|
const push = (p) => {
|
|
530
|
-
const abs =
|
|
557
|
+
const abs = path2.resolve(p);
|
|
531
558
|
if (!seen.has(abs) && dirExists(abs)) {
|
|
532
559
|
seen.add(abs);
|
|
533
560
|
dirs.push(abs);
|
|
@@ -541,9 +568,9 @@ function dotOpencodeDirs(cwd, worktree) {
|
|
|
541
568
|
})) {
|
|
542
569
|
push(dir);
|
|
543
570
|
}
|
|
544
|
-
const home =
|
|
571
|
+
const home = os2.homedir();
|
|
545
572
|
if (home) {
|
|
546
|
-
const homeDot =
|
|
573
|
+
const homeDot = path2.join(home, ".opencode");
|
|
547
574
|
if (dirExists(homeDot)) push(homeDot);
|
|
548
575
|
}
|
|
549
576
|
const envDir = process.env.OPENCODE_CONFIG_DIR;
|
|
@@ -610,7 +637,7 @@ function mergeMcp(target, source) {
|
|
|
610
637
|
}
|
|
611
638
|
return out;
|
|
612
639
|
}
|
|
613
|
-
function bridgeOpencodeMcp(cwd, runtimeStatus) {
|
|
640
|
+
function bridgeOpencodeMcp(cwd, runtimeStatus, excludeServers) {
|
|
614
641
|
const worktree = detectWorktree(cwd);
|
|
615
642
|
let merged = {};
|
|
616
643
|
merged = mergeMcp(merged, extractMcpBlock(loadGlobalConfig()));
|
|
@@ -628,7 +655,7 @@ function bridgeOpencodeMcp(cwd, runtimeStatus) {
|
|
|
628
655
|
const projectDirs = [];
|
|
629
656
|
const seenProjectDirs = /* @__PURE__ */ new Set();
|
|
630
657
|
for (const f of projectFiles) {
|
|
631
|
-
const d =
|
|
658
|
+
const d = path2.dirname(f);
|
|
632
659
|
if (!seenProjectDirs.has(d)) {
|
|
633
660
|
seenProjectDirs.add(d);
|
|
634
661
|
projectDirs.push(d);
|
|
@@ -649,22 +676,44 @@ function bridgeOpencodeMcp(cwd, runtimeStatus) {
|
|
|
649
676
|
merged[name] = { ...base, enabled: status === "connected" };
|
|
650
677
|
}
|
|
651
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
|
+
}
|
|
652
686
|
const servers = {};
|
|
687
|
+
const bridgedServerNames = [];
|
|
653
688
|
for (const [name, spec] of Object.entries(merged)) {
|
|
654
689
|
if (!spec || typeof spec !== "object") continue;
|
|
690
|
+
if (excludeServers?.has(name)) continue;
|
|
655
691
|
const translated = translateServer(name, spec);
|
|
656
|
-
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
|
+
};
|
|
657
708
|
}
|
|
658
|
-
if (Object.keys(servers).length === 0) return null;
|
|
659
709
|
const body = JSON.stringify({ mcpServers: servers }, null, 2);
|
|
660
|
-
const
|
|
661
|
-
|
|
662
|
-
|
|
663
|
-
`opencode-claude-code-mcp-${hash}.json`
|
|
710
|
+
const outPath = path2.join(
|
|
711
|
+
pluginTmpDir(),
|
|
712
|
+
`mcp-${hash}.json`
|
|
664
713
|
);
|
|
665
714
|
try {
|
|
666
715
|
if (!fileExists(outPath)) {
|
|
667
|
-
|
|
716
|
+
fs2.writeFileSync(outPath, body, { encoding: "utf8", mode: 384 });
|
|
668
717
|
}
|
|
669
718
|
} catch (e) {
|
|
670
719
|
log.warn("failed to write bridged MCP config", {
|
|
@@ -675,9 +724,15 @@ function bridgeOpencodeMcp(cwd, runtimeStatus) {
|
|
|
675
724
|
log.info("bridged opencode MCP config", {
|
|
676
725
|
target: outPath,
|
|
677
726
|
hash,
|
|
678
|
-
servers:
|
|
727
|
+
servers: bridgedServerNames,
|
|
728
|
+
excluded: excludeServers ? Array.from(excludeServers) : []
|
|
679
729
|
});
|
|
680
|
-
return {
|
|
730
|
+
return {
|
|
731
|
+
path: outPath,
|
|
732
|
+
hash,
|
|
733
|
+
serverNames: bridgedServerNames,
|
|
734
|
+
allEnabledServerNames
|
|
735
|
+
};
|
|
681
736
|
}
|
|
682
737
|
|
|
683
738
|
// src/runtime-status.ts
|
|
@@ -709,6 +764,35 @@ async function getRuntimeMcpStatus() {
|
|
|
709
764
|
return void 0;
|
|
710
765
|
}
|
|
711
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
|
+
}
|
|
712
796
|
|
|
713
797
|
// src/session-manager.ts
|
|
714
798
|
import { spawn } from "child_process";
|
|
@@ -872,14 +956,14 @@ function sessionKey(cwd, modelId) {
|
|
|
872
956
|
|
|
873
957
|
// src/proxy-mcp.ts
|
|
874
958
|
import { createServer } from "http";
|
|
875
|
-
import * as
|
|
876
|
-
import * as
|
|
877
|
-
import * as os2 from "os";
|
|
959
|
+
import * as fs3 from "fs";
|
|
960
|
+
import * as path3 from "path";
|
|
878
961
|
import * as crypto2 from "crypto";
|
|
879
962
|
import { EventEmitter as EventEmitter2 } from "events";
|
|
880
963
|
var PROTOCOL_VERSION = "2024-11-05";
|
|
881
964
|
var SERVER_NAME = "opencode_proxy";
|
|
882
965
|
var PROXY_TOOL_PREFIX = `mcp__${SERVER_NAME}__`;
|
|
966
|
+
var PROXY_CALL_TIMEOUT_MS = 10 * 60 * 1e3;
|
|
883
967
|
var DEFAULT_PROXY_TOOLS = [
|
|
884
968
|
{
|
|
885
969
|
name: "bash",
|
|
@@ -1050,6 +1134,7 @@ async function createProxyMcpServer(tools = DEFAULT_PROXY_TOOLS) {
|
|
|
1050
1134
|
toolName,
|
|
1051
1135
|
hasInput: input != null
|
|
1052
1136
|
});
|
|
1137
|
+
let timer = null;
|
|
1053
1138
|
const result = await new Promise(
|
|
1054
1139
|
(resolve3, reject) => {
|
|
1055
1140
|
const entry = {
|
|
@@ -1060,9 +1145,24 @@ async function createProxyMcpServer(tools = DEFAULT_PROXY_TOOLS) {
|
|
|
1060
1145
|
reject
|
|
1061
1146
|
};
|
|
1062
1147
|
pending.set(callId, entry);
|
|
1148
|
+
timer = setTimeout(() => {
|
|
1149
|
+
if (!pending.has(callId)) return;
|
|
1150
|
+
pending.delete(callId);
|
|
1151
|
+
log.warn("proxy-mcp tool call timed out", {
|
|
1152
|
+
callId,
|
|
1153
|
+
toolName,
|
|
1154
|
+
timeoutMs: PROXY_CALL_TIMEOUT_MS
|
|
1155
|
+
});
|
|
1156
|
+
reject(
|
|
1157
|
+
new Error(
|
|
1158
|
+
`Proxy tool '${toolName}' timed out after ${PROXY_CALL_TIMEOUT_MS}ms waiting for opencode to resolve the call`
|
|
1159
|
+
)
|
|
1160
|
+
);
|
|
1161
|
+
}, PROXY_CALL_TIMEOUT_MS);
|
|
1063
1162
|
calls.emit("call", entry);
|
|
1064
1163
|
}
|
|
1065
1164
|
).finally(() => {
|
|
1165
|
+
if (timer) clearTimeout(timer);
|
|
1066
1166
|
pending.delete(callId);
|
|
1067
1167
|
});
|
|
1068
1168
|
if (result.kind === "error") {
|
|
@@ -1151,11 +1251,11 @@ async function createProxyMcpServer(tools = DEFAULT_PROXY_TOOLS) {
|
|
|
1151
1251
|
2
|
|
1152
1252
|
);
|
|
1153
1253
|
const hash = crypto2.createHash("sha256").update(body).digest("hex").slice(0, 12);
|
|
1154
|
-
const outPath =
|
|
1155
|
-
|
|
1156
|
-
`
|
|
1254
|
+
const outPath = path3.join(
|
|
1255
|
+
pluginTmpDir(),
|
|
1256
|
+
`proxy-${hash}.json`
|
|
1157
1257
|
);
|
|
1158
|
-
|
|
1258
|
+
fs3.writeFileSync(outPath, body, { encoding: "utf8", mode: 384 });
|
|
1159
1259
|
configFilePath = outPath;
|
|
1160
1260
|
return outPath;
|
|
1161
1261
|
},
|
|
@@ -1167,6 +1267,13 @@ async function createProxyMcpServer(tools = DEFAULT_PROXY_TOOLS) {
|
|
|
1167
1267
|
await new Promise((resolve3) => {
|
|
1168
1268
|
server2.close(() => resolve3());
|
|
1169
1269
|
});
|
|
1270
|
+
if (configFilePath) {
|
|
1271
|
+
try {
|
|
1272
|
+
fs3.unlinkSync(configFilePath);
|
|
1273
|
+
} catch {
|
|
1274
|
+
}
|
|
1275
|
+
configFilePath = null;
|
|
1276
|
+
}
|
|
1170
1277
|
}
|
|
1171
1278
|
};
|
|
1172
1279
|
return api;
|
|
@@ -1214,6 +1321,7 @@ function writeJson(res, body) {
|
|
|
1214
1321
|
import { EventEmitter as EventEmitter3 } from "events";
|
|
1215
1322
|
var pendingBySession = /* @__PURE__ */ new Map();
|
|
1216
1323
|
var emitter = new EventEmitter3();
|
|
1324
|
+
var PENDING_PROXY_CALL_TIMEOUT_MS = 10 * 60 * 1e3;
|
|
1217
1325
|
function eventName(sessionKey2) {
|
|
1218
1326
|
return `pending:${sessionKey2}`;
|
|
1219
1327
|
}
|
|
@@ -1225,16 +1333,50 @@ function onPendingProxyCall(sessionKey2, handler) {
|
|
|
1225
1333
|
function queuePendingProxyCall(sessionKey2, call) {
|
|
1226
1334
|
const existing = pendingBySession.get(sessionKey2);
|
|
1227
1335
|
if (existing) {
|
|
1336
|
+
if (Date.now() - existing.createdAt < PENDING_PROXY_CALL_TIMEOUT_MS) {
|
|
1337
|
+
call.reject(
|
|
1338
|
+
new Error(`Another proxy tool call is already pending for ${sessionKey2}`)
|
|
1339
|
+
);
|
|
1340
|
+
log.warn("rejected overlapping proxy call", {
|
|
1341
|
+
sessionKey: sessionKey2,
|
|
1342
|
+
existingToolCallId: existing.toolCallId,
|
|
1343
|
+
existingToolName: existing.toolName,
|
|
1344
|
+
toolCallId: call.id,
|
|
1345
|
+
toolName: call.toolName
|
|
1346
|
+
});
|
|
1347
|
+
return existing;
|
|
1348
|
+
}
|
|
1349
|
+
clearTimeout(existing.timer);
|
|
1228
1350
|
existing.reject(
|
|
1229
|
-
new Error(
|
|
1351
|
+
new Error(
|
|
1352
|
+
`Stale proxy tool call expired after ${PENDING_PROXY_CALL_TIMEOUT_MS}ms for ${sessionKey2}`
|
|
1353
|
+
)
|
|
1230
1354
|
);
|
|
1231
1355
|
pendingBySession.delete(sessionKey2);
|
|
1232
1356
|
}
|
|
1357
|
+
const timer = setTimeout(() => {
|
|
1358
|
+
const current = pendingBySession.get(sessionKey2);
|
|
1359
|
+
if (!current || current.toolCallId !== call.id) return;
|
|
1360
|
+
pendingBySession.delete(sessionKey2);
|
|
1361
|
+
current.reject(
|
|
1362
|
+
new Error(
|
|
1363
|
+
`Proxy tool call '${call.toolName}' timed out after ${PENDING_PROXY_CALL_TIMEOUT_MS}ms waiting for opencode to resolve the call`
|
|
1364
|
+
)
|
|
1365
|
+
);
|
|
1366
|
+
log.warn("timed out pending proxy call", {
|
|
1367
|
+
sessionKey: sessionKey2,
|
|
1368
|
+
toolCallId: call.id,
|
|
1369
|
+
toolName: call.toolName,
|
|
1370
|
+
timeoutMs: PENDING_PROXY_CALL_TIMEOUT_MS
|
|
1371
|
+
});
|
|
1372
|
+
}, PENDING_PROXY_CALL_TIMEOUT_MS);
|
|
1233
1373
|
const pending = {
|
|
1234
1374
|
sessionKey: sessionKey2,
|
|
1235
1375
|
toolCallId: call.id,
|
|
1236
1376
|
toolName: call.toolName,
|
|
1237
1377
|
input: call.input,
|
|
1378
|
+
createdAt: Date.now(),
|
|
1379
|
+
timer,
|
|
1238
1380
|
resolve: call.resolve,
|
|
1239
1381
|
reject: call.reject
|
|
1240
1382
|
};
|
|
@@ -1254,6 +1396,7 @@ function resolvePendingProxyCall(sessionKey2, result) {
|
|
|
1254
1396
|
const pending = pendingBySession.get(sessionKey2);
|
|
1255
1397
|
if (!pending) return false;
|
|
1256
1398
|
pendingBySession.delete(sessionKey2);
|
|
1399
|
+
clearTimeout(pending.timer);
|
|
1257
1400
|
pending.resolve(result);
|
|
1258
1401
|
log.info("resolved pending proxy call", {
|
|
1259
1402
|
sessionKey: sessionKey2,
|
|
@@ -1266,9 +1409,9 @@ function resolvePendingProxyCall(sessionKey2, result) {
|
|
|
1266
1409
|
// src/claude-code-language-model.ts
|
|
1267
1410
|
import { readFileSync as readFileSync2, writeFileSync as writeFileSync3 } from "fs";
|
|
1268
1411
|
import { unlink as unlink2 } from "fs/promises";
|
|
1269
|
-
import { homedir as homedir2, tmpdir as
|
|
1412
|
+
import { homedir as homedir2, tmpdir as tmpdir2 } from "os";
|
|
1270
1413
|
import { randomUUID as randomUUID2 } from "crypto";
|
|
1271
|
-
import { dirname as dirname2, join as
|
|
1414
|
+
import { dirname as dirname2, join as join4 } from "path";
|
|
1272
1415
|
function hasNewUserContent(prompt) {
|
|
1273
1416
|
for (let i = prompt.length - 1; i >= 0; i--) {
|
|
1274
1417
|
const msg = prompt[i];
|
|
@@ -1283,14 +1426,15 @@ function hasNewUserContent(prompt) {
|
|
|
1283
1426
|
for (const part of content) {
|
|
1284
1427
|
if (part.type === "text" && part.text && part.text.trim()) return true;
|
|
1285
1428
|
if (part.type === "tool-result") return true;
|
|
1429
|
+
if (part.type === "image" || part.type === "file") return true;
|
|
1286
1430
|
}
|
|
1287
1431
|
}
|
|
1288
1432
|
}
|
|
1289
1433
|
return false;
|
|
1290
1434
|
}
|
|
1291
|
-
function readPromptFileIfPresent(
|
|
1435
|
+
function readPromptFileIfPresent(path5) {
|
|
1292
1436
|
try {
|
|
1293
|
-
const content = readFileSync2(
|
|
1437
|
+
const content = readFileSync2(path5, "utf8").trim();
|
|
1294
1438
|
return content || void 0;
|
|
1295
1439
|
} catch {
|
|
1296
1440
|
return void 0;
|
|
@@ -1299,7 +1443,7 @@ function readPromptFileIfPresent(path4) {
|
|
|
1299
1443
|
function nearestWorkspaceAgentsPrompt(cwd) {
|
|
1300
1444
|
let dir = cwd;
|
|
1301
1445
|
while (true) {
|
|
1302
|
-
const content = readPromptFileIfPresent(
|
|
1446
|
+
const content = readPromptFileIfPresent(join4(dir, "AGENTS.md"));
|
|
1303
1447
|
if (content) return content;
|
|
1304
1448
|
const parent = dirname2(dir);
|
|
1305
1449
|
if (parent === dir) return void 0;
|
|
@@ -1308,17 +1452,17 @@ function nearestWorkspaceAgentsPrompt(cwd) {
|
|
|
1308
1452
|
}
|
|
1309
1453
|
function buildAppendedSystemPrompt(cwd) {
|
|
1310
1454
|
const parts = [];
|
|
1311
|
-
const configRoot = process.env.XDG_CONFIG_HOME ??
|
|
1312
|
-
const globalAgents = readPromptFileIfPresent(
|
|
1455
|
+
const configRoot = process.env.XDG_CONFIG_HOME ?? join4(homedir2(), ".config");
|
|
1456
|
+
const globalAgents = readPromptFileIfPresent(join4(configRoot, "opencode", "AGENTS.md"));
|
|
1313
1457
|
const workspaceAgents = nearestWorkspaceAgentsPrompt(cwd);
|
|
1314
1458
|
if (globalAgents) parts.push(globalAgents);
|
|
1315
1459
|
if (workspaceAgents && workspaceAgents !== globalAgents) parts.push(workspaceAgents);
|
|
1316
1460
|
const content = parts.join("\n\n");
|
|
1317
1461
|
if (!content) return void 0;
|
|
1318
|
-
const
|
|
1462
|
+
const path5 = join4(tmpdir2(), `opencode-cc-sys-${randomUUID2()}.md`);
|
|
1319
1463
|
try {
|
|
1320
|
-
writeFileSync3(
|
|
1321
|
-
return
|
|
1464
|
+
writeFileSync3(path5, content, "utf8");
|
|
1465
|
+
return path5;
|
|
1322
1466
|
} catch (err) {
|
|
1323
1467
|
log.warn("failed to write system prompt file", { error: String(err) });
|
|
1324
1468
|
return void 0;
|
|
@@ -1381,18 +1525,20 @@ var ClaudeCodeLanguageModel = class {
|
|
|
1381
1525
|
* provided it overlays opencode's UI-toggled state on top of disk config
|
|
1382
1526
|
* so `/mcps` toggles propagate without a config file write.
|
|
1383
1527
|
*/
|
|
1384
|
-
effectiveMcpConfig(cwd, proxyConfigPath, runtimeStatus) {
|
|
1528
|
+
effectiveMcpConfig(cwd, proxyConfigPath, runtimeStatus, excludeServers) {
|
|
1385
1529
|
const paths = Array.isArray(this.config.mcpConfig) ? this.config.mcpConfig.slice() : this.config.mcpConfig ? [this.config.mcpConfig] : [];
|
|
1386
1530
|
let bridgedHash = null;
|
|
1531
|
+
let allEnabledServerNames = [];
|
|
1387
1532
|
if (this.config.bridgeOpencodeMcp !== false) {
|
|
1388
|
-
const bridged = bridgeOpencodeMcp(cwd, runtimeStatus);
|
|
1533
|
+
const bridged = bridgeOpencodeMcp(cwd, runtimeStatus, excludeServers);
|
|
1389
1534
|
if (bridged) {
|
|
1390
|
-
paths.push(bridged.path);
|
|
1535
|
+
if (bridged.path) paths.push(bridged.path);
|
|
1391
1536
|
bridgedHash = bridged.hash;
|
|
1537
|
+
allEnabledServerNames = bridged.allEnabledServerNames;
|
|
1392
1538
|
}
|
|
1393
1539
|
}
|
|
1394
1540
|
if (proxyConfigPath) paths.push(proxyConfigPath);
|
|
1395
|
-
return { paths, bridgedHash };
|
|
1541
|
+
return { paths, bridgedHash, allEnabledServerNames };
|
|
1396
1542
|
}
|
|
1397
1543
|
/** Resolve ProxyToolDef[] for the configured proxyTools names. */
|
|
1398
1544
|
resolvedProxyTools() {
|
|
@@ -1408,6 +1554,45 @@ var ClaudeCodeLanguageModel = class {
|
|
|
1408
1554
|
}
|
|
1409
1555
|
return picked.length > 0 ? picked : null;
|
|
1410
1556
|
}
|
|
1557
|
+
/**
|
|
1558
|
+
* Resolve ProxyToolDef[] for opencode's MCP-bridged tools so they go
|
|
1559
|
+
* through the in-process proxy instead of being bridged into Claude CLI's
|
|
1560
|
+
* `--mcp-config`. Direct bridging causes double execution because both
|
|
1561
|
+
* Claude CLI's own MCP child and opencode hold their own connection to
|
|
1562
|
+
* the same server; routing through the proxy keeps a single execution
|
|
1563
|
+
* site (opencode). Returns null when the feature is disabled, the SDK
|
|
1564
|
+
* client is unavailable, or no MCP servers are configured.
|
|
1565
|
+
*/
|
|
1566
|
+
async resolvedProxyMcpTools(allEnabledServerNames) {
|
|
1567
|
+
if (this.config.proxyOpencodeMcpTools === false) return null;
|
|
1568
|
+
if (this.config.bridgeOpencodeMcp === false) return null;
|
|
1569
|
+
if (allEnabledServerNames.length === 0) return null;
|
|
1570
|
+
const items = await fetchOpencodeToolList(
|
|
1571
|
+
this.config.provider,
|
|
1572
|
+
this.modelId,
|
|
1573
|
+
this.config.cwd
|
|
1574
|
+
);
|
|
1575
|
+
if (!items || items.length === 0) return null;
|
|
1576
|
+
const serversByLengthDesc = [...allEnabledServerNames].sort(
|
|
1577
|
+
(a, b) => b.length - a.length
|
|
1578
|
+
);
|
|
1579
|
+
const out = [];
|
|
1580
|
+
const seen = /* @__PURE__ */ new Set();
|
|
1581
|
+
for (const item of items) {
|
|
1582
|
+
const matchedServer = serversByLengthDesc.find(
|
|
1583
|
+
(name) => item.id === name || item.id.startsWith(`${name}_`)
|
|
1584
|
+
);
|
|
1585
|
+
if (!matchedServer) continue;
|
|
1586
|
+
if (seen.has(item.id)) continue;
|
|
1587
|
+
seen.add(item.id);
|
|
1588
|
+
out.push({
|
|
1589
|
+
name: item.id,
|
|
1590
|
+
description: item.description ?? "",
|
|
1591
|
+
inputSchema: item.parameters && typeof item.parameters === "object" ? item.parameters : { type: "object", properties: {} }
|
|
1592
|
+
});
|
|
1593
|
+
}
|
|
1594
|
+
return out.length > 0 ? out : null;
|
|
1595
|
+
}
|
|
1411
1596
|
/**
|
|
1412
1597
|
* Create a proxy MCP server for a single active Claude process/session.
|
|
1413
1598
|
* The process lifecycle owns the server lifecycle via session-manager.
|
|
@@ -1689,7 +1874,7 @@ var ClaudeCodeLanguageModel = class {
|
|
|
1689
1874
|
const scope = this.requestScope(options);
|
|
1690
1875
|
const affinity = this.sessionAffinity(options);
|
|
1691
1876
|
const sk = sessionKey(cwd, `${this.modelId}::${scope}::${affinity}`);
|
|
1692
|
-
if (scope === "tools" && this.resolvedProxyTools()) {
|
|
1877
|
+
if (scope === "tools" && (this.resolvedProxyTools() || this.config.proxyOpencodeMcpTools !== false && this.config.bridgeOpencodeMcp !== false)) {
|
|
1693
1878
|
return this.doGenerateViaStream(options);
|
|
1694
1879
|
}
|
|
1695
1880
|
if (scope === "no-tools") {
|
|
@@ -2086,8 +2271,18 @@ ${plan}
|
|
|
2086
2271
|
}
|
|
2087
2272
|
}
|
|
2088
2273
|
const setup = async () => {
|
|
2089
|
-
|
|
2090
|
-
|
|
2274
|
+
const discovery = self.effectiveMcpConfig(
|
|
2275
|
+
cwd,
|
|
2276
|
+
void 0,
|
|
2277
|
+
runtimeStatus
|
|
2278
|
+
);
|
|
2279
|
+
const proxyMcpTools = await self.resolvedProxyMcpTools(
|
|
2280
|
+
discovery.allEnabledServerNames
|
|
2281
|
+
);
|
|
2282
|
+
const excludeServers = proxyMcpTools ? new Set(discovery.allEnabledServerNames) : void 0;
|
|
2283
|
+
const combinedProxyTools = resolvedProxy || proxyMcpTools ? [...resolvedProxy ?? [], ...proxyMcpTools ?? []] : null;
|
|
2284
|
+
if (!proxyServer && combinedProxyTools) {
|
|
2285
|
+
proxyServer = await self.ensureProxyServer(combinedProxyTools, sk);
|
|
2091
2286
|
}
|
|
2092
2287
|
const proxyDisallowed = resolvedProxy ? disallowedToolFlags(resolvedProxy) : [];
|
|
2093
2288
|
const extraDisallowed = [];
|
|
@@ -2096,7 +2291,8 @@ ${plan}
|
|
|
2096
2291
|
const mcp = self.effectiveMcpConfig(
|
|
2097
2292
|
cwd,
|
|
2098
2293
|
proxyServer?.configPath(),
|
|
2099
|
-
runtimeStatus
|
|
2294
|
+
runtimeStatus,
|
|
2295
|
+
excludeServers
|
|
2100
2296
|
);
|
|
2101
2297
|
const systemPromptFile = activeProcess ? void 0 : buildAppendedSystemPrompt(cwd);
|
|
2102
2298
|
const cliArgs = buildCliArgs({
|
|
@@ -2889,7 +3085,7 @@ var defaultModels = {
|
|
|
2889
3085
|
|
|
2890
3086
|
// src/accounts.ts
|
|
2891
3087
|
import { chmod, lstat, mkdir, readlink, symlink, writeFile } from "fs/promises";
|
|
2892
|
-
import
|
|
3088
|
+
import path4 from "path";
|
|
2893
3089
|
var BASE_PROVIDER_ID = "claude-code";
|
|
2894
3090
|
var DEFAULT_ACCOUNT = "default";
|
|
2895
3091
|
var SHARED_CAPABILITY_ITEMS = [
|
|
@@ -2927,7 +3123,7 @@ function expandHome(value) {
|
|
|
2927
3123
|
const home = process.env.HOME ?? process.env.USERPROFILE;
|
|
2928
3124
|
if (value === "~") return home ?? value;
|
|
2929
3125
|
if (value.startsWith("~/") || value.startsWith("~\\")) {
|
|
2930
|
-
return home ?
|
|
3126
|
+
return home ? path4.join(home, value.slice(2)) : value;
|
|
2931
3127
|
}
|
|
2932
3128
|
return value;
|
|
2933
3129
|
}
|
|
@@ -2959,8 +3155,8 @@ async function ensureSharedCapabilities(targetRoot) {
|
|
|
2959
3155
|
}
|
|
2960
3156
|
}
|
|
2961
3157
|
async function ensureSharedCapabilityItem(sourceRoot, targetRoot, item) {
|
|
2962
|
-
const source =
|
|
2963
|
-
const target =
|
|
3158
|
+
const source = path4.join(sourceRoot, item);
|
|
3159
|
+
const target = path4.join(targetRoot, item);
|
|
2964
3160
|
let sourceStat;
|
|
2965
3161
|
try {
|
|
2966
3162
|
sourceStat = await lstat(source);
|
|
@@ -2971,8 +3167,8 @@ async function ensureSharedCapabilityItem(sourceRoot, targetRoot, item) {
|
|
|
2971
3167
|
const targetStat = await lstat(target);
|
|
2972
3168
|
if (targetStat.isSymbolicLink()) {
|
|
2973
3169
|
const current = await readlink(target);
|
|
2974
|
-
const resolvedCurrent =
|
|
2975
|
-
const resolvedSource =
|
|
3170
|
+
const resolvedCurrent = path4.resolve(path4.dirname(target), current);
|
|
3171
|
+
const resolvedSource = path4.resolve(source);
|
|
2976
3172
|
if (resolvedCurrent === resolvedSource) return;
|
|
2977
3173
|
}
|
|
2978
3174
|
log.warn("shared Claude capability already exists; leaving untouched", {
|
|
@@ -2987,11 +3183,11 @@ async function ensureSharedCapabilityItem(sourceRoot, targetRoot, item) {
|
|
|
2987
3183
|
await symlink(source, target, type);
|
|
2988
3184
|
}
|
|
2989
3185
|
async function writeAccountWrapper(account, baseCliPath, configDir) {
|
|
2990
|
-
const cacheRoot =
|
|
3186
|
+
const cacheRoot = path4.join(
|
|
2991
3187
|
process.env.XDG_CACHE_HOME ?? expandHome("~/.cache"),
|
|
2992
3188
|
"opencode-claude-code-plugin"
|
|
2993
3189
|
);
|
|
2994
|
-
const wrapperPath =
|
|
3190
|
+
const wrapperPath = path4.join(cacheRoot, `claude-${account}`);
|
|
2995
3191
|
const suffix = `@${account}`;
|
|
2996
3192
|
await mkdir(cacheRoot, { recursive: true });
|
|
2997
3193
|
const script = `#!/usr/bin/env bash
|
|
@@ -3031,14 +3227,14 @@ function titleizeAccount(account) {
|
|
|
3031
3227
|
|
|
3032
3228
|
// src/cleanup-stale.ts
|
|
3033
3229
|
import {
|
|
3034
|
-
existsSync as
|
|
3230
|
+
existsSync as existsSync3,
|
|
3035
3231
|
readFileSync as readFileSync3,
|
|
3036
3232
|
realpathSync,
|
|
3037
|
-
rmSync,
|
|
3233
|
+
rmSync as rmSync2,
|
|
3038
3234
|
writeFileSync as writeFileSync4
|
|
3039
3235
|
} from "fs";
|
|
3040
3236
|
import { homedir as homedir3 } from "os";
|
|
3041
|
-
import { join as
|
|
3237
|
+
import { join as join5, resolve as resolve2 } from "path";
|
|
3042
3238
|
import { fileURLToPath } from "url";
|
|
3043
3239
|
var STALE_PACKAGE_NAME = "opencode-claude-code-plugin";
|
|
3044
3240
|
var SUSPECT_DESCRIPTION_TOKEN = "Claude Code";
|
|
@@ -3046,18 +3242,18 @@ var alreadyRan = false;
|
|
|
3046
3242
|
function candidateCacheRoots() {
|
|
3047
3243
|
const xdg = process.env.XDG_CACHE_HOME;
|
|
3048
3244
|
return [
|
|
3049
|
-
xdg ?
|
|
3050
|
-
|
|
3051
|
-
|
|
3245
|
+
xdg ? join5(xdg, "opencode") : null,
|
|
3246
|
+
join5(homedir3(), ".cache", "opencode"),
|
|
3247
|
+
join5(homedir3(), "Library", "Caches", "opencode")
|
|
3052
3248
|
].filter((p) => Boolean(p));
|
|
3053
3249
|
}
|
|
3054
3250
|
function userOpencodeJsonPath() {
|
|
3055
|
-
const xdgConfig = process.env.XDG_CONFIG_HOME ??
|
|
3056
|
-
return
|
|
3251
|
+
const xdgConfig = process.env.XDG_CONFIG_HOME ?? join5(homedir3(), ".config");
|
|
3252
|
+
return join5(xdgConfig, "opencode", "opencode.json");
|
|
3057
3253
|
}
|
|
3058
3254
|
function userIntendsToUseUnscoped() {
|
|
3059
3255
|
const cfg = userOpencodeJsonPath();
|
|
3060
|
-
if (!
|
|
3256
|
+
if (!existsSync3(cfg)) return false;
|
|
3061
3257
|
try {
|
|
3062
3258
|
const json = JSON.parse(readFileSync3(cfg, "utf8"));
|
|
3063
3259
|
const plugins = json.plugin;
|
|
@@ -3095,17 +3291,17 @@ function cleanupStaleUnscopedInstall() {
|
|
|
3095
3291
|
}
|
|
3096
3292
|
}
|
|
3097
3293
|
function cleanupOne(cacheRoot, ourDir) {
|
|
3098
|
-
if (!
|
|
3099
|
-
const stalePath =
|
|
3100
|
-
if (!
|
|
3294
|
+
if (!existsSync3(cacheRoot)) return;
|
|
3295
|
+
const stalePath = join5(cacheRoot, "node_modules", STALE_PACKAGE_NAME);
|
|
3296
|
+
if (!existsSync3(stalePath)) return;
|
|
3101
3297
|
let realStalePath = stalePath;
|
|
3102
3298
|
try {
|
|
3103
3299
|
realStalePath = realpathSync(stalePath);
|
|
3104
3300
|
} catch {
|
|
3105
3301
|
}
|
|
3106
3302
|
if (ourDir && realStalePath === ourDir) return;
|
|
3107
|
-
const pkgJsonPath =
|
|
3108
|
-
if (!
|
|
3303
|
+
const pkgJsonPath = join5(stalePath, "package.json");
|
|
3304
|
+
if (!existsSync3(pkgJsonPath)) return;
|
|
3109
3305
|
let pkg = {};
|
|
3110
3306
|
try {
|
|
3111
3307
|
pkg = JSON.parse(readFileSync3(pkgJsonPath, "utf8"));
|
|
@@ -3116,7 +3312,7 @@ function cleanupOne(cacheRoot, ourDir) {
|
|
|
3116
3312
|
if (!pkg.description?.includes(SUSPECT_DESCRIPTION_TOKEN)) return;
|
|
3117
3313
|
log.info("cleanup-stale: removing unscoped install", { stalePath });
|
|
3118
3314
|
try {
|
|
3119
|
-
|
|
3315
|
+
rmSync2(stalePath, { recursive: true, force: true });
|
|
3120
3316
|
} catch (err) {
|
|
3121
3317
|
log.warn("cleanup-stale: rmSync failed", {
|
|
3122
3318
|
stalePath,
|
|
@@ -3124,8 +3320,8 @@ function cleanupOne(cacheRoot, ourDir) {
|
|
|
3124
3320
|
});
|
|
3125
3321
|
return;
|
|
3126
3322
|
}
|
|
3127
|
-
const cachePkgJson =
|
|
3128
|
-
if (!
|
|
3323
|
+
const cachePkgJson = join5(cacheRoot, "package.json");
|
|
3324
|
+
if (!existsSync3(cachePkgJson)) return;
|
|
3129
3325
|
try {
|
|
3130
3326
|
const cfg = JSON.parse(readFileSync3(cachePkgJson, "utf8"));
|
|
3131
3327
|
if (cfg?.dependencies?.[STALE_PACKAGE_NAME]) {
|
|
@@ -3174,7 +3370,8 @@ function createClaudeCode(settings = {}) {
|
|
|
3174
3370
|
controlRequestDenyMessage: settings.controlRequestDenyMessage,
|
|
3175
3371
|
proxyTools,
|
|
3176
3372
|
webSearch: settings.webSearch,
|
|
3177
|
-
hotReloadMcp: settings.hotReloadMcp ?? true
|
|
3373
|
+
hotReloadMcp: settings.hotReloadMcp ?? true,
|
|
3374
|
+
proxyOpencodeMcpTools: settings.proxyOpencodeMcpTools ?? true
|
|
3178
3375
|
});
|
|
3179
3376
|
};
|
|
3180
3377
|
const provider = function(modelId) {
|
|
@@ -3342,12 +3539,12 @@ var server = async (input) => {
|
|
|
3342
3539
|
config.provider ??= {};
|
|
3343
3540
|
const expanded = await expandAccountProviders(config);
|
|
3344
3541
|
if (expanded) {
|
|
3345
|
-
const
|
|
3542
|
+
const registered2 = Object.entries(config.provider).filter(([id]) => id === PROVIDER_ID2 || id.startsWith(`${PROVIDER_ID2}-`)).map(([id, p]) => ({
|
|
3346
3543
|
id,
|
|
3347
3544
|
name: p?.name ?? id,
|
|
3348
3545
|
cwd: p?.options?.cwd
|
|
3349
3546
|
}));
|
|
3350
|
-
log.notice("registered claude-code providers", { providers:
|
|
3547
|
+
log.notice("registered claude-code providers", { providers: registered2 });
|
|
3351
3548
|
return;
|
|
3352
3549
|
}
|
|
3353
3550
|
const existing = config.provider[PROVIDER_ID2];
|