@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 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 = spec.environment;
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 = spec.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 pendingBySession = /* @__PURE__ */ new Map();
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 existing = pendingBySession.get(sessionKey2);
1335
- if (existing) {
1336
- if (Date.now() - existing.createdAt < PENDING_PROXY_CALL_TIMEOUT_MS) {
1337
- call.reject(
1338
- new Error(`Another proxy tool call is already pending for ${sessionKey2}`)
1339
- );
1340
- log.warn("rejected overlapping proxy call", {
1341
- sessionKey: sessionKey2,
1342
- existingToolCallId: existing.toolCallId,
1343
- existingToolName: existing.toolName,
1344
- toolCallId: call.id,
1345
- toolName: call.toolName
1346
- });
1347
- return existing;
1348
- }
1349
- clearTimeout(existing.timer);
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
- pendingBySession.delete(sessionKey2);
1370
+ pendingByCallId.delete(call.id);
1371
+ indexRemove(previous.sessionKey, call.id);
1356
1372
  }
1357
1373
  const timer = setTimeout(() => {
1358
- const current = pendingBySession.get(sessionKey2);
1359
- if (!current || current.toolCallId !== call.id) return;
1360
- pendingBySession.delete(sessionKey2);
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: sessionKey2,
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
- pendingBySession.set(sessionKey2, pending);
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 getPendingProxyCall(sessionKey2) {
1393
- return pendingBySession.get(sessionKey2);
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 resolvePendingProxyCall(sessionKey2, result) {
1396
- const pending = pendingBySession.get(sessionKey2);
1420
+ function resolvePendingProxyCallById(toolCallId, result) {
1421
+ const pending = pendingByCallId.get(toolCallId);
1397
1422
  if (!pending) return false;
1398
- pendingBySession.delete(sessionKey2);
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: sessionKey2,
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 pendingProxyCall = getPendingProxyCall(sk);
2242
- const pendingProxyResult = pendingProxyCall ? this.extractPendingProxyResult(options.prompt, pendingProxyCall.toolCallId) : null;
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 finishWithToolCall = (call) => {
2429
+ const drainBuffer = [];
2430
+ let drainTimer = null;
2431
+ const DRAIN_QUIET_MS = 100;
2432
+ const finishWithToolCalls = (calls) => {
2373
2433
  if (controllerClosed) return;
2374
- controller.enqueue({
2375
- type: "tool-input-start",
2376
- id: call.toolCallId,
2377
- toolName: call.toolName
2378
- });
2379
- controller.enqueue({
2380
- type: "tool-call",
2381
- toolCallId: call.toolCallId,
2382
- toolName: call.toolName,
2383
- input: JSON.stringify(call.input),
2384
- providerExecuted: false
2385
- });
2386
- skipResultForIds.add(call.toolCallId);
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
- finishWithToolCall(call);
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 (pendingProxyCall && pendingProxyResult) {
2907
- log.info("resolving pending proxy call from tool result prompt", {
2908
- sessionKey: sk,
2909
- toolCallId: pendingProxyCall.toolCallId,
2910
- toolName: pendingProxyCall.toolName
2911
- });
2912
- const resolved = resolvePendingProxyCall(sk, pendingProxyResult);
2913
- if (!resolved) {
2914
- log.warn("failed to resolve pending proxy call; no pending state", {
2915
- sessionKey: sk,
2916
- toolCallId: pendingProxyCall.toolCallId
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
  };