@khalilgharbaoui/opencode-claude-code-plugin 0.18.3 → 0.20.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +205 -63
- package/dist/index.d.ts +100 -22
- package/dist/index.js +2383 -1470
- package/dist/index.js.map +1 -1
- package/package.json +2 -2
- package/skills/claude-code-plugin/SKILL.md +60 -15
package/dist/index.js
CHANGED
|
@@ -324,8 +324,8 @@ function mapTool(name, input, opts) {
|
|
|
324
324
|
}
|
|
325
325
|
if (name === "TaskUpdate") {
|
|
326
326
|
if (opts?.sessionId) {
|
|
327
|
-
const
|
|
328
|
-
if (
|
|
327
|
+
const list2 = applyTaskUpdate(opts.sessionId, input);
|
|
328
|
+
if (list2 !== null) return emitTodoWrite(list2);
|
|
329
329
|
}
|
|
330
330
|
return { name, input, executed: true, skip: true };
|
|
331
331
|
}
|
|
@@ -475,9 +475,6 @@ import { randomUUID as randomUUID3 } from "crypto";
|
|
|
475
475
|
import { EventEmitter as EventEmitter3 } from "events";
|
|
476
476
|
import { unlink } from "fs/promises";
|
|
477
477
|
|
|
478
|
-
// src/proxy-broker.ts
|
|
479
|
-
import { EventEmitter as EventEmitter2 } from "events";
|
|
480
|
-
|
|
481
478
|
// src/proxy-mcp.ts
|
|
482
479
|
import { createServer } from "http";
|
|
483
480
|
import * as fs2 from "fs";
|
|
@@ -511,7 +508,7 @@ function pluginTmpDir() {
|
|
|
511
508
|
}
|
|
512
509
|
|
|
513
510
|
// src/proxy-mcp.ts
|
|
514
|
-
var
|
|
511
|
+
var PROXY_KEEPALIVE_MS = 15e3;
|
|
515
512
|
function acceptsEventStream(acceptHeader) {
|
|
516
513
|
return typeof acceptHeader === "string" && acceptHeader.toLowerCase().includes("text/event-stream");
|
|
517
514
|
}
|
|
@@ -523,11 +520,11 @@ var PROTOCOL_VERSION = "2024-11-05";
|
|
|
523
520
|
var SERVER_NAME = "opencode_proxy";
|
|
524
521
|
var PROXY_TOOL_PREFIX = `mcp__${SERVER_NAME}__`;
|
|
525
522
|
var PROXY_DEFAULT_TIMEOUT_MS = 10 * 60 * 1e3;
|
|
523
|
+
var PROXY_NO_DEADLINE_MS = 0;
|
|
526
524
|
var PROXY_PER_TOOL_DEFAULT_TIMEOUT_MS = {
|
|
527
|
-
task:
|
|
528
|
-
|
|
529
|
-
|
|
530
|
-
// 60 min, same reasoning: it IS task calls
|
|
525
|
+
task: PROXY_NO_DEADLINE_MS,
|
|
526
|
+
task_batch: PROXY_NO_DEADLINE_MS,
|
|
527
|
+
// same reasoning: it IS task calls
|
|
531
528
|
question: 30 * 60 * 1e3
|
|
532
529
|
// 30 min
|
|
533
530
|
};
|
|
@@ -537,7 +534,7 @@ function resolveProxyCallTimeoutMs(toolName, input, overrides) {
|
|
|
537
534
|
let ms = PROXY_PER_TOOL_DEFAULT_TIMEOUT_MS[key] ?? PROXY_DEFAULT_TIMEOUT_MS;
|
|
538
535
|
if (overrides) {
|
|
539
536
|
const ov = lookupCaseInsensitive(overrides, key);
|
|
540
|
-
if (
|
|
537
|
+
if (isDeadlineOverride(ov)) ms = ov;
|
|
541
538
|
}
|
|
542
539
|
if (key === "bash") {
|
|
543
540
|
const requested = input?.timeout;
|
|
@@ -545,6 +542,9 @@ function resolveProxyCallTimeoutMs(toolName, input, overrides) {
|
|
|
545
542
|
}
|
|
546
543
|
return Math.min(ms, MAX_PROXY_TIMEOUT_MS);
|
|
547
544
|
}
|
|
545
|
+
function isDeadlineOverride(value) {
|
|
546
|
+
return typeof value === "number" && Number.isFinite(value) && value >= 0;
|
|
547
|
+
}
|
|
548
548
|
function lookupCaseInsensitive(map, key) {
|
|
549
549
|
if (Object.prototype.hasOwnProperty.call(map, key)) return map[key];
|
|
550
550
|
for (const k of Object.keys(map)) {
|
|
@@ -554,14 +554,20 @@ function lookupCaseInsensitive(map, key) {
|
|
|
554
554
|
}
|
|
555
555
|
function resolveProxyClientCeilingMs(overrides) {
|
|
556
556
|
let ms = PROXY_DEFAULT_TIMEOUT_MS;
|
|
557
|
-
|
|
558
|
-
if (
|
|
559
|
-
|
|
560
|
-
|
|
561
|
-
|
|
562
|
-
|
|
557
|
+
const consider = (deadlineMs) => {
|
|
558
|
+
if (deadlineMs === PROXY_NO_DEADLINE_MS) return true;
|
|
559
|
+
if (deadlineMs > ms) ms = deadlineMs;
|
|
560
|
+
return false;
|
|
561
|
+
};
|
|
562
|
+
for (const [toolName, defaultMs] of Object.entries(PROXY_PER_TOOL_DEFAULT_TIMEOUT_MS)) {
|
|
563
|
+
const override = overrides ? lookupCaseInsensitive(overrides, toolName) : void 0;
|
|
564
|
+
if (consider(isDeadlineOverride(override) ? override : defaultMs)) {
|
|
565
|
+
return MAX_PROXY_TIMEOUT_MS;
|
|
563
566
|
}
|
|
564
567
|
}
|
|
568
|
+
for (const value of Object.values(overrides ?? {})) {
|
|
569
|
+
if (isDeadlineOverride(value) && consider(value)) return MAX_PROXY_TIMEOUT_MS;
|
|
570
|
+
}
|
|
565
571
|
return Math.min(ms, MAX_PROXY_TIMEOUT_MS);
|
|
566
572
|
}
|
|
567
573
|
function buildProxyTimeoutError(toolName, ms) {
|
|
@@ -574,9 +580,9 @@ function buildProxyTimeoutError(toolName, ms) {
|
|
|
574
580
|
}
|
|
575
581
|
return new Error(base);
|
|
576
582
|
}
|
|
577
|
-
var TASK_PROXY_NOTE = "This and task_batch are the ONLY tools that dispatch opencode subagents (including user @-mentions). Claude Code's built-in TaskCreate/TaskUpdate manage a local todo list and cannot dispatch subagents. Do not search config files to verify a subagent type exists: invalid types fail fast with a clear error. Foreground calls block until the subagent finishes; set `background` to request opencode's background execution mode. For two or more independent subagents in one response use task_batch, not several task calls: those run one after another. Task calls
|
|
583
|
+
var TASK_PROXY_NOTE = "This and task_batch are the ONLY tools that dispatch opencode subagents (including user @-mentions). Claude Code's built-in TaskCreate/TaskUpdate manage a local todo list and cannot dispatch subagents. Do not search config files to verify a subagent type exists: invalid types fail fast with a clear error. Foreground calls block until the subagent finishes; set `background` to request opencode's background execution mode. For two or more independent subagents in one response use task_batch, not several task calls: those run one after another. Task calls have no proxy deadline by default: the call waits for the subagent to finish (a positive proxyToolTimeoutMs override adds a deadline).";
|
|
578
584
|
var TASK_BATCH_TOOL_NAME = "task_batch";
|
|
579
|
-
var TASK_BATCH_PROXY_NOTE = "Use this instead of several task calls in one response: Claude Code runs MCP tool calls one at a time, so separate task calls run serially even when emitted together, while one task_batch call fans them out as parallel opencode task calls. Each task takes the same fields as the task tool. Results come back in task order, each labelled.
|
|
585
|
+
var TASK_BATCH_PROXY_NOTE = "Use this instead of several task calls in one response: Claude Code runs MCP tool calls one at a time, so separate task calls run serially even when emitted together, while one task_batch call fans them out as parallel opencode task calls. Each task takes the same fields as the task tool. Results come back in task order, each labelled. Like task it has no proxy deadline by default (a positive proxyToolTimeoutMs override adds one).";
|
|
580
586
|
var TASK_INPUT_REQUIRED = ["description", "prompt", "subagent_type"];
|
|
581
587
|
function taskBatchInputError(input) {
|
|
582
588
|
const tasks = input?.tasks;
|
|
@@ -874,9 +880,10 @@ var DEFAULT_PROXY_TOOLS = [
|
|
|
874
880
|
}
|
|
875
881
|
}
|
|
876
882
|
];
|
|
877
|
-
async function createProxyMcpServer(tools = DEFAULT_PROXY_TOOLS, timeoutOverrides, interceptors) {
|
|
883
|
+
async function createProxyMcpServer(tools = DEFAULT_PROXY_TOOLS, timeoutOverrides, interceptors, options = {}) {
|
|
878
884
|
const calls = new EventEmitter();
|
|
879
885
|
const pending = /* @__PURE__ */ new Map();
|
|
886
|
+
const keepaliveMs = options.keepaliveMs ?? PROXY_KEEPALIVE_MS;
|
|
880
887
|
const authToken = crypto.randomBytes(32).toString("hex");
|
|
881
888
|
const expectedAuth = Buffer.from(`Bearer ${authToken}`);
|
|
882
889
|
let boundAuthority = "";
|
|
@@ -925,7 +932,7 @@ async function createProxyMcpServer(tools = DEFAULT_PROXY_TOOLS, timeoutOverride
|
|
|
925
932
|
}
|
|
926
933
|
let requestId = null;
|
|
927
934
|
let requestMethod = null;
|
|
928
|
-
let
|
|
935
|
+
let reply = null;
|
|
929
936
|
try {
|
|
930
937
|
const body = await readBody(req);
|
|
931
938
|
const request = JSON.parse(body);
|
|
@@ -1020,11 +1027,9 @@ async function createProxyMcpServer(tools = DEFAULT_PROXY_TOOLS, timeoutOverride
|
|
|
1020
1027
|
sse: acceptsEventStream(req.headers.accept)
|
|
1021
1028
|
});
|
|
1022
1029
|
const channel = { closed: false };
|
|
1023
|
-
|
|
1024
|
-
sse = openEventStream(res);
|
|
1025
|
-
}
|
|
1030
|
+
reply = acceptsEventStream(req.headers.accept) ? openEventStream(res, keepaliveMs) : openJsonStream(res, keepaliveMs);
|
|
1026
1031
|
res.once("close", () => {
|
|
1027
|
-
|
|
1032
|
+
reply?.stop();
|
|
1028
1033
|
if (res.writableFinished) return;
|
|
1029
1034
|
channel.closed = true;
|
|
1030
1035
|
log.notice("proxy-mcp client closed a tool call before its result", {
|
|
@@ -1049,16 +1054,18 @@ async function createProxyMcpServer(tools = DEFAULT_PROXY_TOOLS, timeoutOverride
|
|
|
1049
1054
|
input,
|
|
1050
1055
|
timeoutOverrides
|
|
1051
1056
|
);
|
|
1052
|
-
|
|
1053
|
-
|
|
1054
|
-
|
|
1055
|
-
|
|
1056
|
-
|
|
1057
|
-
|
|
1058
|
-
|
|
1059
|
-
|
|
1060
|
-
|
|
1061
|
-
|
|
1057
|
+
if (deadlineMs > PROXY_NO_DEADLINE_MS) {
|
|
1058
|
+
timer = setTimeout(() => {
|
|
1059
|
+
if (!pending.has(callId)) return;
|
|
1060
|
+
pending.delete(callId);
|
|
1061
|
+
log.notice("proxy-mcp tool call timed out", {
|
|
1062
|
+
callId,
|
|
1063
|
+
toolName,
|
|
1064
|
+
deadlineMs
|
|
1065
|
+
});
|
|
1066
|
+
reject2(buildProxyTimeoutError(toolName, deadlineMs));
|
|
1067
|
+
}, deadlineMs);
|
|
1068
|
+
}
|
|
1062
1069
|
calls.emit("call", entry);
|
|
1063
1070
|
}
|
|
1064
1071
|
).finally(() => {
|
|
@@ -1072,7 +1079,7 @@ async function createProxyMcpServer(tools = DEFAULT_PROXY_TOOLS, timeoutOverride
|
|
|
1072
1079
|
});
|
|
1073
1080
|
return;
|
|
1074
1081
|
}
|
|
1075
|
-
writeToolCallResult(res, requestId, result,
|
|
1082
|
+
writeToolCallResult(res, requestId, result, reply);
|
|
1076
1083
|
return;
|
|
1077
1084
|
}
|
|
1078
1085
|
writeJson(res, {
|
|
@@ -1092,7 +1099,7 @@ async function createProxyMcpServer(tools = DEFAULT_PROXY_TOOLS, timeoutOverride
|
|
|
1092
1099
|
res,
|
|
1093
1100
|
requestId,
|
|
1094
1101
|
{ kind: "error", message: errorMessage },
|
|
1095
|
-
|
|
1102
|
+
reply
|
|
1096
1103
|
);
|
|
1097
1104
|
} catch {
|
|
1098
1105
|
try {
|
|
@@ -1174,6 +1181,9 @@ async function createProxyMcpServer(tools = DEFAULT_PROXY_TOOLS, timeoutOverride
|
|
|
1174
1181
|
configFilePath = outPath;
|
|
1175
1182
|
return outPath;
|
|
1176
1183
|
},
|
|
1184
|
+
pendingCallIds() {
|
|
1185
|
+
return [...pending.keys()];
|
|
1186
|
+
},
|
|
1177
1187
|
async close() {
|
|
1178
1188
|
for (const entry of pending.values()) {
|
|
1179
1189
|
entry.reject(new Error(SERVER_CLOSED_MESSAGE));
|
|
@@ -1246,7 +1256,7 @@ function readBody(req) {
|
|
|
1246
1256
|
req.on("error", reject);
|
|
1247
1257
|
});
|
|
1248
1258
|
}
|
|
1249
|
-
function writeToolCallResult(res, requestId, result,
|
|
1259
|
+
function writeToolCallResult(res, requestId, result, reply = null) {
|
|
1250
1260
|
const text = result.kind === "error" ? result.message : result.text;
|
|
1251
1261
|
const isError = result.kind === "error" || result.isError === true;
|
|
1252
1262
|
const envelope = {
|
|
@@ -1257,26 +1267,20 @@ function writeToolCallResult(res, requestId, result, sse = null) {
|
|
|
1257
1267
|
isError
|
|
1258
1268
|
}
|
|
1259
1269
|
};
|
|
1260
|
-
if (
|
|
1261
|
-
|
|
1270
|
+
if (reply) {
|
|
1271
|
+
reply.finish(envelope);
|
|
1262
1272
|
return;
|
|
1263
1273
|
}
|
|
1264
1274
|
writeJson(res, envelope);
|
|
1265
1275
|
}
|
|
1266
|
-
function
|
|
1267
|
-
res.statusCode = 200;
|
|
1268
|
-
res.setHeader("Content-Type", "text/event-stream");
|
|
1269
|
-
res.setHeader("Cache-Control", "no-cache, no-transform");
|
|
1270
|
-
res.setHeader("Connection", "keep-alive");
|
|
1271
|
-
res.flushHeaders();
|
|
1272
|
-
res.write(": open\n\n");
|
|
1276
|
+
function startKeepalive(res, keepaliveMs, ping) {
|
|
1273
1277
|
let timer = setInterval(() => {
|
|
1274
1278
|
if (res.writableEnded || res.destroyed) {
|
|
1275
1279
|
stop();
|
|
1276
1280
|
return;
|
|
1277
1281
|
}
|
|
1278
|
-
res.write(
|
|
1279
|
-
},
|
|
1282
|
+
res.write(ping);
|
|
1283
|
+
}, keepaliveMs);
|
|
1280
1284
|
timer.unref?.();
|
|
1281
1285
|
const stop = () => {
|
|
1282
1286
|
if (timer) {
|
|
@@ -1284,6 +1288,16 @@ function openEventStream(res) {
|
|
|
1284
1288
|
timer = null;
|
|
1285
1289
|
}
|
|
1286
1290
|
};
|
|
1291
|
+
return stop;
|
|
1292
|
+
}
|
|
1293
|
+
function openEventStream(res, keepaliveMs) {
|
|
1294
|
+
res.statusCode = 200;
|
|
1295
|
+
res.setHeader("Content-Type", "text/event-stream");
|
|
1296
|
+
res.setHeader("Cache-Control", "no-cache, no-transform");
|
|
1297
|
+
res.setHeader("Connection", "keep-alive");
|
|
1298
|
+
res.flushHeaders();
|
|
1299
|
+
res.write(": open\n\n");
|
|
1300
|
+
const stop = startKeepalive(res, keepaliveMs, ": keepalive\n\n");
|
|
1287
1301
|
return {
|
|
1288
1302
|
stop,
|
|
1289
1303
|
finish(envelope) {
|
|
@@ -1296,6 +1310,21 @@ data: ${JSON.stringify(envelope)}
|
|
|
1296
1310
|
}
|
|
1297
1311
|
};
|
|
1298
1312
|
}
|
|
1313
|
+
function openJsonStream(res, keepaliveMs) {
|
|
1314
|
+
res.statusCode = 200;
|
|
1315
|
+
res.setHeader("Content-Type", "application/json");
|
|
1316
|
+
res.setHeader("Cache-Control", "no-cache, no-transform");
|
|
1317
|
+
res.flushHeaders();
|
|
1318
|
+
const stop = startKeepalive(res, keepaliveMs, " ");
|
|
1319
|
+
return {
|
|
1320
|
+
stop,
|
|
1321
|
+
finish(envelope) {
|
|
1322
|
+
stop();
|
|
1323
|
+
if (res.writableEnded || res.destroyed) return;
|
|
1324
|
+
res.end(JSON.stringify(envelope));
|
|
1325
|
+
}
|
|
1326
|
+
};
|
|
1327
|
+
}
|
|
1299
1328
|
function writeJson(res, body) {
|
|
1300
1329
|
if (res.destroyed || res.writableEnded) return;
|
|
1301
1330
|
const payload = JSON.stringify(body);
|
|
@@ -1306,6 +1335,7 @@ function writeJson(res, body) {
|
|
|
1306
1335
|
}
|
|
1307
1336
|
|
|
1308
1337
|
// src/proxy-broker.ts
|
|
1338
|
+
import { EventEmitter as EventEmitter2 } from "events";
|
|
1309
1339
|
var pendingByCallId = /* @__PURE__ */ new Map();
|
|
1310
1340
|
var callIdsBySession = /* @__PURE__ */ new Map();
|
|
1311
1341
|
var emitter = new EventEmitter2();
|
|
@@ -1334,7 +1364,7 @@ function onPendingProxyCall(sessionKey2, handler) {
|
|
|
1334
1364
|
function queuePendingProxyCall(sessionKey2, call, timeoutOverrides) {
|
|
1335
1365
|
const previous = pendingByCallId.get(call.id);
|
|
1336
1366
|
if (previous) {
|
|
1337
|
-
clearTimeout(previous.timer);
|
|
1367
|
+
if (previous.timer) clearTimeout(previous.timer);
|
|
1338
1368
|
previous.reject(
|
|
1339
1369
|
new Error(`Replaced pending proxy call ${call.id} with a fresh one`)
|
|
1340
1370
|
);
|
|
@@ -1346,7 +1376,7 @@ function queuePendingProxyCall(sessionKey2, call, timeoutOverrides) {
|
|
|
1346
1376
|
call.input,
|
|
1347
1377
|
timeoutOverrides
|
|
1348
1378
|
);
|
|
1349
|
-
const timer = setTimeout(() => {
|
|
1379
|
+
const timer = deadlineMs > PROXY_NO_DEADLINE_MS ? setTimeout(() => {
|
|
1350
1380
|
const current = pendingByCallId.get(call.id);
|
|
1351
1381
|
if (!current) return;
|
|
1352
1382
|
pendingByCallId.delete(call.id);
|
|
@@ -1358,7 +1388,7 @@ function queuePendingProxyCall(sessionKey2, call, timeoutOverrides) {
|
|
|
1358
1388
|
toolName: call.toolName,
|
|
1359
1389
|
deadlineMs
|
|
1360
1390
|
});
|
|
1361
|
-
}, deadlineMs);
|
|
1391
|
+
}, deadlineMs) : null;
|
|
1362
1392
|
const pending = {
|
|
1363
1393
|
sessionKey: sessionKey2,
|
|
1364
1394
|
toolCallId: call.id,
|
|
@@ -1366,6 +1396,7 @@ function queuePendingProxyCall(sessionKey2, call, timeoutOverrides) {
|
|
|
1366
1396
|
input: call.input,
|
|
1367
1397
|
channel: call.channel,
|
|
1368
1398
|
createdAt: Date.now(),
|
|
1399
|
+
deadlineMs,
|
|
1369
1400
|
timer,
|
|
1370
1401
|
resolve: call.resolve,
|
|
1371
1402
|
reject: call.reject
|
|
@@ -1397,12 +1428,27 @@ function getPendingProxyCalls(sessionKey2) {
|
|
|
1397
1428
|
}
|
|
1398
1429
|
return out;
|
|
1399
1430
|
}
|
|
1431
|
+
function snapshotPendingProxyCalls(now = Date.now()) {
|
|
1432
|
+
const out = [];
|
|
1433
|
+
for (const pending of pendingByCallId.values()) {
|
|
1434
|
+
out.push({
|
|
1435
|
+
sessionKey: pending.sessionKey,
|
|
1436
|
+
toolCallId: pending.toolCallId,
|
|
1437
|
+
toolName: pending.toolName,
|
|
1438
|
+
ageMs: Math.max(0, now - pending.createdAt),
|
|
1439
|
+
deadlineMs: pending.deadlineMs,
|
|
1440
|
+
emitted: pending.emitted === true,
|
|
1441
|
+
channelClosed: pending.channel?.closed === true
|
|
1442
|
+
});
|
|
1443
|
+
}
|
|
1444
|
+
return out;
|
|
1445
|
+
}
|
|
1400
1446
|
function resolvePendingProxyCallById(toolCallId, result) {
|
|
1401
1447
|
const pending = pendingByCallId.get(toolCallId);
|
|
1402
1448
|
if (!pending) return false;
|
|
1403
1449
|
pendingByCallId.delete(toolCallId);
|
|
1404
1450
|
indexRemove(pending.sessionKey, toolCallId);
|
|
1405
|
-
clearTimeout(pending.timer);
|
|
1451
|
+
if (pending.timer) clearTimeout(pending.timer);
|
|
1406
1452
|
pending.resolve(result);
|
|
1407
1453
|
log.info("resolved pending proxy call", {
|
|
1408
1454
|
sessionKey: pending.sessionKey,
|
|
@@ -1416,7 +1462,7 @@ function rejectPendingProxyCallById(toolCallId, error) {
|
|
|
1416
1462
|
if (!pending) return false;
|
|
1417
1463
|
pendingByCallId.delete(toolCallId);
|
|
1418
1464
|
indexRemove(pending.sessionKey, toolCallId);
|
|
1419
|
-
clearTimeout(pending.timer);
|
|
1465
|
+
if (pending.timer) clearTimeout(pending.timer);
|
|
1420
1466
|
pending.reject(error);
|
|
1421
1467
|
log.notice("rejected pending proxy call", {
|
|
1422
1468
|
sessionKey: pending.sessionKey,
|
|
@@ -1832,13 +1878,30 @@ function takeUnattendedLines(ap) {
|
|
|
1832
1878
|
ap.unattendedDropped = 0;
|
|
1833
1879
|
return { lines, dropped };
|
|
1834
1880
|
}
|
|
1881
|
+
var STDERR_RETAIN_BYTES = 2 * 1024;
|
|
1882
|
+
function retainStderr(ap, chunk) {
|
|
1883
|
+
ap.lastStderr = ((ap.lastStderr ?? "") + chunk).slice(-STDERR_RETAIN_BYTES);
|
|
1884
|
+
}
|
|
1885
|
+
function describeChildCrash(exitCode, signal, lastStderr) {
|
|
1886
|
+
const how = signal ? `was killed by ${signal}` : typeof exitCode === "number" ? `exited with code ${exitCode}` : "closed its output";
|
|
1887
|
+
const tail = lastStderr?.trim();
|
|
1888
|
+
return `The Claude Code CLI ${how} before finishing this turn (no result was emitted), so the answer above may be incomplete.` + (tail ? `
|
|
1889
|
+
|
|
1890
|
+
Last stderr from the CLI:
|
|
1891
|
+
${tail}` : "");
|
|
1892
|
+
}
|
|
1835
1893
|
var activeProcesses = /* @__PURE__ */ new Map();
|
|
1836
1894
|
var claudeSessions = /* @__PURE__ */ new Map();
|
|
1837
1895
|
var idleEvictionTimers = /* @__PURE__ */ new Map();
|
|
1838
1896
|
var MAX_IDLE_TIMEOUT_MS = 2147483647;
|
|
1897
|
+
var DEFAULT_IDLE_PROCESS_TIMEOUT_MS = 0;
|
|
1898
|
+
function resolveIdleProcessTimeoutMs(configured) {
|
|
1899
|
+
return configured === void 0 ? DEFAULT_IDLE_PROCESS_TIMEOUT_MS : configured;
|
|
1900
|
+
}
|
|
1839
1901
|
var MAX_ACTIVE_PROCESSES = 16;
|
|
1840
1902
|
var PROCESS_EXIT_TIMEOUT_MS = 1500;
|
|
1841
1903
|
var PROCESS_FORCE_EXIT_TIMEOUT_MS = 500;
|
|
1904
|
+
var CHILD_EXITED_MESSAGE = "Claude CLI subprocess closed before pending tool calls were resolved";
|
|
1842
1905
|
function envFlagEnabled(value) {
|
|
1843
1906
|
if (value === void 0) return false;
|
|
1844
1907
|
const normalized = value.trim().toLowerCase();
|
|
@@ -1877,10 +1940,22 @@ function touch(key) {
|
|
|
1877
1940
|
}
|
|
1878
1941
|
function evictIfNeeded() {
|
|
1879
1942
|
while (activeProcesses.size >= MAX_ACTIVE_PROCESSES) {
|
|
1880
|
-
|
|
1881
|
-
|
|
1882
|
-
|
|
1883
|
-
|
|
1943
|
+
let victimKey;
|
|
1944
|
+
for (const [key, ap] of activeProcesses) {
|
|
1945
|
+
if (!isTurnInFlight(ap)) {
|
|
1946
|
+
victimKey = key;
|
|
1947
|
+
break;
|
|
1948
|
+
}
|
|
1949
|
+
}
|
|
1950
|
+
if (!victimKey) {
|
|
1951
|
+
log.warn("every claude process is mid-turn; skipping LRU eviction", {
|
|
1952
|
+
active: activeProcesses.size,
|
|
1953
|
+
cap: MAX_ACTIVE_PROCESSES
|
|
1954
|
+
});
|
|
1955
|
+
return;
|
|
1956
|
+
}
|
|
1957
|
+
log.info("evicting LRU claude process", { sessionKey: victimKey });
|
|
1958
|
+
deleteActiveProcess(victimKey);
|
|
1884
1959
|
}
|
|
1885
1960
|
}
|
|
1886
1961
|
var TURN_INTERRUPT_TIMEOUT_MS = 5e3;
|
|
@@ -1976,6 +2051,11 @@ function scheduleIdleProcessEviction(key, timeoutMs) {
|
|
|
1976
2051
|
const timer = setTimeout(() => {
|
|
1977
2052
|
idleEvictionTimers.delete(key);
|
|
1978
2053
|
if (activeProcesses.get(key) !== scheduledProcess) return;
|
|
2054
|
+
if (isTurnInFlight(scheduledProcess)) {
|
|
2055
|
+
log.info("idle timer found a turn in flight; re-arming", { sessionKey: key, timeoutMs });
|
|
2056
|
+
scheduleIdleProcessEviction(key, timeoutMs);
|
|
2057
|
+
return;
|
|
2058
|
+
}
|
|
1979
2059
|
log.info("evicting idle claude process", { sessionKey: key, timeoutMs });
|
|
1980
2060
|
deleteActiveProcess(key);
|
|
1981
2061
|
}, timeoutMs);
|
|
@@ -1987,9 +2067,44 @@ function detachActiveProcess(key) {
|
|
|
1987
2067
|
const ap = activeProcesses.get(key);
|
|
1988
2068
|
if (!ap) return void 0;
|
|
1989
2069
|
activeProcesses.delete(key);
|
|
1990
|
-
|
|
2070
|
+
if (ap.proxyServer) {
|
|
2071
|
+
void ap.proxyServer.close();
|
|
2072
|
+
rejectAllPendingProxyCallsForSession(key, new Error(SERVER_CLOSED_MESSAGE));
|
|
2073
|
+
}
|
|
1991
2074
|
return ap;
|
|
1992
2075
|
}
|
|
2076
|
+
function deleteActiveProcessesForSession(sessionID) {
|
|
2077
|
+
if (!sessionID || sessionID === "default") return [];
|
|
2078
|
+
const released = [];
|
|
2079
|
+
for (const [key, ap] of [...activeProcesses]) {
|
|
2080
|
+
const owned = ap.opencodeSessionID === sessionID || describeSessionKey(key).session === sessionID;
|
|
2081
|
+
if (!owned) continue;
|
|
2082
|
+
log.info("releasing claude process for deleted session", { sessionKey: key, sessionID });
|
|
2083
|
+
void deleteActiveProcessAndWait(key);
|
|
2084
|
+
released.push(key);
|
|
2085
|
+
}
|
|
2086
|
+
for (const key of [...claudeSessions.keys()]) {
|
|
2087
|
+
if (describeSessionKey(key).session !== sessionID) continue;
|
|
2088
|
+
deleteClaudeSessionId(key);
|
|
2089
|
+
clearCompression(key);
|
|
2090
|
+
if (!released.includes(key)) released.push(key);
|
|
2091
|
+
}
|
|
2092
|
+
return released;
|
|
2093
|
+
}
|
|
2094
|
+
function killAllActiveProcesses() {
|
|
2095
|
+
const keys = [...activeProcesses.keys()];
|
|
2096
|
+
for (const key of keys) deleteActiveProcess(key);
|
|
2097
|
+
return keys;
|
|
2098
|
+
}
|
|
2099
|
+
var processExitCleanupWired = false;
|
|
2100
|
+
function ensureProcessExitCleanup() {
|
|
2101
|
+
if (processExitCleanupWired) return false;
|
|
2102
|
+
processExitCleanupWired = true;
|
|
2103
|
+
process.once("exit", () => {
|
|
2104
|
+
killAllActiveProcesses();
|
|
2105
|
+
});
|
|
2106
|
+
return true;
|
|
2107
|
+
}
|
|
1993
2108
|
function deleteActiveProcess(key) {
|
|
1994
2109
|
const ap = detachActiveProcess(key);
|
|
1995
2110
|
ap?.proc.kill();
|
|
@@ -2095,6 +2210,7 @@ function spawnClaudeProcess(cliPath, cliArgs, cwd, sessionKey2, proxyServer, mcp
|
|
|
2095
2210
|
mcpHash,
|
|
2096
2211
|
systemPromptFile,
|
|
2097
2212
|
effort,
|
|
2213
|
+
startedAt: Date.now(),
|
|
2098
2214
|
cliArgs: [...cliArgs],
|
|
2099
2215
|
unattendedLines: [],
|
|
2100
2216
|
unattendedDropped: 0
|
|
@@ -2118,6 +2234,14 @@ function spawnClaudeProcess(cliPath, cliArgs, cwd, sessionKey2, proxyServer, mcp
|
|
|
2118
2234
|
proc.on("error", (err) => {
|
|
2119
2235
|
log.error("claude process error", { sessionKey: sessionKey2, error: err.message });
|
|
2120
2236
|
});
|
|
2237
|
+
proc.stdin?.on("error", (err) => {
|
|
2238
|
+
log.warn("claude process stdin error", {
|
|
2239
|
+
sessionKey: sessionKey2,
|
|
2240
|
+
code: err.code,
|
|
2241
|
+
error: err.message
|
|
2242
|
+
});
|
|
2243
|
+
settleTurn(ap);
|
|
2244
|
+
});
|
|
2121
2245
|
proc.on("exit", (code, signal) => {
|
|
2122
2246
|
log.info("claude process exited", { code, signal, sessionKey: sessionKey2 });
|
|
2123
2247
|
void proxyServer?.close();
|
|
@@ -2129,6 +2253,12 @@ function spawnClaudeProcess(cliPath, cliArgs, cwd, sessionKey2, proxyServer, mcp
|
|
|
2129
2253
|
if (ownsSessionKey) {
|
|
2130
2254
|
cancelIdleProcessEviction(sessionKey2);
|
|
2131
2255
|
activeProcesses.delete(sessionKey2);
|
|
2256
|
+
if (getPendingProxyCalls(sessionKey2).length > 0) {
|
|
2257
|
+
rejectAllPendingProxyCallsForSession(
|
|
2258
|
+
sessionKey2,
|
|
2259
|
+
new Error(CHILD_EXITED_MESSAGE)
|
|
2260
|
+
);
|
|
2261
|
+
}
|
|
2132
2262
|
}
|
|
2133
2263
|
if (ownsSessionKey && code !== 0 && code !== null) {
|
|
2134
2264
|
log.info("process exited with error, clearing session", {
|
|
@@ -2141,6 +2271,7 @@ function spawnClaudeProcess(cliPath, cliArgs, cwd, sessionKey2, proxyServer, mcp
|
|
|
2141
2271
|
proc.stderr?.on("data", (data) => {
|
|
2142
2272
|
const stderr = data.toString();
|
|
2143
2273
|
log.debug("stderr", { data: stderr.slice(0, 200) });
|
|
2274
|
+
retainStderr(ap, stderr);
|
|
2144
2275
|
if (stderr.includes("No conversation found") || stderr.includes("Session ID") && (stderr.includes("already in use") || stderr.includes("not found") || stderr.includes("invalid"))) {
|
|
2145
2276
|
if (activeProcesses.get(sessionKey2) === ap) {
|
|
2146
2277
|
log.warn("claude session ID error, clearing session", {
|
|
@@ -2168,6 +2299,7 @@ function appendResumeIfNeeded(sessionKey2, cliArgs) {
|
|
|
2168
2299
|
function respawnActiveProcess(sessionKey2, cliPath, cliArgs, cwd, ignoreAnthropicApiKey) {
|
|
2169
2300
|
const old = activeProcesses.get(sessionKey2);
|
|
2170
2301
|
if (!old) return void 0;
|
|
2302
|
+
const turnWasInFlight = isTurnInFlight(old);
|
|
2171
2303
|
activeProcesses.delete(sessionKey2);
|
|
2172
2304
|
old.proc.removeAllListeners("exit");
|
|
2173
2305
|
try {
|
|
@@ -2187,6 +2319,7 @@ function respawnActiveProcess(sessionKey2, cliPath, cliArgs, cwd, ignoreAnthropi
|
|
|
2187
2319
|
);
|
|
2188
2320
|
replacement.pendingProxyCompletions = old.pendingProxyCompletions;
|
|
2189
2321
|
delete old.pendingProxyCompletions;
|
|
2322
|
+
if (turnWasInFlight) noteTurnStarted(replacement);
|
|
2190
2323
|
return replacement;
|
|
2191
2324
|
}
|
|
2192
2325
|
function buildCliArgs(opts) {
|
|
@@ -2263,6 +2396,36 @@ function buildCliArgs(opts) {
|
|
|
2263
2396
|
function sessionKey(cwd, modelId) {
|
|
2264
2397
|
return `${cwd}::${modelId}`;
|
|
2265
2398
|
}
|
|
2399
|
+
function describeSessionKey(key) {
|
|
2400
|
+
const parts = key.split("::");
|
|
2401
|
+
return {
|
|
2402
|
+
cwd: parts[0] ?? "unknown",
|
|
2403
|
+
model: parts[1] ?? "unknown",
|
|
2404
|
+
session: parts[3] ?? "unknown",
|
|
2405
|
+
compaction: parts[2] === "compaction"
|
|
2406
|
+
};
|
|
2407
|
+
}
|
|
2408
|
+
function snapshotActiveProcesses(now = Date.now()) {
|
|
2409
|
+
const out = [];
|
|
2410
|
+
for (const [key, ap] of activeProcesses) {
|
|
2411
|
+
const described = describeSessionKey(key);
|
|
2412
|
+
const lastStderr = ap.lastStderr;
|
|
2413
|
+
out.push({
|
|
2414
|
+
sessionKey: key,
|
|
2415
|
+
session: ap.opencodeSessionID ?? described.session,
|
|
2416
|
+
model: described.model,
|
|
2417
|
+
compaction: described.compaction,
|
|
2418
|
+
pid: ap.proc.pid,
|
|
2419
|
+
inFlight: ap.turnInFlight === true,
|
|
2420
|
+
ageMs: ap.startedAt === void 0 ? void 0 : Math.max(0, now - ap.startedAt),
|
|
2421
|
+
effort: ap.effort,
|
|
2422
|
+
attached: ap.lineEmitter.listenerCount("line") > 0,
|
|
2423
|
+
proxyUrl: ap.proxyServer?.url,
|
|
2424
|
+
lastStderr: typeof lastStderr === "string" ? lastStderr : void 0
|
|
2425
|
+
});
|
|
2426
|
+
}
|
|
2427
|
+
return out;
|
|
2428
|
+
}
|
|
2266
2429
|
|
|
2267
2430
|
// src/btw-command.ts
|
|
2268
2431
|
var BtwHandledError = class extends Error {
|
|
@@ -2548,1359 +2711,2023 @@ async function handleBtwCommand(client, input, options = {}) {
|
|
|
2548
2711
|
}
|
|
2549
2712
|
}
|
|
2550
2713
|
|
|
2551
|
-
// src/
|
|
2552
|
-
|
|
2553
|
-
|
|
2554
|
-
if (!part || part.type !== "text" || typeof part.text !== "string") return false;
|
|
2555
|
-
const text = part.text.trimStart();
|
|
2556
|
-
return ASIDE_MARKERS.some((marker) => text.startsWith(marker));
|
|
2714
|
+
// src/cli-events.ts
|
|
2715
|
+
function isRecord2(value) {
|
|
2716
|
+
return value !== null && typeof value === "object" && !Array.isArray(value);
|
|
2557
2717
|
}
|
|
2558
|
-
function
|
|
2559
|
-
|
|
2560
|
-
|
|
2561
|
-
|
|
2718
|
+
function str(value) {
|
|
2719
|
+
return typeof value === "string" && value.length > 0 ? value : void 0;
|
|
2720
|
+
}
|
|
2721
|
+
function num(value) {
|
|
2722
|
+
return typeof value === "number" && Number.isFinite(value) ? value : void 0;
|
|
2723
|
+
}
|
|
2724
|
+
var RATE_LIMIT_MARKER = "\u258C **rate limit:**";
|
|
2725
|
+
var RATE_LIMIT_ACTION = "Enable extra usage or add credits on the account, wait for the window to reset, switch account, org or plan, or authenticate with an API key.";
|
|
2726
|
+
var OVERAGE_DISABLED_REASONS = {
|
|
2727
|
+
overage_not_provisioned: "extra usage is not set up on this account",
|
|
2728
|
+
org_level_disabled: "extra usage is disabled for your organization",
|
|
2729
|
+
org_level_disabled_until: "extra usage is disabled for your organization for now",
|
|
2730
|
+
out_of_credits: "the account's usage credits are spent",
|
|
2731
|
+
seat_tier_level_disabled: "your seat tier does not allow extra usage",
|
|
2732
|
+
member_level_disabled: "extra usage is disabled for your member account",
|
|
2733
|
+
seat_tier_zero_credit_limit: "your seat tier has a zero credit limit",
|
|
2734
|
+
group_zero_credit_limit: "your group has a zero credit limit",
|
|
2735
|
+
member_zero_credit_limit: "your member account has a zero credit limit",
|
|
2736
|
+
org_service_level_disabled: "your organization's service level does not include extra usage",
|
|
2737
|
+
no_limits_configured: "no usage limits are configured for this account",
|
|
2738
|
+
fetch_error: "the CLI could not read the account's usage limits"
|
|
2739
|
+
};
|
|
2740
|
+
var RATE_LIMIT_WINDOWS = {
|
|
2741
|
+
five_hour: "the 5-hour window",
|
|
2742
|
+
seven_day: "the 7-day window",
|
|
2743
|
+
seven_day_opus: "the 7-day Opus window",
|
|
2744
|
+
seven_day_sonnet: "the 7-day Sonnet window",
|
|
2745
|
+
seven_day_overage_included: "the 7-day extra-usage window",
|
|
2746
|
+
overage: "extra usage"
|
|
2747
|
+
};
|
|
2748
|
+
function parseRateLimitEvent(msg) {
|
|
2749
|
+
if (msg.type !== "rate_limit_event") return null;
|
|
2750
|
+
const info = msg.rate_limit_info;
|
|
2751
|
+
if (!isRecord2(info)) return null;
|
|
2752
|
+
return {
|
|
2753
|
+
status: str(info.status),
|
|
2754
|
+
rateLimitType: str(info.rateLimitType),
|
|
2755
|
+
resetsAt: num(info.resetsAt),
|
|
2756
|
+
utilization: num(info.utilization),
|
|
2757
|
+
isUsingOverage: info.isUsingOverage === true,
|
|
2758
|
+
overageStatus: str(info.overageStatus),
|
|
2759
|
+
overageResetsAt: num(info.overageResetsAt),
|
|
2760
|
+
overageDisabledReason: str(info.overageDisabledReason)
|
|
2761
|
+
};
|
|
2562
2762
|
}
|
|
2563
|
-
function
|
|
2564
|
-
|
|
2565
|
-
const
|
|
2566
|
-
|
|
2567
|
-
|
|
2568
|
-
return !aside;
|
|
2569
|
-
}
|
|
2570
|
-
return message.role !== "assistant" || !aside;
|
|
2571
|
-
});
|
|
2572
|
-
return kept.map(
|
|
2573
|
-
(message) => message.role === "assistant" ? { ...message, content: stripInlineAsides(message.content) } : message
|
|
2574
|
-
);
|
|
2763
|
+
function formatResetsAt(resetsAt) {
|
|
2764
|
+
if (resetsAt === void 0) return void 0;
|
|
2765
|
+
const ms = resetsAt < 1e12 ? resetsAt * 1e3 : resetsAt;
|
|
2766
|
+
const date = new Date(ms);
|
|
2767
|
+
return Number.isNaN(date.getTime()) ? void 0 : date.toISOString();
|
|
2575
2768
|
}
|
|
2576
|
-
|
|
2577
|
-
|
|
2578
|
-
|
|
2579
|
-
|
|
2580
|
-
|
|
2581
|
-
|
|
2582
|
-
|
|
2583
|
-
|
|
2584
|
-
|
|
2585
|
-
|
|
2586
|
-
|
|
2769
|
+
function rateLimitKey(info) {
|
|
2770
|
+
return [
|
|
2771
|
+
info.status ?? "?",
|
|
2772
|
+
info.rateLimitType ?? "?",
|
|
2773
|
+
info.overageStatus ?? "?",
|
|
2774
|
+
info.overageDisabledReason ?? "?"
|
|
2775
|
+
].join("|");
|
|
2776
|
+
}
|
|
2777
|
+
function isRateLimitRejected(info) {
|
|
2778
|
+
return info.status === "rejected" || info.overageStatus === "rejected";
|
|
2779
|
+
}
|
|
2780
|
+
function describeRateLimit(info) {
|
|
2781
|
+
if (!info.status && !info.overageStatus) return null;
|
|
2782
|
+
const window = info.rateLimitType ? RATE_LIMIT_WINDOWS[info.rateLimitType] ?? info.rateLimitType : void 0;
|
|
2783
|
+
const resets = formatResetsAt(info.resetsAt);
|
|
2784
|
+
const overageResets = formatResetsAt(info.overageResetsAt);
|
|
2785
|
+
const key = rateLimitKey(info);
|
|
2786
|
+
if (isRateLimitRejected(info)) {
|
|
2787
|
+
const parts = [];
|
|
2788
|
+
parts.push(
|
|
2789
|
+
info.status === "rejected" ? `Claude Code rejected this request: you are out of usage${window ? ` in ${window}` : ""}.` : "Claude Code rejected this request: paid extra usage is not available on this account."
|
|
2790
|
+
);
|
|
2791
|
+
const reason = info.overageDisabledReason ? OVERAGE_DISABLED_REASONS[info.overageDisabledReason] ?? info.overageDisabledReason : void 0;
|
|
2792
|
+
if (reason) parts.push(`Extra usage is unavailable because ${reason}.`);
|
|
2793
|
+
const resetAt = resets ?? overageResets;
|
|
2794
|
+
if (resetAt) parts.push(`Resets at ${resetAt}.`);
|
|
2795
|
+
parts.push(RATE_LIMIT_ACTION);
|
|
2796
|
+
const message = parts.join(" ");
|
|
2797
|
+
return { key, level: "warn", message, transcript: `
|
|
2798
|
+
${RATE_LIMIT_MARKER} ${message}
|
|
2799
|
+
` };
|
|
2800
|
+
}
|
|
2801
|
+
if (info.status === "allowed_warning" || info.overageStatus === "allowed_warning") {
|
|
2802
|
+
const used = info.utilization === void 0 ? "" : ` (${Math.round(info.utilization * 100)}% used)`;
|
|
2803
|
+
return {
|
|
2804
|
+
key,
|
|
2805
|
+
level: "notice",
|
|
2806
|
+
message: `Approaching the usage limit${window ? ` for ${window}` : ""}${used}${resets ? `, resets at ${resets}` : ""}.`,
|
|
2807
|
+
transcript: null
|
|
2808
|
+
};
|
|
2587
2809
|
}
|
|
2588
|
-
|
|
2589
|
-
|
|
2590
|
-
|
|
2591
|
-
|
|
2592
|
-
|
|
2593
|
-
|
|
2594
|
-
|
|
2595
|
-
|
|
2596
|
-
|
|
2597
|
-
|
|
2598
|
-
|
|
2599
|
-
|
|
2600
|
-
|
|
2601
|
-
|
|
2602
|
-
|
|
2603
|
-
|
|
2604
|
-
}
|
|
2605
|
-
} else if (raw instanceof URL) {
|
|
2606
|
-
log.warn("remote URL images are not supported by Claude CLI, skipping");
|
|
2607
|
-
return null;
|
|
2608
|
-
} else if (raw instanceof Uint8Array || Buffer.isBuffer(raw)) {
|
|
2609
|
-
base64 = Buffer.from(raw).toString("base64");
|
|
2610
|
-
} else {
|
|
2611
|
-
log.warn("unsupported file part data type", { dataType: typeof raw });
|
|
2810
|
+
return {
|
|
2811
|
+
key,
|
|
2812
|
+
level: "info",
|
|
2813
|
+
message: `usage limits updated${window ? ` for ${window}` : ""}`,
|
|
2814
|
+
transcript: null
|
|
2815
|
+
};
|
|
2816
|
+
}
|
|
2817
|
+
var reportedRateLimits = /* @__PURE__ */ new Set();
|
|
2818
|
+
function reportRateLimitEvent(msg) {
|
|
2819
|
+
const info = parseRateLimitEvent(msg);
|
|
2820
|
+
if (!info) return null;
|
|
2821
|
+
const report = describeRateLimit(info);
|
|
2822
|
+
if (!report) return null;
|
|
2823
|
+
const data = { ...info };
|
|
2824
|
+
if (reportedRateLimits.has(report.key)) {
|
|
2825
|
+
log.debug(report.message, data);
|
|
2612
2826
|
return null;
|
|
2613
2827
|
}
|
|
2614
|
-
|
|
2615
|
-
|
|
2616
|
-
|
|
2617
|
-
|
|
2618
|
-
|
|
2828
|
+
reportedRateLimits.add(report.key);
|
|
2829
|
+
log[report.level](report.message, data);
|
|
2830
|
+
return report.transcript;
|
|
2831
|
+
}
|
|
2832
|
+
var API_KEY_SOURCES = /* @__PURE__ */ new Set([
|
|
2833
|
+
"ANTHROPIC_API_KEY",
|
|
2834
|
+
"apiKeyHelper",
|
|
2835
|
+
"/login managed key",
|
|
2836
|
+
"user",
|
|
2837
|
+
"project",
|
|
2838
|
+
"org",
|
|
2839
|
+
"temporary"
|
|
2840
|
+
]);
|
|
2841
|
+
function parseSystemInit(msg) {
|
|
2842
|
+
if (msg.type !== "system" || msg.subtype !== "init") return null;
|
|
2843
|
+
const raw = msg;
|
|
2844
|
+
const servers = [];
|
|
2845
|
+
if (Array.isArray(raw.mcp_servers)) {
|
|
2846
|
+
for (const entry of raw.mcp_servers) {
|
|
2847
|
+
if (!isRecord2(entry)) continue;
|
|
2848
|
+
servers.push({ name: str(entry.name) ?? "unknown", status: str(entry.status) ?? "unknown" });
|
|
2849
|
+
}
|
|
2619
2850
|
}
|
|
2620
2851
|
return {
|
|
2621
|
-
|
|
2622
|
-
|
|
2852
|
+
apiKeySource: str(raw.apiKeySource),
|
|
2853
|
+
permissionMode: str(raw.permissionMode),
|
|
2854
|
+
model: str(raw.model),
|
|
2855
|
+
cliVersion: str(raw.claude_code_version),
|
|
2856
|
+
toolCount: Array.isArray(raw.tools) ? raw.tools.length : 0,
|
|
2857
|
+
mcpServers: servers
|
|
2623
2858
|
};
|
|
2624
2859
|
}
|
|
2625
|
-
function
|
|
2626
|
-
|
|
2627
|
-
|
|
2628
|
-
|
|
2629
|
-
|
|
2630
|
-
|
|
2631
|
-
|
|
2860
|
+
function apiKeySourceWarning(apiKeySource, ignoreAnthropicApiKey) {
|
|
2861
|
+
if (!apiKeySource || !API_KEY_SOURCES.has(apiKeySource)) return null;
|
|
2862
|
+
const base = `Claude Code authenticated with an API key (apiKeySource: ${apiKeySource}), so these turns bill as pay-as-you-go API usage instead of your subscription's Agent SDK credit.`;
|
|
2863
|
+
return ignoreAnthropicApiKey ? `${base} \`ignoreAnthropicApiKey\` is already on, so the key is not coming from the environment: check the CLI's own settings (\`claude config\`) or an \`apiKeyHelper\`.` : `${base} Set the provider option \`ignoreAnthropicApiKey: true\` to strip the key from spawns and fall back to the stored subscription auth.`;
|
|
2864
|
+
}
|
|
2865
|
+
var warnedApiKeySources = /* @__PURE__ */ new Set();
|
|
2866
|
+
var warnedMcpFailures = /* @__PURE__ */ new Set();
|
|
2867
|
+
function reportSystemInit(msg, options = {}) {
|
|
2868
|
+
const info = parseSystemInit(msg);
|
|
2869
|
+
if (!info) return;
|
|
2870
|
+
log.info("claude session init", {
|
|
2871
|
+
apiKeySource: info.apiKeySource ?? null,
|
|
2872
|
+
permissionMode: info.permissionMode ?? null,
|
|
2873
|
+
model: info.model ?? null,
|
|
2874
|
+
cliVersion: info.cliVersion ?? null,
|
|
2875
|
+
tools: info.toolCount,
|
|
2876
|
+
mcpServers: info.mcpServers
|
|
2877
|
+
});
|
|
2878
|
+
for (const server2 of info.mcpServers) {
|
|
2879
|
+
if (server2.status === "connected") continue;
|
|
2880
|
+
const key2 = `${server2.name}:${server2.status}`;
|
|
2881
|
+
const message = `MCP server "${server2.name}" is ${server2.status} in Claude Code; its tools are not available to the model this session.`;
|
|
2882
|
+
if (warnedMcpFailures.has(key2)) {
|
|
2883
|
+
log.debug(message, { server: server2.name, status: server2.status });
|
|
2884
|
+
continue;
|
|
2885
|
+
}
|
|
2886
|
+
warnedMcpFailures.add(key2);
|
|
2887
|
+
log.warn(message, { server: server2.name, status: server2.status });
|
|
2632
2888
|
}
|
|
2633
|
-
|
|
2634
|
-
|
|
2635
|
-
|
|
2636
|
-
|
|
2637
|
-
|
|
2638
|
-
|
|
2639
|
-
return JSON.stringify(value.value);
|
|
2640
|
-
case "execution-denied":
|
|
2641
|
-
return value.reason ? `Execution denied: ${value.reason}` : "Execution denied";
|
|
2642
|
-
case "content":
|
|
2643
|
-
return Array.isArray(value.value) ? value.value.map((item) => {
|
|
2644
|
-
if (item?.type === "text") return item.text;
|
|
2645
|
-
return JSON.stringify(item);
|
|
2646
|
-
}).join("\n") : JSON.stringify(value.value);
|
|
2647
|
-
default:
|
|
2648
|
-
return JSON.stringify(value);
|
|
2889
|
+
const apiKeyMessage = apiKeySourceWarning(info.apiKeySource, options.ignoreAnthropicApiKey);
|
|
2890
|
+
if (!apiKeyMessage) return;
|
|
2891
|
+
const key = `${info.apiKeySource}:${options.ignoreAnthropicApiKey ? "ignored" : "passed"}`;
|
|
2892
|
+
if (warnedApiKeySources.has(key)) {
|
|
2893
|
+
log.debug(apiKeyMessage, { apiKeySource: info.apiKeySource });
|
|
2894
|
+
return;
|
|
2649
2895
|
}
|
|
2896
|
+
warnedApiKeySources.add(key);
|
|
2897
|
+
log.warn(apiKeyMessage, { apiKeySource: info.apiKeySource });
|
|
2650
2898
|
}
|
|
2651
|
-
var
|
|
2652
|
-
|
|
2653
|
-
|
|
2654
|
-
|
|
2655
|
-
|
|
2656
|
-
return
|
|
2657
|
-
|
|
2899
|
+
var COMPACT_BOUNDARY_MARKER = "\u258C **context compacted:**";
|
|
2900
|
+
function parseCompactBoundary(msg) {
|
|
2901
|
+
if (msg.type !== "system" || msg.subtype !== "compact_boundary") return null;
|
|
2902
|
+
const raw = msg;
|
|
2903
|
+
const meta = isRecord2(raw.compact_metadata) ? raw.compact_metadata : isRecord2(raw.compactMetadata) ? raw.compactMetadata : void 0;
|
|
2904
|
+
return {
|
|
2905
|
+
trigger: str(meta?.trigger) ?? "unknown",
|
|
2906
|
+
preTokens: num(meta?.pre_tokens) ?? num(meta?.preTokens),
|
|
2907
|
+
postTokens: num(meta?.post_tokens) ?? num(meta?.postTokens)
|
|
2908
|
+
};
|
|
2658
2909
|
}
|
|
2659
|
-
function
|
|
2660
|
-
|
|
2910
|
+
function formatCompactBoundaryNote(boundary) {
|
|
2911
|
+
const how = boundary.trigger === "auto" ? "on its own" : `on a ${boundary.trigger} request`;
|
|
2912
|
+
const sizes = boundary.preTokens !== void 0 && boundary.postTokens !== void 0 ? ` (${boundary.preTokens.toLocaleString("en-US")} tokens to ${boundary.postTokens.toLocaleString("en-US")})` : "";
|
|
2913
|
+
return `
|
|
2914
|
+
${COMPACT_BOUNDARY_MARKER} Claude Code compacted its own context ${how}${sizes}. Earlier detail in this conversation is now a summary.
|
|
2915
|
+
`;
|
|
2916
|
+
}
|
|
2917
|
+
function reportCompactBoundary(msg) {
|
|
2918
|
+
const boundary = parseCompactBoundary(msg);
|
|
2919
|
+
if (!boundary) return null;
|
|
2920
|
+
log.notice("claude code compacted its own context", {
|
|
2921
|
+
trigger: boundary.trigger,
|
|
2922
|
+
preTokens: boundary.preTokens ?? null,
|
|
2923
|
+
postTokens: boundary.postTokens ?? null
|
|
2924
|
+
});
|
|
2925
|
+
return formatCompactBoundaryNote(boundary);
|
|
2926
|
+
}
|
|
2927
|
+
var RESULT_ERROR_MARKER = "\u258C **claude code error:**";
|
|
2928
|
+
var RESULT_SUBTYPES = {
|
|
2929
|
+
error_max_turns: "it hit its internal turn limit before finishing",
|
|
2930
|
+
error_during_execution: "it failed while running the turn",
|
|
2931
|
+
error_max_budget_usd: "it hit the configured spend limit for the turn",
|
|
2932
|
+
error_max_structured_output_retries: "it could not produce valid structured output"
|
|
2933
|
+
};
|
|
2934
|
+
function describeResultFailure(msg) {
|
|
2935
|
+
if (msg.type !== "result") return null;
|
|
2936
|
+
const subtype = msg.subtype;
|
|
2937
|
+
if (!subtype || subtype === "success") return null;
|
|
2938
|
+
const explanation = RESULT_SUBTYPES[subtype];
|
|
2939
|
+
return explanation ? `Claude Code ended the turn with \`${subtype}\`: ${explanation}.` : `Claude Code ended the turn with \`${subtype}\`.`;
|
|
2940
|
+
}
|
|
2941
|
+
function formatResultFailureNote(message) {
|
|
2942
|
+
return `
|
|
2943
|
+
${RESULT_ERROR_MARKER} ${message}
|
|
2944
|
+
`;
|
|
2945
|
+
}
|
|
2946
|
+
|
|
2947
|
+
// src/startup-diagnostics.ts
|
|
2948
|
+
import { execFile as execFile2 } from "child_process";
|
|
2949
|
+
import * as fs4 from "fs";
|
|
2950
|
+
import * as path4 from "path";
|
|
2951
|
+
import { promisify as promisify2 } from "util";
|
|
2952
|
+
import { fileURLToPath } from "url";
|
|
2953
|
+
|
|
2954
|
+
// src/mcp-bridge.ts
|
|
2955
|
+
import * as fs3 from "fs";
|
|
2956
|
+
import * as path3 from "path";
|
|
2957
|
+
import * as os2 from "os";
|
|
2958
|
+
import * as crypto2 from "crypto";
|
|
2959
|
+
import {
|
|
2960
|
+
parse as parseJsonc,
|
|
2961
|
+
printParseErrorCode
|
|
2962
|
+
} from "jsonc-parser";
|
|
2963
|
+
var FILE_NAMES = ["opencode.jsonc", "opencode.json", "config.json"];
|
|
2964
|
+
var PROJECT_FILE_NAMES = ["opencode.json", "opencode.jsonc"];
|
|
2965
|
+
function fileExists(p) {
|
|
2661
2966
|
try {
|
|
2662
|
-
|
|
2967
|
+
return fs3.statSync(p).isFile();
|
|
2663
2968
|
} catch {
|
|
2664
|
-
|
|
2969
|
+
return false;
|
|
2665
2970
|
}
|
|
2666
|
-
return clipWithMarker(raw, MAX_TOOL_INPUT_CHARS);
|
|
2667
2971
|
}
|
|
2668
|
-
function
|
|
2669
|
-
|
|
2670
|
-
|
|
2671
|
-
|
|
2672
|
-
return
|
|
2972
|
+
function dirExists(p) {
|
|
2973
|
+
try {
|
|
2974
|
+
return fs3.statSync(p).isDirectory();
|
|
2975
|
+
} catch {
|
|
2976
|
+
return false;
|
|
2673
2977
|
}
|
|
2674
|
-
|
|
2675
|
-
|
|
2978
|
+
}
|
|
2979
|
+
function readAndParse(file) {
|
|
2980
|
+
try {
|
|
2981
|
+
const raw = fs3.readFileSync(file, "utf8");
|
|
2982
|
+
const errors = [];
|
|
2983
|
+
const parsed = parseJsonc(raw, errors, { allowTrailingComma: true });
|
|
2984
|
+
if (errors.length > 0) {
|
|
2985
|
+
const first = errors[0];
|
|
2986
|
+
throw new Error(
|
|
2987
|
+
`${printParseErrorCode(first.error)} at offset ${first.offset}`
|
|
2988
|
+
);
|
|
2989
|
+
}
|
|
2990
|
+
return parsed;
|
|
2991
|
+
} catch (e) {
|
|
2992
|
+
log.warn("failed to parse opencode config", {
|
|
2993
|
+
file,
|
|
2994
|
+
error: e instanceof Error ? e.message : String(e)
|
|
2995
|
+
});
|
|
2996
|
+
return null;
|
|
2676
2997
|
}
|
|
2677
|
-
|
|
2678
|
-
|
|
2679
|
-
|
|
2680
|
-
|
|
2681
|
-
|
|
2682
|
-
|
|
2683
|
-
|
|
2684
|
-
|
|
2685
|
-
|
|
2686
|
-
|
|
2687
|
-
|
|
2688
|
-
|
|
2689
|
-
|
|
2690
|
-
lines.push(
|
|
2691
|
-
`[tool_result:${part.toolName ?? part.toolCallId ?? "unknown"}]
|
|
2692
|
-
${clipWithMarker(
|
|
2693
|
-
getToolResultText(part),
|
|
2694
|
-
MAX_TOOL_RESULT_CHARS
|
|
2695
|
-
)}`
|
|
2696
|
-
);
|
|
2697
|
-
break;
|
|
2698
|
-
case "image":
|
|
2699
|
-
lines.push(
|
|
2700
|
-
`[image: ${part.mediaType ?? part.mimeType ?? "unknown"}]`
|
|
2701
|
-
);
|
|
2702
|
-
break;
|
|
2703
|
-
case "file":
|
|
2704
|
-
lines.push(
|
|
2705
|
-
`[file: ${part.mediaType ?? part.mimeType ?? "unknown"}]`
|
|
2706
|
-
);
|
|
2707
|
-
break;
|
|
2708
|
-
case "reasoning":
|
|
2709
|
-
break;
|
|
2998
|
+
}
|
|
2999
|
+
function isPlainObject(x) {
|
|
3000
|
+
return typeof x === "object" && x !== null && !Array.isArray(x);
|
|
3001
|
+
}
|
|
3002
|
+
function deepMerge(target, source) {
|
|
3003
|
+
const out = { ...target };
|
|
3004
|
+
for (const [k, v] of Object.entries(source)) {
|
|
3005
|
+
if (v === void 0) continue;
|
|
3006
|
+
const existing = out[k];
|
|
3007
|
+
if (isPlainObject(existing) && isPlainObject(v)) {
|
|
3008
|
+
out[k] = deepMerge(existing, v);
|
|
3009
|
+
} else {
|
|
3010
|
+
out[k] = v;
|
|
2710
3011
|
}
|
|
2711
3012
|
}
|
|
2712
|
-
return
|
|
3013
|
+
return out;
|
|
2713
3014
|
}
|
|
2714
|
-
function
|
|
2715
|
-
const
|
|
2716
|
-
|
|
2717
|
-
|
|
2718
|
-
|
|
2719
|
-
|
|
2720
|
-
|
|
2721
|
-
|
|
2722
|
-
|
|
2723
|
-
|
|
2724
|
-
|
|
3015
|
+
function walkUp(opts) {
|
|
3016
|
+
const out = [];
|
|
3017
|
+
let current = path3.resolve(opts.start);
|
|
3018
|
+
while (true) {
|
|
3019
|
+
for (const target of opts.targets) {
|
|
3020
|
+
const candidate = path3.join(current, target);
|
|
3021
|
+
if (opts.predicate(candidate)) out.push(candidate);
|
|
3022
|
+
}
|
|
3023
|
+
if (opts.stop && current === path3.resolve(opts.stop)) break;
|
|
3024
|
+
const parent = path3.dirname(current);
|
|
3025
|
+
if (parent === current) break;
|
|
3026
|
+
current = parent;
|
|
2725
3027
|
}
|
|
2726
|
-
|
|
2727
|
-
|
|
2728
|
-
|
|
2729
|
-
|
|
2730
|
-
|
|
2731
|
-
|
|
2732
|
-
|
|
2733
|
-
|
|
3028
|
+
return out;
|
|
3029
|
+
}
|
|
3030
|
+
function detectWorktree(cwd) {
|
|
3031
|
+
const override = process.env.OPENCODE_WORKTREE;
|
|
3032
|
+
if (override) return path3.resolve(override);
|
|
3033
|
+
let current = path3.resolve(cwd);
|
|
3034
|
+
while (true) {
|
|
3035
|
+
const gitPath = path3.join(current, ".git");
|
|
3036
|
+
try {
|
|
3037
|
+
if (fs3.existsSync(gitPath)) return current;
|
|
3038
|
+
} catch {
|
|
2734
3039
|
}
|
|
3040
|
+
const parent = path3.dirname(current);
|
|
3041
|
+
if (parent === current) return void 0;
|
|
3042
|
+
current = parent;
|
|
2735
3043
|
}
|
|
2736
|
-
|
|
2737
|
-
|
|
3044
|
+
}
|
|
3045
|
+
function globalConfigDir() {
|
|
3046
|
+
const xdg = process.env.XDG_CONFIG_HOME ?? path3.join(os2.homedir(), ".config");
|
|
3047
|
+
return path3.join(xdg, "opencode");
|
|
3048
|
+
}
|
|
3049
|
+
function loadGlobalConfig() {
|
|
3050
|
+
const dir = globalConfigDir();
|
|
3051
|
+
let merged = {};
|
|
3052
|
+
for (const name of FILE_NAMES.slice().reverse()) {
|
|
3053
|
+
const file = path3.join(dir, name);
|
|
3054
|
+
if (!fileExists(file)) continue;
|
|
3055
|
+
const parsed = readAndParse(file);
|
|
3056
|
+
if (parsed) merged = deepMerge(merged, parsed);
|
|
2738
3057
|
}
|
|
2739
|
-
return
|
|
3058
|
+
return merged;
|
|
2740
3059
|
}
|
|
2741
|
-
function
|
|
2742
|
-
|
|
2743
|
-
|
|
2744
|
-
|
|
2745
|
-
|
|
2746
|
-
|
|
2747
|
-
|
|
2748
|
-
const msg = prompt[i];
|
|
2749
|
-
const roleLabel = msg.role === "user" ? "User" : msg.role === "assistant" ? "Assistant" : msg.role === "tool" ? "Tool" : msg.role;
|
|
2750
|
-
const { text, toolResultCount } = renderMessageContentForCompaction(msg);
|
|
2751
|
-
if (!text.trim()) continue;
|
|
2752
|
-
const entry = `${roleLabel}: ${text}`;
|
|
2753
|
-
if (total + entry.length > MAX_HISTORY_CHARS) {
|
|
2754
|
-
droppedOldest = i + 1;
|
|
2755
|
-
break;
|
|
2756
|
-
}
|
|
2757
|
-
entries.push(entry);
|
|
2758
|
-
total += entry.length + 2;
|
|
2759
|
-
totalToolResults += toolResultCount;
|
|
3060
|
+
function loadProjectFilesInDir(dir) {
|
|
3061
|
+
let merged = {};
|
|
3062
|
+
for (const name of PROJECT_FILE_NAMES) {
|
|
3063
|
+
const file = path3.join(dir, name);
|
|
3064
|
+
if (!fileExists(file)) continue;
|
|
3065
|
+
const parsed = readAndParse(file);
|
|
3066
|
+
if (parsed) merged = deepMerge(merged, parsed);
|
|
2760
3067
|
}
|
|
2761
|
-
|
|
2762
|
-
entries.reverse();
|
|
2763
|
-
log.info("built compaction history", {
|
|
2764
|
-
entries: entries.length,
|
|
2765
|
-
chars: total,
|
|
2766
|
-
toolResults: totalToolResults,
|
|
2767
|
-
droppedOldestBefore: droppedOldest
|
|
2768
|
-
});
|
|
2769
|
-
return entries.join("\n\n");
|
|
3068
|
+
return merged;
|
|
2770
3069
|
}
|
|
2771
|
-
function
|
|
2772
|
-
const
|
|
2773
|
-
const
|
|
2774
|
-
const
|
|
2775
|
-
|
|
2776
|
-
|
|
2777
|
-
|
|
2778
|
-
|
|
2779
|
-
content.push({ type: "tool_result", tool_use_id: id, content: text });
|
|
2780
|
-
return;
|
|
3070
|
+
function dotOpencodeDirs(cwd, worktree) {
|
|
3071
|
+
const dirs = [];
|
|
3072
|
+
const seen = /* @__PURE__ */ new Set();
|
|
3073
|
+
const push = (p) => {
|
|
3074
|
+
const abs = path3.resolve(p);
|
|
3075
|
+
if (!seen.has(abs) && dirExists(abs)) {
|
|
3076
|
+
seen.add(abs);
|
|
3077
|
+
dirs.push(abs);
|
|
2781
3078
|
}
|
|
2782
|
-
log.info("rendering opencode-side tool result as text", {
|
|
2783
|
-
toolCallId: id,
|
|
2784
|
-
toolName: part.toolName,
|
|
2785
|
-
chars: text.length
|
|
2786
|
-
});
|
|
2787
|
-
content.push({
|
|
2788
|
-
type: "text",
|
|
2789
|
-
text: `<opencode_tool_result tool="${part.toolName ?? "unknown"}">
|
|
2790
|
-
${text}
|
|
2791
|
-
</opencode_tool_result>`
|
|
2792
|
-
});
|
|
2793
3079
|
};
|
|
2794
|
-
|
|
2795
|
-
|
|
2796
|
-
|
|
3080
|
+
for (const dir of walkUp({
|
|
3081
|
+
start: cwd,
|
|
3082
|
+
stop: worktree,
|
|
3083
|
+
targets: [".opencode"],
|
|
3084
|
+
predicate: dirExists
|
|
3085
|
+
})) {
|
|
3086
|
+
push(dir);
|
|
3087
|
+
}
|
|
3088
|
+
const home = os2.homedir();
|
|
3089
|
+
if (home) {
|
|
3090
|
+
const homeDot = path3.join(home, ".opencode");
|
|
3091
|
+
if (dirExists(homeDot)) push(homeDot);
|
|
3092
|
+
}
|
|
3093
|
+
const envDir = process.env.OPENCODE_CONFIG_DIR;
|
|
3094
|
+
if (envDir && dirExists(envDir)) push(envDir);
|
|
3095
|
+
return dirs;
|
|
3096
|
+
}
|
|
3097
|
+
function substituteEnvPlaceholders(source) {
|
|
3098
|
+
const out = {};
|
|
3099
|
+
for (const [k, v] of Object.entries(source)) {
|
|
3100
|
+
if (typeof v !== "string") continue;
|
|
3101
|
+
out[k] = v.replace(/\{env:([A-Za-z_][A-Za-z0-9_]*)\}/g, (_match, name) => {
|
|
3102
|
+
const resolved = process.env[name];
|
|
3103
|
+
return typeof resolved === "string" ? resolved : "";
|
|
2797
3104
|
});
|
|
2798
|
-
|
|
2799
|
-
|
|
2800
|
-
|
|
2801
|
-
|
|
2802
|
-
|
|
2803
|
-
|
|
2804
|
-
|
|
2805
|
-
|
|
2806
|
-
|
|
2807
|
-
|
|
2808
|
-
|
|
2809
|
-
|
|
2810
|
-
`
|
|
2811
|
-
});
|
|
3105
|
+
}
|
|
3106
|
+
return out;
|
|
3107
|
+
}
|
|
3108
|
+
function translateServer(name, spec) {
|
|
3109
|
+
if (spec.enabled === false) return null;
|
|
3110
|
+
const type = spec.type;
|
|
3111
|
+
if (type === "local") {
|
|
3112
|
+
const cmd = spec.command;
|
|
3113
|
+
if (!Array.isArray(cmd) || cmd.length === 0) {
|
|
3114
|
+
log.warn("skipping local MCP server with no command", { name });
|
|
3115
|
+
return null;
|
|
2812
3116
|
}
|
|
2813
|
-
|
|
2814
|
-
|
|
2815
|
-
|
|
2816
|
-
|
|
2817
|
-
|
|
2818
|
-
|
|
2819
|
-
|
|
2820
|
-
|
|
2821
|
-
|
|
2822
|
-
The following is a summary of our conversation so far (from a previous session that couldn't be resumed):
|
|
2823
|
-
|
|
2824
|
-
${historyContext}
|
|
2825
|
-
|
|
2826
|
-
</conversation_history>
|
|
2827
|
-
|
|
2828
|
-
Now continuing with the current message:
|
|
2829
|
-
|
|
2830
|
-
`
|
|
2831
|
-
});
|
|
3117
|
+
const out = {
|
|
3118
|
+
type: "stdio",
|
|
3119
|
+
command: String(cmd[0])
|
|
3120
|
+
};
|
|
3121
|
+
if (cmd.length > 1) out.args = cmd.slice(1).map((s) => String(s));
|
|
3122
|
+
if (spec.environment && typeof spec.environment === "object") {
|
|
3123
|
+
out.env = substituteEnvPlaceholders(
|
|
3124
|
+
spec.environment
|
|
3125
|
+
);
|
|
2832
3126
|
}
|
|
3127
|
+
return out;
|
|
2833
3128
|
}
|
|
2834
|
-
|
|
2835
|
-
|
|
2836
|
-
|
|
2837
|
-
|
|
3129
|
+
if (type === "remote") {
|
|
3130
|
+
if (typeof spec.url !== "string" || !spec.url) {
|
|
3131
|
+
log.warn("skipping remote MCP server with no url", { name });
|
|
3132
|
+
return null;
|
|
3133
|
+
}
|
|
3134
|
+
const out = {
|
|
3135
|
+
type: "http",
|
|
3136
|
+
url: spec.url
|
|
3137
|
+
};
|
|
3138
|
+
if (spec.headers && typeof spec.headers === "object") {
|
|
3139
|
+
out.headers = substituteEnvPlaceholders(
|
|
3140
|
+
spec.headers
|
|
3141
|
+
);
|
|
3142
|
+
}
|
|
3143
|
+
return out;
|
|
2838
3144
|
}
|
|
2839
|
-
|
|
2840
|
-
|
|
2841
|
-
|
|
2842
|
-
|
|
2843
|
-
|
|
2844
|
-
|
|
2845
|
-
|
|
2846
|
-
|
|
2847
|
-
|
|
2848
|
-
|
|
2849
|
-
|
|
2850
|
-
|
|
2851
|
-
|
|
2852
|
-
|
|
2853
|
-
|
|
2854
|
-
|
|
2855
|
-
|
|
2856
|
-
|
|
2857
|
-
|
|
2858
|
-
|
|
2859
|
-
|
|
2860
|
-
|
|
2861
|
-
|
|
2862
|
-
} else if (part.type === "tool-result") {
|
|
2863
|
-
pushToolResult(part);
|
|
2864
|
-
}
|
|
2865
|
-
}
|
|
2866
|
-
}
|
|
2867
|
-
} else if (msg.role === "tool") {
|
|
2868
|
-
if (Array.isArray(msg.content)) {
|
|
2869
|
-
for (const part of msg.content) {
|
|
2870
|
-
if (part?.type === "tool-result") {
|
|
2871
|
-
pushToolResult(part);
|
|
2872
|
-
}
|
|
2873
|
-
}
|
|
2874
|
-
}
|
|
3145
|
+
log.warn("skipping MCP server with unknown type", {
|
|
3146
|
+
name,
|
|
3147
|
+
type: type ?? null
|
|
3148
|
+
});
|
|
3149
|
+
return null;
|
|
3150
|
+
}
|
|
3151
|
+
function extractMcpBlock(config) {
|
|
3152
|
+
const mcp = config.mcp;
|
|
3153
|
+
if (!mcp || typeof mcp !== "object" || Array.isArray(mcp)) return {};
|
|
3154
|
+
return mcp;
|
|
3155
|
+
}
|
|
3156
|
+
function mergeMcp(target, source) {
|
|
3157
|
+
const out = { ...target };
|
|
3158
|
+
for (const [name, spec] of Object.entries(source)) {
|
|
3159
|
+
if (!spec || typeof spec !== "object") continue;
|
|
3160
|
+
const existing = out[name];
|
|
3161
|
+
if (existing && typeof existing === "object") {
|
|
3162
|
+
out[name] = deepMerge(
|
|
3163
|
+
existing,
|
|
3164
|
+
spec
|
|
3165
|
+
);
|
|
3166
|
+
} else {
|
|
3167
|
+
out[name] = spec;
|
|
2875
3168
|
}
|
|
2876
3169
|
}
|
|
2877
|
-
|
|
2878
|
-
|
|
2879
|
-
|
|
2880
|
-
|
|
2881
|
-
|
|
2882
|
-
|
|
2883
|
-
|
|
2884
|
-
|
|
2885
|
-
|
|
2886
|
-
|
|
2887
|
-
|
|
2888
|
-
|
|
2889
|
-
|
|
2890
|
-
|
|
2891
|
-
|
|
3170
|
+
return out;
|
|
3171
|
+
}
|
|
3172
|
+
function bridgeOpencodeMcp(cwd, runtimeStatus, excludeServers) {
|
|
3173
|
+
const {
|
|
3174
|
+
servers: merged,
|
|
3175
|
+
enabledServerNames: allEnabledServerNames,
|
|
3176
|
+
hash
|
|
3177
|
+
} = mergeOpencodeMcp(cwd, runtimeStatus);
|
|
3178
|
+
const servers = {};
|
|
3179
|
+
const bridgedServerNames = [];
|
|
3180
|
+
for (const [name, spec] of Object.entries(merged)) {
|
|
3181
|
+
if (!spec || typeof spec !== "object") continue;
|
|
3182
|
+
if (excludeServers?.has(name)) continue;
|
|
3183
|
+
const translated = translateServer(name, spec);
|
|
3184
|
+
if (translated) {
|
|
3185
|
+
servers[name] = translated;
|
|
3186
|
+
bridgedServerNames.push(name);
|
|
2892
3187
|
}
|
|
3188
|
+
}
|
|
3189
|
+
return finishBridge({
|
|
3190
|
+
servers,
|
|
3191
|
+
bridgedServerNames,
|
|
3192
|
+
allEnabledServerNames,
|
|
3193
|
+
hash,
|
|
3194
|
+
excludeServers
|
|
2893
3195
|
});
|
|
2894
3196
|
}
|
|
2895
|
-
|
|
2896
|
-
|
|
2897
|
-
|
|
2898
|
-
|
|
2899
|
-
|
|
2900
|
-
|
|
2901
|
-
|
|
2902
|
-
|
|
2903
|
-
|
|
2904
|
-
|
|
2905
|
-
|
|
2906
|
-
|
|
2907
|
-
|
|
2908
|
-
|
|
2909
|
-
};
|
|
2910
|
-
|
|
2911
|
-
|
|
2912
|
-
|
|
2913
|
-
|
|
2914
|
-
|
|
2915
|
-
|
|
2916
|
-
|
|
2917
|
-
}
|
|
2918
|
-
|
|
2919
|
-
|
|
2920
|
-
|
|
2921
|
-
|
|
2922
|
-
|
|
2923
|
-
|
|
2924
|
-
|
|
2925
|
-
|
|
2926
|
-
|
|
2927
|
-
|
|
2928
|
-
|
|
2929
|
-
|
|
2930
|
-
|
|
2931
|
-
|
|
2932
|
-
|
|
2933
|
-
|
|
2934
|
-
|
|
2935
|
-
|
|
2936
|
-
|
|
2937
|
-
|
|
3197
|
+
function mergeOpencodeMcp(cwd, runtimeStatus) {
|
|
3198
|
+
const worktree = detectWorktree(cwd);
|
|
3199
|
+
let merged = {};
|
|
3200
|
+
merged = mergeMcp(merged, extractMcpBlock(loadGlobalConfig()));
|
|
3201
|
+
const explicitConfig = process.env.OPENCODE_CONFIG;
|
|
3202
|
+
if (explicitConfig && fileExists(explicitConfig)) {
|
|
3203
|
+
const parsed = readAndParse(explicitConfig);
|
|
3204
|
+
if (parsed) merged = mergeMcp(merged, extractMcpBlock(parsed));
|
|
3205
|
+
}
|
|
3206
|
+
const projectFiles = walkUp({
|
|
3207
|
+
start: cwd,
|
|
3208
|
+
stop: worktree,
|
|
3209
|
+
targets: PROJECT_FILE_NAMES,
|
|
3210
|
+
predicate: fileExists
|
|
3211
|
+
});
|
|
3212
|
+
const projectDirs = [];
|
|
3213
|
+
const seenProjectDirs = /* @__PURE__ */ new Set();
|
|
3214
|
+
for (const f of projectFiles) {
|
|
3215
|
+
const d = path3.dirname(f);
|
|
3216
|
+
if (!seenProjectDirs.has(d)) {
|
|
3217
|
+
seenProjectDirs.add(d);
|
|
3218
|
+
projectDirs.push(d);
|
|
3219
|
+
}
|
|
3220
|
+
}
|
|
3221
|
+
for (const dir of projectDirs.slice().reverse()) {
|
|
3222
|
+
merged = mergeMcp(merged, extractMcpBlock(loadProjectFilesInDir(dir)));
|
|
3223
|
+
}
|
|
3224
|
+
for (const dir of dotOpencodeDirs(cwd, worktree)) {
|
|
3225
|
+
merged = mergeMcp(merged, extractMcpBlock(loadProjectFilesInDir(dir)));
|
|
3226
|
+
}
|
|
3227
|
+
if (runtimeStatus) {
|
|
3228
|
+
for (const name of Object.keys(merged)) {
|
|
3229
|
+
const status = runtimeStatus[name];
|
|
3230
|
+
if (status === void 0) continue;
|
|
3231
|
+
const existing = merged[name];
|
|
3232
|
+
const base = existing && typeof existing === "object" ? existing : {};
|
|
3233
|
+
merged[name] = { ...base, enabled: status === "connected" };
|
|
3234
|
+
}
|
|
3235
|
+
}
|
|
3236
|
+
const enabledServerNames = [];
|
|
3237
|
+
for (const [name, spec] of Object.entries(merged)) {
|
|
3238
|
+
if (!spec || typeof spec !== "object") continue;
|
|
3239
|
+
const enabled = spec.enabled;
|
|
3240
|
+
if (enabled === false) continue;
|
|
3241
|
+
enabledServerNames.push(name);
|
|
3242
|
+
}
|
|
3243
|
+
const mergedBody = JSON.stringify({ mcpServers: merged }, null, 2);
|
|
3244
|
+
const hash = crypto2.createHash("sha256").update(mergedBody).digest("hex").slice(0, 12);
|
|
3245
|
+
return { servers: merged, enabledServerNames, hash };
|
|
2938
3246
|
}
|
|
2939
|
-
|
|
2940
|
-
|
|
2941
|
-
|
|
2942
|
-
|
|
2943
|
-
|
|
2944
|
-
|
|
2945
|
-
|
|
2946
|
-
|
|
2947
|
-
|
|
2948
|
-
|
|
2949
|
-
|
|
3247
|
+
function finishBridge(input) {
|
|
3248
|
+
const { servers, bridgedServerNames, allEnabledServerNames, hash, excludeServers } = input;
|
|
3249
|
+
if (Object.keys(servers).length === 0) {
|
|
3250
|
+
const allEnabledServersExcluded = excludeServers && allEnabledServerNames.length > 0 && allEnabledServerNames.every((name) => excludeServers.has(name));
|
|
3251
|
+
if (!allEnabledServersExcluded) return null;
|
|
3252
|
+
return {
|
|
3253
|
+
path: "",
|
|
3254
|
+
hash,
|
|
3255
|
+
serverNames: [],
|
|
3256
|
+
allEnabledServerNames
|
|
3257
|
+
};
|
|
2950
3258
|
}
|
|
2951
|
-
|
|
2952
|
-
|
|
3259
|
+
const body = JSON.stringify({ mcpServers: servers }, null, 2);
|
|
3260
|
+
const outPath = path3.join(
|
|
3261
|
+
pluginTmpDir(),
|
|
3262
|
+
`mcp-${hash}.json`
|
|
3263
|
+
);
|
|
3264
|
+
try {
|
|
3265
|
+
if (!fileExists(outPath)) {
|
|
3266
|
+
fs3.writeFileSync(outPath, body, { encoding: "utf8", mode: 384 });
|
|
3267
|
+
}
|
|
3268
|
+
} catch (e) {
|
|
3269
|
+
log.warn("failed to write bridged MCP config", {
|
|
3270
|
+
error: e instanceof Error ? e.message : String(e)
|
|
3271
|
+
});
|
|
3272
|
+
return null;
|
|
2953
3273
|
}
|
|
3274
|
+
log.info("bridged opencode MCP config", {
|
|
3275
|
+
target: outPath,
|
|
3276
|
+
hash,
|
|
3277
|
+
servers: bridgedServerNames,
|
|
3278
|
+
excluded: excludeServers ? Array.from(excludeServers) : []
|
|
3279
|
+
});
|
|
2954
3280
|
return {
|
|
2955
|
-
|
|
2956
|
-
|
|
2957
|
-
|
|
2958
|
-
|
|
2959
|
-
release_date: model.release_date,
|
|
2960
|
-
temperature: model.capabilities.temperature,
|
|
2961
|
-
reasoning: model.capabilities.reasoning,
|
|
2962
|
-
attachment: model.capabilities.attachment,
|
|
2963
|
-
tool_call: model.capabilities.toolcall,
|
|
2964
|
-
modalities: { input: inputMods, output: outputMods },
|
|
2965
|
-
cost: {
|
|
2966
|
-
input: model.cost.input,
|
|
2967
|
-
output: model.cost.output,
|
|
2968
|
-
cache_read: model.cost.cache.read,
|
|
2969
|
-
cache_write: model.cost.cache.write
|
|
2970
|
-
},
|
|
2971
|
-
limit: model.limit,
|
|
2972
|
-
options: model.options,
|
|
2973
|
-
headers: model.headers,
|
|
2974
|
-
variants: model.variants
|
|
3281
|
+
path: outPath,
|
|
3282
|
+
hash,
|
|
3283
|
+
serverNames: bridgedServerNames,
|
|
3284
|
+
allEnabledServerNames
|
|
2975
3285
|
};
|
|
2976
3286
|
}
|
|
2977
|
-
var defaultModels = {
|
|
2978
|
-
"claude-haiku-4-5": defineModel({
|
|
2979
|
-
id: "claude-haiku-4-5",
|
|
2980
|
-
name: "Claude Haiku 4.5",
|
|
2981
|
-
family: "haiku",
|
|
2982
|
-
reasoning: false,
|
|
2983
|
-
context: 2e5,
|
|
2984
|
-
output: 64e3,
|
|
2985
|
-
cost: haikuCost,
|
|
2986
|
-
multiplier: 1,
|
|
2987
|
-
releaseDate: "2025-10-01"
|
|
2988
|
-
}),
|
|
2989
|
-
"claude-sonnet-4-5": defineModel({
|
|
2990
|
-
id: "claude-sonnet-4-5",
|
|
2991
|
-
name: "Claude Sonnet 4.5",
|
|
2992
|
-
family: "sonnet",
|
|
2993
|
-
reasoning: true,
|
|
2994
|
-
context: 2e5,
|
|
2995
|
-
output: 64e3,
|
|
2996
|
-
cost: sonnetCost,
|
|
2997
|
-
multiplier: 3,
|
|
2998
|
-
releaseDate: "2025-09-29"
|
|
2999
|
-
}),
|
|
3000
|
-
"claude-sonnet-4-6": defineModel({
|
|
3001
|
-
id: "claude-sonnet-4-6",
|
|
3002
|
-
name: "Claude Sonnet 4.6",
|
|
3003
|
-
family: "sonnet",
|
|
3004
|
-
reasoning: true,
|
|
3005
|
-
context: 1e6,
|
|
3006
|
-
output: 128e3,
|
|
3007
|
-
cost: sonnetCost,
|
|
3008
|
-
multiplier: 3,
|
|
3009
|
-
releaseDate: "2025-06-19"
|
|
3010
|
-
}),
|
|
3011
|
-
"claude-sonnet-5": defineModel({
|
|
3012
|
-
id: "claude-sonnet-5",
|
|
3013
|
-
name: "Claude Sonnet 5",
|
|
3014
|
-
family: "sonnet",
|
|
3015
|
-
reasoning: true,
|
|
3016
|
-
context: 1e6,
|
|
3017
|
-
output: 128e3,
|
|
3018
|
-
cost: sonnetCost,
|
|
3019
|
-
multiplier: 3,
|
|
3020
|
-
releaseDate: "2026-06-30"
|
|
3021
|
-
}),
|
|
3022
|
-
"claude-opus-4-5": defineModel({
|
|
3023
|
-
id: "claude-opus-4-5",
|
|
3024
|
-
name: "Claude Opus 4.5",
|
|
3025
|
-
family: "opus",
|
|
3026
|
-
reasoning: true,
|
|
3027
|
-
context: 2e5,
|
|
3028
|
-
output: 64e3,
|
|
3029
|
-
cost: opusCost,
|
|
3030
|
-
multiplier: 5,
|
|
3031
|
-
releaseDate: "2025-11-01"
|
|
3032
|
-
}),
|
|
3033
|
-
"claude-opus-4-6": defineModel({
|
|
3034
|
-
id: "claude-opus-4-6",
|
|
3035
|
-
name: "Claude Opus 4.6",
|
|
3036
|
-
family: "opus",
|
|
3037
|
-
reasoning: true,
|
|
3038
|
-
context: 1e6,
|
|
3039
|
-
output: 128e3,
|
|
3040
|
-
cost: opusCost,
|
|
3041
|
-
multiplier: 5,
|
|
3042
|
-
releaseDate: "2025-06-19"
|
|
3043
|
-
}),
|
|
3044
|
-
"claude-opus-4-7": defineModel({
|
|
3045
|
-
id: "claude-opus-4-7",
|
|
3046
|
-
name: "Claude Opus 4.7",
|
|
3047
|
-
family: "opus",
|
|
3048
|
-
reasoning: true,
|
|
3049
|
-
context: 1e6,
|
|
3050
|
-
output: 128e3,
|
|
3051
|
-
cost: opusCost,
|
|
3052
|
-
multiplier: 5,
|
|
3053
|
-
releaseDate: "2025-07-16"
|
|
3054
|
-
}),
|
|
3055
|
-
"claude-opus-4-8": defineModel({
|
|
3056
|
-
id: "claude-opus-4-8",
|
|
3057
|
-
name: "Claude Opus 4.8",
|
|
3058
|
-
family: "opus",
|
|
3059
|
-
reasoning: true,
|
|
3060
|
-
context: 1e6,
|
|
3061
|
-
output: 128e3,
|
|
3062
|
-
cost: opusCost,
|
|
3063
|
-
multiplier: 5,
|
|
3064
|
-
releaseDate: "2026-05-28"
|
|
3065
|
-
}),
|
|
3066
|
-
// Fast mode. The `-fast` suffix is OUR marker, not a model name Anthropic
|
|
3067
|
-
// serves: `parseModelId` strips it before `--model` and turns it into
|
|
3068
|
-
// `--settings {"fastMode":true}` on the spawn. Retired `-fast` model strings
|
|
3069
|
-
// (`claude-opus-4-6-fast`) are a different thing and are not registered here.
|
|
3070
|
-
//
|
|
3071
|
-
// Only Opus 4.8 and Opus 5 qualify: the CLI gates fast mode on the resolved
|
|
3072
|
-
// model name containing `opus-4-8` or `opus-5`, so registering a fast entry
|
|
3073
|
-
// for any other model would produce a picker option that silently runs at
|
|
3074
|
-
// standard speed while displaying the 10x price.
|
|
3075
|
-
"claude-opus-4-8-fast": defineModel({
|
|
3076
|
-
id: "claude-opus-4-8-fast",
|
|
3077
|
-
name: "Claude Opus 4.8 Fast",
|
|
3078
|
-
family: "opus",
|
|
3079
|
-
reasoning: true,
|
|
3080
|
-
context: 1e6,
|
|
3081
|
-
output: 128e3,
|
|
3082
|
-
cost: opusFastCost,
|
|
3083
|
-
multiplier: 10,
|
|
3084
|
-
releaseDate: "2026-05-28"
|
|
3085
|
-
}),
|
|
3086
|
-
"claude-opus-5": defineModel({
|
|
3087
|
-
id: "claude-opus-5",
|
|
3088
|
-
name: "Claude Opus 5",
|
|
3089
|
-
family: "opus",
|
|
3090
|
-
reasoning: true,
|
|
3091
|
-
context: 1e6,
|
|
3092
|
-
output: 128e3,
|
|
3093
|
-
cost: opusCost,
|
|
3094
|
-
multiplier: 5,
|
|
3095
|
-
releaseDate: "2026-07-24"
|
|
3096
|
-
}),
|
|
3097
|
-
"claude-opus-5-fast": defineModel({
|
|
3098
|
-
id: "claude-opus-5-fast",
|
|
3099
|
-
name: "Claude Opus 5 Fast",
|
|
3100
|
-
family: "opus",
|
|
3101
|
-
reasoning: true,
|
|
3102
|
-
context: 1e6,
|
|
3103
|
-
output: 128e3,
|
|
3104
|
-
cost: opusFastCost,
|
|
3105
|
-
multiplier: 10,
|
|
3106
|
-
releaseDate: "2026-07-24"
|
|
3107
|
-
}),
|
|
3108
|
-
"claude-fable-5": defineModel({
|
|
3109
|
-
id: "claude-fable-5",
|
|
3110
|
-
name: "Claude Fable 5",
|
|
3111
|
-
family: "fable",
|
|
3112
|
-
reasoning: true,
|
|
3113
|
-
context: 1e6,
|
|
3114
|
-
output: 128e3,
|
|
3115
|
-
cost: fableCost,
|
|
3116
|
-
multiplier: 10,
|
|
3117
|
-
releaseDate: "2026-06-09"
|
|
3118
|
-
}),
|
|
3119
|
-
"claude-fable-5-1": defineModel({
|
|
3120
|
-
id: "claude-fable-5-1",
|
|
3121
|
-
name: "Claude Fable 5.1",
|
|
3122
|
-
family: "fable",
|
|
3123
|
-
reasoning: true,
|
|
3124
|
-
context: 1e6,
|
|
3125
|
-
output: 128e3,
|
|
3126
|
-
cost: fable51Cost,
|
|
3127
|
-
multiplier: 10,
|
|
3128
|
-
releaseDate: "2026-09-01"
|
|
3129
|
-
}),
|
|
3130
|
-
// Mythos 5 and 5.1 share the corresponding Fable models' capabilities and
|
|
3131
|
-
// pricing without the safety classifiers; limited availability via Project
|
|
3132
|
-
// Glasswing. `claude --model` simply errors for accounts without access, so
|
|
3133
|
-
// they are safe to register unconditionally.
|
|
3134
|
-
"claude-mythos-5": defineModel({
|
|
3135
|
-
id: "claude-mythos-5",
|
|
3136
|
-
name: "Claude Mythos 5",
|
|
3137
|
-
family: "mythos",
|
|
3138
|
-
reasoning: true,
|
|
3139
|
-
context: 1e6,
|
|
3140
|
-
output: 128e3,
|
|
3141
|
-
cost: fableCost,
|
|
3142
|
-
multiplier: 10,
|
|
3143
|
-
releaseDate: "2026-06-09"
|
|
3144
|
-
}),
|
|
3145
|
-
"claude-mythos-5-1": defineModel({
|
|
3146
|
-
id: "claude-mythos-5-1",
|
|
3147
|
-
name: "Claude Mythos 5.1",
|
|
3148
|
-
family: "mythos",
|
|
3149
|
-
reasoning: true,
|
|
3150
|
-
context: 1e6,
|
|
3151
|
-
output: 128e3,
|
|
3152
|
-
cost: fable51Cost,
|
|
3153
|
-
multiplier: 10,
|
|
3154
|
-
releaseDate: "2026-09-01"
|
|
3155
|
-
})
|
|
3156
|
-
};
|
|
3157
|
-
var FAST_SUFFIX = "-fast";
|
|
3158
|
-
function parseModelId(modelId) {
|
|
3159
|
-
const at = modelId.indexOf("@");
|
|
3160
|
-
const base = at === -1 ? modelId : modelId.slice(0, at);
|
|
3161
|
-
const account = at === -1 ? "" : modelId.slice(at);
|
|
3162
|
-
if (!base.endsWith(FAST_SUFFIX)) return { model: modelId, fast: false };
|
|
3163
|
-
if (!Object.hasOwn(defaultModels, base)) return { model: modelId, fast: false };
|
|
3164
|
-
return { model: base.slice(0, -FAST_SUFFIX.length) + account, fast: true };
|
|
3165
|
-
}
|
|
3166
3287
|
|
|
3167
|
-
// src/
|
|
3168
|
-
var
|
|
3169
|
-
|
|
3170
|
-
"
|
|
3171
|
-
|
|
3172
|
-
|
|
3173
|
-
"high",
|
|
3174
|
-
"xhigh",
|
|
3175
|
-
"max"
|
|
3176
|
-
];
|
|
3177
|
-
var registry = {};
|
|
3178
|
-
var defaultSubagentModel;
|
|
3179
|
-
function setAgentRegistry(records) {
|
|
3180
|
-
registry = records;
|
|
3288
|
+
// src/runtime-status.ts
|
|
3289
|
+
var opencodeClient = null;
|
|
3290
|
+
function setOpencodeClient(client) {
|
|
3291
|
+
if (client && typeof client === "object") {
|
|
3292
|
+
opencodeClient = client;
|
|
3293
|
+
}
|
|
3181
3294
|
}
|
|
3182
|
-
function
|
|
3183
|
-
return
|
|
3295
|
+
function getOpencodeClient() {
|
|
3296
|
+
return opencodeClient;
|
|
3184
3297
|
}
|
|
3185
|
-
|
|
3186
|
-
|
|
3298
|
+
var opencodeProjectDirectory;
|
|
3299
|
+
function setOpencodeProjectDirectory(dir) {
|
|
3300
|
+
opencodeProjectDirectory = dir;
|
|
3187
3301
|
}
|
|
3188
|
-
function
|
|
3189
|
-
return
|
|
3302
|
+
function getOpencodeProjectDirectory() {
|
|
3303
|
+
return opencodeProjectDirectory;
|
|
3190
3304
|
}
|
|
3191
|
-
function
|
|
3192
|
-
|
|
3193
|
-
return at === -1 ? "" : modelId.slice(at);
|
|
3305
|
+
function isUsableDirectory(d) {
|
|
3306
|
+
return typeof d === "string" && d.length > 1 && d !== "/";
|
|
3194
3307
|
}
|
|
3195
|
-
function
|
|
3196
|
-
|
|
3197
|
-
|
|
3308
|
+
function resolveSpawnCwdFrom(configured, live, captured, sessionDir) {
|
|
3309
|
+
if (configured) return configured;
|
|
3310
|
+
if (isUsableDirectory(sessionDir)) return sessionDir;
|
|
3311
|
+
if (isUsableDirectory(live)) return live;
|
|
3312
|
+
return captured ?? live;
|
|
3198
3313
|
}
|
|
3199
|
-
function
|
|
3200
|
-
if (
|
|
3201
|
-
const
|
|
3202
|
-
|
|
3203
|
-
|
|
3204
|
-
|
|
3205
|
-
|
|
3206
|
-
|
|
3207
|
-
|
|
3208
|
-
const base = withoutAccountMarker(wanted);
|
|
3209
|
-
if (!Object.hasOwn(defaultModels, base)) {
|
|
3210
|
-
log.warn("agent model override refused: unknown model", {
|
|
3211
|
-
agent,
|
|
3212
|
-
wanted: base,
|
|
3213
|
-
keeping: modelId
|
|
3214
|
-
});
|
|
3215
|
-
return modelId;
|
|
3216
|
-
}
|
|
3217
|
-
const resolved = `${base}${accountMarker(modelId)}`;
|
|
3218
|
-
if (resolved !== modelId) {
|
|
3219
|
-
log.debug("agent model override", { agent, from: modelId, to: resolved });
|
|
3220
|
-
}
|
|
3221
|
-
return resolved;
|
|
3314
|
+
async function resolveSpawnCwdForSession(configured, sessionID) {
|
|
3315
|
+
if (configured) return configured;
|
|
3316
|
+
const sessionDir = sessionID ? await fetchSessionDirectory(sessionID) : void 0;
|
|
3317
|
+
return resolveSpawnCwdFrom(
|
|
3318
|
+
configured,
|
|
3319
|
+
process.cwd(),
|
|
3320
|
+
opencodeProjectDirectory,
|
|
3321
|
+
sessionDir
|
|
3322
|
+
);
|
|
3222
3323
|
}
|
|
3223
|
-
function
|
|
3224
|
-
if (!
|
|
3225
|
-
const
|
|
3226
|
-
|
|
3227
|
-
|
|
3228
|
-
|
|
3229
|
-
|
|
3230
|
-
|
|
3231
|
-
|
|
3232
|
-
|
|
3233
|
-
|
|
3234
|
-
|
|
3235
|
-
|
|
3236
|
-
|
|
3237
|
-
log.debug("agent effort override", {
|
|
3238
|
-
agent,
|
|
3239
|
-
from: inherited,
|
|
3240
|
-
to: declared
|
|
3324
|
+
async function fetchSessionDirectory(sessionID) {
|
|
3325
|
+
if (!sessionID || sessionID === "default") return void 0;
|
|
3326
|
+
const client = opencodeClient;
|
|
3327
|
+
if (!client?.session?.get) return void 0;
|
|
3328
|
+
try {
|
|
3329
|
+
const res = await client.session.get({ path: { id: sessionID } });
|
|
3330
|
+
const data = res.data;
|
|
3331
|
+
if (!data || typeof data !== "object") return void 0;
|
|
3332
|
+
const dir = data.directory;
|
|
3333
|
+
return isUsableDirectory(dir) ? dir : void 0;
|
|
3334
|
+
} catch (err) {
|
|
3335
|
+
log.warn("failed to fetch opencode session directory", {
|
|
3336
|
+
sessionID,
|
|
3337
|
+
error: err instanceof Error ? err.message : String(err)
|
|
3241
3338
|
});
|
|
3339
|
+
return void 0;
|
|
3242
3340
|
}
|
|
3243
|
-
return declared;
|
|
3244
|
-
}
|
|
3245
|
-
function parseAgentFrontmatter(text) {
|
|
3246
|
-
const record = {};
|
|
3247
|
-
if (!text.startsWith("---")) return record;
|
|
3248
|
-
const lines = text.split(/\r?\n/);
|
|
3249
|
-
for (let i = 1; i < lines.length; i++) {
|
|
3250
|
-
const line = lines[i];
|
|
3251
|
-
if (line.trim() === "---") break;
|
|
3252
|
-
const match = /^([A-Za-z_][A-Za-z0-9_-]*):[ \t]*(.*)$/.exec(line);
|
|
3253
|
-
if (!match) continue;
|
|
3254
|
-
const key = match[1];
|
|
3255
|
-
if (key !== "mode" && key !== "model" && key !== "forceModel" && key !== "reasoningEffort")
|
|
3256
|
-
continue;
|
|
3257
|
-
const value = match[2].trim().replace(/^["']|["']$/g, "");
|
|
3258
|
-
if (value) record[key] = value;
|
|
3259
|
-
}
|
|
3260
|
-
return record;
|
|
3261
3341
|
}
|
|
3262
|
-
async function
|
|
3263
|
-
const
|
|
3264
|
-
|
|
3265
|
-
|
|
3266
|
-
|
|
3267
|
-
|
|
3268
|
-
|
|
3269
|
-
|
|
3270
|
-
|
|
3271
|
-
|
|
3272
|
-
|
|
3273
|
-
|
|
3274
|
-
if (records[name]) continue;
|
|
3275
|
-
try {
|
|
3276
|
-
const text = await readFile(path3.join(directory, entry), "utf8");
|
|
3277
|
-
records[name] = parseAgentFrontmatter(text);
|
|
3278
|
-
} catch (err) {
|
|
3279
|
-
log.debug("failed to read agent markdown", {
|
|
3280
|
-
file: path3.join(directory, entry),
|
|
3281
|
-
error: String(err)
|
|
3282
|
-
});
|
|
3342
|
+
async function getRuntimeMcpStatus() {
|
|
3343
|
+
const client = opencodeClient;
|
|
3344
|
+
if (!client?.mcp?.status) return void 0;
|
|
3345
|
+
try {
|
|
3346
|
+
const res = await client.mcp.status();
|
|
3347
|
+
const data = res.data;
|
|
3348
|
+
if (!data || typeof data !== "object") return void 0;
|
|
3349
|
+
const out = {};
|
|
3350
|
+
for (const [name, entry] of Object.entries(data)) {
|
|
3351
|
+
if (entry && typeof entry === "object") {
|
|
3352
|
+
const status = entry.status;
|
|
3353
|
+
if (typeof status === "string") out[name] = status;
|
|
3283
3354
|
}
|
|
3284
3355
|
}
|
|
3356
|
+
return out;
|
|
3357
|
+
} catch (err) {
|
|
3358
|
+
log.warn("failed to fetch opencode MCP runtime status", {
|
|
3359
|
+
error: err instanceof Error ? err.message : String(err)
|
|
3360
|
+
});
|
|
3361
|
+
return void 0;
|
|
3285
3362
|
}
|
|
3286
|
-
return records;
|
|
3287
3363
|
}
|
|
3288
|
-
function
|
|
3289
|
-
const
|
|
3290
|
-
if (
|
|
3291
|
-
|
|
3292
|
-
|
|
3293
|
-
|
|
3294
|
-
|
|
3295
|
-
|
|
3296
|
-
|
|
3297
|
-
|
|
3364
|
+
async function fetchOpencodeToolList(provider, model, directory) {
|
|
3365
|
+
const client = opencodeClient;
|
|
3366
|
+
if (!client?.tool?.list) return void 0;
|
|
3367
|
+
try {
|
|
3368
|
+
const res = await client.tool.list({
|
|
3369
|
+
query: { provider, model, ...directory ? { directory } : {} }
|
|
3370
|
+
});
|
|
3371
|
+
const data = res.data;
|
|
3372
|
+
if (!Array.isArray(data)) return void 0;
|
|
3373
|
+
const out = [];
|
|
3374
|
+
for (const entry of data) {
|
|
3375
|
+
if (!entry || typeof entry !== "object") continue;
|
|
3376
|
+
const e = entry;
|
|
3377
|
+
const id = typeof e.id === "string" ? e.id : null;
|
|
3378
|
+
const description = typeof e.description === "string" ? e.description : "";
|
|
3379
|
+
const parameters = e.parameters && typeof e.parameters === "object" ? e.parameters : {};
|
|
3380
|
+
if (!id) continue;
|
|
3381
|
+
out.push({ id, description, parameters });
|
|
3298
3382
|
}
|
|
3383
|
+
return out;
|
|
3384
|
+
} catch (err) {
|
|
3385
|
+
log.warn("failed to fetch opencode tool list", {
|
|
3386
|
+
provider,
|
|
3387
|
+
model,
|
|
3388
|
+
error: err instanceof Error ? err.message : String(err)
|
|
3389
|
+
});
|
|
3390
|
+
return void 0;
|
|
3299
3391
|
}
|
|
3300
|
-
return directories;
|
|
3301
3392
|
}
|
|
3302
3393
|
|
|
3303
|
-
// src/
|
|
3304
|
-
|
|
3305
|
-
|
|
3306
|
-
|
|
3307
|
-
import * as path4 from "path";
|
|
3308
|
-
import { fileURLToPath } from "url";
|
|
3309
|
-
var SKILL_PLUGIN_NAME = "opencode-skills";
|
|
3310
|
-
function bundledSkillsDir() {
|
|
3394
|
+
// src/startup-diagnostics.ts
|
|
3395
|
+
var cachedPluginVersion;
|
|
3396
|
+
function pluginVersion() {
|
|
3397
|
+
if (cachedPluginVersion) return cachedPluginVersion;
|
|
3311
3398
|
try {
|
|
3312
|
-
const here = fileURLToPath(import.meta.url);
|
|
3313
|
-
const
|
|
3314
|
-
|
|
3399
|
+
const here = path4.dirname(fileURLToPath(import.meta.url));
|
|
3400
|
+
const raw = fs4.readFileSync(path4.join(here, "..", "package.json"), "utf8");
|
|
3401
|
+
const version = JSON.parse(raw).version;
|
|
3402
|
+
cachedPluginVersion = typeof version === "string" ? version : "unknown";
|
|
3315
3403
|
} catch {
|
|
3316
|
-
|
|
3404
|
+
cachedPluginVersion = "unknown";
|
|
3317
3405
|
}
|
|
3406
|
+
return cachedPluginVersion;
|
|
3318
3407
|
}
|
|
3319
|
-
function
|
|
3320
|
-
|
|
3321
|
-
|
|
3322
|
-
|
|
3323
|
-
|
|
3408
|
+
function pickOpencodeVersion(input) {
|
|
3409
|
+
if (!input || typeof input !== "object") return void 0;
|
|
3410
|
+
const app = input.app;
|
|
3411
|
+
if (app && typeof app === "object") {
|
|
3412
|
+
const version = app.version;
|
|
3413
|
+
if (typeof version === "string" && version.length > 0) return version;
|
|
3324
3414
|
}
|
|
3415
|
+
const direct = input.version;
|
|
3416
|
+
if (typeof direct === "string" && direct.length > 0) return direct;
|
|
3417
|
+
return void 0;
|
|
3325
3418
|
}
|
|
3326
|
-
|
|
3327
|
-
|
|
3328
|
-
|
|
3329
|
-
|
|
3330
|
-
|
|
3419
|
+
var execFileAsync2 = promisify2(execFile2);
|
|
3420
|
+
var opencodeVersionProbe;
|
|
3421
|
+
function detectOpencodeVersion(execPath = process.execPath) {
|
|
3422
|
+
if (opencodeVersionProbe) return opencodeVersionProbe;
|
|
3423
|
+
opencodeVersionProbe = (async () => {
|
|
3424
|
+
if (!path4.basename(execPath).toLowerCase().includes("opencode")) {
|
|
3425
|
+
log.debug("skipping opencode version probe: execPath is not opencode", { execPath });
|
|
3426
|
+
return void 0;
|
|
3427
|
+
}
|
|
3428
|
+
try {
|
|
3429
|
+
const { stdout } = await execFileAsync2(execPath, ["--version"], { timeout: 5e3 });
|
|
3430
|
+
const match = /\d+\.\d+\.\d+\S*/.exec(stdout.trim());
|
|
3431
|
+
return match ? match[0] : void 0;
|
|
3432
|
+
} catch (err) {
|
|
3433
|
+
log.debug("opencode version probe failed", {
|
|
3434
|
+
execPath,
|
|
3435
|
+
error: err instanceof Error ? err.message : String(err)
|
|
3436
|
+
});
|
|
3437
|
+
return void 0;
|
|
3438
|
+
}
|
|
3439
|
+
})();
|
|
3440
|
+
return opencodeVersionProbe;
|
|
3441
|
+
}
|
|
3442
|
+
function describeSpawnCwd(configured, live = process.cwd(), captured = getOpencodeProjectDirectory()) {
|
|
3443
|
+
if (typeof configured === "string" && configured.length > 0) {
|
|
3444
|
+
return { resolved: configured, source: "configured" };
|
|
3331
3445
|
}
|
|
3446
|
+
if (isUsableDirectory(live)) return { resolved: live, source: "process" };
|
|
3447
|
+
if (isUsableDirectory(captured)) return { resolved: captured, source: "captured" };
|
|
3448
|
+
return { resolved: live, source: "unresolved" };
|
|
3332
3449
|
}
|
|
3333
|
-
function
|
|
3334
|
-
|
|
3335
|
-
|
|
3336
|
-
|
|
3337
|
-
|
|
3338
|
-
|
|
3339
|
-
|
|
3340
|
-
if (
|
|
3341
|
-
};
|
|
3342
|
-
let current = path4.resolve(cwd);
|
|
3343
|
-
while (true) {
|
|
3344
|
-
push(path4.join(current, ".opencode", "skills"));
|
|
3345
|
-
const parent = path4.dirname(current);
|
|
3346
|
-
if (parent === current) break;
|
|
3347
|
-
current = parent;
|
|
3450
|
+
function stringList(value) {
|
|
3451
|
+
if (!Array.isArray(value)) return [];
|
|
3452
|
+
return value.filter((entry) => typeof entry === "string");
|
|
3453
|
+
}
|
|
3454
|
+
function firstOption(providers, key) {
|
|
3455
|
+
for (const entry of Object.values(providers)) {
|
|
3456
|
+
const value = entry?.options?.[key];
|
|
3457
|
+
if (value !== void 0) return value;
|
|
3348
3458
|
}
|
|
3349
|
-
|
|
3350
|
-
if (home) push(path4.join(home, ".opencode", "skills"));
|
|
3351
|
-
const envDir = process.env.OPENCODE_CONFIG_DIR;
|
|
3352
|
-
if (envDir) push(path4.join(envDir, "skills"));
|
|
3353
|
-
const xdg = process.env.XDG_CONFIG_HOME ?? (home ? path4.join(home, ".config") : null);
|
|
3354
|
-
if (xdg) push(path4.join(xdg, "opencode", "skills"));
|
|
3355
|
-
return roots;
|
|
3459
|
+
return void 0;
|
|
3356
3460
|
}
|
|
3357
|
-
function
|
|
3358
|
-
|
|
3359
|
-
|
|
3360
|
-
|
|
3361
|
-
|
|
3362
|
-
return;
|
|
3461
|
+
function collectStartupDiagnostics(providers, opencodeVersion) {
|
|
3462
|
+
const accounts = [];
|
|
3463
|
+
for (const entry of Object.values(providers)) {
|
|
3464
|
+
const account = entry?.options?.account;
|
|
3465
|
+
if (typeof account === "string" && account.length > 0) accounts.push(account);
|
|
3363
3466
|
}
|
|
3364
|
-
|
|
3365
|
-
|
|
3366
|
-
|
|
3367
|
-
|
|
3368
|
-
|
|
3369
|
-
|
|
3370
|
-
|
|
3371
|
-
|
|
3372
|
-
found.push({ name, dir });
|
|
3467
|
+
const cwd = describeSpawnCwd(firstOption(providers, "cwd"));
|
|
3468
|
+
let mcpServers = [];
|
|
3469
|
+
try {
|
|
3470
|
+
mcpServers = mergeOpencodeMcp(cwd.resolved).enabledServerNames;
|
|
3471
|
+
} catch (err) {
|
|
3472
|
+
log.debug("startup diagnostics could not read MCP config", {
|
|
3473
|
+
error: err instanceof Error ? err.message : String(err)
|
|
3474
|
+
});
|
|
3373
3475
|
}
|
|
3476
|
+
return {
|
|
3477
|
+
plugin: pluginVersion(),
|
|
3478
|
+
opencode: opencodeVersion ?? process.env.OPENCODE_VERSION ?? "unknown",
|
|
3479
|
+
claudeCliPath: String(firstOption(providers, "cliPath") ?? "claude"),
|
|
3480
|
+
cwd,
|
|
3481
|
+
providers: Object.keys(providers),
|
|
3482
|
+
accounts,
|
|
3483
|
+
proxyTools: stringList(firstOption(providers, "proxyTools")),
|
|
3484
|
+
mcpServers,
|
|
3485
|
+
interactiveTransport: firstOption(providers, "interactive") === true || process.env.CLAUDE_CODE_INTERACTIVE_TRANSPORT === "1",
|
|
3486
|
+
planModeQuestion: firstOption(providers, "planModeQuestion") === true,
|
|
3487
|
+
anthropicApiKeyInEnv: Boolean(
|
|
3488
|
+
process.env.ANTHROPIC_API_KEY || process.env.ANTHROPIC_AUTH_TOKEN
|
|
3489
|
+
)
|
|
3490
|
+
};
|
|
3374
3491
|
}
|
|
3375
|
-
var
|
|
3376
|
-
|
|
3377
|
-
|
|
3378
|
-
|
|
3379
|
-
|
|
3380
|
-
return found.sort(byName);
|
|
3492
|
+
var logged = false;
|
|
3493
|
+
var lastProviders = {};
|
|
3494
|
+
var lastOpencodeVersion;
|
|
3495
|
+
function lastDiagnosticsProviders() {
|
|
3496
|
+
return lastProviders;
|
|
3381
3497
|
}
|
|
3382
|
-
function
|
|
3383
|
-
|
|
3384
|
-
if (!root) return [];
|
|
3385
|
-
const found = [];
|
|
3386
|
-
collectSkills(root, /* @__PURE__ */ new Set(), found);
|
|
3387
|
-
return found.sort(byName);
|
|
3498
|
+
function lastKnownOpencodeVersion() {
|
|
3499
|
+
return lastOpencodeVersion;
|
|
3388
3500
|
}
|
|
3389
|
-
function
|
|
3390
|
-
|
|
3391
|
-
if (
|
|
3392
|
-
|
|
3393
|
-
|
|
3394
|
-
|
|
3395
|
-
|
|
3396
|
-
|
|
3501
|
+
function logStartupDiagnostics(providers, opencodeVersion) {
|
|
3502
|
+
lastProviders = providers;
|
|
3503
|
+
if (opencodeVersion) lastOpencodeVersion = opencodeVersion;
|
|
3504
|
+
if (logged) return;
|
|
3505
|
+
logged = true;
|
|
3506
|
+
void (async () => {
|
|
3507
|
+
try {
|
|
3508
|
+
const version = opencodeVersion ?? process.env.OPENCODE_VERSION ?? await detectOpencodeVersion();
|
|
3509
|
+
const { claudeCliPath, ...rest } = collectStartupDiagnostics(providers, version);
|
|
3510
|
+
const cli = await detectCliVersion(claudeCliPath);
|
|
3511
|
+
const diagnostics = {
|
|
3512
|
+
...rest,
|
|
3513
|
+
claudeCli: { path: claudeCliPath, version: cli?.raw ?? "not detected" }
|
|
3514
|
+
};
|
|
3515
|
+
log.notice("claude-code plugin ready", { ...diagnostics });
|
|
3516
|
+
} catch (err) {
|
|
3517
|
+
log.debug("startup diagnostics failed", {
|
|
3518
|
+
error: err instanceof Error ? err.message : String(err)
|
|
3519
|
+
});
|
|
3520
|
+
}
|
|
3521
|
+
})();
|
|
3397
3522
|
}
|
|
3398
|
-
|
|
3399
|
-
|
|
3400
|
-
|
|
3401
|
-
|
|
3402
|
-
|
|
3403
|
-
|
|
3404
|
-
}
|
|
3523
|
+
|
|
3524
|
+
// src/doctor.ts
|
|
3525
|
+
var DOCTOR_COMMAND = "claude-code-doctor";
|
|
3526
|
+
var DOCTOR_MARKER = "\u258C **claude-code doctor**";
|
|
3527
|
+
function isRecord3(value) {
|
|
3528
|
+
return value !== null && typeof value === "object" && !Array.isArray(value);
|
|
3405
3529
|
}
|
|
3406
|
-
|
|
3407
|
-
|
|
3408
|
-
|
|
3409
|
-
|
|
3410
|
-
|
|
3411
|
-
|
|
3412
|
-
|
|
3413
|
-
|
|
3414
|
-
|
|
3415
|
-
|
|
3416
|
-
fs3.mkdirSync(path4.join(root, "skills"), { recursive: true });
|
|
3417
|
-
fs3.writeFileSync(
|
|
3418
|
-
manifest,
|
|
3419
|
-
JSON.stringify(
|
|
3420
|
-
{
|
|
3421
|
-
name: SKILL_PLUGIN_NAME,
|
|
3422
|
-
description: "Skills discovered from this opencode installation, bridged into Claude Code."
|
|
3423
|
-
},
|
|
3424
|
-
null,
|
|
3425
|
-
2
|
|
3426
|
-
),
|
|
3427
|
-
{ encoding: "utf8", mode: 384 }
|
|
3428
|
-
);
|
|
3429
|
-
for (const skill of skills) {
|
|
3430
|
-
linkSkill(skill.dir, path4.join(root, "skills", skill.name));
|
|
3530
|
+
var SYSTEM_REMINDER_BLOCK2 = /<system-reminder>[\s\S]*?<\/system-reminder>/g;
|
|
3531
|
+
function parseDoctorCommandContent(content) {
|
|
3532
|
+
let text;
|
|
3533
|
+
if (typeof content === "string") {
|
|
3534
|
+
text = content;
|
|
3535
|
+
} else if (Array.isArray(content)) {
|
|
3536
|
+
const parts = [];
|
|
3537
|
+
for (const part of content) {
|
|
3538
|
+
if (!isRecord3(part) || part.type !== "text" || typeof part.text !== "string") return null;
|
|
3539
|
+
parts.push(part.text);
|
|
3431
3540
|
}
|
|
3432
|
-
|
|
3433
|
-
|
|
3434
|
-
root,
|
|
3435
|
-
error: err instanceof Error ? err.message : String(err)
|
|
3436
|
-
});
|
|
3541
|
+
text = parts.join("\n");
|
|
3542
|
+
} else {
|
|
3437
3543
|
return null;
|
|
3438
3544
|
}
|
|
3439
|
-
|
|
3545
|
+
const match = new RegExp(`^/${DOCTOR_COMMAND}(?:\\s+([\\s\\S]*))?$`).exec(
|
|
3546
|
+
text.replace(SYSTEM_REMINDER_BLOCK2, "").trim()
|
|
3547
|
+
);
|
|
3548
|
+
return match ? { rest: (match[1] ?? "").trim() } : null;
|
|
3440
3549
|
}
|
|
3441
|
-
|
|
3442
|
-
const
|
|
3443
|
-
|
|
3444
|
-
|
|
3445
|
-
|
|
3446
|
-
if (
|
|
3447
|
-
|
|
3448
|
-
if (
|
|
3449
|
-
|
|
3450
|
-
|
|
3451
|
-
|
|
3452
|
-
|
|
3453
|
-
|
|
3550
|
+
function parseDoctorCommand(prompt) {
|
|
3551
|
+
const latest = prompt.at(-1);
|
|
3552
|
+
return latest?.role === "user" ? parseDoctorCommandContent(latest.content) : null;
|
|
3553
|
+
}
|
|
3554
|
+
function formatAge(ms) {
|
|
3555
|
+
if (ms === void 0) return "unknown";
|
|
3556
|
+
if (ms < 1e3) return `${Math.round(ms)}ms`;
|
|
3557
|
+
if (ms < 6e4) return `${(ms / 1e3).toFixed(1)}s`;
|
|
3558
|
+
const minutes = Math.floor(ms / 6e4);
|
|
3559
|
+
if (minutes < 60) return `${minutes}m`;
|
|
3560
|
+
return `${Math.floor(minutes / 60)}h ${minutes % 60}m`;
|
|
3561
|
+
}
|
|
3562
|
+
function list(values) {
|
|
3563
|
+
return values.length ? values.join(", ") : "none";
|
|
3564
|
+
}
|
|
3565
|
+
function describeAuth(auth) {
|
|
3566
|
+
switch (auth.status) {
|
|
3567
|
+
case "ok":
|
|
3568
|
+
return `${auth.code}, good`;
|
|
3569
|
+
case "unsafe":
|
|
3570
|
+
return `${auth.code}, UNSAFE: an unauthenticated caller was accepted. Restart every opencode window; a window opened before 0.13.2 keeps serving an open port.`;
|
|
3571
|
+
case "unreachable":
|
|
3572
|
+
return `could not be checked (${auth.error})`;
|
|
3573
|
+
case "skipped":
|
|
3574
|
+
return "not checked";
|
|
3575
|
+
}
|
|
3576
|
+
}
|
|
3577
|
+
function formatDoctorReport(report) {
|
|
3578
|
+
const lines = [];
|
|
3579
|
+
lines.push(DOCTOR_MARKER);
|
|
3580
|
+
lines.push("");
|
|
3581
|
+
lines.push("| Field | Value |");
|
|
3582
|
+
lines.push("|---|---|");
|
|
3583
|
+
lines.push(`| plugin | ${report.plugin} |`);
|
|
3584
|
+
lines.push(`| opencode | ${report.opencode} |`);
|
|
3585
|
+
lines.push(`| claude CLI | \`${report.claudeCli.path}\` (${report.claudeCli.version}) |`);
|
|
3586
|
+
lines.push(`| cwd | \`${report.cwd.resolved}\` (${report.cwd.source}) |`);
|
|
3587
|
+
lines.push(`| providers | ${list(report.providers)} |`);
|
|
3588
|
+
lines.push(`| accounts | ${list(report.accounts)} |`);
|
|
3589
|
+
lines.push(`| proxyTools | ${list(report.proxyTools)} |`);
|
|
3590
|
+
lines.push(`| MCP servers (on disk) | ${list(report.mcpServers)} |`);
|
|
3591
|
+
lines.push(`| transport | ${report.transport} |`);
|
|
3592
|
+
lines.push(`| planModeQuestion | ${report.planModeQuestion} |`);
|
|
3593
|
+
lines.push(`| turnStats | ${report.turnStats} |`);
|
|
3594
|
+
lines.push(`| ANTHROPIC_API_KEY in env | ${report.anthropicApiKeyInEnv ? "yes" : "no"} |`);
|
|
3595
|
+
lines.push("");
|
|
3596
|
+
lines.push("**Live `claude` processes**");
|
|
3597
|
+
lines.push("");
|
|
3598
|
+
if (report.processes.length === 0) {
|
|
3599
|
+
lines.push("None. The next message in a Claude Code session spawns one.");
|
|
3600
|
+
} else {
|
|
3601
|
+
lines.push("| session | model | pid | in flight | age | effort |");
|
|
3602
|
+
lines.push("|---|---|---|---|---|---|");
|
|
3603
|
+
for (const proc of report.processes) {
|
|
3604
|
+
lines.push(
|
|
3605
|
+
`| ${proc.session}${proc.compaction ? " (compaction)" : ""} | ${proc.model} | ${proc.pid ?? "unknown"} | ${proc.inFlight ? "yes" : "no"} | ${formatAge(proc.ageMs)} | ${proc.effort ?? "inherited"} |`
|
|
3606
|
+
);
|
|
3607
|
+
}
|
|
3454
3608
|
}
|
|
3455
|
-
|
|
3456
|
-
|
|
3457
|
-
|
|
3458
|
-
|
|
3459
|
-
|
|
3460
|
-
|
|
3461
|
-
|
|
3462
|
-
|
|
3463
|
-
|
|
3609
|
+
lines.push("");
|
|
3610
|
+
lines.push("**Pending proxy calls**");
|
|
3611
|
+
lines.push("");
|
|
3612
|
+
if (report.pendingCalls.length === 0) {
|
|
3613
|
+
lines.push("None.");
|
|
3614
|
+
} else {
|
|
3615
|
+
lines.push("| tool | call id | age | deadline |");
|
|
3616
|
+
lines.push("|---|---|---|---|");
|
|
3617
|
+
for (const call of report.pendingCalls) {
|
|
3618
|
+
const deadline = call.deadlineMs > 0 ? formatAge(call.deadlineMs) : "none";
|
|
3619
|
+
lines.push(
|
|
3620
|
+
`| ${call.toolName} | \`${call.toolCallId}\` | ${formatAge(call.ageMs)} | ${deadline} |`
|
|
3621
|
+
);
|
|
3622
|
+
}
|
|
3623
|
+
}
|
|
3624
|
+
lines.push("");
|
|
3625
|
+
lines.push("**Proxy servers**");
|
|
3626
|
+
lines.push("");
|
|
3627
|
+
if (report.proxyServers.length === 0) {
|
|
3628
|
+
lines.push("None running.");
|
|
3629
|
+
} else {
|
|
3630
|
+
lines.push("| url | unauthenticated `initialize` |");
|
|
3631
|
+
lines.push("|---|---|");
|
|
3632
|
+
for (const server2 of report.proxyServers) {
|
|
3633
|
+
lines.push(`| ${server2.url} | ${describeAuth(server2.auth)} |`);
|
|
3634
|
+
}
|
|
3635
|
+
}
|
|
3636
|
+
const stderr = report.processes.filter((proc) => proc.lastStderr);
|
|
3637
|
+
if (stderr.length > 0) {
|
|
3638
|
+
lines.push("");
|
|
3639
|
+
lines.push("**Last stderr**");
|
|
3640
|
+
lines.push("");
|
|
3641
|
+
for (const proc of stderr) {
|
|
3642
|
+
lines.push(`\`${proc.session}\`:`);
|
|
3643
|
+
lines.push("");
|
|
3644
|
+
lines.push("```text");
|
|
3645
|
+
lines.push(proc.lastStderr.trimEnd());
|
|
3646
|
+
lines.push("```");
|
|
3647
|
+
}
|
|
3648
|
+
}
|
|
3649
|
+
return lines.join("\n");
|
|
3464
3650
|
}
|
|
3465
|
-
|
|
3466
|
-
|
|
3467
|
-
import * as fs4 from "fs";
|
|
3468
|
-
import * as path5 from "path";
|
|
3469
|
-
import * as os3 from "os";
|
|
3470
|
-
import * as crypto3 from "crypto";
|
|
3471
|
-
import {
|
|
3472
|
-
parse as parseJsonc,
|
|
3473
|
-
printParseErrorCode
|
|
3474
|
-
} from "jsonc-parser";
|
|
3475
|
-
var FILE_NAMES = ["opencode.jsonc", "opencode.json", "config.json"];
|
|
3476
|
-
var PROJECT_FILE_NAMES = ["opencode.json", "opencode.jsonc"];
|
|
3477
|
-
function fileExists2(p) {
|
|
3651
|
+
async function checkProxyAuth(url, fetchImpl = fetch, timeoutMs = 3e3) {
|
|
3652
|
+
let authority;
|
|
3478
3653
|
try {
|
|
3479
|
-
|
|
3654
|
+
authority = new URL(url).host;
|
|
3480
3655
|
} catch {
|
|
3481
|
-
return
|
|
3656
|
+
return { status: "skipped" };
|
|
3482
3657
|
}
|
|
3483
|
-
|
|
3484
|
-
|
|
3658
|
+
const controller = new AbortController();
|
|
3659
|
+
const timer = setTimeout(() => controller.abort(), timeoutMs);
|
|
3485
3660
|
try {
|
|
3486
|
-
|
|
3487
|
-
|
|
3488
|
-
|
|
3661
|
+
const response = await fetchImpl(url, {
|
|
3662
|
+
method: "POST",
|
|
3663
|
+
headers: { "content-type": "application/json", host: authority },
|
|
3664
|
+
body: JSON.stringify({ jsonrpc: "2.0", id: 0, method: "initialize", params: {} }),
|
|
3665
|
+
signal: controller.signal
|
|
3666
|
+
});
|
|
3667
|
+
await response.text().catch(() => "");
|
|
3668
|
+
return response.status === 401 ? { status: "ok", code: response.status } : { status: "unsafe", code: response.status };
|
|
3669
|
+
} catch (error) {
|
|
3670
|
+
return {
|
|
3671
|
+
status: "unreachable",
|
|
3672
|
+
error: error instanceof Error ? error.message : String(error)
|
|
3673
|
+
};
|
|
3674
|
+
} finally {
|
|
3675
|
+
clearTimeout(timer);
|
|
3489
3676
|
}
|
|
3490
3677
|
}
|
|
3491
|
-
function
|
|
3678
|
+
async function gatherDoctorReport(options) {
|
|
3679
|
+
const providers = lastDiagnosticsProviders();
|
|
3680
|
+
const opencodeVersion = lastKnownOpencodeVersion() ?? process.env.OPENCODE_VERSION ?? await detectOpencodeVersion().catch(() => void 0);
|
|
3681
|
+
const { claudeCliPath, ...base } = collectStartupDiagnostics(providers, opencodeVersion);
|
|
3682
|
+
const cliPath = options.cliPath || claudeCliPath;
|
|
3683
|
+
const cli = await detectCliVersion(cliPath).catch(() => null);
|
|
3684
|
+
const processes = snapshotActiveProcesses();
|
|
3685
|
+
const seen = /* @__PURE__ */ new Set();
|
|
3686
|
+
const proxyServers = [];
|
|
3687
|
+
for (const proc of processes) {
|
|
3688
|
+
if (!proc.proxyUrl || seen.has(proc.proxyUrl)) continue;
|
|
3689
|
+
seen.add(proc.proxyUrl);
|
|
3690
|
+
proxyServers.push({
|
|
3691
|
+
url: proc.proxyUrl,
|
|
3692
|
+
auth: await checkProxyAuth(proc.proxyUrl, options.fetchImpl ?? fetch)
|
|
3693
|
+
});
|
|
3694
|
+
}
|
|
3695
|
+
return {
|
|
3696
|
+
plugin: base.plugin,
|
|
3697
|
+
opencode: base.opencode,
|
|
3698
|
+
claudeCli: { path: cliPath, version: cli?.raw ?? "not detected" },
|
|
3699
|
+
cwd: base.cwd,
|
|
3700
|
+
providers: base.providers,
|
|
3701
|
+
accounts: base.accounts,
|
|
3702
|
+
proxyTools: base.proxyTools,
|
|
3703
|
+
mcpServers: base.mcpServers,
|
|
3704
|
+
transport: options.interactive || base.interactiveTransport ? "interactive" : "headless",
|
|
3705
|
+
planModeQuestion: base.planModeQuestion,
|
|
3706
|
+
turnStats: options.turnStats,
|
|
3707
|
+
anthropicApiKeyInEnv: base.anthropicApiKeyInEnv,
|
|
3708
|
+
processes,
|
|
3709
|
+
pendingCalls: snapshotPendingProxyCalls(),
|
|
3710
|
+
proxyServers
|
|
3711
|
+
};
|
|
3712
|
+
}
|
|
3713
|
+
async function buildDoctorReport(options) {
|
|
3492
3714
|
try {
|
|
3493
|
-
const
|
|
3494
|
-
|
|
3495
|
-
|
|
3496
|
-
|
|
3497
|
-
|
|
3498
|
-
|
|
3499
|
-
|
|
3500
|
-
)
|
|
3501
|
-
}
|
|
3502
|
-
return parsed;
|
|
3503
|
-
} catch (e) {
|
|
3504
|
-
log.warn("failed to parse opencode config", {
|
|
3505
|
-
file,
|
|
3506
|
-
error: e instanceof Error ? e.message : String(e)
|
|
3715
|
+
const report = await gatherDoctorReport(options);
|
|
3716
|
+
log.info("claude-code doctor report", {
|
|
3717
|
+
plugin: report.plugin,
|
|
3718
|
+
opencode: report.opencode,
|
|
3719
|
+
cwd: report.cwd,
|
|
3720
|
+
processes: report.processes.length,
|
|
3721
|
+
pendingCalls: report.pendingCalls.length,
|
|
3722
|
+
proxyServers: report.proxyServers.map((server2) => server2.auth.status)
|
|
3507
3723
|
});
|
|
3508
|
-
return
|
|
3724
|
+
return formatDoctorReport(report);
|
|
3725
|
+
} catch (error) {
|
|
3726
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
3727
|
+
log.warn("claude-code doctor failed to build its report", { error: message });
|
|
3728
|
+
return `${DOCTOR_MARKER}
|
|
3729
|
+
|
|
3730
|
+
Could not build the report: ${message}`;
|
|
3509
3731
|
}
|
|
3510
3732
|
}
|
|
3511
|
-
|
|
3512
|
-
|
|
3733
|
+
|
|
3734
|
+
// src/turn-stats.ts
|
|
3735
|
+
var TURN_STATS_MARKER = "\u258C **stats:**";
|
|
3736
|
+
function extractTurnStats(msg) {
|
|
3737
|
+
const usage = msg.usage;
|
|
3738
|
+
const stats = {};
|
|
3739
|
+
if (typeof msg.total_cost_usd === "number") stats.costUsd = msg.total_cost_usd;
|
|
3740
|
+
if (typeof msg.duration_ms === "number") stats.durationMs = msg.duration_ms;
|
|
3741
|
+
if (typeof msg.duration_api_ms === "number") stats.durationApiMs = msg.duration_api_ms;
|
|
3742
|
+
if (typeof msg.num_turns === "number") stats.numTurns = msg.num_turns;
|
|
3743
|
+
if (typeof usage?.input_tokens === "number") stats.inputTokens = usage.input_tokens;
|
|
3744
|
+
if (typeof usage?.output_tokens === "number") stats.outputTokens = usage.output_tokens;
|
|
3745
|
+
if (typeof usage?.cache_read_input_tokens === "number") {
|
|
3746
|
+
stats.cacheReadTokens = usage.cache_read_input_tokens;
|
|
3747
|
+
}
|
|
3748
|
+
if (typeof usage?.cache_creation_input_tokens === "number") {
|
|
3749
|
+
stats.cacheWriteTokens = usage.cache_creation_input_tokens;
|
|
3750
|
+
}
|
|
3751
|
+
if (msg.modelUsage && typeof msg.modelUsage === "object") stats.modelUsage = msg.modelUsage;
|
|
3752
|
+
if (Array.isArray(msg.permission_denials)) stats.permissionDenials = msg.permission_denials;
|
|
3753
|
+
return stats;
|
|
3754
|
+
}
|
|
3755
|
+
function formatCost(costUsd) {
|
|
3756
|
+
if (!Number.isFinite(costUsd) || costUsd < 0) return "$0.00";
|
|
3757
|
+
return costUsd >= 1 ? `$${costUsd.toFixed(2)}` : `$${costUsd.toFixed(4)}`;
|
|
3758
|
+
}
|
|
3759
|
+
function formatDuration(durationMs) {
|
|
3760
|
+
if (!Number.isFinite(durationMs) || durationMs < 0) return "0.0 s";
|
|
3761
|
+
if (durationMs < 6e4) return `${(durationMs / 1e3).toFixed(1)} s`;
|
|
3762
|
+
const totalSeconds = Math.round(durationMs / 1e3);
|
|
3763
|
+
return `${Math.floor(totalSeconds / 60)}m ${totalSeconds % 60}s`;
|
|
3764
|
+
}
|
|
3765
|
+
function formatTokens(tokens) {
|
|
3766
|
+
if (!Number.isFinite(tokens) || tokens < 0) return "0";
|
|
3767
|
+
if (tokens < 1e3) return String(Math.round(tokens));
|
|
3768
|
+
if (tokens < 1e6) return `${(tokens / 1e3).toFixed(1)}k`;
|
|
3769
|
+
return `${(tokens / 1e6).toFixed(1)}M`;
|
|
3770
|
+
}
|
|
3771
|
+
function formatTurnStatsLine(stats) {
|
|
3772
|
+
const parts = [];
|
|
3773
|
+
if (stats.costUsd !== void 0) parts.push(formatCost(stats.costUsd));
|
|
3774
|
+
if (stats.durationMs !== void 0) parts.push(formatDuration(stats.durationMs));
|
|
3775
|
+
if (stats.numTurns !== void 0) {
|
|
3776
|
+
parts.push(`${stats.numTurns} CLI ${stats.numTurns === 1 ? "turn" : "turns"}`);
|
|
3777
|
+
}
|
|
3778
|
+
if (stats.inputTokens !== void 0) parts.push(`in ${formatTokens(stats.inputTokens)}`);
|
|
3779
|
+
if (stats.outputTokens !== void 0) parts.push(`out ${formatTokens(stats.outputTokens)}`);
|
|
3780
|
+
if (stats.cacheReadTokens) parts.push(`cache read ${formatTokens(stats.cacheReadTokens)}`);
|
|
3781
|
+
if (stats.cacheWriteTokens) parts.push(`cache write ${formatTokens(stats.cacheWriteTokens)}`);
|
|
3782
|
+
if (stats.permissionDenials?.length) {
|
|
3783
|
+
const count = stats.permissionDenials.length;
|
|
3784
|
+
parts.push(`${count} permission ${count === 1 ? "denial" : "denials"}`);
|
|
3785
|
+
}
|
|
3786
|
+
if (parts.length === 0) return null;
|
|
3787
|
+
return `${TURN_STATS_MARKER} ${parts.join(" \xB7 ")}`;
|
|
3788
|
+
}
|
|
3789
|
+
function formatTurnStatsBlock(stats) {
|
|
3790
|
+
const line = formatTurnStatsLine(stats);
|
|
3791
|
+
return line === null ? null : `
|
|
3792
|
+
${line}
|
|
3793
|
+
`;
|
|
3513
3794
|
}
|
|
3514
|
-
function
|
|
3515
|
-
|
|
3516
|
-
|
|
3517
|
-
|
|
3518
|
-
|
|
3519
|
-
|
|
3520
|
-
|
|
3795
|
+
function turnStatsLogPayload(stats) {
|
|
3796
|
+
return {
|
|
3797
|
+
costUsd: stats.costUsd ?? null,
|
|
3798
|
+
durationMs: stats.durationMs ?? null,
|
|
3799
|
+
durationApiMs: stats.durationApiMs ?? null,
|
|
3800
|
+
numTurns: stats.numTurns ?? null,
|
|
3801
|
+
inputTokens: stats.inputTokens ?? null,
|
|
3802
|
+
outputTokens: stats.outputTokens ?? null,
|
|
3803
|
+
cacheReadTokens: stats.cacheReadTokens ?? null,
|
|
3804
|
+
cacheWriteTokens: stats.cacheWriteTokens ?? null,
|
|
3805
|
+
modelUsage: stats.modelUsage ?? null,
|
|
3806
|
+
permissionDenials: stats.permissionDenials?.length ?? 0
|
|
3807
|
+
};
|
|
3808
|
+
}
|
|
3809
|
+
|
|
3810
|
+
// src/message-builder.ts
|
|
3811
|
+
var PLUGIN_NOTE_MARKERS = [
|
|
3812
|
+
INLINE_ASIDE_MARKER,
|
|
3813
|
+
...LEGACY_INLINE_ASIDE_MARKERS,
|
|
3814
|
+
TURN_STATS_MARKER,
|
|
3815
|
+
COMPACT_BOUNDARY_MARKER,
|
|
3816
|
+
RATE_LIMIT_MARKER,
|
|
3817
|
+
RESULT_ERROR_MARKER,
|
|
3818
|
+
DOCTOR_MARKER
|
|
3819
|
+
];
|
|
3820
|
+
function isPluginNote(part) {
|
|
3821
|
+
if (!part || part.type !== "text" || typeof part.text !== "string") return false;
|
|
3822
|
+
const text = part.text.trimStart();
|
|
3823
|
+
return PLUGIN_NOTE_MARKERS.some((marker) => text.startsWith(marker));
|
|
3824
|
+
}
|
|
3825
|
+
function stripPluginNotes(content) {
|
|
3826
|
+
if (!Array.isArray(content)) return content;
|
|
3827
|
+
const kept = content.filter((part) => !isPluginNote(part));
|
|
3828
|
+
return kept.length === content.length ? content : kept;
|
|
3829
|
+
}
|
|
3830
|
+
function filterSideQuestionHistory(prompt) {
|
|
3831
|
+
let pluginCommand = false;
|
|
3832
|
+
const kept = prompt.filter((message) => {
|
|
3833
|
+
if (message.role === "user") {
|
|
3834
|
+
pluginCommand = parseSideQuestionContent(message.content) !== null || parseDoctorCommandContent(message.content) !== null;
|
|
3835
|
+
return !pluginCommand;
|
|
3836
|
+
}
|
|
3837
|
+
return message.role !== "assistant" || !pluginCommand;
|
|
3838
|
+
});
|
|
3839
|
+
return kept.map(
|
|
3840
|
+
(message) => message.role === "assistant" ? { ...message, content: stripPluginNotes(message.content) } : message
|
|
3841
|
+
);
|
|
3842
|
+
}
|
|
3843
|
+
var SUPPORTED_IMAGE_TYPES = /* @__PURE__ */ new Set([
|
|
3844
|
+
"image/jpeg",
|
|
3845
|
+
"image/png",
|
|
3846
|
+
"image/gif",
|
|
3847
|
+
"image/webp"
|
|
3848
|
+
]);
|
|
3849
|
+
function toImageBlock(part) {
|
|
3850
|
+
const raw = part.image ?? part.data ?? part.url ?? part.source?.data;
|
|
3851
|
+
if (!raw) {
|
|
3852
|
+
log.warn("file part without data, skipping");
|
|
3853
|
+
return null;
|
|
3854
|
+
}
|
|
3855
|
+
let resolvedMediaType = part.mediaType || part.mimeType || part.mime || "";
|
|
3856
|
+
let base64 = null;
|
|
3857
|
+
if (typeof raw === "string") {
|
|
3858
|
+
if (raw.startsWith("data:")) {
|
|
3859
|
+
const match = /^data:([^;,]+)(?:;[^,]*)*(?:;base64)?,(.*)$/s.exec(raw);
|
|
3860
|
+
if (!match) {
|
|
3861
|
+
log.warn("malformed data URI, skipping file part");
|
|
3862
|
+
return null;
|
|
3863
|
+
}
|
|
3864
|
+
resolvedMediaType = resolvedMediaType || match[1];
|
|
3865
|
+
base64 = match[2];
|
|
3866
|
+
} else if (/^https?:\/\//i.test(raw)) {
|
|
3867
|
+
log.warn("remote URL images are not supported by Claude CLI, skipping");
|
|
3868
|
+
return null;
|
|
3521
3869
|
} else {
|
|
3522
|
-
|
|
3870
|
+
base64 = raw;
|
|
3523
3871
|
}
|
|
3872
|
+
} else if (raw instanceof URL) {
|
|
3873
|
+
log.warn("remote URL images are not supported by Claude CLI, skipping");
|
|
3874
|
+
return null;
|
|
3875
|
+
} else if (raw instanceof Uint8Array || Buffer.isBuffer(raw)) {
|
|
3876
|
+
base64 = Buffer.from(raw).toString("base64");
|
|
3877
|
+
} else {
|
|
3878
|
+
log.warn("unsupported file part data type", { dataType: typeof raw });
|
|
3879
|
+
return null;
|
|
3524
3880
|
}
|
|
3525
|
-
|
|
3881
|
+
if (!resolvedMediaType || !SUPPORTED_IMAGE_TYPES.has(resolvedMediaType)) {
|
|
3882
|
+
log.warn("unsupported media type for Claude image block, skipping", {
|
|
3883
|
+
mediaType: resolvedMediaType
|
|
3884
|
+
});
|
|
3885
|
+
return null;
|
|
3886
|
+
}
|
|
3887
|
+
return {
|
|
3888
|
+
type: "image",
|
|
3889
|
+
source: { type: "base64", media_type: resolvedMediaType, data: base64 }
|
|
3890
|
+
};
|
|
3891
|
+
}
|
|
3892
|
+
function getToolResultText(part) {
|
|
3893
|
+
const value = part.output ?? part.result;
|
|
3894
|
+
if (typeof value === "string") {
|
|
3895
|
+
return value;
|
|
3896
|
+
}
|
|
3897
|
+
if (!value || typeof value !== "object") {
|
|
3898
|
+
return JSON.stringify(value);
|
|
3899
|
+
}
|
|
3900
|
+
switch (value.type) {
|
|
3901
|
+
case "text":
|
|
3902
|
+
case "error-text":
|
|
3903
|
+
return String(value.value);
|
|
3904
|
+
case "json":
|
|
3905
|
+
case "error-json":
|
|
3906
|
+
return JSON.stringify(value.value);
|
|
3907
|
+
case "execution-denied":
|
|
3908
|
+
return value.reason ? `Execution denied: ${value.reason}` : "Execution denied";
|
|
3909
|
+
case "content":
|
|
3910
|
+
return Array.isArray(value.value) ? value.value.map((item) => {
|
|
3911
|
+
if (item?.type === "text") return item.text;
|
|
3912
|
+
return JSON.stringify(item);
|
|
3913
|
+
}).join("\n") : JSON.stringify(value.value);
|
|
3914
|
+
default:
|
|
3915
|
+
return JSON.stringify(value);
|
|
3916
|
+
}
|
|
3917
|
+
}
|
|
3918
|
+
var MAX_HISTORY_CHARS = 18e4;
|
|
3919
|
+
var MAX_TOOL_RESULT_CHARS = 1e4;
|
|
3920
|
+
var MAX_TOOL_INPUT_CHARS = 2e3;
|
|
3921
|
+
function clipWithMarker(text, max) {
|
|
3922
|
+
if (text.length <= max) return text;
|
|
3923
|
+
return `${text.slice(0, max)}
|
|
3924
|
+
\u2026[truncated ${text.length - max} chars]`;
|
|
3925
|
+
}
|
|
3926
|
+
function renderToolInput(input) {
|
|
3927
|
+
let raw;
|
|
3928
|
+
try {
|
|
3929
|
+
raw = typeof input === "string" ? input : JSON.stringify(input);
|
|
3930
|
+
} catch {
|
|
3931
|
+
raw = String(input);
|
|
3932
|
+
}
|
|
3933
|
+
return clipWithMarker(raw, MAX_TOOL_INPUT_CHARS);
|
|
3934
|
+
}
|
|
3935
|
+
function renderMessageContentForCompaction(msg) {
|
|
3936
|
+
const lines = [];
|
|
3937
|
+
let toolResultCount = 0;
|
|
3938
|
+
if (typeof msg.content === "string") {
|
|
3939
|
+
return { text: msg.content, toolResultCount: 0 };
|
|
3940
|
+
}
|
|
3941
|
+
if (!Array.isArray(msg.content)) {
|
|
3942
|
+
return { text: "", toolResultCount: 0 };
|
|
3943
|
+
}
|
|
3944
|
+
for (const part of msg.content) {
|
|
3945
|
+
if (!part) continue;
|
|
3946
|
+
switch (part.type) {
|
|
3947
|
+
case "text":
|
|
3948
|
+
if (part.text) lines.push(part.text);
|
|
3949
|
+
break;
|
|
3950
|
+
case "tool-call":
|
|
3951
|
+
lines.push(
|
|
3952
|
+
`[tool_use:${part.toolName ?? "unknown"}(${renderToolInput(part.input)})]`
|
|
3953
|
+
);
|
|
3954
|
+
break;
|
|
3955
|
+
case "tool-result":
|
|
3956
|
+
toolResultCount++;
|
|
3957
|
+
lines.push(
|
|
3958
|
+
`[tool_result:${part.toolName ?? part.toolCallId ?? "unknown"}]
|
|
3959
|
+
${clipWithMarker(
|
|
3960
|
+
getToolResultText(part),
|
|
3961
|
+
MAX_TOOL_RESULT_CHARS
|
|
3962
|
+
)}`
|
|
3963
|
+
);
|
|
3964
|
+
break;
|
|
3965
|
+
case "image":
|
|
3966
|
+
lines.push(
|
|
3967
|
+
`[image: ${part.mediaType ?? part.mimeType ?? "unknown"}]`
|
|
3968
|
+
);
|
|
3969
|
+
break;
|
|
3970
|
+
case "file":
|
|
3971
|
+
lines.push(
|
|
3972
|
+
`[file: ${part.mediaType ?? part.mimeType ?? "unknown"}]`
|
|
3973
|
+
);
|
|
3974
|
+
break;
|
|
3975
|
+
case "reasoning":
|
|
3976
|
+
break;
|
|
3977
|
+
}
|
|
3978
|
+
}
|
|
3979
|
+
return { text: lines.join("\n"), toolResultCount };
|
|
3980
|
+
}
|
|
3981
|
+
function compactConversationHistory(prompt, opts = {}) {
|
|
3982
|
+
const mode = opts.mode ?? "fresh-session";
|
|
3983
|
+
prompt = filterSideQuestionHistory(prompt);
|
|
3984
|
+
if (mode === "compaction") {
|
|
3985
|
+
return buildCompactionHistory(prompt);
|
|
3986
|
+
}
|
|
3987
|
+
const conversationMessages = prompt.filter(
|
|
3988
|
+
(m) => m.role === "user" || m.role === "assistant" || m.role === "tool"
|
|
3989
|
+
);
|
|
3990
|
+
if (conversationMessages.length <= 1) {
|
|
3991
|
+
return null;
|
|
3992
|
+
}
|
|
3993
|
+
const historyParts = [];
|
|
3994
|
+
for (let i = 0; i < conversationMessages.length - 1; i++) {
|
|
3995
|
+
const msg = conversationMessages[i];
|
|
3996
|
+
const role = msg.role === "user" ? "User" : msg.role === "assistant" ? "Assistant" : "Tool";
|
|
3997
|
+
const { text } = renderMessageContentForCompaction(msg);
|
|
3998
|
+
if (text.trim()) {
|
|
3999
|
+
const truncated = text.length > 2e3 ? text.slice(0, 2e3) + "..." : text;
|
|
4000
|
+
historyParts.push(`${role}: ${truncated}`);
|
|
4001
|
+
}
|
|
4002
|
+
}
|
|
4003
|
+
if (historyParts.length === 0) {
|
|
4004
|
+
return null;
|
|
4005
|
+
}
|
|
4006
|
+
return historyParts.join("\n\n");
|
|
4007
|
+
}
|
|
4008
|
+
function buildCompactionHistory(prompt) {
|
|
4009
|
+
const entries = [];
|
|
4010
|
+
let total = 0;
|
|
4011
|
+
let totalToolResults = 0;
|
|
4012
|
+
let droppedOldest = 0;
|
|
4013
|
+
const end = prompt.length > 0 && prompt[prompt.length - 1].role === "user" ? prompt.length - 1 : prompt.length;
|
|
4014
|
+
for (let i = end - 1; i >= 0; i--) {
|
|
4015
|
+
const msg = prompt[i];
|
|
4016
|
+
const roleLabel = msg.role === "user" ? "User" : msg.role === "assistant" ? "Assistant" : msg.role === "tool" ? "Tool" : msg.role;
|
|
4017
|
+
const { text, toolResultCount } = renderMessageContentForCompaction(msg);
|
|
4018
|
+
if (!text.trim()) continue;
|
|
4019
|
+
const entry = `${roleLabel}: ${text}`;
|
|
4020
|
+
if (total + entry.length > MAX_HISTORY_CHARS) {
|
|
4021
|
+
droppedOldest = i + 1;
|
|
4022
|
+
break;
|
|
4023
|
+
}
|
|
4024
|
+
entries.push(entry);
|
|
4025
|
+
total += entry.length + 2;
|
|
4026
|
+
totalToolResults += toolResultCount;
|
|
4027
|
+
}
|
|
4028
|
+
if (entries.length === 0) return null;
|
|
4029
|
+
entries.reverse();
|
|
4030
|
+
log.info("built compaction history", {
|
|
4031
|
+
entries: entries.length,
|
|
4032
|
+
chars: total,
|
|
4033
|
+
toolResults: totalToolResults,
|
|
4034
|
+
droppedOldestBefore: droppedOldest
|
|
4035
|
+
});
|
|
4036
|
+
return entries.join("\n\n");
|
|
4037
|
+
}
|
|
4038
|
+
function getClaudeUserMessage(prompt, includeHistoryContext = false, opts = {}) {
|
|
4039
|
+
const compactionMode = opts.compactionMode === true;
|
|
4040
|
+
const cliToolCallIds = opts.cliToolCallIds;
|
|
4041
|
+
const content = [];
|
|
4042
|
+
const pushToolResult = (part) => {
|
|
4043
|
+
const id = part.toolCallId;
|
|
4044
|
+
const text = getToolResultText(part);
|
|
4045
|
+
if (!cliToolCallIds || cliToolCallIds.has(id)) {
|
|
4046
|
+
content.push({ type: "tool_result", tool_use_id: id, content: text });
|
|
4047
|
+
return;
|
|
4048
|
+
}
|
|
4049
|
+
log.info("rendering opencode-side tool result as text", {
|
|
4050
|
+
toolCallId: id,
|
|
4051
|
+
toolName: part.toolName,
|
|
4052
|
+
chars: text.length
|
|
4053
|
+
});
|
|
4054
|
+
content.push({
|
|
4055
|
+
type: "text",
|
|
4056
|
+
text: `<opencode_tool_result tool="${part.toolName ?? "unknown"}">
|
|
4057
|
+
${text}
|
|
4058
|
+
</opencode_tool_result>`
|
|
4059
|
+
});
|
|
4060
|
+
};
|
|
4061
|
+
if (compactionMode) {
|
|
4062
|
+
const transcript = compactConversationHistory(prompt, {
|
|
4063
|
+
mode: "compaction"
|
|
4064
|
+
});
|
|
4065
|
+
if (transcript) {
|
|
4066
|
+
log.info("including compaction transcript", {
|
|
4067
|
+
historyLength: transcript.length
|
|
4068
|
+
});
|
|
4069
|
+
content.push({
|
|
4070
|
+
type: "text",
|
|
4071
|
+
text: `<conversation_transcript>
|
|
4072
|
+
${transcript}
|
|
4073
|
+
</conversation_transcript>
|
|
4074
|
+
|
|
4075
|
+
The complete prior conversation appears above. The synthesis instructions follow below.
|
|
4076
|
+
|
|
4077
|
+
`
|
|
4078
|
+
});
|
|
4079
|
+
}
|
|
4080
|
+
} else if (includeHistoryContext) {
|
|
4081
|
+
const historyContext = compactConversationHistory(prompt);
|
|
4082
|
+
if (historyContext) {
|
|
4083
|
+
log.info("including conversation history context", {
|
|
4084
|
+
historyLength: historyContext.length
|
|
4085
|
+
});
|
|
4086
|
+
content.push({
|
|
4087
|
+
type: "text",
|
|
4088
|
+
text: `<conversation_history>
|
|
4089
|
+
The following is a summary of our conversation so far (from a previous session that couldn't be resumed):
|
|
4090
|
+
|
|
4091
|
+
${historyContext}
|
|
4092
|
+
|
|
4093
|
+
</conversation_history>
|
|
4094
|
+
|
|
4095
|
+
Now continuing with the current message:
|
|
4096
|
+
|
|
4097
|
+
`
|
|
4098
|
+
});
|
|
4099
|
+
}
|
|
4100
|
+
}
|
|
4101
|
+
const messages = [];
|
|
4102
|
+
for (let i = prompt.length - 1; i >= 0; i--) {
|
|
4103
|
+
if (prompt[i].role === "assistant") break;
|
|
4104
|
+
messages.unshift(prompt[i]);
|
|
4105
|
+
}
|
|
4106
|
+
for (const msg of messages) {
|
|
4107
|
+
if (msg.role === "user") {
|
|
4108
|
+
if (parseSideQuestionContent(msg.content) !== null) continue;
|
|
4109
|
+
if (typeof msg.content === "string") {
|
|
4110
|
+
const str2 = msg.content;
|
|
4111
|
+
if (str2.trim()) {
|
|
4112
|
+
content.push({ type: "text", text: str2 });
|
|
4113
|
+
}
|
|
4114
|
+
} else if (Array.isArray(msg.content)) {
|
|
4115
|
+
for (const part of msg.content) {
|
|
4116
|
+
if (part.type === "text") {
|
|
4117
|
+
if (part.text && part.text.trim()) {
|
|
4118
|
+
content.push({ type: "text", text: part.text });
|
|
4119
|
+
}
|
|
4120
|
+
} else if (part.type === "file" || part.type === "image") {
|
|
4121
|
+
const block = toImageBlock(part);
|
|
4122
|
+
if (block) {
|
|
4123
|
+
content.push(block);
|
|
4124
|
+
} else {
|
|
4125
|
+
log.debug("skipped non-image file part", {
|
|
4126
|
+
mediaType: part.mediaType
|
|
4127
|
+
});
|
|
4128
|
+
}
|
|
4129
|
+
} else if (part.type === "tool-result") {
|
|
4130
|
+
pushToolResult(part);
|
|
4131
|
+
}
|
|
4132
|
+
}
|
|
4133
|
+
}
|
|
4134
|
+
} else if (msg.role === "tool") {
|
|
4135
|
+
if (Array.isArray(msg.content)) {
|
|
4136
|
+
for (const part of msg.content) {
|
|
4137
|
+
if (part?.type === "tool-result") {
|
|
4138
|
+
pushToolResult(part);
|
|
4139
|
+
}
|
|
4140
|
+
}
|
|
4141
|
+
}
|
|
4142
|
+
}
|
|
4143
|
+
}
|
|
4144
|
+
if (content.length === 0) {
|
|
4145
|
+
log.warn("empty user content; sending sentinel to satisfy CLI");
|
|
4146
|
+
return JSON.stringify({
|
|
4147
|
+
type: "user",
|
|
4148
|
+
message: {
|
|
4149
|
+
role: "user",
|
|
4150
|
+
content: [{ type: "text", text: "(empty)" }]
|
|
4151
|
+
}
|
|
4152
|
+
});
|
|
4153
|
+
}
|
|
4154
|
+
return JSON.stringify({
|
|
4155
|
+
type: "user",
|
|
4156
|
+
message: {
|
|
4157
|
+
role: "user",
|
|
4158
|
+
content
|
|
4159
|
+
}
|
|
4160
|
+
});
|
|
4161
|
+
}
|
|
4162
|
+
|
|
4163
|
+
// src/agent-models.ts
|
|
4164
|
+
import { readFile, readdir } from "fs/promises";
|
|
4165
|
+
import path5 from "path";
|
|
4166
|
+
|
|
4167
|
+
// src/models.ts
|
|
4168
|
+
var PROVIDER_ID = "claude-code";
|
|
4169
|
+
var NPM = "@khalilgharbaoui/opencode-claude-code-plugin";
|
|
4170
|
+
var reasoningVariants = {
|
|
4171
|
+
low: { reasoningEffort: "low" },
|
|
4172
|
+
medium: { reasoningEffort: "medium" },
|
|
4173
|
+
high: { reasoningEffort: "high" },
|
|
4174
|
+
xhigh: { reasoningEffort: "xhigh" },
|
|
4175
|
+
max: { reasoningEffort: "max" }
|
|
4176
|
+
};
|
|
4177
|
+
var baseCapabilities = {
|
|
4178
|
+
temperature: false,
|
|
4179
|
+
attachment: true,
|
|
4180
|
+
toolcall: true,
|
|
4181
|
+
input: { text: true, audio: false, image: true, video: false, pdf: false },
|
|
4182
|
+
output: { text: true, audio: false, image: false, video: false, pdf: false },
|
|
4183
|
+
interleaved: false
|
|
4184
|
+
};
|
|
4185
|
+
function defineModel(opts) {
|
|
4186
|
+
return {
|
|
4187
|
+
id: opts.id,
|
|
4188
|
+
providerID: PROVIDER_ID,
|
|
4189
|
+
api: { id: opts.id, url: "", npm: NPM },
|
|
4190
|
+
name: `${opts.name} (${opts.multiplier}\xD7)`,
|
|
4191
|
+
family: opts.family,
|
|
4192
|
+
capabilities: { ...baseCapabilities, reasoning: opts.reasoning },
|
|
4193
|
+
cost: {
|
|
4194
|
+
input: opts.cost.input,
|
|
4195
|
+
output: opts.cost.output,
|
|
4196
|
+
cache: { read: opts.cost.cacheRead, write: opts.cost.cacheWrite }
|
|
4197
|
+
},
|
|
4198
|
+
limit: { context: opts.context, output: opts.output },
|
|
4199
|
+
status: opts.status ?? "active",
|
|
4200
|
+
options: {},
|
|
4201
|
+
headers: {},
|
|
4202
|
+
release_date: opts.releaseDate,
|
|
4203
|
+
variants: opts.reasoning ? reasoningVariants : void 0
|
|
4204
|
+
};
|
|
4205
|
+
}
|
|
4206
|
+
var haikuCost = { input: 1, output: 5, cacheRead: 0.1, cacheWrite: 1.25 };
|
|
4207
|
+
var sonnetCost = { input: 3, output: 15, cacheRead: 0.3, cacheWrite: 3.75 };
|
|
4208
|
+
var opusCost = { input: 5, output: 25, cacheRead: 0.5, cacheWrite: 6.25 };
|
|
4209
|
+
var fableCost = { input: 10, output: 50, cacheRead: 1, cacheWrite: 12.5 };
|
|
4210
|
+
var fable51Cost = { input: 10, output: 50, cacheRead: 0.25, cacheWrite: 12.5 };
|
|
4211
|
+
var opusFastCost = { input: 10, output: 50, cacheRead: 1, cacheWrite: 12.5 };
|
|
4212
|
+
function toConfigModel(model) {
|
|
4213
|
+
const inputMods = [];
|
|
4214
|
+
const outputMods = [];
|
|
4215
|
+
for (const [k, v] of Object.entries(model.capabilities.input)) {
|
|
4216
|
+
if (v) inputMods.push(k);
|
|
4217
|
+
}
|
|
4218
|
+
for (const [k, v] of Object.entries(model.capabilities.output)) {
|
|
4219
|
+
if (v) outputMods.push(k);
|
|
4220
|
+
}
|
|
4221
|
+
return {
|
|
4222
|
+
id: model.api.id,
|
|
4223
|
+
name: model.name,
|
|
4224
|
+
status: model.status,
|
|
4225
|
+
family: model.family ?? "",
|
|
4226
|
+
release_date: model.release_date,
|
|
4227
|
+
temperature: model.capabilities.temperature,
|
|
4228
|
+
reasoning: model.capabilities.reasoning,
|
|
4229
|
+
attachment: model.capabilities.attachment,
|
|
4230
|
+
tool_call: model.capabilities.toolcall,
|
|
4231
|
+
modalities: { input: inputMods, output: outputMods },
|
|
4232
|
+
cost: {
|
|
4233
|
+
input: model.cost.input,
|
|
4234
|
+
output: model.cost.output,
|
|
4235
|
+
cache_read: model.cost.cache.read,
|
|
4236
|
+
cache_write: model.cost.cache.write
|
|
4237
|
+
},
|
|
4238
|
+
limit: model.limit,
|
|
4239
|
+
options: model.options,
|
|
4240
|
+
headers: model.headers,
|
|
4241
|
+
variants: model.variants
|
|
4242
|
+
};
|
|
4243
|
+
}
|
|
4244
|
+
var defaultModels = {
|
|
4245
|
+
"claude-haiku-4-5": defineModel({
|
|
4246
|
+
id: "claude-haiku-4-5",
|
|
4247
|
+
name: "Claude Haiku 4.5",
|
|
4248
|
+
family: "haiku",
|
|
4249
|
+
reasoning: false,
|
|
4250
|
+
context: 2e5,
|
|
4251
|
+
output: 64e3,
|
|
4252
|
+
cost: haikuCost,
|
|
4253
|
+
multiplier: 1,
|
|
4254
|
+
releaseDate: "2025-10-01"
|
|
4255
|
+
}),
|
|
4256
|
+
"claude-sonnet-4-5": defineModel({
|
|
4257
|
+
id: "claude-sonnet-4-5",
|
|
4258
|
+
name: "Claude Sonnet 4.5",
|
|
4259
|
+
family: "sonnet",
|
|
4260
|
+
reasoning: true,
|
|
4261
|
+
context: 2e5,
|
|
4262
|
+
output: 64e3,
|
|
4263
|
+
cost: sonnetCost,
|
|
4264
|
+
multiplier: 3,
|
|
4265
|
+
releaseDate: "2025-09-29"
|
|
4266
|
+
}),
|
|
4267
|
+
"claude-sonnet-4-6": defineModel({
|
|
4268
|
+
id: "claude-sonnet-4-6",
|
|
4269
|
+
name: "Claude Sonnet 4.6",
|
|
4270
|
+
family: "sonnet",
|
|
4271
|
+
reasoning: true,
|
|
4272
|
+
context: 1e6,
|
|
4273
|
+
output: 128e3,
|
|
4274
|
+
cost: sonnetCost,
|
|
4275
|
+
multiplier: 3,
|
|
4276
|
+
releaseDate: "2025-06-19"
|
|
4277
|
+
}),
|
|
4278
|
+
"claude-sonnet-5": defineModel({
|
|
4279
|
+
id: "claude-sonnet-5",
|
|
4280
|
+
name: "Claude Sonnet 5",
|
|
4281
|
+
family: "sonnet",
|
|
4282
|
+
reasoning: true,
|
|
4283
|
+
context: 1e6,
|
|
4284
|
+
output: 128e3,
|
|
4285
|
+
cost: sonnetCost,
|
|
4286
|
+
multiplier: 3,
|
|
4287
|
+
releaseDate: "2026-06-30"
|
|
4288
|
+
}),
|
|
4289
|
+
"claude-opus-4-5": defineModel({
|
|
4290
|
+
id: "claude-opus-4-5",
|
|
4291
|
+
name: "Claude Opus 4.5",
|
|
4292
|
+
family: "opus",
|
|
4293
|
+
reasoning: true,
|
|
4294
|
+
context: 2e5,
|
|
4295
|
+
output: 64e3,
|
|
4296
|
+
cost: opusCost,
|
|
4297
|
+
multiplier: 5,
|
|
4298
|
+
releaseDate: "2025-11-01"
|
|
4299
|
+
}),
|
|
4300
|
+
"claude-opus-4-6": defineModel({
|
|
4301
|
+
id: "claude-opus-4-6",
|
|
4302
|
+
name: "Claude Opus 4.6",
|
|
4303
|
+
family: "opus",
|
|
4304
|
+
reasoning: true,
|
|
4305
|
+
context: 1e6,
|
|
4306
|
+
output: 128e3,
|
|
4307
|
+
cost: opusCost,
|
|
4308
|
+
multiplier: 5,
|
|
4309
|
+
releaseDate: "2025-06-19"
|
|
4310
|
+
}),
|
|
4311
|
+
"claude-opus-4-7": defineModel({
|
|
4312
|
+
id: "claude-opus-4-7",
|
|
4313
|
+
name: "Claude Opus 4.7",
|
|
4314
|
+
family: "opus",
|
|
4315
|
+
reasoning: true,
|
|
4316
|
+
context: 1e6,
|
|
4317
|
+
output: 128e3,
|
|
4318
|
+
cost: opusCost,
|
|
4319
|
+
multiplier: 5,
|
|
4320
|
+
releaseDate: "2025-07-16"
|
|
4321
|
+
}),
|
|
4322
|
+
"claude-opus-4-8": defineModel({
|
|
4323
|
+
id: "claude-opus-4-8",
|
|
4324
|
+
name: "Claude Opus 4.8",
|
|
4325
|
+
family: "opus",
|
|
4326
|
+
reasoning: true,
|
|
4327
|
+
context: 1e6,
|
|
4328
|
+
output: 128e3,
|
|
4329
|
+
cost: opusCost,
|
|
4330
|
+
multiplier: 5,
|
|
4331
|
+
releaseDate: "2026-05-28"
|
|
4332
|
+
}),
|
|
4333
|
+
// Fast mode. The `-fast` suffix is OUR marker, not a model name Anthropic
|
|
4334
|
+
// serves: `parseModelId` strips it before `--model` and turns it into
|
|
4335
|
+
// `--settings {"fastMode":true}` on the spawn. Retired `-fast` model strings
|
|
4336
|
+
// (`claude-opus-4-6-fast`) are a different thing and are not registered here.
|
|
4337
|
+
//
|
|
4338
|
+
// Only Opus 4.8 and Opus 5 qualify: the CLI gates fast mode on the resolved
|
|
4339
|
+
// model name containing `opus-4-8` or `opus-5`, so registering a fast entry
|
|
4340
|
+
// for any other model would produce a picker option that silently runs at
|
|
4341
|
+
// standard speed while displaying the 10x price.
|
|
4342
|
+
"claude-opus-4-8-fast": defineModel({
|
|
4343
|
+
id: "claude-opus-4-8-fast",
|
|
4344
|
+
name: "Claude Opus 4.8 Fast",
|
|
4345
|
+
family: "opus",
|
|
4346
|
+
reasoning: true,
|
|
4347
|
+
context: 1e6,
|
|
4348
|
+
output: 128e3,
|
|
4349
|
+
cost: opusFastCost,
|
|
4350
|
+
multiplier: 10,
|
|
4351
|
+
releaseDate: "2026-05-28"
|
|
4352
|
+
}),
|
|
4353
|
+
"claude-opus-5": defineModel({
|
|
4354
|
+
id: "claude-opus-5",
|
|
4355
|
+
name: "Claude Opus 5",
|
|
4356
|
+
family: "opus",
|
|
4357
|
+
reasoning: true,
|
|
4358
|
+
context: 1e6,
|
|
4359
|
+
output: 128e3,
|
|
4360
|
+
cost: opusCost,
|
|
4361
|
+
multiplier: 5,
|
|
4362
|
+
releaseDate: "2026-07-24"
|
|
4363
|
+
}),
|
|
4364
|
+
"claude-opus-5-fast": defineModel({
|
|
4365
|
+
id: "claude-opus-5-fast",
|
|
4366
|
+
name: "Claude Opus 5 Fast",
|
|
4367
|
+
family: "opus",
|
|
4368
|
+
reasoning: true,
|
|
4369
|
+
context: 1e6,
|
|
4370
|
+
output: 128e3,
|
|
4371
|
+
cost: opusFastCost,
|
|
4372
|
+
multiplier: 10,
|
|
4373
|
+
releaseDate: "2026-07-24"
|
|
4374
|
+
}),
|
|
4375
|
+
"claude-fable-5": defineModel({
|
|
4376
|
+
id: "claude-fable-5",
|
|
4377
|
+
name: "Claude Fable 5",
|
|
4378
|
+
family: "fable",
|
|
4379
|
+
reasoning: true,
|
|
4380
|
+
context: 1e6,
|
|
4381
|
+
output: 128e3,
|
|
4382
|
+
cost: fableCost,
|
|
4383
|
+
multiplier: 10,
|
|
4384
|
+
releaseDate: "2026-06-09"
|
|
4385
|
+
}),
|
|
4386
|
+
"claude-fable-5-1": defineModel({
|
|
4387
|
+
id: "claude-fable-5-1",
|
|
4388
|
+
name: "Claude Fable 5.1",
|
|
4389
|
+
family: "fable",
|
|
4390
|
+
reasoning: true,
|
|
4391
|
+
context: 1e6,
|
|
4392
|
+
output: 128e3,
|
|
4393
|
+
cost: fable51Cost,
|
|
4394
|
+
multiplier: 10,
|
|
4395
|
+
releaseDate: "2026-09-01"
|
|
4396
|
+
}),
|
|
4397
|
+
// Mythos 5 and 5.1 share the corresponding Fable models' capabilities and
|
|
4398
|
+
// pricing without the safety classifiers; limited availability via Project
|
|
4399
|
+
// Glasswing. `claude --model` simply errors for accounts without access, so
|
|
4400
|
+
// they are safe to register unconditionally.
|
|
4401
|
+
"claude-mythos-5": defineModel({
|
|
4402
|
+
id: "claude-mythos-5",
|
|
4403
|
+
name: "Claude Mythos 5",
|
|
4404
|
+
family: "mythos",
|
|
4405
|
+
reasoning: true,
|
|
4406
|
+
context: 1e6,
|
|
4407
|
+
output: 128e3,
|
|
4408
|
+
cost: fableCost,
|
|
4409
|
+
multiplier: 10,
|
|
4410
|
+
releaseDate: "2026-06-09"
|
|
4411
|
+
}),
|
|
4412
|
+
"claude-mythos-5-1": defineModel({
|
|
4413
|
+
id: "claude-mythos-5-1",
|
|
4414
|
+
name: "Claude Mythos 5.1",
|
|
4415
|
+
family: "mythos",
|
|
4416
|
+
reasoning: true,
|
|
4417
|
+
context: 1e6,
|
|
4418
|
+
output: 128e3,
|
|
4419
|
+
cost: fable51Cost,
|
|
4420
|
+
multiplier: 10,
|
|
4421
|
+
releaseDate: "2026-09-01"
|
|
4422
|
+
})
|
|
4423
|
+
};
|
|
4424
|
+
var FAST_SUFFIX = "-fast";
|
|
4425
|
+
function parseModelId(modelId) {
|
|
4426
|
+
const at = modelId.indexOf("@");
|
|
4427
|
+
const base = at === -1 ? modelId : modelId.slice(0, at);
|
|
4428
|
+
const account = at === -1 ? "" : modelId.slice(at);
|
|
4429
|
+
if (!base.endsWith(FAST_SUFFIX)) return { model: modelId, fast: false };
|
|
4430
|
+
if (!Object.hasOwn(defaultModels, base)) return { model: modelId, fast: false };
|
|
4431
|
+
return { model: base.slice(0, -FAST_SUFFIX.length) + account, fast: true };
|
|
3526
4432
|
}
|
|
3527
|
-
|
|
3528
|
-
|
|
3529
|
-
|
|
3530
|
-
|
|
3531
|
-
|
|
3532
|
-
|
|
3533
|
-
|
|
3534
|
-
|
|
3535
|
-
|
|
3536
|
-
|
|
3537
|
-
|
|
3538
|
-
|
|
3539
|
-
|
|
3540
|
-
|
|
4433
|
+
|
|
4434
|
+
// src/agent-models.ts
|
|
4435
|
+
var AGENT_DIR_NAMES = ["agents", "agent"];
|
|
4436
|
+
var REASONING_EFFORTS = [
|
|
4437
|
+
"minimal",
|
|
4438
|
+
"low",
|
|
4439
|
+
"medium",
|
|
4440
|
+
"high",
|
|
4441
|
+
"xhigh",
|
|
4442
|
+
"max"
|
|
4443
|
+
];
|
|
4444
|
+
var registry = {};
|
|
4445
|
+
var defaultSubagentModel;
|
|
4446
|
+
function setAgentRegistry(records) {
|
|
4447
|
+
registry = records;
|
|
3541
4448
|
}
|
|
3542
|
-
function
|
|
3543
|
-
|
|
3544
|
-
if (override) return path5.resolve(override);
|
|
3545
|
-
let current = path5.resolve(cwd);
|
|
3546
|
-
while (true) {
|
|
3547
|
-
const gitPath = path5.join(current, ".git");
|
|
3548
|
-
try {
|
|
3549
|
-
if (fs4.existsSync(gitPath)) return current;
|
|
3550
|
-
} catch {
|
|
3551
|
-
}
|
|
3552
|
-
const parent = path5.dirname(current);
|
|
3553
|
-
if (parent === current) return void 0;
|
|
3554
|
-
current = parent;
|
|
3555
|
-
}
|
|
4449
|
+
function getAgentRegistry() {
|
|
4450
|
+
return registry;
|
|
3556
4451
|
}
|
|
3557
|
-
function
|
|
3558
|
-
|
|
3559
|
-
return path5.join(xdg, "opencode");
|
|
4452
|
+
function setDefaultSubagentModel(model) {
|
|
4453
|
+
defaultSubagentModel = model?.trim() || void 0;
|
|
3560
4454
|
}
|
|
3561
|
-
function
|
|
3562
|
-
|
|
3563
|
-
let merged = {};
|
|
3564
|
-
for (const name of FILE_NAMES.slice().reverse()) {
|
|
3565
|
-
const file = path5.join(dir, name);
|
|
3566
|
-
if (!fileExists2(file)) continue;
|
|
3567
|
-
const parsed = readAndParse(file);
|
|
3568
|
-
if (parsed) merged = deepMerge(merged, parsed);
|
|
3569
|
-
}
|
|
3570
|
-
return merged;
|
|
4455
|
+
function getDefaultSubagentModel() {
|
|
4456
|
+
return defaultSubagentModel;
|
|
3571
4457
|
}
|
|
3572
|
-
function
|
|
3573
|
-
|
|
3574
|
-
|
|
3575
|
-
const file = path5.join(dir, name);
|
|
3576
|
-
if (!fileExists2(file)) continue;
|
|
3577
|
-
const parsed = readAndParse(file);
|
|
3578
|
-
if (parsed) merged = deepMerge(merged, parsed);
|
|
3579
|
-
}
|
|
3580
|
-
return merged;
|
|
4458
|
+
function accountMarker(modelId) {
|
|
4459
|
+
const at = modelId.indexOf("@");
|
|
4460
|
+
return at === -1 ? "" : modelId.slice(at);
|
|
3581
4461
|
}
|
|
3582
|
-
function
|
|
3583
|
-
const
|
|
3584
|
-
|
|
3585
|
-
const push = (p) => {
|
|
3586
|
-
const abs = path5.resolve(p);
|
|
3587
|
-
if (!seen.has(abs) && dirExists2(abs)) {
|
|
3588
|
-
seen.add(abs);
|
|
3589
|
-
dirs.push(abs);
|
|
3590
|
-
}
|
|
3591
|
-
};
|
|
3592
|
-
for (const dir of walkUp({
|
|
3593
|
-
start: cwd,
|
|
3594
|
-
stop: worktree,
|
|
3595
|
-
targets: [".opencode"],
|
|
3596
|
-
predicate: dirExists2
|
|
3597
|
-
})) {
|
|
3598
|
-
push(dir);
|
|
3599
|
-
}
|
|
3600
|
-
const home = os3.homedir();
|
|
3601
|
-
if (home) {
|
|
3602
|
-
const homeDot = path5.join(home, ".opencode");
|
|
3603
|
-
if (dirExists2(homeDot)) push(homeDot);
|
|
3604
|
-
}
|
|
3605
|
-
const envDir = process.env.OPENCODE_CONFIG_DIR;
|
|
3606
|
-
if (envDir && dirExists2(envDir)) push(envDir);
|
|
3607
|
-
return dirs;
|
|
4462
|
+
function withoutAccountMarker(modelId) {
|
|
4463
|
+
const at = modelId.indexOf("@");
|
|
4464
|
+
return at === -1 ? modelId : modelId.slice(0, at);
|
|
3608
4465
|
}
|
|
3609
|
-
function
|
|
3610
|
-
|
|
3611
|
-
|
|
3612
|
-
|
|
3613
|
-
|
|
3614
|
-
|
|
3615
|
-
|
|
4466
|
+
function resolveAgentModel(agent, modelId, overrides) {
|
|
4467
|
+
if (!agent) return modelId;
|
|
4468
|
+
const record = (overrides?.records ?? registry)[agent];
|
|
4469
|
+
if (!record) return modelId;
|
|
4470
|
+
if (record.model?.includes("/")) return modelId;
|
|
4471
|
+
const fallback = overrides ? overrides.defaultSubagentModel : defaultSubagentModel;
|
|
4472
|
+
const declared = record.forceModel?.trim();
|
|
4473
|
+
const wanted = declared || (record.mode === "subagent" ? fallback : void 0);
|
|
4474
|
+
if (!wanted) return modelId;
|
|
4475
|
+
const base = withoutAccountMarker(wanted);
|
|
4476
|
+
if (!Object.hasOwn(defaultModels, base)) {
|
|
4477
|
+
log.warn("agent model override refused: unknown model", {
|
|
4478
|
+
agent,
|
|
4479
|
+
wanted: base,
|
|
4480
|
+
keeping: modelId
|
|
3616
4481
|
});
|
|
4482
|
+
return modelId;
|
|
3617
4483
|
}
|
|
3618
|
-
|
|
3619
|
-
|
|
3620
|
-
|
|
3621
|
-
if (spec.enabled === false) return null;
|
|
3622
|
-
const type = spec.type;
|
|
3623
|
-
if (type === "local") {
|
|
3624
|
-
const cmd = spec.command;
|
|
3625
|
-
if (!Array.isArray(cmd) || cmd.length === 0) {
|
|
3626
|
-
log.warn("skipping local MCP server with no command", { name });
|
|
3627
|
-
return null;
|
|
3628
|
-
}
|
|
3629
|
-
const out = {
|
|
3630
|
-
type: "stdio",
|
|
3631
|
-
command: String(cmd[0])
|
|
3632
|
-
};
|
|
3633
|
-
if (cmd.length > 1) out.args = cmd.slice(1).map((s) => String(s));
|
|
3634
|
-
if (spec.environment && typeof spec.environment === "object") {
|
|
3635
|
-
out.env = substituteEnvPlaceholders(
|
|
3636
|
-
spec.environment
|
|
3637
|
-
);
|
|
3638
|
-
}
|
|
3639
|
-
return out;
|
|
3640
|
-
}
|
|
3641
|
-
if (type === "remote") {
|
|
3642
|
-
if (typeof spec.url !== "string" || !spec.url) {
|
|
3643
|
-
log.warn("skipping remote MCP server with no url", { name });
|
|
3644
|
-
return null;
|
|
3645
|
-
}
|
|
3646
|
-
const out = {
|
|
3647
|
-
type: "http",
|
|
3648
|
-
url: spec.url
|
|
3649
|
-
};
|
|
3650
|
-
if (spec.headers && typeof spec.headers === "object") {
|
|
3651
|
-
out.headers = substituteEnvPlaceholders(
|
|
3652
|
-
spec.headers
|
|
3653
|
-
);
|
|
3654
|
-
}
|
|
3655
|
-
return out;
|
|
4484
|
+
const resolved = `${base}${accountMarker(modelId)}`;
|
|
4485
|
+
if (resolved !== modelId) {
|
|
4486
|
+
log.debug("agent model override", { agent, from: modelId, to: resolved });
|
|
3656
4487
|
}
|
|
3657
|
-
|
|
3658
|
-
name,
|
|
3659
|
-
type: type ?? null
|
|
3660
|
-
});
|
|
3661
|
-
return null;
|
|
3662
|
-
}
|
|
3663
|
-
function extractMcpBlock(config) {
|
|
3664
|
-
const mcp = config.mcp;
|
|
3665
|
-
if (!mcp || typeof mcp !== "object" || Array.isArray(mcp)) return {};
|
|
3666
|
-
return mcp;
|
|
4488
|
+
return resolved;
|
|
3667
4489
|
}
|
|
3668
|
-
function
|
|
3669
|
-
|
|
3670
|
-
|
|
3671
|
-
|
|
3672
|
-
|
|
3673
|
-
|
|
3674
|
-
|
|
3675
|
-
|
|
3676
|
-
|
|
3677
|
-
|
|
3678
|
-
}
|
|
3679
|
-
|
|
3680
|
-
}
|
|
4490
|
+
function resolveAgentEffort(agent, inherited, overrides) {
|
|
4491
|
+
if (!agent) return inherited;
|
|
4492
|
+
const record = (overrides?.records ?? registry)[agent];
|
|
4493
|
+
const declared = record?.reasoningEffort?.trim();
|
|
4494
|
+
if (!declared) return inherited;
|
|
4495
|
+
if (!REASONING_EFFORTS.includes(declared)) {
|
|
4496
|
+
log.warn("agent effort override refused: unknown level", {
|
|
4497
|
+
agent,
|
|
4498
|
+
wanted: declared,
|
|
4499
|
+
keeping: inherited
|
|
4500
|
+
});
|
|
4501
|
+
return inherited;
|
|
3681
4502
|
}
|
|
3682
|
-
|
|
3683
|
-
|
|
3684
|
-
|
|
3685
|
-
|
|
3686
|
-
|
|
3687
|
-
|
|
3688
|
-
hash
|
|
3689
|
-
} = mergeOpencodeMcp(cwd, runtimeStatus);
|
|
3690
|
-
const servers = {};
|
|
3691
|
-
const bridgedServerNames = [];
|
|
3692
|
-
for (const [name, spec] of Object.entries(merged)) {
|
|
3693
|
-
if (!spec || typeof spec !== "object") continue;
|
|
3694
|
-
if (excludeServers?.has(name)) continue;
|
|
3695
|
-
const translated = translateServer(name, spec);
|
|
3696
|
-
if (translated) {
|
|
3697
|
-
servers[name] = translated;
|
|
3698
|
-
bridgedServerNames.push(name);
|
|
3699
|
-
}
|
|
4503
|
+
if (declared !== inherited) {
|
|
4504
|
+
log.debug("agent effort override", {
|
|
4505
|
+
agent,
|
|
4506
|
+
from: inherited,
|
|
4507
|
+
to: declared
|
|
4508
|
+
});
|
|
3700
4509
|
}
|
|
3701
|
-
return
|
|
3702
|
-
servers,
|
|
3703
|
-
bridgedServerNames,
|
|
3704
|
-
allEnabledServerNames,
|
|
3705
|
-
hash,
|
|
3706
|
-
excludeServers
|
|
3707
|
-
});
|
|
4510
|
+
return declared;
|
|
3708
4511
|
}
|
|
3709
|
-
function
|
|
3710
|
-
const
|
|
3711
|
-
|
|
3712
|
-
|
|
3713
|
-
|
|
3714
|
-
|
|
3715
|
-
|
|
3716
|
-
|
|
4512
|
+
function parseAgentFrontmatter(text) {
|
|
4513
|
+
const record = {};
|
|
4514
|
+
if (!text.startsWith("---")) return record;
|
|
4515
|
+
const lines = text.split(/\r?\n/);
|
|
4516
|
+
for (let i = 1; i < lines.length; i++) {
|
|
4517
|
+
const line = lines[i];
|
|
4518
|
+
if (line.trim() === "---") break;
|
|
4519
|
+
const match = /^([A-Za-z_][A-Za-z0-9_-]*):[ \t]*(.*)$/.exec(line);
|
|
4520
|
+
if (!match) continue;
|
|
4521
|
+
const key = match[1];
|
|
4522
|
+
if (key !== "mode" && key !== "model" && key !== "forceModel" && key !== "reasoningEffort")
|
|
4523
|
+
continue;
|
|
4524
|
+
const value = match[2].trim().replace(/^["']|["']$/g, "");
|
|
4525
|
+
if (value) record[key] = value;
|
|
3717
4526
|
}
|
|
3718
|
-
|
|
3719
|
-
|
|
3720
|
-
|
|
3721
|
-
|
|
3722
|
-
|
|
3723
|
-
|
|
3724
|
-
|
|
3725
|
-
|
|
3726
|
-
|
|
3727
|
-
|
|
3728
|
-
|
|
3729
|
-
|
|
3730
|
-
|
|
4527
|
+
return record;
|
|
4528
|
+
}
|
|
4529
|
+
async function readAgentMarkdownRecords(directories) {
|
|
4530
|
+
const records = {};
|
|
4531
|
+
for (const directory of directories) {
|
|
4532
|
+
let entries;
|
|
4533
|
+
try {
|
|
4534
|
+
entries = await readdir(directory);
|
|
4535
|
+
} catch {
|
|
4536
|
+
continue;
|
|
4537
|
+
}
|
|
4538
|
+
for (const entry of entries) {
|
|
4539
|
+
if (!entry.endsWith(".md")) continue;
|
|
4540
|
+
const name = entry.slice(0, -3);
|
|
4541
|
+
if (records[name]) continue;
|
|
4542
|
+
try {
|
|
4543
|
+
const text = await readFile(path5.join(directory, entry), "utf8");
|
|
4544
|
+
records[name] = parseAgentFrontmatter(text);
|
|
4545
|
+
} catch (err) {
|
|
4546
|
+
log.debug("failed to read agent markdown", {
|
|
4547
|
+
file: path5.join(directory, entry),
|
|
4548
|
+
error: String(err)
|
|
4549
|
+
});
|
|
4550
|
+
}
|
|
3731
4551
|
}
|
|
3732
4552
|
}
|
|
3733
|
-
|
|
3734
|
-
|
|
3735
|
-
|
|
3736
|
-
|
|
3737
|
-
|
|
3738
|
-
|
|
3739
|
-
|
|
3740
|
-
for (const name of Object.keys(merged)) {
|
|
3741
|
-
const status = runtimeStatus[name];
|
|
3742
|
-
if (status === void 0) continue;
|
|
3743
|
-
const existing = merged[name];
|
|
3744
|
-
const base = existing && typeof existing === "object" ? existing : {};
|
|
3745
|
-
merged[name] = { ...base, enabled: status === "connected" };
|
|
4553
|
+
return records;
|
|
4554
|
+
}
|
|
4555
|
+
function agentDirectories(home, projectDirectory) {
|
|
4556
|
+
const directories = [];
|
|
4557
|
+
if (projectDirectory) {
|
|
4558
|
+
for (const name of AGENT_DIR_NAMES) {
|
|
4559
|
+
directories.push(path5.join(projectDirectory, ".opencode", name));
|
|
3746
4560
|
}
|
|
3747
4561
|
}
|
|
3748
|
-
|
|
3749
|
-
|
|
3750
|
-
|
|
3751
|
-
|
|
3752
|
-
if (enabled === false) continue;
|
|
3753
|
-
enabledServerNames.push(name);
|
|
4562
|
+
if (home) {
|
|
4563
|
+
for (const name of AGENT_DIR_NAMES) {
|
|
4564
|
+
directories.push(path5.join(home, ".config", "opencode", name));
|
|
4565
|
+
}
|
|
3754
4566
|
}
|
|
3755
|
-
|
|
3756
|
-
const hash = crypto3.createHash("sha256").update(mergedBody).digest("hex").slice(0, 12);
|
|
3757
|
-
return { servers: merged, enabledServerNames, hash };
|
|
4567
|
+
return directories;
|
|
3758
4568
|
}
|
|
3759
|
-
|
|
3760
|
-
|
|
3761
|
-
|
|
3762
|
-
|
|
3763
|
-
|
|
3764
|
-
|
|
3765
|
-
|
|
3766
|
-
|
|
3767
|
-
|
|
3768
|
-
allEnabledServerNames
|
|
3769
|
-
};
|
|
3770
|
-
}
|
|
3771
|
-
const body = JSON.stringify({ mcpServers: servers }, null, 2);
|
|
3772
|
-
const outPath = path5.join(
|
|
3773
|
-
pluginTmpDir(),
|
|
3774
|
-
`mcp-${hash}.json`
|
|
3775
|
-
);
|
|
4569
|
+
|
|
4570
|
+
// src/skill-bridge.ts
|
|
4571
|
+
import * as crypto3 from "crypto";
|
|
4572
|
+
import * as fs5 from "fs";
|
|
4573
|
+
import * as os3 from "os";
|
|
4574
|
+
import * as path6 from "path";
|
|
4575
|
+
import { fileURLToPath as fileURLToPath2 } from "url";
|
|
4576
|
+
var SKILL_PLUGIN_NAME = "opencode-skills";
|
|
4577
|
+
function bundledSkillsDir() {
|
|
3776
4578
|
try {
|
|
3777
|
-
|
|
3778
|
-
|
|
3779
|
-
|
|
3780
|
-
} catch
|
|
3781
|
-
log.warn("failed to write bridged MCP config", {
|
|
3782
|
-
error: e instanceof Error ? e.message : String(e)
|
|
3783
|
-
});
|
|
4579
|
+
const here = fileURLToPath2(import.meta.url);
|
|
4580
|
+
const dir = path6.resolve(path6.dirname(here), "..", "skills");
|
|
4581
|
+
return dirExists2(dir) ? dir : null;
|
|
4582
|
+
} catch {
|
|
3784
4583
|
return null;
|
|
3785
4584
|
}
|
|
3786
|
-
log.info("bridged opencode MCP config", {
|
|
3787
|
-
target: outPath,
|
|
3788
|
-
hash,
|
|
3789
|
-
servers: bridgedServerNames,
|
|
3790
|
-
excluded: excludeServers ? Array.from(excludeServers) : []
|
|
3791
|
-
});
|
|
3792
|
-
return {
|
|
3793
|
-
path: outPath,
|
|
3794
|
-
hash,
|
|
3795
|
-
serverNames: bridgedServerNames,
|
|
3796
|
-
allEnabledServerNames
|
|
3797
|
-
};
|
|
3798
4585
|
}
|
|
3799
|
-
|
|
3800
|
-
|
|
3801
|
-
|
|
3802
|
-
|
|
3803
|
-
|
|
3804
|
-
opencodeClient = client;
|
|
4586
|
+
function dirExists2(p) {
|
|
4587
|
+
try {
|
|
4588
|
+
return fs5.statSync(p).isDirectory();
|
|
4589
|
+
} catch {
|
|
4590
|
+
return false;
|
|
3805
4591
|
}
|
|
3806
4592
|
}
|
|
3807
|
-
function
|
|
3808
|
-
|
|
4593
|
+
function fileExists2(p) {
|
|
4594
|
+
try {
|
|
4595
|
+
return fs5.statSync(p).isFile();
|
|
4596
|
+
} catch {
|
|
4597
|
+
return false;
|
|
4598
|
+
}
|
|
3809
4599
|
}
|
|
3810
|
-
|
|
3811
|
-
|
|
3812
|
-
|
|
4600
|
+
function skillRoots(cwd) {
|
|
4601
|
+
const roots = [];
|
|
4602
|
+
const seen = /* @__PURE__ */ new Set();
|
|
4603
|
+
const push = (p) => {
|
|
4604
|
+
const abs = path6.resolve(p);
|
|
4605
|
+
if (seen.has(abs)) return;
|
|
4606
|
+
seen.add(abs);
|
|
4607
|
+
if (dirExists2(abs)) roots.push(abs);
|
|
4608
|
+
};
|
|
4609
|
+
let current = path6.resolve(cwd);
|
|
4610
|
+
while (true) {
|
|
4611
|
+
push(path6.join(current, ".opencode", "skills"));
|
|
4612
|
+
const parent = path6.dirname(current);
|
|
4613
|
+
if (parent === current) break;
|
|
4614
|
+
current = parent;
|
|
4615
|
+
}
|
|
4616
|
+
const home = os3.homedir();
|
|
4617
|
+
if (home) push(path6.join(home, ".opencode", "skills"));
|
|
4618
|
+
const envDir = process.env.OPENCODE_CONFIG_DIR;
|
|
4619
|
+
if (envDir) push(path6.join(envDir, "skills"));
|
|
4620
|
+
const xdg = process.env.XDG_CONFIG_HOME ?? (home ? path6.join(home, ".config") : null);
|
|
4621
|
+
if (xdg) push(path6.join(xdg, "opencode", "skills"));
|
|
4622
|
+
return roots;
|
|
3813
4623
|
}
|
|
3814
|
-
function
|
|
3815
|
-
|
|
4624
|
+
function collectSkills(root, claimed, found) {
|
|
4625
|
+
let entries;
|
|
4626
|
+
try {
|
|
4627
|
+
entries = fs5.readdirSync(root, { withFileTypes: true });
|
|
4628
|
+
} catch {
|
|
4629
|
+
return;
|
|
4630
|
+
}
|
|
4631
|
+
for (const entry of entries) {
|
|
4632
|
+
if (!entry.isDirectory() && !entry.isSymbolicLink()) continue;
|
|
4633
|
+
const name = entry.name;
|
|
4634
|
+
if (name.startsWith(".")) continue;
|
|
4635
|
+
if (claimed.has(name)) continue;
|
|
4636
|
+
const dir = path6.join(root, name);
|
|
4637
|
+
if (!fileExists2(path6.join(dir, "SKILL.md"))) continue;
|
|
4638
|
+
claimed.add(name);
|
|
4639
|
+
found.push({ name, dir });
|
|
4640
|
+
}
|
|
3816
4641
|
}
|
|
3817
|
-
|
|
3818
|
-
|
|
4642
|
+
var byName = (a, b) => a.name.localeCompare(b.name);
|
|
4643
|
+
function discoverOpencodeSkills(cwd) {
|
|
4644
|
+
const found = [];
|
|
4645
|
+
const claimed = /* @__PURE__ */ new Set();
|
|
4646
|
+
for (const root of skillRoots(cwd)) collectSkills(root, claimed, found);
|
|
4647
|
+
return found.sort(byName);
|
|
3819
4648
|
}
|
|
3820
|
-
function
|
|
3821
|
-
|
|
3822
|
-
if (
|
|
3823
|
-
|
|
3824
|
-
|
|
4649
|
+
function discoverBundledSkills() {
|
|
4650
|
+
const root = bundledSkillsDir();
|
|
4651
|
+
if (!root) return [];
|
|
4652
|
+
const found = [];
|
|
4653
|
+
collectSkills(root, /* @__PURE__ */ new Set(), found);
|
|
4654
|
+
return found.sort(byName);
|
|
3825
4655
|
}
|
|
3826
|
-
|
|
3827
|
-
|
|
3828
|
-
|
|
3829
|
-
|
|
3830
|
-
|
|
3831
|
-
|
|
3832
|
-
|
|
3833
|
-
|
|
3834
|
-
);
|
|
4656
|
+
function registerBundledSkillPath(config) {
|
|
4657
|
+
const dir = bundledSkillsDir();
|
|
4658
|
+
if (!dir) return false;
|
|
4659
|
+
config.skills ??= {};
|
|
4660
|
+
const paths = config.skills.paths ??= [];
|
|
4661
|
+
if (paths.some((p) => path6.resolve(p) === dir)) return false;
|
|
4662
|
+
paths.push(dir);
|
|
4663
|
+
return true;
|
|
3835
4664
|
}
|
|
3836
|
-
|
|
3837
|
-
if (!sessionID || sessionID === "default") return void 0;
|
|
3838
|
-
const client = opencodeClient;
|
|
3839
|
-
if (!client?.session?.get) return void 0;
|
|
4665
|
+
function linkSkill(source, target) {
|
|
3840
4666
|
try {
|
|
3841
|
-
|
|
3842
|
-
|
|
3843
|
-
|
|
3844
|
-
|
|
3845
|
-
return isUsableDirectory(dir) ? dir : void 0;
|
|
3846
|
-
} catch (err) {
|
|
3847
|
-
log.warn("failed to fetch opencode session directory", {
|
|
3848
|
-
sessionID,
|
|
3849
|
-
error: err instanceof Error ? err.message : String(err)
|
|
3850
|
-
});
|
|
3851
|
-
return void 0;
|
|
4667
|
+
fs5.symlinkSync(source, target, process.platform === "win32" ? "junction" : "dir");
|
|
4668
|
+
return;
|
|
4669
|
+
} catch {
|
|
4670
|
+
fs5.cpSync(source, target, { recursive: true, dereference: true });
|
|
3852
4671
|
}
|
|
3853
4672
|
}
|
|
3854
|
-
|
|
3855
|
-
|
|
3856
|
-
|
|
4673
|
+
function buildSkillPluginDir(skills) {
|
|
4674
|
+
if (skills.length === 0) return null;
|
|
4675
|
+
const fingerprint = skills.map((s) => `${s.name}\0${s.dir}`).join("\n");
|
|
4676
|
+
const hash = crypto3.createHash("sha256").update(fingerprint).digest("hex").slice(0, 12);
|
|
4677
|
+
const root = path6.join(pluginTmpDir(), `skills-${hash}`);
|
|
4678
|
+
const manifest = path6.join(root, ".claude-plugin", "plugin.json");
|
|
4679
|
+
if (fileExists2(manifest)) return root;
|
|
3857
4680
|
try {
|
|
3858
|
-
|
|
3859
|
-
|
|
3860
|
-
|
|
3861
|
-
|
|
3862
|
-
|
|
3863
|
-
|
|
3864
|
-
|
|
3865
|
-
|
|
3866
|
-
|
|
4681
|
+
fs5.rmSync(root, { recursive: true, force: true });
|
|
4682
|
+
fs5.mkdirSync(path6.join(root, ".claude-plugin"), { recursive: true });
|
|
4683
|
+
fs5.mkdirSync(path6.join(root, "skills"), { recursive: true });
|
|
4684
|
+
fs5.writeFileSync(
|
|
4685
|
+
manifest,
|
|
4686
|
+
JSON.stringify(
|
|
4687
|
+
{
|
|
4688
|
+
name: SKILL_PLUGIN_NAME,
|
|
4689
|
+
description: "Skills discovered from this opencode installation, bridged into Claude Code."
|
|
4690
|
+
},
|
|
4691
|
+
null,
|
|
4692
|
+
2
|
|
4693
|
+
),
|
|
4694
|
+
{ encoding: "utf8", mode: 384 }
|
|
4695
|
+
);
|
|
4696
|
+
for (const skill of skills) {
|
|
4697
|
+
linkSkill(skill.dir, path6.join(root, "skills", skill.name));
|
|
3867
4698
|
}
|
|
3868
|
-
return out;
|
|
3869
4699
|
} catch (err) {
|
|
3870
|
-
log.warn("failed to
|
|
4700
|
+
log.warn("failed to stage opencode skill plugin dir", {
|
|
4701
|
+
root,
|
|
3871
4702
|
error: err instanceof Error ? err.message : String(err)
|
|
3872
4703
|
});
|
|
3873
|
-
return
|
|
4704
|
+
return null;
|
|
3874
4705
|
}
|
|
4706
|
+
return root;
|
|
3875
4707
|
}
|
|
3876
|
-
async function
|
|
3877
|
-
const
|
|
3878
|
-
|
|
3879
|
-
|
|
3880
|
-
|
|
3881
|
-
|
|
3882
|
-
|
|
3883
|
-
|
|
3884
|
-
|
|
3885
|
-
|
|
3886
|
-
|
|
3887
|
-
|
|
3888
|
-
|
|
3889
|
-
const id = typeof e.id === "string" ? e.id : null;
|
|
3890
|
-
const description = typeof e.description === "string" ? e.description : "";
|
|
3891
|
-
const parameters = e.parameters && typeof e.parameters === "object" ? e.parameters : {};
|
|
3892
|
-
if (!id) continue;
|
|
3893
|
-
out.push({ id, description, parameters });
|
|
3894
|
-
}
|
|
3895
|
-
return out;
|
|
3896
|
-
} catch (err) {
|
|
3897
|
-
log.warn("failed to fetch opencode tool list", {
|
|
3898
|
-
provider,
|
|
3899
|
-
model,
|
|
3900
|
-
error: err instanceof Error ? err.message : String(err)
|
|
3901
|
-
});
|
|
3902
|
-
return void 0;
|
|
4708
|
+
async function resolveSkillPluginDirs(opts) {
|
|
4709
|
+
const user = opts.enabled ? discoverOpencodeSkills(opts.cwd) : [];
|
|
4710
|
+
const claimed = new Set(user.map((s) => s.name));
|
|
4711
|
+
const bundled = discoverBundledSkills().filter((s) => !claimed.has(s.name));
|
|
4712
|
+
const skills = [...user, ...bundled].sort(byName);
|
|
4713
|
+
if (skills.length === 0) return [];
|
|
4714
|
+
const supported = await detectCliSupportsFlag(opts.cliPath, "--plugin-dir");
|
|
4715
|
+
if (!supported) {
|
|
4716
|
+
log.notice(
|
|
4717
|
+
"claude cli does not support --plugin-dir; opencode skills will not be bridged. Run `npm i -g @anthropic-ai/claude-code` to upgrade.",
|
|
4718
|
+
{ skills: skills.length }
|
|
4719
|
+
);
|
|
4720
|
+
return [];
|
|
3903
4721
|
}
|
|
4722
|
+
const dir = buildSkillPluginDir(skills);
|
|
4723
|
+
if (!dir) return [];
|
|
4724
|
+
log.info("bridged opencode skills into claude", {
|
|
4725
|
+
count: skills.length,
|
|
4726
|
+
names: skills.map((s) => s.name),
|
|
4727
|
+
bundled: bundled.map((s) => s.name),
|
|
4728
|
+
pluginDir: dir
|
|
4729
|
+
});
|
|
4730
|
+
return [dir];
|
|
3904
4731
|
}
|
|
3905
4732
|
|
|
3906
4733
|
// src/claude-session-wrapper.ts
|
|
@@ -3909,12 +4736,12 @@ import { unlink as unlink2 } from "fs/promises";
|
|
|
3909
4736
|
|
|
3910
4737
|
// src/claude-session-bun.ts
|
|
3911
4738
|
import * as os4 from "os";
|
|
3912
|
-
import * as
|
|
3913
|
-
import * as
|
|
4739
|
+
import * as fs6 from "fs";
|
|
4740
|
+
import * as path7 from "path";
|
|
3914
4741
|
import { execFileSync } from "child_process";
|
|
3915
4742
|
import { randomUUID as randomUUID4 } from "crypto";
|
|
3916
4743
|
function resolveClaude(cmd = "claude") {
|
|
3917
|
-
if (
|
|
4744
|
+
if (path7.isAbsolute(cmd) && fs6.existsSync(cmd)) return cmd;
|
|
3918
4745
|
const viaBun = Bun.which(cmd);
|
|
3919
4746
|
if (viaBun) return viaBun;
|
|
3920
4747
|
const isWin = os4.platform() === "win32";
|
|
@@ -3923,25 +4750,25 @@ function resolveClaude(cmd = "claude") {
|
|
|
3923
4750
|
encoding: "utf8",
|
|
3924
4751
|
stdio: ["ignore", "pipe", "ignore"]
|
|
3925
4752
|
});
|
|
3926
|
-
const first = out.split(/\r?\n/).map((l) => l.trim()).filter(Boolean).find((p) =>
|
|
4753
|
+
const first = out.split(/\r?\n/).map((l) => l.trim()).filter(Boolean).find((p) => fs6.existsSync(p));
|
|
3927
4754
|
if (first) return first;
|
|
3928
4755
|
} catch {
|
|
3929
4756
|
}
|
|
3930
4757
|
throw new Error(`Could not resolve command on PATH: ${cmd}`);
|
|
3931
4758
|
}
|
|
3932
4759
|
function encodeCwd(cwd) {
|
|
3933
|
-
return
|
|
4760
|
+
return path7.resolve(cwd).replace(/[^a-zA-Z0-9]/g, "-");
|
|
3934
4761
|
}
|
|
3935
4762
|
var TERMINAL_STOP = /* @__PURE__ */ new Set(["end_turn", "stop_sequence", "max_tokens"]);
|
|
3936
4763
|
var delay = (ms) => new Promise((r) => setTimeout(r, ms));
|
|
3937
4764
|
function resolveConfigDir(configDir) {
|
|
3938
4765
|
const value = configDir ?? process.env.CLAUDE_CONFIG_DIR;
|
|
3939
|
-
if (!value) return
|
|
4766
|
+
if (!value) return path7.join(os4.homedir(), ".claude");
|
|
3940
4767
|
if (value === "~") return os4.homedir();
|
|
3941
4768
|
if (value.startsWith("~/") || value.startsWith("~\\")) {
|
|
3942
|
-
return
|
|
4769
|
+
return path7.join(os4.homedir(), value.slice(2));
|
|
3943
4770
|
}
|
|
3944
|
-
return
|
|
4771
|
+
return path7.resolve(value);
|
|
3945
4772
|
}
|
|
3946
4773
|
var ClaudeSession = class {
|
|
3947
4774
|
sessionId;
|
|
@@ -3959,11 +4786,11 @@ var ClaudeSession = class {
|
|
|
3959
4786
|
signal;
|
|
3960
4787
|
o;
|
|
3961
4788
|
constructor(opts = {}) {
|
|
3962
|
-
this.cwd =
|
|
4789
|
+
this.cwd = path7.resolve(opts.cwd ?? process.cwd());
|
|
3963
4790
|
this.configDir = resolveConfigDir(opts.configDir);
|
|
3964
4791
|
this.signal = opts.signal;
|
|
3965
4792
|
this.sessionId = randomUUID4();
|
|
3966
|
-
this.jsonlPath =
|
|
4793
|
+
this.jsonlPath = path7.join(
|
|
3967
4794
|
this.configDir,
|
|
3968
4795
|
"projects",
|
|
3969
4796
|
encodeCwd(this.cwd),
|
|
@@ -4086,7 +4913,7 @@ var ClaudeSession = class {
|
|
|
4086
4913
|
}
|
|
4087
4914
|
readRawLines() {
|
|
4088
4915
|
try {
|
|
4089
|
-
return
|
|
4916
|
+
return fs6.readFileSync(this.jsonlPath, "utf8").split("\n");
|
|
4090
4917
|
} catch {
|
|
4091
4918
|
return [];
|
|
4092
4919
|
}
|
|
@@ -4314,7 +5141,7 @@ ${text}`
|
|
|
4314
5141
|
}
|
|
4315
5142
|
return parts.join("\n\n");
|
|
4316
5143
|
}
|
|
4317
|
-
function
|
|
5144
|
+
function interactiveExtraArgs(opts) {
|
|
4318
5145
|
const extraArgs = [];
|
|
4319
5146
|
if (opts.mcpConfigPaths && opts.mcpConfigPaths.length > 0) {
|
|
4320
5147
|
extraArgs.push(
|
|
@@ -4323,6 +5150,9 @@ function spawnInteractiveProcess(opts) {
|
|
|
4323
5150
|
"--strict-mcp-config"
|
|
4324
5151
|
);
|
|
4325
5152
|
}
|
|
5153
|
+
for (const dir of opts.pluginDirs ?? []) {
|
|
5154
|
+
extraArgs.push("--plugin-dir", dir);
|
|
5155
|
+
}
|
|
4326
5156
|
const flagSettings = {};
|
|
4327
5157
|
if (opts.permissionsAllow && opts.permissionsAllow.length > 0) {
|
|
4328
5158
|
flagSettings.permissions = { allow: opts.permissionsAllow };
|
|
@@ -4343,6 +5173,10 @@ function spawnInteractiveProcess(opts) {
|
|
|
4343
5173
|
if (opts.systemPromptFile) {
|
|
4344
5174
|
extraArgs.push("--append-system-prompt-file", opts.systemPromptFile);
|
|
4345
5175
|
}
|
|
5176
|
+
return extraArgs;
|
|
5177
|
+
}
|
|
5178
|
+
function spawnInteractiveProcess(opts) {
|
|
5179
|
+
const extraArgs = interactiveExtraArgs(opts);
|
|
4346
5180
|
const session = new ClaudeSession({
|
|
4347
5181
|
cwd: opts.cwd,
|
|
4348
5182
|
cliPath: opts.cliPath,
|
|
@@ -4464,11 +5298,11 @@ function spawnInteractiveProcess(opts) {
|
|
|
4464
5298
|
}
|
|
4465
5299
|
|
|
4466
5300
|
// src/claude-code-language-model.ts
|
|
4467
|
-
import { readFileSync as
|
|
5301
|
+
import { readFileSync as readFileSync4, writeFileSync as writeFileSync4 } from "fs";
|
|
4468
5302
|
import { unlink as unlink3 } from "fs/promises";
|
|
4469
5303
|
import { homedir as homedir5, tmpdir as tmpdir2 } from "os";
|
|
4470
5304
|
import { randomUUID as randomUUID5 } from "crypto";
|
|
4471
|
-
import { dirname as
|
|
5305
|
+
import { dirname as dirname5, join as join8 } from "path";
|
|
4472
5306
|
var DEFAULT_COMPACTION_MODEL = "claude-haiku-4-5";
|
|
4473
5307
|
function resolveCompactionModel(configured) {
|
|
4474
5308
|
const env = process.env.CLAUDE_CODE_COMPACTION_MODEL?.trim();
|
|
@@ -4532,6 +5366,7 @@ var AUTO_CONTINUE_MAX_ATTEMPTS = 8;
|
|
|
4532
5366
|
var AUTO_CONTINUE_MAX_ELAPSED_MS = 10 * 60 * 1e3;
|
|
4533
5367
|
var AUTO_CONTINUE_NO_PROGRESS_LIMIT = 2;
|
|
4534
5368
|
var PROXY_RESULT_BOUNDARY_GRACE_MS = 250;
|
|
5369
|
+
var CHILD_EXIT_STATUS_GRACE_MS = 250;
|
|
4535
5370
|
var AUTO_CONTINUE_PROMPT = "Continue the task from where you stopped. Do not summarize; keep working until the requested task is complete, you need clarification, or you hit a real blocker.";
|
|
4536
5371
|
function autoContinueEnabledFor(compactionMode, configured) {
|
|
4537
5372
|
return compactionMode ? false : configured;
|
|
@@ -4693,7 +5528,7 @@ ${body}`;
|
|
|
4693
5528
|
}
|
|
4694
5529
|
function readPromptFileIfPresent(path9) {
|
|
4695
5530
|
try {
|
|
4696
|
-
const content =
|
|
5531
|
+
const content = readFileSync4(path9, "utf8").trim();
|
|
4697
5532
|
return content || void 0;
|
|
4698
5533
|
} catch {
|
|
4699
5534
|
return void 0;
|
|
@@ -4702,9 +5537,9 @@ function readPromptFileIfPresent(path9) {
|
|
|
4702
5537
|
function nearestWorkspaceAgentsPrompt(cwd) {
|
|
4703
5538
|
let dir = cwd;
|
|
4704
5539
|
while (true) {
|
|
4705
|
-
const content = readPromptFileIfPresent(
|
|
5540
|
+
const content = readPromptFileIfPresent(join8(dir, "AGENTS.md"));
|
|
4706
5541
|
if (content) return content;
|
|
4707
|
-
const parent =
|
|
5542
|
+
const parent = dirname5(dir);
|
|
4708
5543
|
if (parent === dir) return void 0;
|
|
4709
5544
|
dir = parent;
|
|
4710
5545
|
}
|
|
@@ -4788,8 +5623,8 @@ ${options.compressionSummary.trim()}`
|
|
|
4788
5623
|
for (const s of extraSystemContent) {
|
|
4789
5624
|
if (s.trim()) parts.push(s.trim());
|
|
4790
5625
|
}
|
|
4791
|
-
const configRoot = process.env.XDG_CONFIG_HOME ??
|
|
4792
|
-
const globalAgents = readPromptFileIfPresent(
|
|
5626
|
+
const configRoot = process.env.XDG_CONFIG_HOME ?? join8(homedir5(), ".config");
|
|
5627
|
+
const globalAgents = readPromptFileIfPresent(join8(configRoot, "opencode", "AGENTS.md"));
|
|
4793
5628
|
const workspaceAgents = nearestWorkspaceAgentsPrompt(cwd);
|
|
4794
5629
|
const forwarded = extraSystemContent.join("\n\n");
|
|
4795
5630
|
const pushGlobal = !!globalAgents && !forwarded.includes(globalAgents);
|
|
@@ -4800,7 +5635,7 @@ ${options.compressionSummary.trim()}`
|
|
|
4800
5635
|
if (includeMultiStepHint) parts.push(MULTI_STEP_TASK_HINT);
|
|
4801
5636
|
const content = parts.join("\n\n");
|
|
4802
5637
|
if (!content) return void 0;
|
|
4803
|
-
const path9 =
|
|
5638
|
+
const path9 = join8(tmpdir2(), `opencode-cc-sys-${randomUUID5()}.md`);
|
|
4804
5639
|
try {
|
|
4805
5640
|
writeFileSync4(path9, content, "utf8");
|
|
4806
5641
|
return path9;
|
|
@@ -5528,6 +6363,11 @@ var ClaudeCodeLanguageModel = class {
|
|
|
5528
6363
|
{ compressEnabled: false, compressionSummary: getCompressionSummary(sk) }
|
|
5529
6364
|
);
|
|
5530
6365
|
const { model: spawnModelId, fast: fastMode } = parseModelId(effectiveModelId);
|
|
6366
|
+
const skillPluginDirs = await resolveSkillPluginDirs({
|
|
6367
|
+
cwd,
|
|
6368
|
+
cliPath: this.config.cliPath,
|
|
6369
|
+
enabled: this.config.bridgeOpencodeSkills === true
|
|
6370
|
+
});
|
|
5531
6371
|
const cliArgs = buildCliArgs({
|
|
5532
6372
|
sessionKey: sk,
|
|
5533
6373
|
skipPermissions: this.config.skipPermissions !== false,
|
|
@@ -5538,6 +6378,7 @@ var ClaudeCodeLanguageModel = class {
|
|
|
5538
6378
|
strictMcpConfig: this.config.strictMcpConfig,
|
|
5539
6379
|
disallowedTools: this.config.webSearch === "disabled" ? ["WebSearch"] : void 0,
|
|
5540
6380
|
appendSystemPromptFile: systemPromptFile,
|
|
6381
|
+
pluginDirs: skillPluginDirs,
|
|
5541
6382
|
...this.thinkingCliOptions(),
|
|
5542
6383
|
fastMode,
|
|
5543
6384
|
cliVersion
|
|
@@ -5596,6 +6437,13 @@ var ClaudeCodeLanguageModel = class {
|
|
|
5596
6437
|
setClaudeSessionId(sk, msg.session_id);
|
|
5597
6438
|
}
|
|
5598
6439
|
reportFastModeState(msg, fastMode);
|
|
6440
|
+
reportSystemInit(msg, {
|
|
6441
|
+
ignoreAnthropicApiKey: this.config.ignoreAnthropicApiKey
|
|
6442
|
+
});
|
|
6443
|
+
}
|
|
6444
|
+
if (msg.type === "rate_limit_event") {
|
|
6445
|
+
reportRateLimitEvent(msg);
|
|
6446
|
+
return;
|
|
5599
6447
|
}
|
|
5600
6448
|
if (msg.type === "assistant" && msg.message?.content && !gotPartialEvents) {
|
|
5601
6449
|
for (const block of msg.message.content) {
|
|
@@ -5705,8 +6553,21 @@ ${plan}
|
|
|
5705
6553
|
sessionId: msg.session_id,
|
|
5706
6554
|
costUsd: msg.total_cost_usd,
|
|
5707
6555
|
durationMs: msg.duration_ms,
|
|
5708
|
-
|
|
6556
|
+
durationApiMs: msg.duration_api_ms,
|
|
6557
|
+
numTurns: msg.num_turns,
|
|
6558
|
+
usage: msg.usage,
|
|
6559
|
+
modelUsage: msg.modelUsage,
|
|
6560
|
+
permissionDenials: msg.permission_denials?.map((denial) => ({
|
|
6561
|
+
tool_name: denial.tool_name,
|
|
6562
|
+
tool_use_id: denial.tool_use_id
|
|
6563
|
+
}))
|
|
5709
6564
|
};
|
|
6565
|
+
log.info("conversation result", {
|
|
6566
|
+
sessionId: msg.session_id,
|
|
6567
|
+
isError: msg.is_error,
|
|
6568
|
+
subtype: msg.subtype,
|
|
6569
|
+
...turnStatsLogPayload(extractTurnStats(msg))
|
|
6570
|
+
});
|
|
5710
6571
|
cleanup();
|
|
5711
6572
|
resolve5({
|
|
5712
6573
|
...resultMeta,
|
|
@@ -5854,6 +6715,39 @@ ${plan}
|
|
|
5854
6715
|
const useInteractive = interactivePref && typeof globalThis.Bun?.Terminal === "function";
|
|
5855
6716
|
const interactiveBypassRequested = this.config.interactiveBypass ?? flagOn(process.env.CLAUDE_CODE_INTERACTIVE_BYPASS);
|
|
5856
6717
|
const asideTransportRef = { cliPath, interactive: !!useInteractive };
|
|
6718
|
+
const doctor = !compactionMode && scope !== "no-tools" ? parseDoctorCommand(options.prompt) : null;
|
|
6719
|
+
if (doctor) {
|
|
6720
|
+
const doctorOptions = {
|
|
6721
|
+
cliPath,
|
|
6722
|
+
interactive: !!useInteractive,
|
|
6723
|
+
turnStats: this.config.turnStats === true
|
|
6724
|
+
};
|
|
6725
|
+
const stream2 = new ReadableStream({
|
|
6726
|
+
async start(controller) {
|
|
6727
|
+
controller.enqueue({ type: "stream-start", warnings });
|
|
6728
|
+
try {
|
|
6729
|
+
const text = await buildDoctorReport(doctorOptions);
|
|
6730
|
+
const id = generateId();
|
|
6731
|
+
controller.enqueue({ type: "text-start", id });
|
|
6732
|
+
controller.enqueue({ type: "text-delta", id, delta: text });
|
|
6733
|
+
controller.enqueue({ type: "text-end", id });
|
|
6734
|
+
controller.enqueue({
|
|
6735
|
+
type: "finish",
|
|
6736
|
+
finishReason: toFinishReason("stop"),
|
|
6737
|
+
usage: toUsage({ input_tokens: 0, output_tokens: 0 }),
|
|
6738
|
+
providerMetadata: {
|
|
6739
|
+
"claude-code": { path: "doctor", synthetic: true, usageUnavailable: true }
|
|
6740
|
+
}
|
|
6741
|
+
});
|
|
6742
|
+
} catch (error) {
|
|
6743
|
+
controller.enqueue({ type: "error", error });
|
|
6744
|
+
} finally {
|
|
6745
|
+
controller.close();
|
|
6746
|
+
}
|
|
6747
|
+
}
|
|
6748
|
+
});
|
|
6749
|
+
return { stream: stream2, request: { body: { text: `/${DOCTOR_COMMAND}` } } };
|
|
6750
|
+
}
|
|
5857
6751
|
const aside = !compactionMode && scope !== "no-tools" ? parseSideQuestion(options.prompt) : null;
|
|
5858
6752
|
if (aside) {
|
|
5859
6753
|
const active = getActiveProcess(sk);
|
|
@@ -6084,6 +6978,11 @@ ${plan}
|
|
|
6084
6978
|
"interactiveBypass ignored: Claude Code prompts for bypassPermissions confirmation in the interactive TUI"
|
|
6085
6979
|
);
|
|
6086
6980
|
}
|
|
6981
|
+
const skillPluginDirs = await resolveSkillPluginDirs({
|
|
6982
|
+
cwd,
|
|
6983
|
+
cliPath,
|
|
6984
|
+
enabled: self.config.bridgeOpencodeSkills === true
|
|
6985
|
+
});
|
|
6087
6986
|
const ap = spawnInteractiveProcess({
|
|
6088
6987
|
cwd,
|
|
6089
6988
|
cliPath,
|
|
@@ -6091,6 +6990,7 @@ ${plan}
|
|
|
6091
6990
|
model: spawnModelId,
|
|
6092
6991
|
fastMode,
|
|
6093
6992
|
mcpConfigPaths: mcp.paths,
|
|
6993
|
+
pluginDirs: skillPluginDirs,
|
|
6094
6994
|
permissionsAllow: allow,
|
|
6095
6995
|
systemPromptFile,
|
|
6096
6996
|
ignoreAnthropicApiKey: self.config.ignoreAnthropicApiKey,
|
|
@@ -6422,6 +7322,7 @@ ${plan}
|
|
|
6422
7322
|
if (entries.length === 0) return false;
|
|
6423
7323
|
endTextBlock();
|
|
6424
7324
|
watchdogMessage = makeLateProxyResultMessage(entries);
|
|
7325
|
+
if (activeProcess) noteTurnStarted(activeProcess);
|
|
6425
7326
|
proc.stdin.write(watchdogMessage + "\n");
|
|
6426
7327
|
for (const { call } of entries) pending.delete(call.toolCallId);
|
|
6427
7328
|
log.warn("delivering proxy results after interrupted continuation", {
|
|
@@ -6442,6 +7343,7 @@ ${plan}
|
|
|
6442
7343
|
const skipResultForIds = /* @__PURE__ */ new Set();
|
|
6443
7344
|
const toolCallsById = /* @__PURE__ */ new Map();
|
|
6444
7345
|
let resultMeta = {};
|
|
7346
|
+
let resultFailure;
|
|
6445
7347
|
const drainBuffer = [];
|
|
6446
7348
|
let drainTimer = null;
|
|
6447
7349
|
const DRAIN_QUIET_MS = 100;
|
|
@@ -6664,11 +7566,12 @@ ${plan}
|
|
|
6664
7566
|
}
|
|
6665
7567
|
controller.enqueue({
|
|
6666
7568
|
type: "finish",
|
|
6667
|
-
finishReason: toFinishReason("stop"),
|
|
7569
|
+
finishReason: resultFailure ? { unified: "error", raw: resultFailure } : toFinishReason("stop"),
|
|
6668
7570
|
usage: toUsage(msg.usage),
|
|
6669
7571
|
providerMetadata: {
|
|
6670
7572
|
"claude-code": {
|
|
6671
7573
|
...resultMeta,
|
|
7574
|
+
...resultFailure ? { resultSubtype: resultFailure } : {},
|
|
6672
7575
|
...compactionMode ? { compactionModel: effectiveModelId } : {}
|
|
6673
7576
|
},
|
|
6674
7577
|
...typeof msg.usage?.cache_creation_input_tokens === "number" ? {
|
|
@@ -6681,7 +7584,10 @@ ${plan}
|
|
|
6681
7584
|
controllerClosed = true;
|
|
6682
7585
|
cleanupTurn();
|
|
6683
7586
|
if (!useInteractive && !compactionMode) {
|
|
6684
|
-
scheduleIdleProcessEviction(
|
|
7587
|
+
scheduleIdleProcessEviction(
|
|
7588
|
+
sk,
|
|
7589
|
+
resolveIdleProcessTimeoutMs(self.config.idleProcessTimeoutMs)
|
|
7590
|
+
);
|
|
6685
7591
|
}
|
|
6686
7592
|
try {
|
|
6687
7593
|
controller.close();
|
|
@@ -6720,6 +7626,24 @@ ${plan}
|
|
|
6720
7626
|
});
|
|
6721
7627
|
}
|
|
6722
7628
|
reportFastModeState(msg, fastMode);
|
|
7629
|
+
reportSystemInit(msg, {
|
|
7630
|
+
ignoreAnthropicApiKey: self.config.ignoreAnthropicApiKey
|
|
7631
|
+
});
|
|
7632
|
+
}
|
|
7633
|
+
if (msg.type === "system" && msg.subtype === "compact_boundary") {
|
|
7634
|
+
const note = reportCompactBoundary(msg);
|
|
7635
|
+
if (note) {
|
|
7636
|
+
controller.enqueue({ type: "text-delta", id: startTextBlock(), delta: note });
|
|
7637
|
+
endTextBlock();
|
|
7638
|
+
}
|
|
7639
|
+
}
|
|
7640
|
+
if (msg.type === "rate_limit_event") {
|
|
7641
|
+
const note = reportRateLimitEvent(msg);
|
|
7642
|
+
if (note) {
|
|
7643
|
+
controller.enqueue({ type: "text-delta", id: startTextBlock(), delta: note });
|
|
7644
|
+
endTextBlock();
|
|
7645
|
+
}
|
|
7646
|
+
return;
|
|
6723
7647
|
}
|
|
6724
7648
|
if (msg.type === "content_block_start" && msg.content_block && msg.index !== void 0) {
|
|
6725
7649
|
const block = msg.content_block;
|
|
@@ -7158,12 +8082,12 @@ ${plan}
|
|
|
7158
8082
|
}
|
|
7159
8083
|
const claudeSessionId = getClaudeSessionId(sk);
|
|
7160
8084
|
if (claudeSessionId) {
|
|
7161
|
-
const
|
|
8085
|
+
const list2 = applyTaskCreateToolResult(
|
|
7162
8086
|
claudeSessionId,
|
|
7163
8087
|
block.tool_use_id,
|
|
7164
8088
|
resultText
|
|
7165
8089
|
);
|
|
7166
|
-
if (
|
|
8090
|
+
if (list2) {
|
|
7167
8091
|
const synthId = `todowrite_${block.tool_use_id}`;
|
|
7168
8092
|
controller.enqueue({
|
|
7169
8093
|
type: "tool-input-start",
|
|
@@ -7176,7 +8100,7 @@ ${plan}
|
|
|
7176
8100
|
toolCallId: synthId,
|
|
7177
8101
|
toolName: "todowrite",
|
|
7178
8102
|
input: JSON.stringify({
|
|
7179
|
-
todos:
|
|
8103
|
+
todos: list2.map((t) => ({
|
|
7180
8104
|
id: t.id,
|
|
7181
8105
|
content: t.content,
|
|
7182
8106
|
status: t.status,
|
|
@@ -7190,6 +8114,7 @@ ${plan}
|
|
|
7190
8114
|
}
|
|
7191
8115
|
const toolCall = toolCallsById.get(block.tool_use_id);
|
|
7192
8116
|
if (toolCall) {
|
|
8117
|
+
const isError = block.is_error === true;
|
|
7193
8118
|
controller.enqueue({
|
|
7194
8119
|
type: "tool-result",
|
|
7195
8120
|
toolCallId: block.tool_use_id,
|
|
@@ -7197,14 +8122,16 @@ ${plan}
|
|
|
7197
8122
|
result: {
|
|
7198
8123
|
output: resultText,
|
|
7199
8124
|
title: toolCall.name,
|
|
7200
|
-
metadata: {}
|
|
8125
|
+
metadata: isError ? { error: true } : {}
|
|
7201
8126
|
},
|
|
8127
|
+
...isError ? { isError: true } : {},
|
|
7202
8128
|
providerExecuted: true
|
|
7203
8129
|
});
|
|
7204
8130
|
noteToolActivity();
|
|
7205
8131
|
log.info("tool result emitted", {
|
|
7206
8132
|
toolUseId: block.tool_use_id,
|
|
7207
|
-
name: toolCall.name
|
|
8133
|
+
name: toolCall.name,
|
|
8134
|
+
isError
|
|
7208
8135
|
});
|
|
7209
8136
|
toolCallsById.delete(block.tool_use_id);
|
|
7210
8137
|
}
|
|
@@ -7227,18 +8154,49 @@ ${plan}
|
|
|
7227
8154
|
delta: msg.result
|
|
7228
8155
|
});
|
|
7229
8156
|
}
|
|
8157
|
+
const failure = describeResultFailure(msg);
|
|
8158
|
+
if (failure) {
|
|
8159
|
+
resultFailure = msg.subtype;
|
|
8160
|
+
controller.enqueue({
|
|
8161
|
+
type: "text-delta",
|
|
8162
|
+
id: startTextBlock(),
|
|
8163
|
+
delta: formatResultFailureNote(failure)
|
|
8164
|
+
});
|
|
8165
|
+
log.warn(failure, { sessionKey: sk, subtype: msg.subtype });
|
|
8166
|
+
}
|
|
8167
|
+
const turnStats = extractTurnStats(msg);
|
|
7230
8168
|
resultMeta = {
|
|
7231
8169
|
sessionId: msg.session_id,
|
|
7232
8170
|
costUsd: msg.total_cost_usd,
|
|
7233
8171
|
durationMs: msg.duration_ms,
|
|
7234
|
-
|
|
8172
|
+
durationApiMs: msg.duration_api_ms,
|
|
8173
|
+
numTurns: msg.num_turns,
|
|
8174
|
+
usage: msg.usage,
|
|
8175
|
+
modelUsage: msg.modelUsage,
|
|
8176
|
+
// Names and ids only: a denial's `tool_input` can be a whole
|
|
8177
|
+
// file write payload and has no business in metadata.
|
|
8178
|
+
permissionDenials: msg.permission_denials?.map((denial) => ({
|
|
8179
|
+
tool_name: denial.tool_name,
|
|
8180
|
+
tool_use_id: denial.tool_use_id
|
|
8181
|
+
}))
|
|
7235
8182
|
};
|
|
7236
8183
|
log.info("conversation result", {
|
|
7237
8184
|
sessionId: msg.session_id,
|
|
7238
|
-
durationMs: msg.duration_ms,
|
|
7239
8185
|
numTurns: msg.num_turns,
|
|
7240
|
-
isError: msg.is_error
|
|
8186
|
+
isError: msg.is_error,
|
|
8187
|
+
subtype: msg.subtype,
|
|
8188
|
+
...turnStatsLogPayload(turnStats)
|
|
7241
8189
|
});
|
|
8190
|
+
if (self.config.turnStats && !compactionMode && !msg.is_error && !failure) {
|
|
8191
|
+
const footer = formatTurnStatsBlock(turnStats);
|
|
8192
|
+
if (footer) {
|
|
8193
|
+
controller.enqueue({
|
|
8194
|
+
type: "text-delta",
|
|
8195
|
+
id: startTextBlock(),
|
|
8196
|
+
delta: footer
|
|
8197
|
+
});
|
|
8198
|
+
}
|
|
8199
|
+
}
|
|
7242
8200
|
turnCompleted = true;
|
|
7243
8201
|
endTextBlock();
|
|
7244
8202
|
const shouldDeferResult = !msg.is_error && !autoContinueState.aborted && !autoContinueState.sawAskUserQuestion;
|
|
@@ -7290,24 +8248,59 @@ ${plan}
|
|
|
7290
8248
|
);
|
|
7291
8249
|
drainBuffer.length = 0;
|
|
7292
8250
|
}
|
|
8251
|
+
const crashed = !turnCompleted && !autoContinueState.aborted;
|
|
7293
8252
|
controllerClosed = true;
|
|
7294
8253
|
cleanupTurn();
|
|
7295
8254
|
endTextBlock();
|
|
7296
|
-
|
|
7297
|
-
|
|
7298
|
-
|
|
7299
|
-
|
|
7300
|
-
|
|
7301
|
-
|
|
7302
|
-
|
|
7303
|
-
|
|
8255
|
+
const finishClose = (exitCode, signal) => {
|
|
8256
|
+
if (crashed) {
|
|
8257
|
+
log.warn("claude process closed without a result", {
|
|
8258
|
+
sessionKey: sk,
|
|
8259
|
+
exitCode,
|
|
8260
|
+
signal,
|
|
8261
|
+
stderrBytes: activeProcess?.lastStderr?.length ?? 0
|
|
8262
|
+
});
|
|
8263
|
+
controller.enqueue({
|
|
8264
|
+
type: "error",
|
|
8265
|
+
error: new Error(
|
|
8266
|
+
describeChildCrash(exitCode, signal, activeProcess?.lastStderr)
|
|
8267
|
+
)
|
|
8268
|
+
});
|
|
8269
|
+
}
|
|
8270
|
+
controller.enqueue({
|
|
8271
|
+
type: "finish",
|
|
8272
|
+
finishReason: toFinishReason(crashed ? "error" : "stop"),
|
|
8273
|
+
usage: toUsage(),
|
|
8274
|
+
providerMetadata: {
|
|
8275
|
+
"claude-code": {
|
|
8276
|
+
...resultMeta,
|
|
8277
|
+
...compactionMode ? { compactionModel: effectiveModelId } : {}
|
|
8278
|
+
}
|
|
7304
8279
|
}
|
|
8280
|
+
});
|
|
8281
|
+
try {
|
|
8282
|
+
controller.close();
|
|
8283
|
+
} catch {
|
|
7305
8284
|
}
|
|
7306
|
-
}
|
|
7307
|
-
|
|
7308
|
-
|
|
7309
|
-
|
|
8285
|
+
};
|
|
8286
|
+
if (crashed && proc.exitCode === null && proc.signalCode === null) {
|
|
8287
|
+
let reported = false;
|
|
8288
|
+
const report = (exitCode, signal) => {
|
|
8289
|
+
if (reported) return;
|
|
8290
|
+
reported = true;
|
|
8291
|
+
clearTimeout(exitGrace);
|
|
8292
|
+
proc.off("exit", onExit);
|
|
8293
|
+
finishClose(exitCode, signal);
|
|
8294
|
+
};
|
|
8295
|
+
const onExit = (code, signal) => report(code, signal);
|
|
8296
|
+
const exitGrace = setTimeout(
|
|
8297
|
+
() => report(proc.exitCode, proc.signalCode),
|
|
8298
|
+
CHILD_EXIT_STATUS_GRACE_MS
|
|
8299
|
+
);
|
|
8300
|
+
proc.once("exit", onExit);
|
|
8301
|
+
return;
|
|
7310
8302
|
}
|
|
8303
|
+
finishClose(proc.exitCode, proc.signalCode);
|
|
7311
8304
|
};
|
|
7312
8305
|
let cleanedUp = false;
|
|
7313
8306
|
const cleanupTurn = () => {
|
|
@@ -7389,7 +8382,7 @@ ${plan}
|
|
|
7389
8382
|
if (outer.session_id) setClaudeSessionId(sk, outer.session_id);
|
|
7390
8383
|
if (msg.is_error && msg.result) text = msg.result;
|
|
7391
8384
|
}
|
|
7392
|
-
if (text) controller.enqueue({ type: "text-delta", id: startTextBlock(), delta: text });
|
|
8385
|
+
if (text) controller.enqueue({ type: "text-delta", id: currentTextId ?? startTextBlock(), delta: text });
|
|
7393
8386
|
} catch {
|
|
7394
8387
|
}
|
|
7395
8388
|
}
|
|
@@ -7446,9 +8439,25 @@ ${plan}
|
|
|
7446
8439
|
});
|
|
7447
8440
|
proc.on("error", procErrorHandler);
|
|
7448
8441
|
if (options.abortSignal) {
|
|
8442
|
+
const releaseAbandonedProxyCalls = (reason) => {
|
|
8443
|
+
if (drainBuffer.length === 0 && getPendingProxyCalls(sk).length === 0) return;
|
|
8444
|
+
rejectAllPendingProxyCallsForSession(sk, new Error(reason));
|
|
8445
|
+
drainBuffer.length = 0;
|
|
8446
|
+
};
|
|
7449
8447
|
options.abortSignal.addEventListener("abort", () => {
|
|
7450
8448
|
autoContinueState.aborted = true;
|
|
7451
|
-
if (turnCompleted || controllerClosed)
|
|
8449
|
+
if (turnCompleted || controllerClosed) {
|
|
8450
|
+
if (activeProcess && activeProcess.lineEmitter.listenerCount("line") === 0 && getPendingProxyCalls(sk).length > 0) {
|
|
8451
|
+
log.info("abort between proxy tool boundaries; releasing pending calls", { sk });
|
|
8452
|
+
void interruptTurn(activeProcess).then((idle) => {
|
|
8453
|
+
log.info("interrupt sent for aborted turn", { sk, idle });
|
|
8454
|
+
});
|
|
8455
|
+
releaseAbandonedProxyCalls(
|
|
8456
|
+
"Provider stream was aborted while opencode was running its proxy tool calls"
|
|
8457
|
+
);
|
|
8458
|
+
}
|
|
8459
|
+
return;
|
|
8460
|
+
}
|
|
7452
8461
|
if (activeProcess) {
|
|
7453
8462
|
void interruptTurn(activeProcess).then((idle) => {
|
|
7454
8463
|
log.info("interrupt sent for aborted turn", { sk, idle });
|
|
@@ -7459,15 +8468,9 @@ ${plan}
|
|
|
7459
8468
|
"abort signal received before content, closing stream immediately",
|
|
7460
8469
|
{ cwd }
|
|
7461
8470
|
);
|
|
7462
|
-
|
|
7463
|
-
|
|
7464
|
-
|
|
7465
|
-
new Error(
|
|
7466
|
-
"Provider stream was aborted before pending proxy calls were emitted"
|
|
7467
|
-
)
|
|
7468
|
-
);
|
|
7469
|
-
drainBuffer.length = 0;
|
|
7470
|
-
}
|
|
8471
|
+
releaseAbandonedProxyCalls(
|
|
8472
|
+
"Provider stream was aborted before pending proxy calls were emitted"
|
|
8473
|
+
);
|
|
7471
8474
|
controllerClosed = true;
|
|
7472
8475
|
cleanupTurn();
|
|
7473
8476
|
try {
|
|
@@ -7480,6 +8483,9 @@ ${plan}
|
|
|
7480
8483
|
"abort signal received mid-turn, starting grace period",
|
|
7481
8484
|
{ cwd }
|
|
7482
8485
|
);
|
|
8486
|
+
releaseAbandonedProxyCalls(
|
|
8487
|
+
"Provider stream was aborted while proxy tool calls were pending"
|
|
8488
|
+
);
|
|
7483
8489
|
startResultFallback(5e3);
|
|
7484
8490
|
});
|
|
7485
8491
|
}
|
|
@@ -7573,7 +8579,7 @@ ${plan}
|
|
|
7573
8579
|
|
|
7574
8580
|
// src/accounts.ts
|
|
7575
8581
|
import { chmod, lstat, mkdir, readlink, symlink, writeFile } from "fs/promises";
|
|
7576
|
-
import
|
|
8582
|
+
import path8 from "path";
|
|
7577
8583
|
var BASE_PROVIDER_ID = "claude-code";
|
|
7578
8584
|
var DEFAULT_ACCOUNT = "default";
|
|
7579
8585
|
var SHARED_CAPABILITY_ITEMS = [
|
|
@@ -7611,7 +8617,7 @@ function expandHome(value) {
|
|
|
7611
8617
|
const home = process.env.HOME ?? process.env.USERPROFILE;
|
|
7612
8618
|
if (value === "~") return home ?? value;
|
|
7613
8619
|
if (value.startsWith("~/") || value.startsWith("~\\")) {
|
|
7614
|
-
return home ?
|
|
8620
|
+
return home ? path8.join(home, value.slice(2)) : value;
|
|
7615
8621
|
}
|
|
7616
8622
|
return value;
|
|
7617
8623
|
}
|
|
@@ -7643,8 +8649,8 @@ async function ensureSharedCapabilities(targetRoot) {
|
|
|
7643
8649
|
}
|
|
7644
8650
|
}
|
|
7645
8651
|
async function ensureSharedCapabilityItem(sourceRoot, targetRoot, item) {
|
|
7646
|
-
const source =
|
|
7647
|
-
const target =
|
|
8652
|
+
const source = path8.join(sourceRoot, item);
|
|
8653
|
+
const target = path8.join(targetRoot, item);
|
|
7648
8654
|
let sourceStat;
|
|
7649
8655
|
try {
|
|
7650
8656
|
sourceStat = await lstat(source);
|
|
@@ -7655,8 +8661,8 @@ async function ensureSharedCapabilityItem(sourceRoot, targetRoot, item) {
|
|
|
7655
8661
|
const targetStat = await lstat(target);
|
|
7656
8662
|
if (targetStat.isSymbolicLink()) {
|
|
7657
8663
|
const current = await readlink(target);
|
|
7658
|
-
const resolvedCurrent =
|
|
7659
|
-
const resolvedSource =
|
|
8664
|
+
const resolvedCurrent = path8.resolve(path8.dirname(target), current);
|
|
8665
|
+
const resolvedSource = path8.resolve(source);
|
|
7660
8666
|
if (resolvedCurrent === resolvedSource) return;
|
|
7661
8667
|
}
|
|
7662
8668
|
log.warn("shared Claude capability already exists; leaving untouched", {
|
|
@@ -7671,11 +8677,11 @@ async function ensureSharedCapabilityItem(sourceRoot, targetRoot, item) {
|
|
|
7671
8677
|
await symlink(source, target, type);
|
|
7672
8678
|
}
|
|
7673
8679
|
async function writeAccountWrapper(account, baseCliPath, configDir) {
|
|
7674
|
-
const cacheRoot =
|
|
8680
|
+
const cacheRoot = path8.join(
|
|
7675
8681
|
process.env.XDG_CACHE_HOME ?? expandHome("~/.cache"),
|
|
7676
8682
|
"opencode-claude-code-plugin"
|
|
7677
8683
|
);
|
|
7678
|
-
const wrapperPath =
|
|
8684
|
+
const wrapperPath = path8.join(cacheRoot, `claude-${account}`);
|
|
7679
8685
|
const suffix = `@${account}`;
|
|
7680
8686
|
await mkdir(cacheRoot, { recursive: true });
|
|
7681
8687
|
const script = `#!/usr/bin/env bash
|
|
@@ -7716,34 +8722,34 @@ function titleizeAccount(account) {
|
|
|
7716
8722
|
// src/cleanup-stale.ts
|
|
7717
8723
|
import {
|
|
7718
8724
|
existsSync as existsSync4,
|
|
7719
|
-
readFileSync as
|
|
8725
|
+
readFileSync as readFileSync5,
|
|
7720
8726
|
realpathSync,
|
|
7721
8727
|
rmSync as rmSync3,
|
|
7722
8728
|
writeFileSync as writeFileSync5
|
|
7723
8729
|
} from "fs";
|
|
7724
8730
|
import { homedir as homedir6 } from "os";
|
|
7725
|
-
import { join as
|
|
7726
|
-
import { fileURLToPath as
|
|
8731
|
+
import { join as join9, resolve as resolve4 } from "path";
|
|
8732
|
+
import { fileURLToPath as fileURLToPath3 } from "url";
|
|
7727
8733
|
var STALE_PACKAGE_NAME = "opencode-claude-code-plugin";
|
|
7728
8734
|
var SUSPECT_DESCRIPTION_TOKEN = "Claude Code";
|
|
7729
8735
|
var alreadyRan = false;
|
|
7730
8736
|
function candidateCacheRoots() {
|
|
7731
8737
|
const xdg = process.env.XDG_CACHE_HOME;
|
|
7732
8738
|
return [
|
|
7733
|
-
xdg ?
|
|
7734
|
-
|
|
7735
|
-
|
|
8739
|
+
xdg ? join9(xdg, "opencode") : null,
|
|
8740
|
+
join9(homedir6(), ".cache", "opencode"),
|
|
8741
|
+
join9(homedir6(), "Library", "Caches", "opencode")
|
|
7736
8742
|
].filter((p) => Boolean(p));
|
|
7737
8743
|
}
|
|
7738
8744
|
function userOpencodeJsonPath() {
|
|
7739
|
-
const xdgConfig = process.env.XDG_CONFIG_HOME ??
|
|
7740
|
-
return
|
|
8745
|
+
const xdgConfig = process.env.XDG_CONFIG_HOME ?? join9(homedir6(), ".config");
|
|
8746
|
+
return join9(xdgConfig, "opencode", "opencode.json");
|
|
7741
8747
|
}
|
|
7742
8748
|
function userIntendsToUseUnscoped() {
|
|
7743
8749
|
const cfg = userOpencodeJsonPath();
|
|
7744
8750
|
if (!existsSync4(cfg)) return false;
|
|
7745
8751
|
try {
|
|
7746
|
-
const json = JSON.parse(
|
|
8752
|
+
const json = JSON.parse(readFileSync5(cfg, "utf8"));
|
|
7747
8753
|
const plugins = json.plugin;
|
|
7748
8754
|
if (!Array.isArray(plugins)) return false;
|
|
7749
8755
|
return plugins.some(
|
|
@@ -7755,7 +8761,7 @@ function userIntendsToUseUnscoped() {
|
|
|
7755
8761
|
}
|
|
7756
8762
|
function ourLoadedDir() {
|
|
7757
8763
|
try {
|
|
7758
|
-
const filePath =
|
|
8764
|
+
const filePath = fileURLToPath3(import.meta.url);
|
|
7759
8765
|
return realpathSync(resolve4(filePath, "..", ".."));
|
|
7760
8766
|
} catch {
|
|
7761
8767
|
return null;
|
|
@@ -7780,7 +8786,7 @@ function cleanupStaleUnscopedInstall() {
|
|
|
7780
8786
|
}
|
|
7781
8787
|
function cleanupOne(cacheRoot, ourDir) {
|
|
7782
8788
|
if (!existsSync4(cacheRoot)) return;
|
|
7783
|
-
const stalePath =
|
|
8789
|
+
const stalePath = join9(cacheRoot, "node_modules", STALE_PACKAGE_NAME);
|
|
7784
8790
|
if (!existsSync4(stalePath)) return;
|
|
7785
8791
|
let realStalePath = stalePath;
|
|
7786
8792
|
try {
|
|
@@ -7788,11 +8794,11 @@ function cleanupOne(cacheRoot, ourDir) {
|
|
|
7788
8794
|
} catch {
|
|
7789
8795
|
}
|
|
7790
8796
|
if (ourDir && realStalePath === ourDir) return;
|
|
7791
|
-
const pkgJsonPath =
|
|
8797
|
+
const pkgJsonPath = join9(stalePath, "package.json");
|
|
7792
8798
|
if (!existsSync4(pkgJsonPath)) return;
|
|
7793
8799
|
let pkg = {};
|
|
7794
8800
|
try {
|
|
7795
|
-
pkg = JSON.parse(
|
|
8801
|
+
pkg = JSON.parse(readFileSync5(pkgJsonPath, "utf8"));
|
|
7796
8802
|
} catch {
|
|
7797
8803
|
return;
|
|
7798
8804
|
}
|
|
@@ -7808,10 +8814,10 @@ function cleanupOne(cacheRoot, ourDir) {
|
|
|
7808
8814
|
});
|
|
7809
8815
|
return;
|
|
7810
8816
|
}
|
|
7811
|
-
const cachePkgJson =
|
|
8817
|
+
const cachePkgJson = join9(cacheRoot, "package.json");
|
|
7812
8818
|
if (!existsSync4(cachePkgJson)) return;
|
|
7813
8819
|
try {
|
|
7814
|
-
const cfg = JSON.parse(
|
|
8820
|
+
const cfg = JSON.parse(readFileSync5(cachePkgJson, "utf8"));
|
|
7815
8821
|
if (cfg?.dependencies?.[STALE_PACKAGE_NAME]) {
|
|
7816
8822
|
delete cfg.dependencies[STALE_PACKAGE_NAME];
|
|
7817
8823
|
writeFileSync5(cachePkgJson, JSON.stringify(cfg, null, 2) + "\n");
|
|
@@ -7824,131 +8830,6 @@ function cleanupOne(cacheRoot, ourDir) {
|
|
|
7824
8830
|
}
|
|
7825
8831
|
}
|
|
7826
8832
|
|
|
7827
|
-
// src/startup-diagnostics.ts
|
|
7828
|
-
import { execFile as execFile2 } from "child_process";
|
|
7829
|
-
import * as fs6 from "fs";
|
|
7830
|
-
import * as path8 from "path";
|
|
7831
|
-
import { promisify as promisify2 } from "util";
|
|
7832
|
-
import { fileURLToPath as fileURLToPath3 } from "url";
|
|
7833
|
-
var cachedPluginVersion;
|
|
7834
|
-
function pluginVersion() {
|
|
7835
|
-
if (cachedPluginVersion) return cachedPluginVersion;
|
|
7836
|
-
try {
|
|
7837
|
-
const here = path8.dirname(fileURLToPath3(import.meta.url));
|
|
7838
|
-
const raw = fs6.readFileSync(path8.join(here, "..", "package.json"), "utf8");
|
|
7839
|
-
const version = JSON.parse(raw).version;
|
|
7840
|
-
cachedPluginVersion = typeof version === "string" ? version : "unknown";
|
|
7841
|
-
} catch {
|
|
7842
|
-
cachedPluginVersion = "unknown";
|
|
7843
|
-
}
|
|
7844
|
-
return cachedPluginVersion;
|
|
7845
|
-
}
|
|
7846
|
-
function pickOpencodeVersion(input) {
|
|
7847
|
-
if (!input || typeof input !== "object") return void 0;
|
|
7848
|
-
const app = input.app;
|
|
7849
|
-
if (app && typeof app === "object") {
|
|
7850
|
-
const version = app.version;
|
|
7851
|
-
if (typeof version === "string" && version.length > 0) return version;
|
|
7852
|
-
}
|
|
7853
|
-
const direct = input.version;
|
|
7854
|
-
if (typeof direct === "string" && direct.length > 0) return direct;
|
|
7855
|
-
return void 0;
|
|
7856
|
-
}
|
|
7857
|
-
var execFileAsync2 = promisify2(execFile2);
|
|
7858
|
-
var opencodeVersionProbe;
|
|
7859
|
-
function detectOpencodeVersion(execPath = process.execPath) {
|
|
7860
|
-
if (opencodeVersionProbe) return opencodeVersionProbe;
|
|
7861
|
-
opencodeVersionProbe = (async () => {
|
|
7862
|
-
if (!path8.basename(execPath).toLowerCase().includes("opencode")) {
|
|
7863
|
-
log.debug("skipping opencode version probe: execPath is not opencode", { execPath });
|
|
7864
|
-
return void 0;
|
|
7865
|
-
}
|
|
7866
|
-
try {
|
|
7867
|
-
const { stdout } = await execFileAsync2(execPath, ["--version"], { timeout: 5e3 });
|
|
7868
|
-
const match = /\d+\.\d+\.\d+\S*/.exec(stdout.trim());
|
|
7869
|
-
return match ? match[0] : void 0;
|
|
7870
|
-
} catch (err) {
|
|
7871
|
-
log.debug("opencode version probe failed", {
|
|
7872
|
-
execPath,
|
|
7873
|
-
error: err instanceof Error ? err.message : String(err)
|
|
7874
|
-
});
|
|
7875
|
-
return void 0;
|
|
7876
|
-
}
|
|
7877
|
-
})();
|
|
7878
|
-
return opencodeVersionProbe;
|
|
7879
|
-
}
|
|
7880
|
-
function describeSpawnCwd(configured, live = process.cwd(), captured = getOpencodeProjectDirectory()) {
|
|
7881
|
-
if (typeof configured === "string" && configured.length > 0) {
|
|
7882
|
-
return { resolved: configured, source: "configured" };
|
|
7883
|
-
}
|
|
7884
|
-
if (isUsableDirectory(live)) return { resolved: live, source: "process" };
|
|
7885
|
-
if (isUsableDirectory(captured)) return { resolved: captured, source: "captured" };
|
|
7886
|
-
return { resolved: live, source: "unresolved" };
|
|
7887
|
-
}
|
|
7888
|
-
function stringList(value) {
|
|
7889
|
-
if (!Array.isArray(value)) return [];
|
|
7890
|
-
return value.filter((entry) => typeof entry === "string");
|
|
7891
|
-
}
|
|
7892
|
-
function firstOption(providers, key) {
|
|
7893
|
-
for (const entry of Object.values(providers)) {
|
|
7894
|
-
const value = entry?.options?.[key];
|
|
7895
|
-
if (value !== void 0) return value;
|
|
7896
|
-
}
|
|
7897
|
-
return void 0;
|
|
7898
|
-
}
|
|
7899
|
-
function collectStartupDiagnostics(providers, opencodeVersion) {
|
|
7900
|
-
const accounts = [];
|
|
7901
|
-
for (const entry of Object.values(providers)) {
|
|
7902
|
-
const account = entry?.options?.account;
|
|
7903
|
-
if (typeof account === "string" && account.length > 0) accounts.push(account);
|
|
7904
|
-
}
|
|
7905
|
-
const cwd = describeSpawnCwd(firstOption(providers, "cwd"));
|
|
7906
|
-
let mcpServers = [];
|
|
7907
|
-
try {
|
|
7908
|
-
mcpServers = mergeOpencodeMcp(cwd.resolved).enabledServerNames;
|
|
7909
|
-
} catch (err) {
|
|
7910
|
-
log.debug("startup diagnostics could not read MCP config", {
|
|
7911
|
-
error: err instanceof Error ? err.message : String(err)
|
|
7912
|
-
});
|
|
7913
|
-
}
|
|
7914
|
-
return {
|
|
7915
|
-
plugin: pluginVersion(),
|
|
7916
|
-
opencode: opencodeVersion ?? process.env.OPENCODE_VERSION ?? "unknown",
|
|
7917
|
-
claudeCliPath: String(firstOption(providers, "cliPath") ?? "claude"),
|
|
7918
|
-
cwd,
|
|
7919
|
-
providers: Object.keys(providers),
|
|
7920
|
-
accounts,
|
|
7921
|
-
proxyTools: stringList(firstOption(providers, "proxyTools")),
|
|
7922
|
-
mcpServers,
|
|
7923
|
-
interactiveTransport: firstOption(providers, "interactive") === true || process.env.CLAUDE_CODE_INTERACTIVE_TRANSPORT === "1",
|
|
7924
|
-
planModeQuestion: firstOption(providers, "planModeQuestion") === true,
|
|
7925
|
-
anthropicApiKeyInEnv: Boolean(
|
|
7926
|
-
process.env.ANTHROPIC_API_KEY || process.env.ANTHROPIC_AUTH_TOKEN
|
|
7927
|
-
)
|
|
7928
|
-
};
|
|
7929
|
-
}
|
|
7930
|
-
var logged = false;
|
|
7931
|
-
function logStartupDiagnostics(providers, opencodeVersion) {
|
|
7932
|
-
if (logged) return;
|
|
7933
|
-
logged = true;
|
|
7934
|
-
void (async () => {
|
|
7935
|
-
try {
|
|
7936
|
-
const version = opencodeVersion ?? process.env.OPENCODE_VERSION ?? await detectOpencodeVersion();
|
|
7937
|
-
const { claudeCliPath, ...rest } = collectStartupDiagnostics(providers, version);
|
|
7938
|
-
const cli = await detectCliVersion(claudeCliPath);
|
|
7939
|
-
const diagnostics = {
|
|
7940
|
-
...rest,
|
|
7941
|
-
claudeCli: { path: claudeCliPath, version: cli?.raw ?? "not detected" }
|
|
7942
|
-
};
|
|
7943
|
-
log.notice("claude-code plugin ready", { ...diagnostics });
|
|
7944
|
-
} catch (err) {
|
|
7945
|
-
log.debug("startup diagnostics failed", {
|
|
7946
|
-
error: err instanceof Error ? err.message : String(err)
|
|
7947
|
-
});
|
|
7948
|
-
}
|
|
7949
|
-
})();
|
|
7950
|
-
}
|
|
7951
|
-
|
|
7952
8833
|
// src/index.ts
|
|
7953
8834
|
function pickOpencodeDirectory(input) {
|
|
7954
8835
|
if (!input || typeof input !== "object") return void 0;
|
|
@@ -7975,6 +8856,15 @@ function registerSideQuestionCommand(config) {
|
|
|
7975
8856
|
};
|
|
7976
8857
|
return true;
|
|
7977
8858
|
}
|
|
8859
|
+
function registerDoctorCommand(config) {
|
|
8860
|
+
config.command ??= {};
|
|
8861
|
+
if (config.command[DOCTOR_COMMAND]) return false;
|
|
8862
|
+
config.command[DOCTOR_COMMAND] = {
|
|
8863
|
+
template: `/${DOCTOR_COMMAND} $ARGUMENTS`,
|
|
8864
|
+
description: "Report what the Claude Code plugin sees: versions, cwd, live processes, pending proxy calls"
|
|
8865
|
+
};
|
|
8866
|
+
return true;
|
|
8867
|
+
}
|
|
7978
8868
|
var ownsSideQuestionCommand = false;
|
|
7979
8869
|
function warnIfAnthropicApiKey(ignore) {
|
|
7980
8870
|
if (warnedAnthropicApiKey) return;
|
|
@@ -8045,6 +8935,7 @@ function createClaudeCode(settings = {}) {
|
|
|
8045
8935
|
ignoreAnthropicApiKey: settings.ignoreAnthropicApiKey,
|
|
8046
8936
|
idleProcessTimeoutMs: settings.idleProcessTimeoutMs,
|
|
8047
8937
|
bridgeOpencodeSkills: settings.bridgeOpencodeSkills === true,
|
|
8938
|
+
turnStats: settings.turnStats === true,
|
|
8048
8939
|
interactive: settings.interactive,
|
|
8049
8940
|
interactiveBypass: settings.interactiveBypass,
|
|
8050
8941
|
interactiveAllowTools: settings.interactiveAllowTools,
|
|
@@ -8234,8 +9125,16 @@ async function buildAgentRegistry(config) {
|
|
|
8234
9125
|
defaultSubagentModel: getDefaultSubagentModel()
|
|
8235
9126
|
});
|
|
8236
9127
|
}
|
|
9128
|
+
function extractDeletedSessionId(event) {
|
|
9129
|
+
const payload = event?.payload ?? event;
|
|
9130
|
+
if (!payload || payload.type !== "session.deleted") return void 0;
|
|
9131
|
+
const properties = payload.properties;
|
|
9132
|
+
const id = properties?.info?.id;
|
|
9133
|
+
return typeof id === "string" && id.length > 0 ? id : void 0;
|
|
9134
|
+
}
|
|
8237
9135
|
var server = async (input) => {
|
|
8238
9136
|
cleanupStaleUnscopedInstall();
|
|
9137
|
+
ensureProcessExitCleanup();
|
|
8239
9138
|
const opencodeVersion = pickOpencodeVersion(input);
|
|
8240
9139
|
if (input && typeof input === "object" && "client" in input) {
|
|
8241
9140
|
setOpencodeClient(input.client);
|
|
@@ -8244,6 +9143,7 @@ var server = async (input) => {
|
|
|
8244
9143
|
return {
|
|
8245
9144
|
config: async (config) => {
|
|
8246
9145
|
if (registerSideQuestionCommand(config)) ownsSideQuestionCommand = true;
|
|
9146
|
+
registerDoctorCommand(config);
|
|
8247
9147
|
registerBundledSkillPath(config);
|
|
8248
9148
|
config.provider ??= {};
|
|
8249
9149
|
await buildAgentRegistry(config);
|
|
@@ -8269,10 +9169,21 @@ var server = async (input) => {
|
|
|
8269
9169
|
opencodeVersion
|
|
8270
9170
|
);
|
|
8271
9171
|
},
|
|
8272
|
-
//
|
|
8273
|
-
// hot-reload check in `claude-code-language-model.ts`,
|
|
8274
|
-
// claude safely between turns
|
|
8275
|
-
// an in-flight stream and abort the user's
|
|
9172
|
+
// Only `session.deleted` is acted on. MCP config drift is still detected
|
|
9173
|
+
// at turn start by the hot-reload check in `claude-code-language-model.ts`,
|
|
9174
|
+
// which respawns claude safely between turns, and eviction on
|
|
9175
|
+
// `global.disposed` would kill an in-flight stream and abort the user's
|
|
9176
|
+
// current turn. A deleted session has no turn left to abort, and its
|
|
9177
|
+
// `claude` child would otherwise linger until the idle timer or LRU
|
|
9178
|
+
// pressure took it, with its session id kept for a resume that never comes.
|
|
9179
|
+
event: async ({ event }) => {
|
|
9180
|
+
const sessionID = extractDeletedSessionId(event);
|
|
9181
|
+
if (!sessionID) return;
|
|
9182
|
+
const released = deleteActiveProcessesForSession(sessionID);
|
|
9183
|
+
if (released.length > 0) {
|
|
9184
|
+
log.info("released claude state for deleted session", { sessionID, released });
|
|
9185
|
+
}
|
|
9186
|
+
},
|
|
8276
9187
|
provider: {
|
|
8277
9188
|
id: PROVIDER_ID2,
|
|
8278
9189
|
models: async (provider) => defaultModelsForProvider(provider.models)
|
|
@@ -8327,8 +9238,10 @@ export {
|
|
|
8327
9238
|
createClaudeCode,
|
|
8328
9239
|
index_default as default,
|
|
8329
9240
|
defaultModels,
|
|
9241
|
+
extractDeletedSessionId,
|
|
8330
9242
|
getAgentRegistry,
|
|
8331
9243
|
getDefaultSubagentModel,
|
|
9244
|
+
registerDoctorCommand,
|
|
8332
9245
|
registerSideQuestionCommand,
|
|
8333
9246
|
resolveAgentModel,
|
|
8334
9247
|
warnIfPlanModeCannotExit
|