@khalilgharbaoui/opencode-claude-code-plugin 0.4.1 → 0.4.3
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/index.js +233 -64
- package/dist/index.js.map +1 -1
- package/package.json +2 -2
package/dist/index.js
CHANGED
|
@@ -577,6 +577,17 @@ function dotOpencodeDirs(cwd, worktree) {
|
|
|
577
577
|
if (envDir && dirExists(envDir)) push(envDir);
|
|
578
578
|
return dirs;
|
|
579
579
|
}
|
|
580
|
+
function substituteEnvPlaceholders(source) {
|
|
581
|
+
const out = {};
|
|
582
|
+
for (const [k, v] of Object.entries(source)) {
|
|
583
|
+
if (typeof v !== "string") continue;
|
|
584
|
+
out[k] = v.replace(/\{env:([A-Za-z_][A-Za-z0-9_]*)\}/g, (_match, name) => {
|
|
585
|
+
const resolved = process.env[name];
|
|
586
|
+
return typeof resolved === "string" ? resolved : "";
|
|
587
|
+
});
|
|
588
|
+
}
|
|
589
|
+
return out;
|
|
590
|
+
}
|
|
580
591
|
function translateServer(name, spec) {
|
|
581
592
|
if (spec.enabled === false) return null;
|
|
582
593
|
const type = spec.type;
|
|
@@ -592,7 +603,9 @@ function translateServer(name, spec) {
|
|
|
592
603
|
};
|
|
593
604
|
if (cmd.length > 1) out.args = cmd.slice(1).map((s) => String(s));
|
|
594
605
|
if (spec.environment && typeof spec.environment === "object") {
|
|
595
|
-
out.env =
|
|
606
|
+
out.env = substituteEnvPlaceholders(
|
|
607
|
+
spec.environment
|
|
608
|
+
);
|
|
596
609
|
}
|
|
597
610
|
return out;
|
|
598
611
|
}
|
|
@@ -606,7 +619,9 @@ function translateServer(name, spec) {
|
|
|
606
619
|
url: spec.url
|
|
607
620
|
};
|
|
608
621
|
if (spec.headers && typeof spec.headers === "object") {
|
|
609
|
-
out.headers =
|
|
622
|
+
out.headers = substituteEnvPlaceholders(
|
|
623
|
+
spec.headers
|
|
624
|
+
);
|
|
610
625
|
}
|
|
611
626
|
return out;
|
|
612
627
|
}
|
|
@@ -1319,52 +1334,54 @@ function writeJson(res, body) {
|
|
|
1319
1334
|
|
|
1320
1335
|
// src/proxy-broker.ts
|
|
1321
1336
|
import { EventEmitter as EventEmitter3 } from "events";
|
|
1322
|
-
var
|
|
1337
|
+
var pendingByCallId = /* @__PURE__ */ new Map();
|
|
1338
|
+
var callIdsBySession = /* @__PURE__ */ new Map();
|
|
1323
1339
|
var emitter = new EventEmitter3();
|
|
1324
1340
|
var PENDING_PROXY_CALL_TIMEOUT_MS = 10 * 60 * 1e3;
|
|
1325
1341
|
function eventName(sessionKey2) {
|
|
1326
1342
|
return `pending:${sessionKey2}`;
|
|
1327
1343
|
}
|
|
1344
|
+
function indexAdd(sessionKey2, callId) {
|
|
1345
|
+
let s = callIdsBySession.get(sessionKey2);
|
|
1346
|
+
if (!s) {
|
|
1347
|
+
s = /* @__PURE__ */ new Set();
|
|
1348
|
+
callIdsBySession.set(sessionKey2, s);
|
|
1349
|
+
}
|
|
1350
|
+
s.add(callId);
|
|
1351
|
+
}
|
|
1352
|
+
function indexRemove(sessionKey2, callId) {
|
|
1353
|
+
const s = callIdsBySession.get(sessionKey2);
|
|
1354
|
+
if (!s) return;
|
|
1355
|
+
s.delete(callId);
|
|
1356
|
+
if (s.size === 0) callIdsBySession.delete(sessionKey2);
|
|
1357
|
+
}
|
|
1328
1358
|
function onPendingProxyCall(sessionKey2, handler) {
|
|
1329
1359
|
const name = eventName(sessionKey2);
|
|
1330
1360
|
emitter.on(name, handler);
|
|
1331
1361
|
return () => emitter.off(name, handler);
|
|
1332
1362
|
}
|
|
1333
1363
|
function queuePendingProxyCall(sessionKey2, call) {
|
|
1334
|
-
const
|
|
1335
|
-
if (
|
|
1336
|
-
|
|
1337
|
-
|
|
1338
|
-
|
|
1339
|
-
);
|
|
1340
|
-
log.warn("rejected overlapping proxy call", {
|
|
1341
|
-
sessionKey: sessionKey2,
|
|
1342
|
-
existingToolCallId: existing.toolCallId,
|
|
1343
|
-
existingToolName: existing.toolName,
|
|
1344
|
-
toolCallId: call.id,
|
|
1345
|
-
toolName: call.toolName
|
|
1346
|
-
});
|
|
1347
|
-
return existing;
|
|
1348
|
-
}
|
|
1349
|
-
clearTimeout(existing.timer);
|
|
1350
|
-
existing.reject(
|
|
1351
|
-
new Error(
|
|
1352
|
-
`Stale proxy tool call expired after ${PENDING_PROXY_CALL_TIMEOUT_MS}ms for ${sessionKey2}`
|
|
1353
|
-
)
|
|
1364
|
+
const previous = pendingByCallId.get(call.id);
|
|
1365
|
+
if (previous) {
|
|
1366
|
+
clearTimeout(previous.timer);
|
|
1367
|
+
previous.reject(
|
|
1368
|
+
new Error(`Replaced pending proxy call ${call.id} with a fresh one`)
|
|
1354
1369
|
);
|
|
1355
|
-
|
|
1370
|
+
pendingByCallId.delete(call.id);
|
|
1371
|
+
indexRemove(previous.sessionKey, call.id);
|
|
1356
1372
|
}
|
|
1357
1373
|
const timer = setTimeout(() => {
|
|
1358
|
-
const current =
|
|
1359
|
-
if (!current
|
|
1360
|
-
|
|
1374
|
+
const current = pendingByCallId.get(call.id);
|
|
1375
|
+
if (!current) return;
|
|
1376
|
+
pendingByCallId.delete(call.id);
|
|
1377
|
+
indexRemove(current.sessionKey, call.id);
|
|
1361
1378
|
current.reject(
|
|
1362
1379
|
new Error(
|
|
1363
1380
|
`Proxy tool call '${call.toolName}' timed out after ${PENDING_PROXY_CALL_TIMEOUT_MS}ms waiting for opencode to resolve the call`
|
|
1364
1381
|
)
|
|
1365
1382
|
);
|
|
1366
1383
|
log.warn("timed out pending proxy call", {
|
|
1367
|
-
sessionKey:
|
|
1384
|
+
sessionKey: current.sessionKey,
|
|
1368
1385
|
toolCallId: call.id,
|
|
1369
1386
|
toolName: call.toolName,
|
|
1370
1387
|
timeoutMs: PENDING_PROXY_CALL_TIMEOUT_MS
|
|
@@ -1380,7 +1397,8 @@ function queuePendingProxyCall(sessionKey2, call) {
|
|
|
1380
1397
|
resolve: call.resolve,
|
|
1381
1398
|
reject: call.reject
|
|
1382
1399
|
};
|
|
1383
|
-
|
|
1400
|
+
pendingByCallId.set(call.id, pending);
|
|
1401
|
+
indexAdd(sessionKey2, call.id);
|
|
1384
1402
|
emitter.emit(eventName(sessionKey2), pending);
|
|
1385
1403
|
log.info("queued pending proxy call", {
|
|
1386
1404
|
sessionKey: sessionKey2,
|
|
@@ -1389,22 +1407,55 @@ function queuePendingProxyCall(sessionKey2, call) {
|
|
|
1389
1407
|
});
|
|
1390
1408
|
return pending;
|
|
1391
1409
|
}
|
|
1392
|
-
function
|
|
1393
|
-
|
|
1410
|
+
function getPendingProxyCalls(sessionKey2) {
|
|
1411
|
+
const s = callIdsBySession.get(sessionKey2);
|
|
1412
|
+
if (!s || s.size === 0) return [];
|
|
1413
|
+
const out = [];
|
|
1414
|
+
for (const id of s) {
|
|
1415
|
+
const p = pendingByCallId.get(id);
|
|
1416
|
+
if (p) out.push(p);
|
|
1417
|
+
}
|
|
1418
|
+
return out;
|
|
1394
1419
|
}
|
|
1395
|
-
function
|
|
1396
|
-
const pending =
|
|
1420
|
+
function resolvePendingProxyCallById(toolCallId, result) {
|
|
1421
|
+
const pending = pendingByCallId.get(toolCallId);
|
|
1397
1422
|
if (!pending) return false;
|
|
1398
|
-
|
|
1423
|
+
pendingByCallId.delete(toolCallId);
|
|
1424
|
+
indexRemove(pending.sessionKey, toolCallId);
|
|
1399
1425
|
clearTimeout(pending.timer);
|
|
1400
1426
|
pending.resolve(result);
|
|
1401
1427
|
log.info("resolved pending proxy call", {
|
|
1402
|
-
sessionKey:
|
|
1428
|
+
sessionKey: pending.sessionKey,
|
|
1403
1429
|
toolCallId: pending.toolCallId,
|
|
1404
1430
|
toolName: pending.toolName
|
|
1405
1431
|
});
|
|
1406
1432
|
return true;
|
|
1407
1433
|
}
|
|
1434
|
+
function rejectPendingProxyCallById(toolCallId, error) {
|
|
1435
|
+
const pending = pendingByCallId.get(toolCallId);
|
|
1436
|
+
if (!pending) return false;
|
|
1437
|
+
pendingByCallId.delete(toolCallId);
|
|
1438
|
+
indexRemove(pending.sessionKey, toolCallId);
|
|
1439
|
+
clearTimeout(pending.timer);
|
|
1440
|
+
pending.reject(error);
|
|
1441
|
+
log.warn("rejected pending proxy call", {
|
|
1442
|
+
sessionKey: pending.sessionKey,
|
|
1443
|
+
toolCallId: pending.toolCallId,
|
|
1444
|
+
toolName: pending.toolName,
|
|
1445
|
+
error: error.message
|
|
1446
|
+
});
|
|
1447
|
+
return true;
|
|
1448
|
+
}
|
|
1449
|
+
function rejectAllPendingProxyCallsForSession(sessionKey2, error) {
|
|
1450
|
+
const s = callIdsBySession.get(sessionKey2);
|
|
1451
|
+
if (!s) return 0;
|
|
1452
|
+
const ids = [...s];
|
|
1453
|
+
let count = 0;
|
|
1454
|
+
for (const id of ids) {
|
|
1455
|
+
if (rejectPendingProxyCallById(id, error)) count++;
|
|
1456
|
+
}
|
|
1457
|
+
return count;
|
|
1458
|
+
}
|
|
1408
1459
|
|
|
1409
1460
|
// src/claude-code-language-model.ts
|
|
1410
1461
|
import { readFileSync as readFileSync2, writeFileSync as writeFileSync3 } from "fs";
|
|
@@ -2238,8 +2289,14 @@ ${plan}
|
|
|
2238
2289
|
);
|
|
2239
2290
|
const resolvedProxy = this.resolvedProxyTools();
|
|
2240
2291
|
const self = this;
|
|
2241
|
-
const
|
|
2242
|
-
const
|
|
2292
|
+
const previousPendingProxyCalls = getPendingProxyCalls(sk);
|
|
2293
|
+
const previousPendingProxyMatches = previousPendingProxyCalls.map((call) => ({
|
|
2294
|
+
call,
|
|
2295
|
+
result: this.extractPendingProxyResult(options.prompt, call.toolCallId)
|
|
2296
|
+
}));
|
|
2297
|
+
const hasMatchedPendingResults = previousPendingProxyMatches.some(
|
|
2298
|
+
(m) => m.result !== null
|
|
2299
|
+
);
|
|
2243
2300
|
const runtimeStatus = await getRuntimeMcpStatus();
|
|
2244
2301
|
log.info("doStream starting", {
|
|
2245
2302
|
cwd,
|
|
@@ -2369,21 +2426,27 @@ ${plan}
|
|
|
2369
2426
|
const skipResultForIds = /* @__PURE__ */ new Set();
|
|
2370
2427
|
const toolCallsById = /* @__PURE__ */ new Map();
|
|
2371
2428
|
let resultMeta = {};
|
|
2372
|
-
const
|
|
2429
|
+
const drainBuffer = [];
|
|
2430
|
+
let drainTimer = null;
|
|
2431
|
+
const DRAIN_QUIET_MS = 100;
|
|
2432
|
+
const finishWithToolCalls = (calls) => {
|
|
2373
2433
|
if (controllerClosed) return;
|
|
2374
|
-
|
|
2375
|
-
|
|
2376
|
-
|
|
2377
|
-
|
|
2378
|
-
|
|
2379
|
-
|
|
2380
|
-
|
|
2381
|
-
|
|
2382
|
-
|
|
2383
|
-
|
|
2384
|
-
|
|
2385
|
-
|
|
2386
|
-
|
|
2434
|
+
if (calls.length === 0) return;
|
|
2435
|
+
for (const call of calls) {
|
|
2436
|
+
controller.enqueue({
|
|
2437
|
+
type: "tool-input-start",
|
|
2438
|
+
id: call.toolCallId,
|
|
2439
|
+
toolName: call.toolName
|
|
2440
|
+
});
|
|
2441
|
+
controller.enqueue({
|
|
2442
|
+
type: "tool-call",
|
|
2443
|
+
toolCallId: call.toolCallId,
|
|
2444
|
+
toolName: call.toolName,
|
|
2445
|
+
input: JSON.stringify(call.input),
|
|
2446
|
+
providerExecuted: false
|
|
2447
|
+
});
|
|
2448
|
+
skipResultForIds.add(call.toolCallId);
|
|
2449
|
+
}
|
|
2387
2450
|
controller.enqueue({
|
|
2388
2451
|
type: "finish",
|
|
2389
2452
|
finishReason: toFinishReason("tool-calls"),
|
|
@@ -2399,6 +2462,21 @@ ${plan}
|
|
|
2399
2462
|
} catch {
|
|
2400
2463
|
}
|
|
2401
2464
|
};
|
|
2465
|
+
const drainNow = () => {
|
|
2466
|
+
if (drainTimer) {
|
|
2467
|
+
clearTimeout(drainTimer);
|
|
2468
|
+
drainTimer = null;
|
|
2469
|
+
}
|
|
2470
|
+
if (drainBuffer.length === 0) return;
|
|
2471
|
+
if (controllerClosed) return;
|
|
2472
|
+
const batch = drainBuffer.splice(0, drainBuffer.length);
|
|
2473
|
+
log.info("draining pending proxy calls into stream finish", {
|
|
2474
|
+
sessionKey: sk,
|
|
2475
|
+
count: batch.length,
|
|
2476
|
+
toolCallIds: batch.map((c) => c.toolCallId)
|
|
2477
|
+
});
|
|
2478
|
+
finishWithToolCalls(batch);
|
|
2479
|
+
};
|
|
2402
2480
|
let gotPartialEvents = false;
|
|
2403
2481
|
const lineHandler = (line) => {
|
|
2404
2482
|
if (!line.trim()) return;
|
|
@@ -2794,6 +2872,33 @@ ${plan}
|
|
|
2794
2872
|
});
|
|
2795
2873
|
turnCompleted = true;
|
|
2796
2874
|
endTextBlock();
|
|
2875
|
+
if (drainBuffer.length > 0) {
|
|
2876
|
+
log.info(
|
|
2877
|
+
"draining pending proxy calls at turn-result boundary",
|
|
2878
|
+
{
|
|
2879
|
+
sessionKey: sk,
|
|
2880
|
+
count: drainBuffer.length
|
|
2881
|
+
}
|
|
2882
|
+
);
|
|
2883
|
+
drainNow();
|
|
2884
|
+
return;
|
|
2885
|
+
}
|
|
2886
|
+
const orphanPending = getPendingProxyCalls(sk);
|
|
2887
|
+
if (orphanPending.length > 0) {
|
|
2888
|
+
log.warn(
|
|
2889
|
+
"rejecting orphan pending proxy calls at turn-result boundary",
|
|
2890
|
+
{
|
|
2891
|
+
sessionKey: sk,
|
|
2892
|
+
count: orphanPending.length
|
|
2893
|
+
}
|
|
2894
|
+
);
|
|
2895
|
+
rejectAllPendingProxyCallsForSession(
|
|
2896
|
+
sk,
|
|
2897
|
+
new Error(
|
|
2898
|
+
"Claude CLI emitted result with pending proxy calls not in drain buffer"
|
|
2899
|
+
)
|
|
2900
|
+
);
|
|
2901
|
+
}
|
|
2797
2902
|
for (const [idx, reasoningId] of reasoningIds) {
|
|
2798
2903
|
if (reasoningStarted.get(idx)) {
|
|
2799
2904
|
controller.enqueue({
|
|
@@ -2831,6 +2936,15 @@ ${plan}
|
|
|
2831
2936
|
const closeHandler = () => {
|
|
2832
2937
|
log.debug("readline closed");
|
|
2833
2938
|
if (controllerClosed) return;
|
|
2939
|
+
if (drainBuffer.length > 0 || getPendingProxyCalls(sk).length > 0) {
|
|
2940
|
+
rejectAllPendingProxyCallsForSession(
|
|
2941
|
+
sk,
|
|
2942
|
+
new Error(
|
|
2943
|
+
"Claude CLI subprocess closed before pending tool calls were resolved"
|
|
2944
|
+
)
|
|
2945
|
+
);
|
|
2946
|
+
drainBuffer.length = 0;
|
|
2947
|
+
}
|
|
2834
2948
|
controllerClosed = true;
|
|
2835
2949
|
cleanupTurn();
|
|
2836
2950
|
endTextBlock();
|
|
@@ -2852,6 +2966,10 @@ ${plan}
|
|
|
2852
2966
|
if (cleanedUp) return;
|
|
2853
2967
|
cleanedUp = true;
|
|
2854
2968
|
clearFallbackTimer();
|
|
2969
|
+
if (drainTimer) {
|
|
2970
|
+
clearTimeout(drainTimer);
|
|
2971
|
+
drainTimer = null;
|
|
2972
|
+
}
|
|
2855
2973
|
lineEmitter.off("line", lineHandler);
|
|
2856
2974
|
lineEmitter.off("close", closeHandler);
|
|
2857
2975
|
pendingProxyUnsubscribe?.();
|
|
@@ -2861,6 +2979,15 @@ ${plan}
|
|
|
2861
2979
|
const procErrorHandler = (err) => {
|
|
2862
2980
|
log.error("process error", { error: err.message });
|
|
2863
2981
|
if (controllerClosed) return;
|
|
2982
|
+
if (drainBuffer.length > 0 || getPendingProxyCalls(sk).length > 0) {
|
|
2983
|
+
rejectAllPendingProxyCallsForSession(
|
|
2984
|
+
sk,
|
|
2985
|
+
new Error(
|
|
2986
|
+
`Claude CLI subprocess error: ${err.message}`
|
|
2987
|
+
)
|
|
2988
|
+
);
|
|
2989
|
+
drainBuffer.length = 0;
|
|
2990
|
+
}
|
|
2864
2991
|
controllerClosed = true;
|
|
2865
2992
|
cleanupTurn();
|
|
2866
2993
|
controller.enqueue({ type: "error", error: err });
|
|
@@ -2872,12 +2999,31 @@ ${plan}
|
|
|
2872
2999
|
lineEmitter.on("line", lineHandler);
|
|
2873
3000
|
lineEmitter.on("close", closeHandler);
|
|
2874
3001
|
pendingProxyUnsubscribe = onPendingProxyCall(sk, (call) => {
|
|
3002
|
+
if (controllerClosed) {
|
|
3003
|
+
log.warn(
|
|
3004
|
+
"pending proxy call arrived after stream close; rejecting",
|
|
3005
|
+
{
|
|
3006
|
+
sessionKey: sk,
|
|
3007
|
+
toolCallId: call.toolCallId,
|
|
3008
|
+
toolName: call.toolName
|
|
3009
|
+
}
|
|
3010
|
+
);
|
|
3011
|
+
rejectPendingProxyCallById(
|
|
3012
|
+
call.toolCallId,
|
|
3013
|
+
new Error(
|
|
3014
|
+
`Pending proxy call '${call.toolName}' arrived after the stream was already closed`
|
|
3015
|
+
)
|
|
3016
|
+
);
|
|
3017
|
+
return;
|
|
3018
|
+
}
|
|
2875
3019
|
log.info("received pending proxy call for session", {
|
|
2876
3020
|
sessionKey: sk,
|
|
2877
3021
|
toolCallId: call.toolCallId,
|
|
2878
3022
|
toolName: call.toolName
|
|
2879
3023
|
});
|
|
2880
|
-
|
|
3024
|
+
drainBuffer.push(call);
|
|
3025
|
+
if (drainTimer) clearTimeout(drainTimer);
|
|
3026
|
+
drainTimer = setTimeout(drainNow, DRAIN_QUIET_MS);
|
|
2881
3027
|
});
|
|
2882
3028
|
proc.on("error", procErrorHandler);
|
|
2883
3029
|
if (options.abortSignal) {
|
|
@@ -2903,21 +3049,44 @@ ${plan}
|
|
|
2903
3049
|
startResultFallback(5e3);
|
|
2904
3050
|
});
|
|
2905
3051
|
}
|
|
2906
|
-
if (
|
|
2907
|
-
|
|
2908
|
-
|
|
2909
|
-
|
|
2910
|
-
|
|
2911
|
-
|
|
2912
|
-
|
|
2913
|
-
|
|
2914
|
-
|
|
2915
|
-
|
|
2916
|
-
|
|
2917
|
-
|
|
3052
|
+
if (hasMatchedPendingResults) {
|
|
3053
|
+
for (const { call, result } of previousPendingProxyMatches) {
|
|
3054
|
+
if (result) {
|
|
3055
|
+
log.info("resolving pending proxy call from tool result prompt", {
|
|
3056
|
+
sessionKey: sk,
|
|
3057
|
+
toolCallId: call.toolCallId,
|
|
3058
|
+
toolName: call.toolName
|
|
3059
|
+
});
|
|
3060
|
+
resolvePendingProxyCallById(call.toolCallId, result);
|
|
3061
|
+
} else {
|
|
3062
|
+
log.warn(
|
|
3063
|
+
"pending proxy call had no matching tool-result; rejecting as orphan",
|
|
3064
|
+
{
|
|
3065
|
+
sessionKey: sk,
|
|
3066
|
+
toolCallId: call.toolCallId,
|
|
3067
|
+
toolName: call.toolName
|
|
3068
|
+
}
|
|
3069
|
+
);
|
|
3070
|
+
rejectPendingProxyCallById(
|
|
3071
|
+
call.toolCallId,
|
|
3072
|
+
new Error(
|
|
3073
|
+
`Pending proxy call '${call.toolName}' (${call.toolCallId}) was not matched in tool-result turn; rejecting as orphaned`
|
|
3074
|
+
)
|
|
3075
|
+
);
|
|
3076
|
+
}
|
|
2918
3077
|
}
|
|
2919
3078
|
return;
|
|
2920
3079
|
}
|
|
3080
|
+
if (previousPendingProxyCalls.length > 0) {
|
|
3081
|
+
for (const call of previousPendingProxyCalls) {
|
|
3082
|
+
rejectPendingProxyCallById(
|
|
3083
|
+
call.toolCallId,
|
|
3084
|
+
new Error(
|
|
3085
|
+
`Pending proxy call '${call.toolName}' (${call.toolCallId}) was orphaned by a new user turn; rejecting`
|
|
3086
|
+
)
|
|
3087
|
+
);
|
|
3088
|
+
}
|
|
3089
|
+
}
|
|
2921
3090
|
proc.stdin?.write(userMsg + "\n");
|
|
2922
3091
|
log.debug("sent user message", { textLength: userMsg.length });
|
|
2923
3092
|
};
|