@khalilgharbaoui/opencode-claude-code-plugin 0.19.0 → 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 +38 -18
- package/dist/index.d.ts +29 -14
- package/dist/index.js +211 -73
- package/dist/index.js.map +1 -1
- package/package.json +2 -2
- package/skills/claude-code-plugin/SKILL.md +34 -16
package/dist/index.js
CHANGED
|
@@ -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,
|
|
@@ -1418,7 +1448,7 @@ function resolvePendingProxyCallById(toolCallId, result) {
|
|
|
1418
1448
|
if (!pending) return false;
|
|
1419
1449
|
pendingByCallId.delete(toolCallId);
|
|
1420
1450
|
indexRemove(pending.sessionKey, toolCallId);
|
|
1421
|
-
clearTimeout(pending.timer);
|
|
1451
|
+
if (pending.timer) clearTimeout(pending.timer);
|
|
1422
1452
|
pending.resolve(result);
|
|
1423
1453
|
log.info("resolved pending proxy call", {
|
|
1424
1454
|
sessionKey: pending.sessionKey,
|
|
@@ -1432,7 +1462,7 @@ function rejectPendingProxyCallById(toolCallId, error) {
|
|
|
1432
1462
|
if (!pending) return false;
|
|
1433
1463
|
pendingByCallId.delete(toolCallId);
|
|
1434
1464
|
indexRemove(pending.sessionKey, toolCallId);
|
|
1435
|
-
clearTimeout(pending.timer);
|
|
1465
|
+
if (pending.timer) clearTimeout(pending.timer);
|
|
1436
1466
|
pending.reject(error);
|
|
1437
1467
|
log.notice("rejected pending proxy call", {
|
|
1438
1468
|
sessionKey: pending.sessionKey,
|
|
@@ -1864,9 +1894,14 @@ var activeProcesses = /* @__PURE__ */ new Map();
|
|
|
1864
1894
|
var claudeSessions = /* @__PURE__ */ new Map();
|
|
1865
1895
|
var idleEvictionTimers = /* @__PURE__ */ new Map();
|
|
1866
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
|
+
}
|
|
1867
1901
|
var MAX_ACTIVE_PROCESSES = 16;
|
|
1868
1902
|
var PROCESS_EXIT_TIMEOUT_MS = 1500;
|
|
1869
1903
|
var PROCESS_FORCE_EXIT_TIMEOUT_MS = 500;
|
|
1904
|
+
var CHILD_EXITED_MESSAGE = "Claude CLI subprocess closed before pending tool calls were resolved";
|
|
1870
1905
|
function envFlagEnabled(value) {
|
|
1871
1906
|
if (value === void 0) return false;
|
|
1872
1907
|
const normalized = value.trim().toLowerCase();
|
|
@@ -2016,6 +2051,11 @@ function scheduleIdleProcessEviction(key, timeoutMs) {
|
|
|
2016
2051
|
const timer = setTimeout(() => {
|
|
2017
2052
|
idleEvictionTimers.delete(key);
|
|
2018
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
|
+
}
|
|
2019
2059
|
log.info("evicting idle claude process", { sessionKey: key, timeoutMs });
|
|
2020
2060
|
deleteActiveProcess(key);
|
|
2021
2061
|
}, timeoutMs);
|
|
@@ -2027,9 +2067,44 @@ function detachActiveProcess(key) {
|
|
|
2027
2067
|
const ap = activeProcesses.get(key);
|
|
2028
2068
|
if (!ap) return void 0;
|
|
2029
2069
|
activeProcesses.delete(key);
|
|
2030
|
-
|
|
2070
|
+
if (ap.proxyServer) {
|
|
2071
|
+
void ap.proxyServer.close();
|
|
2072
|
+
rejectAllPendingProxyCallsForSession(key, new Error(SERVER_CLOSED_MESSAGE));
|
|
2073
|
+
}
|
|
2031
2074
|
return ap;
|
|
2032
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
|
+
}
|
|
2033
2108
|
function deleteActiveProcess(key) {
|
|
2034
2109
|
const ap = detachActiveProcess(key);
|
|
2035
2110
|
ap?.proc.kill();
|
|
@@ -2178,6 +2253,12 @@ function spawnClaudeProcess(cliPath, cliArgs, cwd, sessionKey2, proxyServer, mcp
|
|
|
2178
2253
|
if (ownsSessionKey) {
|
|
2179
2254
|
cancelIdleProcessEviction(sessionKey2);
|
|
2180
2255
|
activeProcesses.delete(sessionKey2);
|
|
2256
|
+
if (getPendingProxyCalls(sessionKey2).length > 0) {
|
|
2257
|
+
rejectAllPendingProxyCallsForSession(
|
|
2258
|
+
sessionKey2,
|
|
2259
|
+
new Error(CHILD_EXITED_MESSAGE)
|
|
2260
|
+
);
|
|
2261
|
+
}
|
|
2181
2262
|
}
|
|
2182
2263
|
if (ownsSessionKey && code !== 0 && code !== null) {
|
|
2183
2264
|
log.info("process exited with error, clearing session", {
|
|
@@ -2218,6 +2299,7 @@ function appendResumeIfNeeded(sessionKey2, cliArgs) {
|
|
|
2218
2299
|
function respawnActiveProcess(sessionKey2, cliPath, cliArgs, cwd, ignoreAnthropicApiKey) {
|
|
2219
2300
|
const old = activeProcesses.get(sessionKey2);
|
|
2220
2301
|
if (!old) return void 0;
|
|
2302
|
+
const turnWasInFlight = isTurnInFlight(old);
|
|
2221
2303
|
activeProcesses.delete(sessionKey2);
|
|
2222
2304
|
old.proc.removeAllListeners("exit");
|
|
2223
2305
|
try {
|
|
@@ -2237,6 +2319,7 @@ function respawnActiveProcess(sessionKey2, cliPath, cliArgs, cwd, ignoreAnthropi
|
|
|
2237
2319
|
);
|
|
2238
2320
|
replacement.pendingProxyCompletions = old.pendingProxyCompletions;
|
|
2239
2321
|
delete old.pendingProxyCompletions;
|
|
2322
|
+
if (turnWasInFlight) noteTurnStarted(replacement);
|
|
2240
2323
|
return replacement;
|
|
2241
2324
|
}
|
|
2242
2325
|
function buildCliArgs(opts) {
|
|
@@ -3532,10 +3615,9 @@ function formatDoctorReport(report) {
|
|
|
3532
3615
|
lines.push("| tool | call id | age | deadline |");
|
|
3533
3616
|
lines.push("|---|---|---|---|");
|
|
3534
3617
|
for (const call of report.pendingCalls) {
|
|
3618
|
+
const deadline = call.deadlineMs > 0 ? formatAge(call.deadlineMs) : "none";
|
|
3535
3619
|
lines.push(
|
|
3536
|
-
`| ${call.toolName} | \`${call.toolCallId}\` | ${formatAge(call.ageMs)} | ${
|
|
3537
|
-
call.deadlineMs
|
|
3538
|
-
)} |`
|
|
3620
|
+
`| ${call.toolName} | \`${call.toolCallId}\` | ${formatAge(call.ageMs)} | ${deadline} |`
|
|
3539
3621
|
);
|
|
3540
3622
|
}
|
|
3541
3623
|
}
|
|
@@ -5059,7 +5141,7 @@ ${text}`
|
|
|
5059
5141
|
}
|
|
5060
5142
|
return parts.join("\n\n");
|
|
5061
5143
|
}
|
|
5062
|
-
function
|
|
5144
|
+
function interactiveExtraArgs(opts) {
|
|
5063
5145
|
const extraArgs = [];
|
|
5064
5146
|
if (opts.mcpConfigPaths && opts.mcpConfigPaths.length > 0) {
|
|
5065
5147
|
extraArgs.push(
|
|
@@ -5068,6 +5150,9 @@ function spawnInteractiveProcess(opts) {
|
|
|
5068
5150
|
"--strict-mcp-config"
|
|
5069
5151
|
);
|
|
5070
5152
|
}
|
|
5153
|
+
for (const dir of opts.pluginDirs ?? []) {
|
|
5154
|
+
extraArgs.push("--plugin-dir", dir);
|
|
5155
|
+
}
|
|
5071
5156
|
const flagSettings = {};
|
|
5072
5157
|
if (opts.permissionsAllow && opts.permissionsAllow.length > 0) {
|
|
5073
5158
|
flagSettings.permissions = { allow: opts.permissionsAllow };
|
|
@@ -5088,6 +5173,10 @@ function spawnInteractiveProcess(opts) {
|
|
|
5088
5173
|
if (opts.systemPromptFile) {
|
|
5089
5174
|
extraArgs.push("--append-system-prompt-file", opts.systemPromptFile);
|
|
5090
5175
|
}
|
|
5176
|
+
return extraArgs;
|
|
5177
|
+
}
|
|
5178
|
+
function spawnInteractiveProcess(opts) {
|
|
5179
|
+
const extraArgs = interactiveExtraArgs(opts);
|
|
5091
5180
|
const session = new ClaudeSession({
|
|
5092
5181
|
cwd: opts.cwd,
|
|
5093
5182
|
cliPath: opts.cliPath,
|
|
@@ -6274,6 +6363,11 @@ var ClaudeCodeLanguageModel = class {
|
|
|
6274
6363
|
{ compressEnabled: false, compressionSummary: getCompressionSummary(sk) }
|
|
6275
6364
|
);
|
|
6276
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
|
+
});
|
|
6277
6371
|
const cliArgs = buildCliArgs({
|
|
6278
6372
|
sessionKey: sk,
|
|
6279
6373
|
skipPermissions: this.config.skipPermissions !== false,
|
|
@@ -6284,6 +6378,7 @@ var ClaudeCodeLanguageModel = class {
|
|
|
6284
6378
|
strictMcpConfig: this.config.strictMcpConfig,
|
|
6285
6379
|
disallowedTools: this.config.webSearch === "disabled" ? ["WebSearch"] : void 0,
|
|
6286
6380
|
appendSystemPromptFile: systemPromptFile,
|
|
6381
|
+
pluginDirs: skillPluginDirs,
|
|
6287
6382
|
...this.thinkingCliOptions(),
|
|
6288
6383
|
fastMode,
|
|
6289
6384
|
cliVersion
|
|
@@ -6883,6 +6978,11 @@ ${plan}
|
|
|
6883
6978
|
"interactiveBypass ignored: Claude Code prompts for bypassPermissions confirmation in the interactive TUI"
|
|
6884
6979
|
);
|
|
6885
6980
|
}
|
|
6981
|
+
const skillPluginDirs = await resolveSkillPluginDirs({
|
|
6982
|
+
cwd,
|
|
6983
|
+
cliPath,
|
|
6984
|
+
enabled: self.config.bridgeOpencodeSkills === true
|
|
6985
|
+
});
|
|
6886
6986
|
const ap = spawnInteractiveProcess({
|
|
6887
6987
|
cwd,
|
|
6888
6988
|
cliPath,
|
|
@@ -6890,6 +6990,7 @@ ${plan}
|
|
|
6890
6990
|
model: spawnModelId,
|
|
6891
6991
|
fastMode,
|
|
6892
6992
|
mcpConfigPaths: mcp.paths,
|
|
6993
|
+
pluginDirs: skillPluginDirs,
|
|
6893
6994
|
permissionsAllow: allow,
|
|
6894
6995
|
systemPromptFile,
|
|
6895
6996
|
ignoreAnthropicApiKey: self.config.ignoreAnthropicApiKey,
|
|
@@ -7221,6 +7322,7 @@ ${plan}
|
|
|
7221
7322
|
if (entries.length === 0) return false;
|
|
7222
7323
|
endTextBlock();
|
|
7223
7324
|
watchdogMessage = makeLateProxyResultMessage(entries);
|
|
7325
|
+
if (activeProcess) noteTurnStarted(activeProcess);
|
|
7224
7326
|
proc.stdin.write(watchdogMessage + "\n");
|
|
7225
7327
|
for (const { call } of entries) pending.delete(call.toolCallId);
|
|
7226
7328
|
log.warn("delivering proxy results after interrupted continuation", {
|
|
@@ -7482,7 +7584,10 @@ ${plan}
|
|
|
7482
7584
|
controllerClosed = true;
|
|
7483
7585
|
cleanupTurn();
|
|
7484
7586
|
if (!useInteractive && !compactionMode) {
|
|
7485
|
-
scheduleIdleProcessEviction(
|
|
7587
|
+
scheduleIdleProcessEviction(
|
|
7588
|
+
sk,
|
|
7589
|
+
resolveIdleProcessTimeoutMs(self.config.idleProcessTimeoutMs)
|
|
7590
|
+
);
|
|
7486
7591
|
}
|
|
7487
7592
|
try {
|
|
7488
7593
|
controller.close();
|
|
@@ -8277,7 +8382,7 @@ ${plan}
|
|
|
8277
8382
|
if (outer.session_id) setClaudeSessionId(sk, outer.session_id);
|
|
8278
8383
|
if (msg.is_error && msg.result) text = msg.result;
|
|
8279
8384
|
}
|
|
8280
|
-
if (text) controller.enqueue({ type: "text-delta", id: startTextBlock(), delta: text });
|
|
8385
|
+
if (text) controller.enqueue({ type: "text-delta", id: currentTextId ?? startTextBlock(), delta: text });
|
|
8281
8386
|
} catch {
|
|
8282
8387
|
}
|
|
8283
8388
|
}
|
|
@@ -8334,9 +8439,25 @@ ${plan}
|
|
|
8334
8439
|
});
|
|
8335
8440
|
proc.on("error", procErrorHandler);
|
|
8336
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
|
+
};
|
|
8337
8447
|
options.abortSignal.addEventListener("abort", () => {
|
|
8338
8448
|
autoContinueState.aborted = true;
|
|
8339
|
-
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
|
+
}
|
|
8340
8461
|
if (activeProcess) {
|
|
8341
8462
|
void interruptTurn(activeProcess).then((idle) => {
|
|
8342
8463
|
log.info("interrupt sent for aborted turn", { sk, idle });
|
|
@@ -8347,15 +8468,9 @@ ${plan}
|
|
|
8347
8468
|
"abort signal received before content, closing stream immediately",
|
|
8348
8469
|
{ cwd }
|
|
8349
8470
|
);
|
|
8350
|
-
|
|
8351
|
-
|
|
8352
|
-
|
|
8353
|
-
new Error(
|
|
8354
|
-
"Provider stream was aborted before pending proxy calls were emitted"
|
|
8355
|
-
)
|
|
8356
|
-
);
|
|
8357
|
-
drainBuffer.length = 0;
|
|
8358
|
-
}
|
|
8471
|
+
releaseAbandonedProxyCalls(
|
|
8472
|
+
"Provider stream was aborted before pending proxy calls were emitted"
|
|
8473
|
+
);
|
|
8359
8474
|
controllerClosed = true;
|
|
8360
8475
|
cleanupTurn();
|
|
8361
8476
|
try {
|
|
@@ -8368,6 +8483,9 @@ ${plan}
|
|
|
8368
8483
|
"abort signal received mid-turn, starting grace period",
|
|
8369
8484
|
{ cwd }
|
|
8370
8485
|
);
|
|
8486
|
+
releaseAbandonedProxyCalls(
|
|
8487
|
+
"Provider stream was aborted while proxy tool calls were pending"
|
|
8488
|
+
);
|
|
8371
8489
|
startResultFallback(5e3);
|
|
8372
8490
|
});
|
|
8373
8491
|
}
|
|
@@ -9007,8 +9125,16 @@ async function buildAgentRegistry(config) {
|
|
|
9007
9125
|
defaultSubagentModel: getDefaultSubagentModel()
|
|
9008
9126
|
});
|
|
9009
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
|
+
}
|
|
9010
9135
|
var server = async (input) => {
|
|
9011
9136
|
cleanupStaleUnscopedInstall();
|
|
9137
|
+
ensureProcessExitCleanup();
|
|
9012
9138
|
const opencodeVersion = pickOpencodeVersion(input);
|
|
9013
9139
|
if (input && typeof input === "object" && "client" in input) {
|
|
9014
9140
|
setOpencodeClient(input.client);
|
|
@@ -9043,10 +9169,21 @@ var server = async (input) => {
|
|
|
9043
9169
|
opencodeVersion
|
|
9044
9170
|
);
|
|
9045
9171
|
},
|
|
9046
|
-
//
|
|
9047
|
-
// hot-reload check in `claude-code-language-model.ts`,
|
|
9048
|
-
// claude safely between turns
|
|
9049
|
-
// 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
|
+
},
|
|
9050
9187
|
provider: {
|
|
9051
9188
|
id: PROVIDER_ID2,
|
|
9052
9189
|
models: async (provider) => defaultModelsForProvider(provider.models)
|
|
@@ -9101,6 +9238,7 @@ export {
|
|
|
9101
9238
|
createClaudeCode,
|
|
9102
9239
|
index_default as default,
|
|
9103
9240
|
defaultModels,
|
|
9241
|
+
extractDeletedSessionId,
|
|
9104
9242
|
getAgentRegistry,
|
|
9105
9243
|
getDefaultSubagentModel,
|
|
9106
9244
|
registerDoctorCommand,
|