@khalilgharbaoui/opencode-claude-code-plugin 0.14.0 → 0.15.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 +100 -10
- package/dist/index.d.ts +49 -1
- package/dist/index.js +2400 -1691
- package/dist/index.js.map +1 -1
- package/package.json +2 -2
package/dist/index.js
CHANGED
|
@@ -376,18 +376,241 @@ function mapTool(name, input, opts) {
|
|
|
376
376
|
return { name, input, executed: true };
|
|
377
377
|
}
|
|
378
378
|
|
|
379
|
+
// src/side-question.ts
|
|
380
|
+
import { randomUUID } from "crypto";
|
|
381
|
+
|
|
382
|
+
// src/cli-version.ts
|
|
383
|
+
import { execFile } from "child_process";
|
|
384
|
+
import { promisify } from "util";
|
|
385
|
+
var execFileAsync = promisify(execFile);
|
|
386
|
+
var cache = /* @__PURE__ */ new Map();
|
|
387
|
+
function detectCliVersion(cliPath) {
|
|
388
|
+
const cached = cache.get(cliPath);
|
|
389
|
+
if (cached) return cached;
|
|
390
|
+
const promise = (async () => {
|
|
391
|
+
try {
|
|
392
|
+
const { stdout } = await execFileAsync(cliPath, ["--version"], {
|
|
393
|
+
timeout: 5e3
|
|
394
|
+
});
|
|
395
|
+
const match = /(\d+)\.(\d+)\.(\d+)/.exec(stdout.trim());
|
|
396
|
+
if (!match) {
|
|
397
|
+
log.warn("claude --version output unparseable", { stdout: stdout.trim() });
|
|
398
|
+
return null;
|
|
399
|
+
}
|
|
400
|
+
const v = {
|
|
401
|
+
major: Number(match[1]),
|
|
402
|
+
minor: Number(match[2]),
|
|
403
|
+
patch: Number(match[3]),
|
|
404
|
+
raw: stdout.trim()
|
|
405
|
+
};
|
|
406
|
+
log.info("detected claude cli version", { cliPath, version: v.raw });
|
|
407
|
+
if (!cliSupportsThinkingDisplay(v)) {
|
|
408
|
+
log.notice(
|
|
409
|
+
"claude cli < 2.1.142 detected; Opus 4.7 thinking summaries unavailable. Run `npm i -g @anthropic-ai/claude-code` to upgrade.",
|
|
410
|
+
{ version: v.raw }
|
|
411
|
+
);
|
|
412
|
+
}
|
|
413
|
+
return v;
|
|
414
|
+
} catch (err) {
|
|
415
|
+
log.warn("failed to detect claude cli version", {
|
|
416
|
+
cliPath,
|
|
417
|
+
error: err instanceof Error ? err.message : String(err)
|
|
418
|
+
});
|
|
419
|
+
return null;
|
|
420
|
+
}
|
|
421
|
+
})();
|
|
422
|
+
cache.set(cliPath, promise);
|
|
423
|
+
return promise;
|
|
424
|
+
}
|
|
425
|
+
function gte(v, target) {
|
|
426
|
+
if (v.major !== target.major) return v.major > target.major;
|
|
427
|
+
if (v.minor !== target.minor) return v.minor > target.minor;
|
|
428
|
+
return v.patch >= target.patch;
|
|
429
|
+
}
|
|
430
|
+
function cliSupportsThinkingDisplay(v) {
|
|
431
|
+
if (!v) return false;
|
|
432
|
+
return gte(v, { major: 2, minor: 1, patch: 142 });
|
|
433
|
+
}
|
|
434
|
+
function cliSupportsFastMode(v) {
|
|
435
|
+
if (!v) return false;
|
|
436
|
+
return gte(v, { major: 2, minor: 1, patch: 220 });
|
|
437
|
+
}
|
|
438
|
+
function cliSupportsSideQuestion(v) {
|
|
439
|
+
if (!v) return false;
|
|
440
|
+
return gte(v, { major: 2, minor: 1, patch: 258 });
|
|
441
|
+
}
|
|
442
|
+
function cliSupportsThinking(v) {
|
|
443
|
+
if (!v) return false;
|
|
444
|
+
return gte(v, { major: 2, minor: 0, patch: 0 });
|
|
445
|
+
}
|
|
446
|
+
|
|
447
|
+
// src/side-question.ts
|
|
448
|
+
var SIDE_QUESTION_USAGE = "Usage: /btw <question>. Ask a side question about the current conversation without adding it to the main context.";
|
|
449
|
+
var pendingProcesses = /* @__PURE__ */ new WeakSet();
|
|
450
|
+
function isRecord(value) {
|
|
451
|
+
return value !== null && typeof value === "object" && !Array.isArray(value);
|
|
452
|
+
}
|
|
453
|
+
function parseSideQuestionContent(content) {
|
|
454
|
+
let text;
|
|
455
|
+
if (typeof content === "string") {
|
|
456
|
+
text = content;
|
|
457
|
+
} else if (Array.isArray(content)) {
|
|
458
|
+
const parts = [];
|
|
459
|
+
for (const part of content) {
|
|
460
|
+
if (!isRecord(part) || part.type !== "text" || typeof part.text !== "string") return null;
|
|
461
|
+
parts.push(part.text);
|
|
462
|
+
}
|
|
463
|
+
text = parts.join("\n");
|
|
464
|
+
} else {
|
|
465
|
+
return null;
|
|
466
|
+
}
|
|
467
|
+
const match = /^\/btw(?:\s+([\s\S]*))?$/.exec(text.trim());
|
|
468
|
+
return match ? { question: (match[1] ?? "").trim() } : null;
|
|
469
|
+
}
|
|
470
|
+
function parseSideQuestion(prompt) {
|
|
471
|
+
const latest = prompt.at(-1);
|
|
472
|
+
return latest?.role === "user" ? parseSideQuestionContent(latest.content) : null;
|
|
473
|
+
}
|
|
474
|
+
function isSideQuestionPending(activeProcess) {
|
|
475
|
+
return pendingProcesses.has(activeProcess.proc);
|
|
476
|
+
}
|
|
477
|
+
function dispatchSideQuestionResponse(activeProcess, line) {
|
|
478
|
+
if (!pendingProcesses.has(activeProcess.proc)) return false;
|
|
479
|
+
let message;
|
|
480
|
+
try {
|
|
481
|
+
message = JSON.parse(line);
|
|
482
|
+
} catch {
|
|
483
|
+
return false;
|
|
484
|
+
}
|
|
485
|
+
if (!isRecord(message) || message.type !== "control_response") return false;
|
|
486
|
+
const response = message.response;
|
|
487
|
+
if (!isRecord(response) || typeof response.request_id !== "string") return false;
|
|
488
|
+
return activeProcess.lineEmitter.emit(`side-question:${response.request_id}`, response);
|
|
489
|
+
}
|
|
490
|
+
async function requestSideQuestion(activeProcess, question, options) {
|
|
491
|
+
question = question.trim();
|
|
492
|
+
if (!question) return { response: SIDE_QUESTION_USAGE, synthetic: true };
|
|
493
|
+
options.abortSignal?.throwIfAborted();
|
|
494
|
+
const { proc, lineEmitter } = activeProcess;
|
|
495
|
+
if (options.interactive || !proc.stdout) {
|
|
496
|
+
throw new Error("/btw requires the headless Claude Code transport; interactive sessions are not supported.");
|
|
497
|
+
}
|
|
498
|
+
if (!cliSupportsSideQuestion(options.cliVersion)) {
|
|
499
|
+
throw new Error("/btw requires Claude Code CLI 2.1.258 or newer (the oldest verified version).");
|
|
500
|
+
}
|
|
501
|
+
if (options.busy || lineEmitter.listenerCount("line") > 0 || pendingProcesses.has(proc)) {
|
|
502
|
+
throw new Error("/btw requires an idle Claude Code session. Wait for the current turn to finish.");
|
|
503
|
+
}
|
|
504
|
+
const stdin = proc.stdin;
|
|
505
|
+
if (proc.killed || proc.exitCode != null || proc.signalCode != null || !stdin || stdin.destroyed || stdin.writableEnded || !stdin.writable) {
|
|
506
|
+
throw new Error("/btw requires a live Claude Code session with writable stdin.");
|
|
507
|
+
}
|
|
508
|
+
const timeoutMs = options.timeoutMs ?? 12e4;
|
|
509
|
+
if (!Number.isInteger(timeoutMs) || timeoutMs <= 0 || timeoutMs > 2147483647) {
|
|
510
|
+
throw new Error("/btw timeoutMs must be a positive 32-bit integer.");
|
|
511
|
+
}
|
|
512
|
+
const requestId = randomUUID();
|
|
513
|
+
const request = JSON.stringify({
|
|
514
|
+
type: "control_request",
|
|
515
|
+
request_id: requestId,
|
|
516
|
+
request: {
|
|
517
|
+
subtype: "side_question",
|
|
518
|
+
question,
|
|
519
|
+
...options.history === void 0 ? {} : { history: options.history }
|
|
520
|
+
}
|
|
521
|
+
});
|
|
522
|
+
pendingProcesses.add(proc);
|
|
523
|
+
return new Promise((resolve4, reject) => {
|
|
524
|
+
const event = `side-question:${requestId}`;
|
|
525
|
+
let settled = false;
|
|
526
|
+
let sent = false;
|
|
527
|
+
let cancelPending = false;
|
|
528
|
+
const cleanup = () => {
|
|
529
|
+
clearTimeout(timer);
|
|
530
|
+
lineEmitter.off(event, onResponse);
|
|
531
|
+
lineEmitter.off("close", onClose);
|
|
532
|
+
lineEmitter.off("error", onError);
|
|
533
|
+
proc.off("exit", onClose);
|
|
534
|
+
proc.off("close", onClose);
|
|
535
|
+
proc.off("error", onError);
|
|
536
|
+
if (!cancelPending) stdin.off("error", onError);
|
|
537
|
+
options.abortSignal?.removeEventListener("abort", onAbort);
|
|
538
|
+
pendingProcesses.delete(proc);
|
|
539
|
+
};
|
|
540
|
+
const fail = (error, cancel = false) => {
|
|
541
|
+
if (settled) return;
|
|
542
|
+
settled = true;
|
|
543
|
+
if (cancel && sent && !stdin.destroyed && !stdin.writableEnded && stdin.writable) {
|
|
544
|
+
try {
|
|
545
|
+
cancelPending = true;
|
|
546
|
+
stdin.write(
|
|
547
|
+
JSON.stringify({ type: "control_cancel_request", request_id: requestId }) + "\n",
|
|
548
|
+
() => {
|
|
549
|
+
queueMicrotask(() => stdin.off("error", onError));
|
|
550
|
+
}
|
|
551
|
+
);
|
|
552
|
+
} catch {
|
|
553
|
+
cancelPending = false;
|
|
554
|
+
}
|
|
555
|
+
}
|
|
556
|
+
cleanup();
|
|
557
|
+
reject(error);
|
|
558
|
+
};
|
|
559
|
+
const onClose = () => fail(new Error("Claude Code closed before answering /btw."));
|
|
560
|
+
const onError = (error) => fail(error);
|
|
561
|
+
const onAbort = () => fail(
|
|
562
|
+
options.abortSignal?.reason ?? new DOMException("/btw was aborted.", "AbortError"),
|
|
563
|
+
true
|
|
564
|
+
);
|
|
565
|
+
const onResponse = (response) => {
|
|
566
|
+
if (settled || response.request_id !== requestId) return;
|
|
567
|
+
if (response.subtype === "error") {
|
|
568
|
+
fail(new Error(typeof response.error === "string" ? response.error : "Claude Code rejected /btw."));
|
|
569
|
+
return;
|
|
570
|
+
}
|
|
571
|
+
const result = response.response;
|
|
572
|
+
if (response.subtype !== "success" || !isRecord(result) || typeof result.response !== "string" || typeof result.synthetic !== "boolean") {
|
|
573
|
+
fail(new Error("Claude Code returned an invalid /btw response."));
|
|
574
|
+
return;
|
|
575
|
+
}
|
|
576
|
+
settled = true;
|
|
577
|
+
cleanup();
|
|
578
|
+
resolve4({ response: result.response, synthetic: result.synthetic });
|
|
579
|
+
};
|
|
580
|
+
const timer = setTimeout(() => {
|
|
581
|
+
fail(new Error(`/btw timed out after ${timeoutMs}ms.`), true);
|
|
582
|
+
}, timeoutMs);
|
|
583
|
+
lineEmitter.on(event, onResponse);
|
|
584
|
+
lineEmitter.on("close", onClose);
|
|
585
|
+
lineEmitter.on("error", onError);
|
|
586
|
+
proc.on("exit", onClose);
|
|
587
|
+
proc.on("close", onClose);
|
|
588
|
+
proc.on("error", onError);
|
|
589
|
+
stdin.on("error", onError);
|
|
590
|
+
options.abortSignal?.addEventListener("abort", onAbort, { once: true });
|
|
591
|
+
if (options.abortSignal?.aborted) {
|
|
592
|
+
onAbort();
|
|
593
|
+
return;
|
|
594
|
+
}
|
|
595
|
+
try {
|
|
596
|
+
sent = true;
|
|
597
|
+
stdin.write(request + "\n");
|
|
598
|
+
} catch (error) {
|
|
599
|
+
fail(error);
|
|
600
|
+
}
|
|
601
|
+
});
|
|
602
|
+
}
|
|
603
|
+
|
|
379
604
|
// src/message-builder.ts
|
|
380
|
-
|
|
381
|
-
|
|
382
|
-
|
|
383
|
-
|
|
384
|
-
|
|
385
|
-
|
|
386
|
-
|
|
387
|
-
|
|
388
|
-
|
|
389
|
-
if (!effort) return null;
|
|
390
|
-
return THINKING_KEYWORDS[effort] ?? null;
|
|
605
|
+
function filterSideQuestionHistory(prompt) {
|
|
606
|
+
let aside = false;
|
|
607
|
+
return prompt.filter((message) => {
|
|
608
|
+
if (message.role === "user") {
|
|
609
|
+
aside = parseSideQuestionContent(message.content) !== null;
|
|
610
|
+
return !aside;
|
|
611
|
+
}
|
|
612
|
+
return message.role !== "assistant" || !aside;
|
|
613
|
+
});
|
|
391
614
|
}
|
|
392
615
|
var SUPPORTED_IMAGE_TYPES = /* @__PURE__ */ new Set([
|
|
393
616
|
"image/jpeg",
|
|
@@ -529,6 +752,7 @@ ${clipWithMarker(
|
|
|
529
752
|
}
|
|
530
753
|
function compactConversationHistory(prompt, opts = {}) {
|
|
531
754
|
const mode = opts.mode ?? "fresh-session";
|
|
755
|
+
prompt = filterSideQuestionHistory(prompt);
|
|
532
756
|
if (mode === "compaction") {
|
|
533
757
|
return buildCompactionHistory(prompt);
|
|
534
758
|
}
|
|
@@ -603,7 +827,7 @@ function buildCompactionHistory(prompt) {
|
|
|
603
827
|
});
|
|
604
828
|
return entries.join("\n\n");
|
|
605
829
|
}
|
|
606
|
-
function getClaudeUserMessage(prompt, includeHistoryContext = false,
|
|
830
|
+
function getClaudeUserMessage(prompt, includeHistoryContext = false, opts = {}) {
|
|
607
831
|
const compactionMode = opts.compactionMode === true;
|
|
608
832
|
const content = [];
|
|
609
833
|
if (compactionMode) {
|
|
@@ -653,6 +877,7 @@ Now continuing with the current message:
|
|
|
653
877
|
}
|
|
654
878
|
for (const msg of messages) {
|
|
655
879
|
if (msg.role === "user") {
|
|
880
|
+
if (parseSideQuestionContent(msg.content) !== null) continue;
|
|
656
881
|
if (typeof msg.content === "string") {
|
|
657
882
|
const str = msg.content;
|
|
658
883
|
if (str.trim()) {
|
|
@@ -708,20 +933,6 @@ Now continuing with the current message:
|
|
|
708
933
|
}
|
|
709
934
|
});
|
|
710
935
|
}
|
|
711
|
-
if (!compactionMode) {
|
|
712
|
-
const keyword = reasoningKeyword(reasoningEffort);
|
|
713
|
-
if (keyword) {
|
|
714
|
-
const lastTextPart = [...content].reverse().find((p) => p.type === "text");
|
|
715
|
-
if (lastTextPart) {
|
|
716
|
-
lastTextPart.text = lastTextPart.text ? `${lastTextPart.text}
|
|
717
|
-
|
|
718
|
-
(${keyword})` : `(${keyword})`;
|
|
719
|
-
} else {
|
|
720
|
-
content.push({ type: "text", text: `(${keyword})` });
|
|
721
|
-
}
|
|
722
|
-
log.debug("injected reasoning keyword", { effort: reasoningEffort, keyword });
|
|
723
|
-
}
|
|
724
|
-
}
|
|
725
936
|
return JSON.stringify({
|
|
726
937
|
type: "user",
|
|
727
938
|
message: {
|
|
@@ -731,6 +942,10 @@ Now continuing with the current message:
|
|
|
731
942
|
});
|
|
732
943
|
}
|
|
733
944
|
|
|
945
|
+
// src/agent-models.ts
|
|
946
|
+
import { readFile, readdir } from "fs/promises";
|
|
947
|
+
import path from "path";
|
|
948
|
+
|
|
734
949
|
// src/models.ts
|
|
735
950
|
var PROVIDER_ID = "claude-code";
|
|
736
951
|
var NPM = "@khalilgharbaoui/opencode-claude-code-plugin";
|
|
@@ -772,9 +987,9 @@ function defineModel(opts) {
|
|
|
772
987
|
}
|
|
773
988
|
var haikuCost = { input: 1, output: 5, cacheRead: 0.1, cacheWrite: 1.25 };
|
|
774
989
|
var sonnetCost = { input: 3, output: 15, cacheRead: 0.3, cacheWrite: 3.75 };
|
|
775
|
-
var sonnet5Cost = { input: 2, output: 10, cacheRead: 0.2, cacheWrite: 2.5 };
|
|
776
990
|
var opusCost = { input: 5, output: 25, cacheRead: 0.5, cacheWrite: 6.25 };
|
|
777
991
|
var fableCost = { input: 10, output: 50, cacheRead: 1, cacheWrite: 12.5 };
|
|
992
|
+
var fable51Cost = { input: 10, output: 50, cacheRead: 0.25, cacheWrite: 12.5 };
|
|
778
993
|
var opusFastCost = { input: 10, output: 50, cacheRead: 1, cacheWrite: 12.5 };
|
|
779
994
|
function toConfigModel(model) {
|
|
780
995
|
const inputMods = [];
|
|
@@ -849,8 +1064,8 @@ var defaultModels = {
|
|
|
849
1064
|
reasoning: true,
|
|
850
1065
|
context: 1e6,
|
|
851
1066
|
output: 128e3,
|
|
852
|
-
cost:
|
|
853
|
-
multiplier:
|
|
1067
|
+
cost: sonnetCost,
|
|
1068
|
+
multiplier: 3,
|
|
854
1069
|
releaseDate: "2026-06-30"
|
|
855
1070
|
}),
|
|
856
1071
|
"claude-opus-4-5": defineModel({
|
|
@@ -950,10 +1165,21 @@ var defaultModels = {
|
|
|
950
1165
|
multiplier: 10,
|
|
951
1166
|
releaseDate: "2026-06-09"
|
|
952
1167
|
}),
|
|
953
|
-
|
|
954
|
-
|
|
955
|
-
|
|
956
|
-
|
|
1168
|
+
"claude-fable-5-1": defineModel({
|
|
1169
|
+
id: "claude-fable-5-1",
|
|
1170
|
+
name: "Claude Fable 5.1",
|
|
1171
|
+
family: "fable",
|
|
1172
|
+
reasoning: true,
|
|
1173
|
+
context: 1e6,
|
|
1174
|
+
output: 128e3,
|
|
1175
|
+
cost: fable51Cost,
|
|
1176
|
+
multiplier: 10,
|
|
1177
|
+
releaseDate: "2026-09-01"
|
|
1178
|
+
}),
|
|
1179
|
+
// Mythos 5 and 5.1 share the corresponding Fable models' capabilities and
|
|
1180
|
+
// pricing without the safety classifiers; limited availability via Project
|
|
1181
|
+
// Glasswing. `claude --model` simply errors for accounts without access, so
|
|
1182
|
+
// they are safe to register unconditionally.
|
|
957
1183
|
"claude-mythos-5": defineModel({
|
|
958
1184
|
id: "claude-mythos-5",
|
|
959
1185
|
name: "Claude Mythos 5",
|
|
@@ -964,6 +1190,17 @@ var defaultModels = {
|
|
|
964
1190
|
cost: fableCost,
|
|
965
1191
|
multiplier: 10,
|
|
966
1192
|
releaseDate: "2026-06-09"
|
|
1193
|
+
}),
|
|
1194
|
+
"claude-mythos-5-1": defineModel({
|
|
1195
|
+
id: "claude-mythos-5-1",
|
|
1196
|
+
name: "Claude Mythos 5.1",
|
|
1197
|
+
family: "mythos",
|
|
1198
|
+
reasoning: true,
|
|
1199
|
+
context: 1e6,
|
|
1200
|
+
output: 128e3,
|
|
1201
|
+
cost: fable51Cost,
|
|
1202
|
+
multiplier: 10,
|
|
1203
|
+
releaseDate: "2026-09-01"
|
|
967
1204
|
})
|
|
968
1205
|
};
|
|
969
1206
|
var FAST_SUFFIX = "-fast";
|
|
@@ -976,6 +1213,142 @@ function parseModelId(modelId) {
|
|
|
976
1213
|
return { model: base.slice(0, -FAST_SUFFIX.length) + account, fast: true };
|
|
977
1214
|
}
|
|
978
1215
|
|
|
1216
|
+
// src/agent-models.ts
|
|
1217
|
+
var AGENT_DIR_NAMES = ["agents", "agent"];
|
|
1218
|
+
var REASONING_EFFORTS = [
|
|
1219
|
+
"minimal",
|
|
1220
|
+
"low",
|
|
1221
|
+
"medium",
|
|
1222
|
+
"high",
|
|
1223
|
+
"xhigh",
|
|
1224
|
+
"max"
|
|
1225
|
+
];
|
|
1226
|
+
var registry = {};
|
|
1227
|
+
var defaultSubagentModel;
|
|
1228
|
+
function setAgentRegistry(records) {
|
|
1229
|
+
registry = records;
|
|
1230
|
+
}
|
|
1231
|
+
function getAgentRegistry() {
|
|
1232
|
+
return registry;
|
|
1233
|
+
}
|
|
1234
|
+
function setDefaultSubagentModel(model) {
|
|
1235
|
+
defaultSubagentModel = model?.trim() || void 0;
|
|
1236
|
+
}
|
|
1237
|
+
function getDefaultSubagentModel() {
|
|
1238
|
+
return defaultSubagentModel;
|
|
1239
|
+
}
|
|
1240
|
+
function accountMarker(modelId) {
|
|
1241
|
+
const at = modelId.indexOf("@");
|
|
1242
|
+
return at === -1 ? "" : modelId.slice(at);
|
|
1243
|
+
}
|
|
1244
|
+
function withoutAccountMarker(modelId) {
|
|
1245
|
+
const at = modelId.indexOf("@");
|
|
1246
|
+
return at === -1 ? modelId : modelId.slice(0, at);
|
|
1247
|
+
}
|
|
1248
|
+
function resolveAgentModel(agent, modelId, overrides) {
|
|
1249
|
+
if (!agent) return modelId;
|
|
1250
|
+
const record = (overrides?.records ?? registry)[agent];
|
|
1251
|
+
if (!record) return modelId;
|
|
1252
|
+
if (record.model?.includes("/")) return modelId;
|
|
1253
|
+
const fallback = overrides ? overrides.defaultSubagentModel : defaultSubagentModel;
|
|
1254
|
+
const declared = record.forceModel?.trim();
|
|
1255
|
+
const wanted = declared || (record.mode === "subagent" ? fallback : void 0);
|
|
1256
|
+
if (!wanted) return modelId;
|
|
1257
|
+
const base = withoutAccountMarker(wanted);
|
|
1258
|
+
if (!Object.hasOwn(defaultModels, base)) {
|
|
1259
|
+
log.warn("agent model override refused: unknown model", {
|
|
1260
|
+
agent,
|
|
1261
|
+
wanted: base,
|
|
1262
|
+
keeping: modelId
|
|
1263
|
+
});
|
|
1264
|
+
return modelId;
|
|
1265
|
+
}
|
|
1266
|
+
const resolved = `${base}${accountMarker(modelId)}`;
|
|
1267
|
+
if (resolved !== modelId) {
|
|
1268
|
+
log.debug("agent model override", { agent, from: modelId, to: resolved });
|
|
1269
|
+
}
|
|
1270
|
+
return resolved;
|
|
1271
|
+
}
|
|
1272
|
+
function resolveAgentEffort(agent, inherited, overrides) {
|
|
1273
|
+
if (!agent) return inherited;
|
|
1274
|
+
const record = (overrides?.records ?? registry)[agent];
|
|
1275
|
+
const declared = record?.reasoningEffort?.trim();
|
|
1276
|
+
if (!declared) return inherited;
|
|
1277
|
+
if (!REASONING_EFFORTS.includes(declared)) {
|
|
1278
|
+
log.warn("agent effort override refused: unknown level", {
|
|
1279
|
+
agent,
|
|
1280
|
+
wanted: declared,
|
|
1281
|
+
keeping: inherited
|
|
1282
|
+
});
|
|
1283
|
+
return inherited;
|
|
1284
|
+
}
|
|
1285
|
+
if (declared !== inherited) {
|
|
1286
|
+
log.debug("agent effort override", {
|
|
1287
|
+
agent,
|
|
1288
|
+
from: inherited,
|
|
1289
|
+
to: declared
|
|
1290
|
+
});
|
|
1291
|
+
}
|
|
1292
|
+
return declared;
|
|
1293
|
+
}
|
|
1294
|
+
function parseAgentFrontmatter(text) {
|
|
1295
|
+
const record = {};
|
|
1296
|
+
if (!text.startsWith("---")) return record;
|
|
1297
|
+
const lines = text.split(/\r?\n/);
|
|
1298
|
+
for (let i = 1; i < lines.length; i++) {
|
|
1299
|
+
const line = lines[i];
|
|
1300
|
+
if (line.trim() === "---") break;
|
|
1301
|
+
const match = /^([A-Za-z_][A-Za-z0-9_-]*):[ \t]*(.*)$/.exec(line);
|
|
1302
|
+
if (!match) continue;
|
|
1303
|
+
const key = match[1];
|
|
1304
|
+
if (key !== "mode" && key !== "model" && key !== "forceModel" && key !== "reasoningEffort")
|
|
1305
|
+
continue;
|
|
1306
|
+
const value = match[2].trim().replace(/^["']|["']$/g, "");
|
|
1307
|
+
if (value) record[key] = value;
|
|
1308
|
+
}
|
|
1309
|
+
return record;
|
|
1310
|
+
}
|
|
1311
|
+
async function readAgentMarkdownRecords(directories) {
|
|
1312
|
+
const records = {};
|
|
1313
|
+
for (const directory of directories) {
|
|
1314
|
+
let entries;
|
|
1315
|
+
try {
|
|
1316
|
+
entries = await readdir(directory);
|
|
1317
|
+
} catch {
|
|
1318
|
+
continue;
|
|
1319
|
+
}
|
|
1320
|
+
for (const entry of entries) {
|
|
1321
|
+
if (!entry.endsWith(".md")) continue;
|
|
1322
|
+
const name = entry.slice(0, -3);
|
|
1323
|
+
if (records[name]) continue;
|
|
1324
|
+
try {
|
|
1325
|
+
const text = await readFile(path.join(directory, entry), "utf8");
|
|
1326
|
+
records[name] = parseAgentFrontmatter(text);
|
|
1327
|
+
} catch (err) {
|
|
1328
|
+
log.debug("failed to read agent markdown", {
|
|
1329
|
+
file: path.join(directory, entry),
|
|
1330
|
+
error: String(err)
|
|
1331
|
+
});
|
|
1332
|
+
}
|
|
1333
|
+
}
|
|
1334
|
+
}
|
|
1335
|
+
return records;
|
|
1336
|
+
}
|
|
1337
|
+
function agentDirectories(home, projectDirectory) {
|
|
1338
|
+
const directories = [];
|
|
1339
|
+
if (projectDirectory) {
|
|
1340
|
+
for (const name of AGENT_DIR_NAMES) {
|
|
1341
|
+
directories.push(path.join(projectDirectory, ".opencode", name));
|
|
1342
|
+
}
|
|
1343
|
+
}
|
|
1344
|
+
if (home) {
|
|
1345
|
+
for (const name of AGENT_DIR_NAMES) {
|
|
1346
|
+
directories.push(path.join(home, ".config", "opencode", name));
|
|
1347
|
+
}
|
|
1348
|
+
}
|
|
1349
|
+
return directories;
|
|
1350
|
+
}
|
|
1351
|
+
|
|
979
1352
|
// src/plan-mode-question.ts
|
|
980
1353
|
var QUESTION_TOOL_NAME = "question";
|
|
981
1354
|
var APPROVED_EXIT_PLAN_MODE_MESSAGE = "User has approved your plan. You can now start coding. Start with updating your todo list if applicable.";
|
|
@@ -999,6 +1372,10 @@ function clearExitPlanModeQuestions(sessionKey2) {
|
|
|
999
1372
|
if (key.startsWith(prefix)) pendingQuestions.delete(key);
|
|
1000
1373
|
}
|
|
1001
1374
|
}
|
|
1375
|
+
function hasExitPlanModeQuestions(sessionKey2) {
|
|
1376
|
+
const prefix = `${sessionKey2}${KEY_SEPARATOR}`;
|
|
1377
|
+
return [...pendingQuestions.keys()].some((key) => key.startsWith(prefix));
|
|
1378
|
+
}
|
|
1002
1379
|
function createExitPlanModeQuestionCall(sessionKey2, exitPlanModeToolUseId, plan, questionToolCallId = `exit_plan_question_${exitPlanModeToolUseId}`) {
|
|
1003
1380
|
pendingQuestions.set(pendingKey(sessionKey2, questionToolCallId), exitPlanModeToolUseId);
|
|
1004
1381
|
return {
|
|
@@ -1133,7 +1510,7 @@ function consumeExitPlanModeQuestionResult(sessionKey2, prompt) {
|
|
|
1133
1510
|
|
|
1134
1511
|
// src/mcp-bridge.ts
|
|
1135
1512
|
import * as fs2 from "fs";
|
|
1136
|
-
import * as
|
|
1513
|
+
import * as path3 from "path";
|
|
1137
1514
|
import * as os2 from "os";
|
|
1138
1515
|
import * as crypto from "crypto";
|
|
1139
1516
|
import {
|
|
@@ -1144,8 +1521,8 @@ import {
|
|
|
1144
1521
|
// src/tmp.ts
|
|
1145
1522
|
import * as fs from "fs";
|
|
1146
1523
|
import * as os from "os";
|
|
1147
|
-
import * as
|
|
1148
|
-
var PLUGIN_TMP_DIR =
|
|
1524
|
+
import * as path2 from "path";
|
|
1525
|
+
var PLUGIN_TMP_DIR = path2.join(
|
|
1149
1526
|
os.tmpdir(),
|
|
1150
1527
|
`opencode-claude-code-${process.pid}`
|
|
1151
1528
|
);
|
|
@@ -1221,14 +1598,14 @@ function deepMerge(target, source) {
|
|
|
1221
1598
|
}
|
|
1222
1599
|
function walkUp(opts) {
|
|
1223
1600
|
const out = [];
|
|
1224
|
-
let current =
|
|
1601
|
+
let current = path3.resolve(opts.start);
|
|
1225
1602
|
while (true) {
|
|
1226
1603
|
for (const target of opts.targets) {
|
|
1227
|
-
const candidate =
|
|
1604
|
+
const candidate = path3.join(current, target);
|
|
1228
1605
|
if (opts.predicate(candidate)) out.push(candidate);
|
|
1229
1606
|
}
|
|
1230
|
-
if (opts.stop && current ===
|
|
1231
|
-
const parent =
|
|
1607
|
+
if (opts.stop && current === path3.resolve(opts.stop)) break;
|
|
1608
|
+
const parent = path3.dirname(current);
|
|
1232
1609
|
if (parent === current) break;
|
|
1233
1610
|
current = parent;
|
|
1234
1611
|
}
|
|
@@ -1236,28 +1613,28 @@ function walkUp(opts) {
|
|
|
1236
1613
|
}
|
|
1237
1614
|
function detectWorktree(cwd) {
|
|
1238
1615
|
const override = process.env.OPENCODE_WORKTREE;
|
|
1239
|
-
if (override) return
|
|
1240
|
-
let current =
|
|
1616
|
+
if (override) return path3.resolve(override);
|
|
1617
|
+
let current = path3.resolve(cwd);
|
|
1241
1618
|
while (true) {
|
|
1242
|
-
const gitPath =
|
|
1619
|
+
const gitPath = path3.join(current, ".git");
|
|
1243
1620
|
try {
|
|
1244
1621
|
if (fs2.existsSync(gitPath)) return current;
|
|
1245
1622
|
} catch {
|
|
1246
1623
|
}
|
|
1247
|
-
const parent =
|
|
1624
|
+
const parent = path3.dirname(current);
|
|
1248
1625
|
if (parent === current) return void 0;
|
|
1249
1626
|
current = parent;
|
|
1250
1627
|
}
|
|
1251
1628
|
}
|
|
1252
1629
|
function globalConfigDir() {
|
|
1253
|
-
const xdg = process.env.XDG_CONFIG_HOME ??
|
|
1254
|
-
return
|
|
1630
|
+
const xdg = process.env.XDG_CONFIG_HOME ?? path3.join(os2.homedir(), ".config");
|
|
1631
|
+
return path3.join(xdg, "opencode");
|
|
1255
1632
|
}
|
|
1256
1633
|
function loadGlobalConfig() {
|
|
1257
1634
|
const dir = globalConfigDir();
|
|
1258
1635
|
let merged = {};
|
|
1259
1636
|
for (const name of FILE_NAMES.slice().reverse()) {
|
|
1260
|
-
const file =
|
|
1637
|
+
const file = path3.join(dir, name);
|
|
1261
1638
|
if (!fileExists(file)) continue;
|
|
1262
1639
|
const parsed = readAndParse(file);
|
|
1263
1640
|
if (parsed) merged = deepMerge(merged, parsed);
|
|
@@ -1267,7 +1644,7 @@ function loadGlobalConfig() {
|
|
|
1267
1644
|
function loadProjectFilesInDir(dir) {
|
|
1268
1645
|
let merged = {};
|
|
1269
1646
|
for (const name of PROJECT_FILE_NAMES) {
|
|
1270
|
-
const file =
|
|
1647
|
+
const file = path3.join(dir, name);
|
|
1271
1648
|
if (!fileExists(file)) continue;
|
|
1272
1649
|
const parsed = readAndParse(file);
|
|
1273
1650
|
if (parsed) merged = deepMerge(merged, parsed);
|
|
@@ -1278,7 +1655,7 @@ function dotOpencodeDirs(cwd, worktree) {
|
|
|
1278
1655
|
const dirs = [];
|
|
1279
1656
|
const seen = /* @__PURE__ */ new Set();
|
|
1280
1657
|
const push = (p) => {
|
|
1281
|
-
const abs =
|
|
1658
|
+
const abs = path3.resolve(p);
|
|
1282
1659
|
if (!seen.has(abs) && dirExists(abs)) {
|
|
1283
1660
|
seen.add(abs);
|
|
1284
1661
|
dirs.push(abs);
|
|
@@ -1294,7 +1671,7 @@ function dotOpencodeDirs(cwd, worktree) {
|
|
|
1294
1671
|
}
|
|
1295
1672
|
const home = os2.homedir();
|
|
1296
1673
|
if (home) {
|
|
1297
|
-
const homeDot =
|
|
1674
|
+
const homeDot = path3.join(home, ".opencode");
|
|
1298
1675
|
if (dirExists(homeDot)) push(homeDot);
|
|
1299
1676
|
}
|
|
1300
1677
|
const envDir = process.env.OPENCODE_CONFIG_DIR;
|
|
@@ -1419,7 +1796,7 @@ function mergeOpencodeMcp(cwd, runtimeStatus) {
|
|
|
1419
1796
|
const projectDirs = [];
|
|
1420
1797
|
const seenProjectDirs = /* @__PURE__ */ new Set();
|
|
1421
1798
|
for (const f of projectFiles) {
|
|
1422
|
-
const d =
|
|
1799
|
+
const d = path3.dirname(f);
|
|
1423
1800
|
if (!seenProjectDirs.has(d)) {
|
|
1424
1801
|
seenProjectDirs.add(d);
|
|
1425
1802
|
projectDirs.push(d);
|
|
@@ -1464,7 +1841,7 @@ function finishBridge(input) {
|
|
|
1464
1841
|
};
|
|
1465
1842
|
}
|
|
1466
1843
|
const body = JSON.stringify({ mcpServers: servers }, null, 2);
|
|
1467
|
-
const outPath =
|
|
1844
|
+
const outPath = path3.join(
|
|
1468
1845
|
pluginTmpDir(),
|
|
1469
1846
|
`mcp-${hash}.json`
|
|
1470
1847
|
);
|
|
@@ -1576,1718 +1953,1807 @@ async function fetchOpencodeToolList(provider, model, directory) {
|
|
|
1576
1953
|
// src/session-manager.ts
|
|
1577
1954
|
import { spawn } from "child_process";
|
|
1578
1955
|
import { createInterface } from "readline";
|
|
1579
|
-
import { EventEmitter } from "events";
|
|
1956
|
+
import { EventEmitter as EventEmitter3 } from "events";
|
|
1580
1957
|
import { unlink } from "fs/promises";
|
|
1581
1958
|
|
|
1582
|
-
// src/
|
|
1583
|
-
import {
|
|
1584
|
-
import { promisify } from "util";
|
|
1585
|
-
var execFileAsync = promisify(execFile);
|
|
1586
|
-
var cache = /* @__PURE__ */ new Map();
|
|
1587
|
-
function detectCliVersion(cliPath) {
|
|
1588
|
-
const cached = cache.get(cliPath);
|
|
1589
|
-
if (cached) return cached;
|
|
1590
|
-
const promise = (async () => {
|
|
1591
|
-
try {
|
|
1592
|
-
const { stdout } = await execFileAsync(cliPath, ["--version"], {
|
|
1593
|
-
timeout: 5e3
|
|
1594
|
-
});
|
|
1595
|
-
const match = /(\d+)\.(\d+)\.(\d+)/.exec(stdout.trim());
|
|
1596
|
-
if (!match) {
|
|
1597
|
-
log.warn("claude --version output unparseable", { stdout: stdout.trim() });
|
|
1598
|
-
return null;
|
|
1599
|
-
}
|
|
1600
|
-
const v = {
|
|
1601
|
-
major: Number(match[1]),
|
|
1602
|
-
minor: Number(match[2]),
|
|
1603
|
-
patch: Number(match[3]),
|
|
1604
|
-
raw: stdout.trim()
|
|
1605
|
-
};
|
|
1606
|
-
log.info("detected claude cli version", { cliPath, version: v.raw });
|
|
1607
|
-
if (!cliSupportsThinkingDisplay(v)) {
|
|
1608
|
-
log.notice(
|
|
1609
|
-
"claude cli < 2.1.142 detected; Opus 4.7 thinking summaries unavailable. Run `npm i -g @anthropic-ai/claude-code` to upgrade.",
|
|
1610
|
-
{ version: v.raw }
|
|
1611
|
-
);
|
|
1612
|
-
}
|
|
1613
|
-
return v;
|
|
1614
|
-
} catch (err) {
|
|
1615
|
-
log.warn("failed to detect claude cli version", {
|
|
1616
|
-
cliPath,
|
|
1617
|
-
error: err instanceof Error ? err.message : String(err)
|
|
1618
|
-
});
|
|
1619
|
-
return null;
|
|
1620
|
-
}
|
|
1621
|
-
})();
|
|
1622
|
-
cache.set(cliPath, promise);
|
|
1623
|
-
return promise;
|
|
1624
|
-
}
|
|
1625
|
-
function gte(v, target) {
|
|
1626
|
-
if (v.major !== target.major) return v.major > target.major;
|
|
1627
|
-
if (v.minor !== target.minor) return v.minor > target.minor;
|
|
1628
|
-
return v.patch >= target.patch;
|
|
1629
|
-
}
|
|
1630
|
-
function cliSupportsThinkingDisplay(v) {
|
|
1631
|
-
if (!v) return false;
|
|
1632
|
-
return gte(v, { major: 2, minor: 1, patch: 142 });
|
|
1633
|
-
}
|
|
1634
|
-
function cliSupportsFastMode(v) {
|
|
1635
|
-
if (!v) return false;
|
|
1636
|
-
return gte(v, { major: 2, minor: 1, patch: 220 });
|
|
1637
|
-
}
|
|
1638
|
-
function cliSupportsThinking(v) {
|
|
1639
|
-
if (!v) return false;
|
|
1640
|
-
return gte(v, { major: 2, minor: 0, patch: 0 });
|
|
1641
|
-
}
|
|
1959
|
+
// src/proxy-broker.ts
|
|
1960
|
+
import { EventEmitter as EventEmitter2 } from "events";
|
|
1642
1961
|
|
|
1643
|
-
// src/
|
|
1644
|
-
|
|
1645
|
-
|
|
1646
|
-
|
|
1647
|
-
|
|
1648
|
-
|
|
1649
|
-
|
|
1650
|
-
|
|
1651
|
-
|
|
1652
|
-
if (!normalized) return false;
|
|
1653
|
-
return !["0", "false", "no", "off"].includes(normalized);
|
|
1962
|
+
// src/proxy-mcp.ts
|
|
1963
|
+
import { createServer } from "http";
|
|
1964
|
+
import * as fs3 from "fs";
|
|
1965
|
+
import * as path4 from "path";
|
|
1966
|
+
import * as crypto2 from "crypto";
|
|
1967
|
+
import { EventEmitter } from "events";
|
|
1968
|
+
var SSE_KEEPALIVE_MS = 15e3;
|
|
1969
|
+
function acceptsEventStream(acceptHeader) {
|
|
1970
|
+
return typeof acceptHeader === "string" && acceptHeader.toLowerCase().includes("text/event-stream");
|
|
1654
1971
|
}
|
|
1655
|
-
|
|
1656
|
-
|
|
1972
|
+
var SERVER_CLOSED_MESSAGE = "proxy MCP server closed";
|
|
1973
|
+
function isExpectedCleanupError(message) {
|
|
1974
|
+
return message.includes("timed out after") && message.includes("waiting for opencode to resolve") || message.includes("rejecting as orphaned") || message.includes("was orphaned by a new user turn") || message.includes("stream was aborted") || message.includes(SERVER_CLOSED_MESSAGE);
|
|
1657
1975
|
}
|
|
1658
|
-
|
|
1659
|
-
|
|
1660
|
-
|
|
1661
|
-
|
|
1662
|
-
|
|
1663
|
-
|
|
1664
|
-
|
|
1665
|
-
|
|
1976
|
+
var PROTOCOL_VERSION = "2024-11-05";
|
|
1977
|
+
var SERVER_NAME = "opencode_proxy";
|
|
1978
|
+
var PROXY_TOOL_PREFIX = `mcp__${SERVER_NAME}__`;
|
|
1979
|
+
var PROXY_DEFAULT_TIMEOUT_MS = 10 * 60 * 1e3;
|
|
1980
|
+
var PROXY_PER_TOOL_DEFAULT_TIMEOUT_MS = {
|
|
1981
|
+
task: 60 * 60 * 1e3,
|
|
1982
|
+
// 60 min
|
|
1983
|
+
question: 30 * 60 * 1e3
|
|
1984
|
+
// 30 min
|
|
1985
|
+
};
|
|
1986
|
+
var MAX_PROXY_TIMEOUT_MS = 2 ** 31 - 1;
|
|
1987
|
+
function resolveProxyCallTimeoutMs(toolName, input, overrides) {
|
|
1988
|
+
const key = toolName.toLowerCase();
|
|
1989
|
+
let ms = PROXY_PER_TOOL_DEFAULT_TIMEOUT_MS[key] ?? PROXY_DEFAULT_TIMEOUT_MS;
|
|
1990
|
+
if (overrides) {
|
|
1991
|
+
const ov = lookupCaseInsensitive(overrides, key);
|
|
1992
|
+
if (typeof ov === "number" && ov > 0) ms = ov;
|
|
1666
1993
|
}
|
|
1667
|
-
if (
|
|
1668
|
-
|
|
1994
|
+
if (key === "bash") {
|
|
1995
|
+
const requested = input?.timeout;
|
|
1996
|
+
if (typeof requested === "number" && requested > ms) ms = requested;
|
|
1669
1997
|
}
|
|
1670
|
-
return
|
|
1998
|
+
return Math.min(ms, MAX_PROXY_TIMEOUT_MS);
|
|
1671
1999
|
}
|
|
1672
|
-
function
|
|
1673
|
-
|
|
1674
|
-
|
|
1675
|
-
|
|
1676
|
-
activeProcesses.set(key, existing);
|
|
2000
|
+
function lookupCaseInsensitive(map, key) {
|
|
2001
|
+
if (Object.prototype.hasOwnProperty.call(map, key)) return map[key];
|
|
2002
|
+
for (const k of Object.keys(map)) {
|
|
2003
|
+
if (k.toLowerCase() === key) return map[k];
|
|
1677
2004
|
}
|
|
2005
|
+
return void 0;
|
|
1678
2006
|
}
|
|
1679
|
-
function
|
|
1680
|
-
|
|
1681
|
-
|
|
1682
|
-
if (
|
|
1683
|
-
log.info("evicting LRU claude process", { sessionKey: oldestKey });
|
|
1684
|
-
deleteActiveProcess(oldestKey);
|
|
2007
|
+
function resolveProxyClientCeilingMs(overrides) {
|
|
2008
|
+
let ms = PROXY_DEFAULT_TIMEOUT_MS;
|
|
2009
|
+
for (const v of Object.values(PROXY_PER_TOOL_DEFAULT_TIMEOUT_MS)) {
|
|
2010
|
+
if (v > ms) ms = v;
|
|
1685
2011
|
}
|
|
2012
|
+
if (overrides) {
|
|
2013
|
+
for (const v of Object.values(overrides)) {
|
|
2014
|
+
if (typeof v === "number" && v > ms) ms = v;
|
|
2015
|
+
}
|
|
2016
|
+
}
|
|
2017
|
+
return Math.min(ms, MAX_PROXY_TIMEOUT_MS);
|
|
1686
2018
|
}
|
|
1687
|
-
function
|
|
1688
|
-
const
|
|
1689
|
-
|
|
1690
|
-
|
|
1691
|
-
|
|
1692
|
-
|
|
1693
|
-
|
|
1694
|
-
}
|
|
1695
|
-
|
|
1696
|
-
const ap = activeProcesses.get(key);
|
|
1697
|
-
if (!ap) return void 0;
|
|
1698
|
-
activeProcesses.delete(key);
|
|
1699
|
-
void ap.proxyServer?.close();
|
|
1700
|
-
return ap;
|
|
1701
|
-
}
|
|
1702
|
-
function deleteActiveProcess(key) {
|
|
1703
|
-
const ap = detachActiveProcess(key);
|
|
1704
|
-
ap?.proc.kill();
|
|
1705
|
-
}
|
|
1706
|
-
function hasProcessExited(proc) {
|
|
1707
|
-
return proc.exitCode !== null || proc.signalCode !== null;
|
|
2019
|
+
function buildProxyTimeoutError(toolName, ms) {
|
|
2020
|
+
const key = toolName.toLowerCase();
|
|
2021
|
+
const base = `Proxy tool '${toolName}' timed out after ${ms}ms waiting for opencode to resolve the call`;
|
|
2022
|
+
if (key === "task") {
|
|
2023
|
+
return new Error(
|
|
2024
|
+
base + " (the subagent). The subagent may still be running but its result is no longer reachable in this session. Do not declare the dispatch failed, and do not 'schedule a wake-up' or defer -- that mechanism does not apply here. If the result is required, re-dispatch or verify it directly now."
|
|
2025
|
+
);
|
|
2026
|
+
}
|
|
2027
|
+
return new Error(base);
|
|
1708
2028
|
}
|
|
1709
|
-
|
|
1710
|
-
|
|
1711
|
-
|
|
1712
|
-
|
|
1713
|
-
|
|
1714
|
-
|
|
1715
|
-
|
|
1716
|
-
|
|
1717
|
-
|
|
1718
|
-
|
|
1719
|
-
|
|
1720
|
-
|
|
1721
|
-
|
|
2029
|
+
var TASK_PROXY_NOTE = "This is the ONLY tool that dispatches 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 \u2014 invalid types fail fast with a clear error. Foreground calls block until the subagent finishes; set `background` to request opencode's background execution mode. Task calls get a 60-minute proxy deadline by default (configurable via proxyToolTimeoutMs).";
|
|
2030
|
+
var AGENT_TYPES_HEADING = "Available agent types";
|
|
2031
|
+
var AGENT_BLURB_LIMIT = 140;
|
|
2032
|
+
var QUESTION_PROXY_NOTE = "This routes structured questions through opencode's native `question` tool, which renders a TUI form with the options you provide and blocks until the operator answers. Claude Code's built-in AskUserQuestion is disabled in this environment; this proxy is the ONLY way to ask the operator for a decision or clarification. Answers come back as arrays of selected labels (set `multiple: true` to allow more than one). If the operator dismisses the form the call returns an error \u2014 treat that as 'no answer' and stop, do not guess. Question calls get a 30-minute proxy deadline by default (configurable via proxyToolTimeoutMs); for long-AFK scenarios prefer fewer, high-signal questions.";
|
|
2033
|
+
var COMPRESS_PROXY_NOTE = "The current turn continues normally after this call \u2014 finish what you are doing. The reset happens at the START of the next turn: the Claude Code session is discarded and a fresh one begins with your summary as its only prior context. Everything else, including tool output and files you read, is gone, so write the summary as the authoritative record. Call this once per compression, when older resolved work no longer needs full detail.";
|
|
2034
|
+
function extractAgentTypeList(liveDescription) {
|
|
2035
|
+
const live = liveDescription?.trim();
|
|
2036
|
+
if (!live) return void 0;
|
|
2037
|
+
const start = live.indexOf(AGENT_TYPES_HEADING);
|
|
2038
|
+
if (start === -1) return void 0;
|
|
2039
|
+
const entries = [];
|
|
2040
|
+
for (const raw of live.slice(start).split("\n")) {
|
|
2041
|
+
const match = /^-\s*([^:]+):\s*(.+)$/.exec(raw.trim());
|
|
2042
|
+
if (!match) continue;
|
|
2043
|
+
const name = match[1].trim();
|
|
2044
|
+
const blurb = match[2].trim();
|
|
2045
|
+
entries.push(
|
|
2046
|
+
`- ${name}: ${blurb.length > AGENT_BLURB_LIMIT ? `${blurb.slice(0, AGENT_BLURB_LIMIT).trimEnd()}\u2026` : blurb}`
|
|
2047
|
+
);
|
|
2048
|
+
}
|
|
2049
|
+
if (entries.length === 0) return void 0;
|
|
2050
|
+
return `Valid subagent_type values, from opencode's live registry \u2014 anything else fails:
|
|
2051
|
+
${entries.join("\n")}`;
|
|
1722
2052
|
}
|
|
1723
|
-
|
|
1724
|
-
const
|
|
1725
|
-
if (!
|
|
1726
|
-
|
|
1727
|
-
|
|
1728
|
-
|
|
1729
|
-
|
|
1730
|
-
ap.proc.kill();
|
|
1731
|
-
if (await gracefulExit) return true;
|
|
1732
|
-
const forcedExit = waitForProcessExit(
|
|
1733
|
-
ap.proc,
|
|
1734
|
-
options.forceExitTimeoutMs ?? PROCESS_FORCE_EXIT_TIMEOUT_MS
|
|
2053
|
+
function overlayTaskProxyDescription(tools, liveDescription) {
|
|
2054
|
+
const agentTypes = extractAgentTypeList(liveDescription);
|
|
2055
|
+
if (!agentTypes) return tools;
|
|
2056
|
+
return tools.map(
|
|
2057
|
+
(t) => t.name === "task" ? { ...t, description: `${agentTypes}
|
|
2058
|
+
|
|
2059
|
+
${t.description}` } : t
|
|
1735
2060
|
);
|
|
1736
|
-
ap.proc.kill("SIGKILL");
|
|
1737
|
-
if (await forcedExit) return true;
|
|
1738
|
-
log.warn("claude process did not exit; starting a fresh session", {
|
|
1739
|
-
sessionKey: key
|
|
1740
|
-
});
|
|
1741
|
-
deleteClaudeSessionId(key);
|
|
1742
|
-
return false;
|
|
1743
|
-
}
|
|
1744
|
-
function getClaudeSessionId(key) {
|
|
1745
|
-
return claudeSessions.get(key);
|
|
1746
2061
|
}
|
|
1747
|
-
function
|
|
1748
|
-
|
|
2062
|
+
function overlayQuestionProxyDescription(tools, liveDescription) {
|
|
2063
|
+
const live = liveDescription?.trim();
|
|
2064
|
+
if (!live) return tools;
|
|
2065
|
+
return tools.map(
|
|
2066
|
+
(t) => t.name === "question" ? { ...t, description: `${live}
|
|
2067
|
+
|
|
2068
|
+
${QUESTION_PROXY_NOTE}` } : t
|
|
2069
|
+
);
|
|
1749
2070
|
}
|
|
1750
|
-
function
|
|
1751
|
-
|
|
1752
|
-
|
|
1753
|
-
if (claudeSessionId) clearLedger(claudeSessionId);
|
|
1754
|
-
claudeSessions.delete(key);
|
|
2071
|
+
function filterQuestionProxyByOpencodeSupport(tools, opencodeHasQuestion) {
|
|
2072
|
+
if (opencodeHasQuestion) return tools;
|
|
2073
|
+
return tools.filter((t) => t.name !== "question");
|
|
1755
2074
|
}
|
|
1756
|
-
|
|
1757
|
-
|
|
1758
|
-
|
|
1759
|
-
|
|
1760
|
-
|
|
1761
|
-
|
|
1762
|
-
|
|
1763
|
-
|
|
1764
|
-
|
|
1765
|
-
|
|
1766
|
-
|
|
1767
|
-
|
|
1768
|
-
|
|
1769
|
-
|
|
1770
|
-
|
|
1771
|
-
|
|
1772
|
-
|
|
1773
|
-
|
|
1774
|
-
|
|
1775
|
-
|
|
1776
|
-
|
|
1777
|
-
mcpHash,
|
|
1778
|
-
systemPromptFile
|
|
1779
|
-
};
|
|
1780
|
-
activeProcesses.set(sessionKey2, ap);
|
|
1781
|
-
proc.on("error", (err) => {
|
|
1782
|
-
log.error("claude process error", { sessionKey: sessionKey2, error: err.message });
|
|
1783
|
-
});
|
|
1784
|
-
proc.on("exit", (code, signal) => {
|
|
1785
|
-
log.info("claude process exited", { code, signal, sessionKey: sessionKey2 });
|
|
1786
|
-
void proxyServer?.close();
|
|
1787
|
-
if (systemPromptFile) {
|
|
1788
|
-
void unlink(systemPromptFile).catch(() => {
|
|
1789
|
-
});
|
|
2075
|
+
var DEFAULT_PROXY_TOOLS = [
|
|
2076
|
+
{
|
|
2077
|
+
name: "bash",
|
|
2078
|
+
description: "Execute a shell command. Routed through opencode's bash tool so permission prompts flow through opencode's UI.",
|
|
2079
|
+
inputSchema: {
|
|
2080
|
+
type: "object",
|
|
2081
|
+
properties: {
|
|
2082
|
+
command: {
|
|
2083
|
+
type: "string",
|
|
2084
|
+
description: "The shell command to execute."
|
|
2085
|
+
},
|
|
2086
|
+
description: {
|
|
2087
|
+
type: "string",
|
|
2088
|
+
description: "Short human-readable description of what the command does."
|
|
2089
|
+
},
|
|
2090
|
+
timeout: {
|
|
2091
|
+
type: "number",
|
|
2092
|
+
description: "Optional timeout in milliseconds."
|
|
2093
|
+
}
|
|
2094
|
+
},
|
|
2095
|
+
required: ["command"]
|
|
1790
2096
|
}
|
|
1791
|
-
|
|
1792
|
-
|
|
1793
|
-
|
|
1794
|
-
|
|
1795
|
-
|
|
1796
|
-
|
|
1797
|
-
|
|
1798
|
-
|
|
2097
|
+
},
|
|
2098
|
+
{
|
|
2099
|
+
name: "write",
|
|
2100
|
+
description: "Write a file. Routed through opencode's write tool so permission prompts flow through opencode's UI.",
|
|
2101
|
+
inputSchema: {
|
|
2102
|
+
type: "object",
|
|
2103
|
+
properties: {
|
|
2104
|
+
filePath: {
|
|
2105
|
+
type: "string",
|
|
2106
|
+
description: "The file to write. Absolute paths are preferred."
|
|
2107
|
+
},
|
|
2108
|
+
content: {
|
|
2109
|
+
type: "string",
|
|
2110
|
+
description: "The full content to write to the file."
|
|
2111
|
+
}
|
|
2112
|
+
},
|
|
2113
|
+
required: ["filePath", "content"]
|
|
1799
2114
|
}
|
|
1800
|
-
}
|
|
1801
|
-
|
|
1802
|
-
|
|
1803
|
-
|
|
1804
|
-
|
|
1805
|
-
|
|
1806
|
-
|
|
1807
|
-
|
|
1808
|
-
|
|
1809
|
-
|
|
1810
|
-
|
|
1811
|
-
|
|
1812
|
-
|
|
1813
|
-
|
|
1814
|
-
}
|
|
1815
|
-
|
|
2115
|
+
},
|
|
2116
|
+
{
|
|
2117
|
+
name: "edit",
|
|
2118
|
+
description: "Replace text in an existing file. Routed through opencode's edit tool so permission prompts flow through opencode's UI.",
|
|
2119
|
+
inputSchema: {
|
|
2120
|
+
type: "object",
|
|
2121
|
+
properties: {
|
|
2122
|
+
filePath: {
|
|
2123
|
+
type: "string",
|
|
2124
|
+
description: "The file to edit. Absolute paths are preferred."
|
|
2125
|
+
},
|
|
2126
|
+
oldString: {
|
|
2127
|
+
type: "string",
|
|
2128
|
+
description: "The exact text to replace."
|
|
2129
|
+
},
|
|
2130
|
+
newString: {
|
|
2131
|
+
type: "string",
|
|
2132
|
+
description: "The replacement text."
|
|
2133
|
+
},
|
|
2134
|
+
replaceAll: {
|
|
2135
|
+
type: "boolean",
|
|
2136
|
+
description: "Replace all occurrences instead of just the first one."
|
|
2137
|
+
}
|
|
2138
|
+
},
|
|
2139
|
+
required: ["filePath", "oldString", "newString"]
|
|
1816
2140
|
}
|
|
1817
|
-
}
|
|
1818
|
-
|
|
1819
|
-
|
|
1820
|
-
|
|
1821
|
-
|
|
1822
|
-
|
|
1823
|
-
|
|
1824
|
-
|
|
1825
|
-
|
|
1826
|
-
|
|
1827
|
-
}
|
|
1828
|
-
|
|
1829
|
-
|
|
1830
|
-
|
|
1831
|
-
|
|
1832
|
-
|
|
1833
|
-
|
|
1834
|
-
|
|
1835
|
-
|
|
1836
|
-
|
|
1837
|
-
|
|
1838
|
-
|
|
1839
|
-
appendResumeIfNeeded(sessionKey2, cliArgs),
|
|
1840
|
-
cwd,
|
|
1841
|
-
sessionKey2,
|
|
1842
|
-
old.proxyServer,
|
|
1843
|
-
old.mcpHash,
|
|
1844
|
-
old.systemPromptFile,
|
|
1845
|
-
ignoreAnthropicApiKey
|
|
1846
|
-
);
|
|
1847
|
-
}
|
|
1848
|
-
function buildCliArgs(opts) {
|
|
1849
|
-
const {
|
|
1850
|
-
sessionKey: sessionKey2,
|
|
1851
|
-
skipPermissions,
|
|
1852
|
-
includeSessionId = true,
|
|
1853
|
-
model,
|
|
1854
|
-
permissionMode,
|
|
1855
|
-
mcpConfig,
|
|
1856
|
-
strictMcpConfig,
|
|
1857
|
-
disallowedTools,
|
|
1858
|
-
appendSystemPromptFile,
|
|
1859
|
-
thinking,
|
|
1860
|
-
thinkingDisplay,
|
|
1861
|
-
fastMode,
|
|
1862
|
-
cliVersion
|
|
1863
|
-
} = opts;
|
|
1864
|
-
const args = [
|
|
1865
|
-
"--print",
|
|
1866
|
-
"--output-format",
|
|
1867
|
-
"stream-json",
|
|
1868
|
-
"--input-format",
|
|
1869
|
-
"stream-json",
|
|
1870
|
-
"--include-partial-messages",
|
|
1871
|
-
"--verbose"
|
|
1872
|
-
];
|
|
1873
|
-
if (model) {
|
|
1874
|
-
args.push("--model", model);
|
|
1875
|
-
}
|
|
1876
|
-
if (permissionMode) {
|
|
1877
|
-
args.push("--permission-mode", permissionMode);
|
|
1878
|
-
}
|
|
1879
|
-
if (includeSessionId) {
|
|
1880
|
-
const sessionId = claudeSessions.get(sessionKey2);
|
|
1881
|
-
if (sessionId && !activeProcesses.has(sessionKey2)) {
|
|
1882
|
-
args.push("--resume", sessionId);
|
|
2141
|
+
},
|
|
2142
|
+
{
|
|
2143
|
+
name: "webfetch",
|
|
2144
|
+
description: "Fetch content from a URL. Routed through opencode's webfetch tool so permission prompts flow through opencode's UI. Returns the page content in the requested format.",
|
|
2145
|
+
inputSchema: {
|
|
2146
|
+
type: "object",
|
|
2147
|
+
properties: {
|
|
2148
|
+
url: {
|
|
2149
|
+
type: "string",
|
|
2150
|
+
description: "The URL to fetch content from. Must start with http:// or https://."
|
|
2151
|
+
},
|
|
2152
|
+
format: {
|
|
2153
|
+
type: "string",
|
|
2154
|
+
enum: ["text", "markdown", "html"],
|
|
2155
|
+
description: "The format to return the content in. Defaults to markdown."
|
|
2156
|
+
},
|
|
2157
|
+
timeout: {
|
|
2158
|
+
type: "number",
|
|
2159
|
+
description: "Optional timeout in seconds (max 120)."
|
|
2160
|
+
}
|
|
2161
|
+
},
|
|
2162
|
+
required: ["url"]
|
|
1883
2163
|
}
|
|
1884
|
-
}
|
|
1885
|
-
|
|
1886
|
-
|
|
1887
|
-
|
|
1888
|
-
|
|
1889
|
-
|
|
2164
|
+
},
|
|
2165
|
+
{
|
|
2166
|
+
name: "task",
|
|
2167
|
+
description: "Launch an opencode subagent to handle a complex multi-step task autonomously. Routed through opencode's task tool so subagent orchestration, permission, and lifecycle are handled by opencode. Use `subagent_type` to pick which configured subagent runs (e.g. `build`, `general`, `explore`, or any custom subagent declared in opencode.json). " + TASK_PROXY_NOTE,
|
|
2168
|
+
inputSchema: {
|
|
2169
|
+
type: "object",
|
|
2170
|
+
properties: {
|
|
2171
|
+
description: {
|
|
2172
|
+
type: "string",
|
|
2173
|
+
description: "A short (3-5 words) description of the task"
|
|
2174
|
+
},
|
|
2175
|
+
prompt: {
|
|
2176
|
+
type: "string",
|
|
2177
|
+
description: "The task for the agent to perform"
|
|
2178
|
+
},
|
|
2179
|
+
subagent_type: {
|
|
2180
|
+
type: "string",
|
|
2181
|
+
description: "The type of specialized agent to use for this task"
|
|
2182
|
+
},
|
|
2183
|
+
task_id: {
|
|
2184
|
+
type: "string",
|
|
2185
|
+
description: "Set this only if you mean to resume a previous task \u2014 pass the prior task_id to continue the same subagent session instead of creating a fresh one."
|
|
2186
|
+
},
|
|
2187
|
+
command: {
|
|
2188
|
+
type: "string",
|
|
2189
|
+
description: "The command that triggered this task"
|
|
2190
|
+
},
|
|
2191
|
+
background: {
|
|
2192
|
+
type: "boolean",
|
|
2193
|
+
description: "Run the task in the background when supported by opencode"
|
|
2194
|
+
}
|
|
2195
|
+
},
|
|
2196
|
+
required: ["description", "prompt", "subagent_type"]
|
|
2197
|
+
}
|
|
2198
|
+
},
|
|
2199
|
+
{
|
|
2200
|
+
name: "question",
|
|
2201
|
+
description: "Ask the operator structured questions with options and receive their answers back. Routed through opencode's native `question` tool so the prompt renders as a real TUI form (with options and a custom-answer field) instead of a plain text turn. Use this when you need a decision, clarification, or preference from the operator mid-task. " + QUESTION_PROXY_NOTE,
|
|
2202
|
+
inputSchema: {
|
|
2203
|
+
type: "object",
|
|
2204
|
+
properties: {
|
|
2205
|
+
questions: {
|
|
2206
|
+
type: "array",
|
|
2207
|
+
description: "Questions to ask.",
|
|
2208
|
+
items: {
|
|
2209
|
+
type: "object",
|
|
2210
|
+
properties: {
|
|
2211
|
+
question: {
|
|
2212
|
+
type: "string",
|
|
2213
|
+
description: "Complete question."
|
|
2214
|
+
},
|
|
2215
|
+
header: {
|
|
2216
|
+
type: "string",
|
|
2217
|
+
description: "Very short label (max 30 chars)."
|
|
2218
|
+
},
|
|
2219
|
+
options: {
|
|
2220
|
+
type: "array",
|
|
2221
|
+
description: "Available choices.",
|
|
2222
|
+
items: {
|
|
2223
|
+
type: "object",
|
|
2224
|
+
properties: {
|
|
2225
|
+
label: {
|
|
2226
|
+
type: "string",
|
|
2227
|
+
description: "Display text (1-5 words, concise)."
|
|
2228
|
+
},
|
|
2229
|
+
description: {
|
|
2230
|
+
type: "string",
|
|
2231
|
+
description: "Explanation of choice."
|
|
2232
|
+
}
|
|
2233
|
+
},
|
|
2234
|
+
required: ["label", "description"]
|
|
2235
|
+
}
|
|
2236
|
+
},
|
|
2237
|
+
multiple: {
|
|
2238
|
+
type: "boolean",
|
|
2239
|
+
description: "Allow selecting multiple choices. Defaults to false."
|
|
2240
|
+
}
|
|
2241
|
+
},
|
|
2242
|
+
required: ["question", "header", "options"]
|
|
2243
|
+
}
|
|
2244
|
+
}
|
|
2245
|
+
},
|
|
2246
|
+
required: ["questions"]
|
|
2247
|
+
}
|
|
2248
|
+
},
|
|
2249
|
+
{
|
|
2250
|
+
name: "compress",
|
|
2251
|
+
description: "Replace older conversation detail with a summary you write, then continue in a fresh Claude Code session. Handled inside the plugin, so it never prompts the operator. " + COMPRESS_PROXY_NOTE,
|
|
2252
|
+
inputSchema: {
|
|
2253
|
+
type: "object",
|
|
2254
|
+
properties: {
|
|
2255
|
+
summary: {
|
|
2256
|
+
type: "string",
|
|
2257
|
+
description: "Dense technical summary of the work being compressed: decisions made, files changed, commands run and their outcomes, and what is still open. This is the ONLY prior context that survives, so anything omitted is lost."
|
|
2258
|
+
}
|
|
2259
|
+
},
|
|
2260
|
+
required: ["summary"]
|
|
1890
2261
|
}
|
|
1891
2262
|
}
|
|
1892
|
-
|
|
1893
|
-
|
|
1894
|
-
|
|
1895
|
-
|
|
1896
|
-
|
|
1897
|
-
}
|
|
1898
|
-
|
|
1899
|
-
|
|
1900
|
-
|
|
1901
|
-
|
|
1902
|
-
|
|
2263
|
+
];
|
|
2264
|
+
async function createProxyMcpServer(tools = DEFAULT_PROXY_TOOLS, timeoutOverrides, interceptors) {
|
|
2265
|
+
const calls = new EventEmitter();
|
|
2266
|
+
const pending = /* @__PURE__ */ new Map();
|
|
2267
|
+
const authToken = crypto2.randomBytes(32).toString("hex");
|
|
2268
|
+
const expectedAuth = Buffer.from(`Bearer ${authToken}`);
|
|
2269
|
+
let boundAuthority = "";
|
|
2270
|
+
function authOk(req) {
|
|
2271
|
+
const got = req.headers.authorization;
|
|
2272
|
+
if (typeof got !== "string") return false;
|
|
2273
|
+
const candidate = Buffer.from(got);
|
|
2274
|
+
if (candidate.length !== expectedAuth.length) return false;
|
|
2275
|
+
return crypto2.timingSafeEqual(candidate, expectedAuth);
|
|
1903
2276
|
}
|
|
1904
|
-
|
|
1905
|
-
|
|
2277
|
+
function reject(req, res, statusCode, reason) {
|
|
2278
|
+
log.notice("proxy-mcp rejected a request", {
|
|
2279
|
+
statusCode,
|
|
2280
|
+
reason,
|
|
2281
|
+
method: req.method,
|
|
2282
|
+
hasAuthorization: typeof req.headers.authorization === "string"
|
|
2283
|
+
});
|
|
2284
|
+
res.statusCode = statusCode;
|
|
2285
|
+
res.setHeader("Connection", "close");
|
|
2286
|
+
res.on("finish", () => {
|
|
2287
|
+
req.socket?.destroy();
|
|
2288
|
+
});
|
|
2289
|
+
res.end();
|
|
1906
2290
|
}
|
|
1907
|
-
|
|
1908
|
-
|
|
1909
|
-
|
|
1910
|
-
|
|
1911
|
-
args.push("--dangerously-skip-permissions");
|
|
1912
|
-
}
|
|
1913
|
-
return args;
|
|
1914
|
-
}
|
|
1915
|
-
function sessionKey(cwd, modelId) {
|
|
1916
|
-
return `${cwd}::${modelId}`;
|
|
1917
|
-
}
|
|
1918
|
-
|
|
1919
|
-
// src/claude-session-wrapper.ts
|
|
1920
|
-
import { EventEmitter as EventEmitter2 } from "events";
|
|
1921
|
-
import { unlink as unlink2 } from "fs/promises";
|
|
1922
|
-
|
|
1923
|
-
// src/claude-session-bun.ts
|
|
1924
|
-
import * as os3 from "os";
|
|
1925
|
-
import * as fs3 from "fs";
|
|
1926
|
-
import * as path3 from "path";
|
|
1927
|
-
import { execFileSync } from "child_process";
|
|
1928
|
-
import { randomUUID } from "crypto";
|
|
1929
|
-
function resolveClaude(cmd = "claude") {
|
|
1930
|
-
if (path3.isAbsolute(cmd) && fs3.existsSync(cmd)) return cmd;
|
|
1931
|
-
const viaBun = Bun.which(cmd);
|
|
1932
|
-
if (viaBun) return viaBun;
|
|
1933
|
-
const isWin = os3.platform() === "win32";
|
|
1934
|
-
try {
|
|
1935
|
-
const out = execFileSync(isWin ? "where" : "which", [cmd], {
|
|
1936
|
-
encoding: "utf8",
|
|
1937
|
-
stdio: ["ignore", "pipe", "ignore"]
|
|
1938
|
-
});
|
|
1939
|
-
const first = out.split(/\r?\n/).map((l) => l.trim()).filter(Boolean).find((p) => fs3.existsSync(p));
|
|
1940
|
-
if (first) return first;
|
|
1941
|
-
} catch {
|
|
1942
|
-
}
|
|
1943
|
-
throw new Error(`Could not resolve command on PATH: ${cmd}`);
|
|
1944
|
-
}
|
|
1945
|
-
function encodeCwd(cwd) {
|
|
1946
|
-
return path3.resolve(cwd).replace(/[^a-zA-Z0-9]/g, "-");
|
|
1947
|
-
}
|
|
1948
|
-
var TERMINAL_STOP = /* @__PURE__ */ new Set(["end_turn", "stop_sequence", "max_tokens"]);
|
|
1949
|
-
var delay = (ms) => new Promise((r) => setTimeout(r, ms));
|
|
1950
|
-
function resolveConfigDir(configDir) {
|
|
1951
|
-
const value = configDir ?? process.env.CLAUDE_CONFIG_DIR;
|
|
1952
|
-
if (!value) return path3.join(os3.homedir(), ".claude");
|
|
1953
|
-
if (value === "~") return os3.homedir();
|
|
1954
|
-
if (value.startsWith("~/") || value.startsWith("~\\")) {
|
|
1955
|
-
return path3.join(os3.homedir(), value.slice(2));
|
|
1956
|
-
}
|
|
1957
|
-
return path3.resolve(value);
|
|
1958
|
-
}
|
|
1959
|
-
var ClaudeSession = class {
|
|
1960
|
-
sessionId;
|
|
1961
|
-
cwd;
|
|
1962
|
-
configDir;
|
|
1963
|
-
jsonlPath;
|
|
1964
|
-
raw = "";
|
|
1965
|
-
proc = null;
|
|
1966
|
-
cursor = 0;
|
|
1967
|
-
// index into transcript split('\n')
|
|
1968
|
-
lastDataAt = 0;
|
|
1969
|
-
exited = false;
|
|
1970
|
-
exitCode = null;
|
|
1971
|
-
aborted = false;
|
|
1972
|
-
signal;
|
|
1973
|
-
o;
|
|
1974
|
-
constructor(opts = {}) {
|
|
1975
|
-
this.cwd = path3.resolve(opts.cwd ?? process.cwd());
|
|
1976
|
-
this.configDir = resolveConfigDir(opts.configDir);
|
|
1977
|
-
this.signal = opts.signal;
|
|
1978
|
-
this.sessionId = randomUUID();
|
|
1979
|
-
this.jsonlPath = path3.join(
|
|
1980
|
-
this.configDir,
|
|
1981
|
-
"projects",
|
|
1982
|
-
encodeCwd(this.cwd),
|
|
1983
|
-
`${this.sessionId}.jsonl`
|
|
1984
|
-
);
|
|
1985
|
-
this.o = {
|
|
1986
|
-
cwd: this.cwd,
|
|
1987
|
-
cliPath: opts.cliPath,
|
|
1988
|
-
configDir: this.configDir,
|
|
1989
|
-
model: opts.model,
|
|
1990
|
-
settingSources: opts.settingSources,
|
|
1991
|
-
extraArgs: opts.extraArgs ?? [],
|
|
1992
|
-
ignoreAnthropicApiKey: opts.ignoreAnthropicApiKey,
|
|
1993
|
-
cols: opts.cols ?? 200,
|
|
1994
|
-
rows: opts.rows ?? 50,
|
|
1995
|
-
bootMinMs: opts.bootMinMs ?? 3e3,
|
|
1996
|
-
bootQuietMs: opts.bootQuietMs ?? 1500,
|
|
1997
|
-
bootMaxMs: opts.bootMaxMs ?? 25e3,
|
|
1998
|
-
pollMs: opts.pollMs ?? 250,
|
|
1999
|
-
// Agentic turns (tool loops) routinely run for many minutes; a short
|
|
2000
|
-
// cap would surface as a mid-task error result. 30 min mirrors the
|
|
2001
|
-
// proxy-tool ceiling rather than a chat-reply expectation.
|
|
2002
|
-
turnTimeoutMs: opts.turnTimeoutMs ?? 18e5,
|
|
2003
|
-
bracketedPaste: opts.bracketedPaste ?? true,
|
|
2004
|
-
submitMinMs: opts.submitMinMs ?? 200,
|
|
2005
|
-
submitConfirmMs: opts.submitConfirmMs ?? 1500,
|
|
2006
|
-
submitMaxRetries: opts.submitMaxRetries ?? 8,
|
|
2007
|
-
debug: opts.debug ?? false
|
|
2008
|
-
};
|
|
2009
|
-
}
|
|
2010
|
-
async start() {
|
|
2011
|
-
if (this.signal?.aborted) throw new Error("aborted before start");
|
|
2012
|
-
this.signal?.addEventListener(
|
|
2013
|
-
"abort",
|
|
2014
|
-
() => {
|
|
2015
|
-
this.aborted = true;
|
|
2016
|
-
this.dispose();
|
|
2017
|
-
},
|
|
2018
|
-
{ once: true }
|
|
2019
|
-
);
|
|
2020
|
-
const claude = resolveClaude(this.o.cliPath ?? "claude");
|
|
2021
|
-
const args = ["--session-id", this.sessionId];
|
|
2022
|
-
if (this.o.model) args.push("--model", this.o.model);
|
|
2023
|
-
if (this.o.settingSources !== null && this.o.settingSources !== void 0) {
|
|
2024
|
-
args.push("--setting-sources", this.o.settingSources);
|
|
2025
|
-
}
|
|
2026
|
-
if (this.o.extraArgs && this.o.extraArgs.length) args.push(...this.o.extraArgs);
|
|
2027
|
-
if (this.o.debug)
|
|
2028
|
-
process.stderr.write(`[session] spawn: ${claude} ${args.join(" ")}
|
|
2029
|
-
`);
|
|
2030
|
-
this.lastDataAt = Date.now();
|
|
2031
|
-
this.proc = Bun.spawn([claude, ...args], {
|
|
2032
|
-
cwd: this.cwd,
|
|
2033
|
-
env: {
|
|
2034
|
-
...process.env,
|
|
2035
|
-
CLAUDE_CONFIG_DIR: this.o.configDir,
|
|
2036
|
-
TERM: "xterm-256color",
|
|
2037
|
-
...this.o.ignoreAnthropicApiKey ? { ANTHROPIC_API_KEY: void 0, ANTHROPIC_AUTH_TOKEN: void 0 } : {}
|
|
2038
|
-
},
|
|
2039
|
-
terminal: {
|
|
2040
|
-
cols: this.o.cols,
|
|
2041
|
-
rows: this.o.rows,
|
|
2042
|
-
data: (_term, d) => {
|
|
2043
|
-
this.lastDataAt = Date.now();
|
|
2044
|
-
const chunk = Buffer.from(d).toString("utf8");
|
|
2045
|
-
this.raw += chunk;
|
|
2046
|
-
if (this.o.debug) process.stdout.write(chunk);
|
|
2047
|
-
}
|
|
2048
|
-
}
|
|
2049
|
-
});
|
|
2050
|
-
this.proc.exited.then((code) => {
|
|
2051
|
-
this.exitCode = typeof code === "number" ? code : null;
|
|
2052
|
-
this.exited = true;
|
|
2053
|
-
this.proc = null;
|
|
2054
|
-
}).catch(() => {
|
|
2055
|
-
this.exited = true;
|
|
2056
|
-
this.proc = null;
|
|
2057
|
-
});
|
|
2058
|
-
await this.waitForBoot();
|
|
2059
|
-
this.cursor = this.lineCount();
|
|
2060
|
-
}
|
|
2061
|
-
/** Wait until the TUI has been quiet for bootQuietMs (Ink ready), bounded by
|
|
2062
|
-
* bootMinMs..bootMaxMs. */
|
|
2063
|
-
async waitForBoot() {
|
|
2064
|
-
const start = Date.now();
|
|
2065
|
-
while (Date.now() - start < this.o.bootMaxMs) {
|
|
2066
|
-
await delay(150);
|
|
2067
|
-
if (this.aborted) throw new Error("aborted during boot");
|
|
2068
|
-
if (this.exited) {
|
|
2069
|
-
throw new Error(this.failureMessage("claude exited during boot", true));
|
|
2070
|
-
}
|
|
2071
|
-
const elapsed = Date.now() - start;
|
|
2072
|
-
const sinceData = Date.now() - this.lastDataAt;
|
|
2073
|
-
if (elapsed >= this.o.bootMinMs && sinceData >= this.o.bootQuietMs) return;
|
|
2291
|
+
const server2 = createServer(async (req, res) => {
|
|
2292
|
+
if (req.method !== "POST" || !req.url?.startsWith("/mcp")) {
|
|
2293
|
+
reject(req, res, 404, "not a POST to /mcp");
|
|
2294
|
+
return;
|
|
2074
2295
|
}
|
|
2075
|
-
|
|
2076
|
-
|
|
2077
|
-
|
|
2078
|
-
* placeholder; an Enter sent while claude is still ingesting the paste is
|
|
2079
|
-
* silently dropped, so a single fixed-delay Enter races the paste and can
|
|
2080
|
-
* leave the prompt sitting unsubmitted (→ hang until turnTimeoutMs). Send
|
|
2081
|
-
* Enter, then poll for transcript growth past the cursor (the turn's records
|
|
2082
|
-
* are written on acceptance); resend Enter until accepted or the retry
|
|
2083
|
-
* budget is spent. Polling growth (not a blind delay) also stops us from
|
|
2084
|
-
* sending a stray Enter once the turn is in flight. */
|
|
2085
|
-
async submitTurn() {
|
|
2086
|
-
await delay(this.o.submitMinMs);
|
|
2087
|
-
for (let attempt = 0; attempt < this.o.submitMaxRetries; attempt++) {
|
|
2088
|
-
if (this.aborted || this.exited || !this.proc) return;
|
|
2089
|
-
this.proc.terminal.write("\r");
|
|
2090
|
-
const until = Date.now() + this.o.submitConfirmMs;
|
|
2091
|
-
while (Date.now() < until) {
|
|
2092
|
-
await delay(80);
|
|
2093
|
-
if (this.aborted || this.exited) return;
|
|
2094
|
-
if (this.lineCount() > this.cursor) return;
|
|
2095
|
-
}
|
|
2296
|
+
if (req.headers.host !== boundAuthority) {
|
|
2297
|
+
reject(req, res, 403, "host header is not the bound authority");
|
|
2298
|
+
return;
|
|
2096
2299
|
}
|
|
2097
|
-
|
|
2098
|
-
|
|
2099
|
-
|
|
2100
|
-
return fs3.readFileSync(this.jsonlPath, "utf8").split("\n");
|
|
2101
|
-
} catch {
|
|
2102
|
-
return [];
|
|
2300
|
+
if (req.headers.origin !== void 0) {
|
|
2301
|
+
reject(req, res, 403, "origin header present");
|
|
2302
|
+
return;
|
|
2103
2303
|
}
|
|
2104
|
-
|
|
2105
|
-
|
|
2106
|
-
|
|
2107
|
-
|
|
2108
|
-
return lines.length > 0 ? lines.length - 1 : 0;
|
|
2109
|
-
}
|
|
2110
|
-
rawTail(max = 600) {
|
|
2111
|
-
const clean = this.raw.replace(/\x1B(?:[@-Z\\-_]|\[[0-?]*[ -/]*[@-~])/g, "").replace(/\s+/g, " ").trim();
|
|
2112
|
-
return clean.length > max ? clean.slice(-max) : clean;
|
|
2113
|
-
}
|
|
2114
|
-
failureMessage(reason, includeRaw = false) {
|
|
2115
|
-
const parts = [
|
|
2116
|
-
`${reason} (sessionId=${this.sessionId}, jsonlPath=${this.jsonlPath}, exitCode=${this.exitCode ?? "unknown"})`
|
|
2117
|
-
];
|
|
2118
|
-
if (includeRaw) {
|
|
2119
|
-
const tail = this.rawTail();
|
|
2120
|
-
if (tail) parts.push(`terminalTail=${JSON.stringify(tail)}`);
|
|
2304
|
+
const contentType = String(req.headers["content-type"] ?? "").split(";")[0].trim().toLowerCase();
|
|
2305
|
+
if (contentType !== "application/json") {
|
|
2306
|
+
reject(req, res, 415, "content-type is not application/json");
|
|
2307
|
+
return;
|
|
2121
2308
|
}
|
|
2122
|
-
|
|
2123
|
-
|
|
2124
|
-
|
|
2125
|
-
* Inject a turn into the live session and return the assistant reply once a
|
|
2126
|
-
* terminal stop_reason is observed in the transcript.
|
|
2127
|
-
*/
|
|
2128
|
-
async ask(prompt, perTurnTimeoutMs) {
|
|
2129
|
-
if (this.aborted) throw new Error("aborted");
|
|
2130
|
-
if (!this.proc || this.exited)
|
|
2131
|
-
throw new Error("session not started or already exited");
|
|
2132
|
-
const timeout = perTurnTimeoutMs ?? this.o.turnTimeoutMs;
|
|
2133
|
-
const t0 = Date.now();
|
|
2134
|
-
if (this.o.bracketedPaste) {
|
|
2135
|
-
this.proc.terminal.write("\x1B[200~" + prompt + "\x1B[201~");
|
|
2136
|
-
} else {
|
|
2137
|
-
this.proc.terminal.write(prompt);
|
|
2309
|
+
if (!authOk(req)) {
|
|
2310
|
+
reject(req, res, 401, "missing or invalid bearer token");
|
|
2311
|
+
return;
|
|
2138
2312
|
}
|
|
2139
|
-
|
|
2140
|
-
|
|
2141
|
-
let
|
|
2142
|
-
|
|
2143
|
-
|
|
2144
|
-
|
|
2145
|
-
|
|
2146
|
-
|
|
2147
|
-
|
|
2148
|
-
|
|
2149
|
-
|
|
2150
|
-
|
|
2151
|
-
|
|
2152
|
-
|
|
2153
|
-
|
|
2154
|
-
|
|
2155
|
-
|
|
2156
|
-
|
|
2157
|
-
|
|
2158
|
-
|
|
2159
|
-
|
|
2160
|
-
|
|
2161
|
-
|
|
2162
|
-
|
|
2163
|
-
|
|
2164
|
-
|
|
2165
|
-
|
|
2313
|
+
let requestId = null;
|
|
2314
|
+
let requestMethod = null;
|
|
2315
|
+
let sse = null;
|
|
2316
|
+
try {
|
|
2317
|
+
const body = await readBody(req);
|
|
2318
|
+
const request = JSON.parse(body);
|
|
2319
|
+
requestId = request?.id ?? null;
|
|
2320
|
+
requestMethod = typeof request?.method === "string" ? request.method : null;
|
|
2321
|
+
if (request?.jsonrpc !== "2.0" || typeof request.method !== "string") {
|
|
2322
|
+
writeJson(res, {
|
|
2323
|
+
jsonrpc: "2.0",
|
|
2324
|
+
id: requestId,
|
|
2325
|
+
error: { code: -32600, message: "Invalid request" }
|
|
2326
|
+
});
|
|
2327
|
+
return;
|
|
2328
|
+
}
|
|
2329
|
+
log.debug("proxy-mcp request", {
|
|
2330
|
+
method: request.method,
|
|
2331
|
+
id: request.id
|
|
2332
|
+
});
|
|
2333
|
+
if (request.method === "initialize") {
|
|
2334
|
+
writeJson(res, {
|
|
2335
|
+
jsonrpc: "2.0",
|
|
2336
|
+
id: requestId,
|
|
2337
|
+
result: {
|
|
2338
|
+
protocolVersion: PROTOCOL_VERSION,
|
|
2339
|
+
capabilities: { tools: {} },
|
|
2340
|
+
serverInfo: {
|
|
2341
|
+
name: SERVER_NAME,
|
|
2342
|
+
version: "0.1.0"
|
|
2343
|
+
}
|
|
2166
2344
|
}
|
|
2167
|
-
|
|
2168
|
-
|
|
2169
|
-
|
|
2345
|
+
});
|
|
2346
|
+
return;
|
|
2347
|
+
}
|
|
2348
|
+
if (request.method === "notifications/initialized") {
|
|
2349
|
+
res.statusCode = 204;
|
|
2350
|
+
res.end();
|
|
2351
|
+
return;
|
|
2352
|
+
}
|
|
2353
|
+
if (request.method === "tools/list") {
|
|
2354
|
+
writeJson(res, {
|
|
2355
|
+
jsonrpc: "2.0",
|
|
2356
|
+
id: requestId,
|
|
2357
|
+
result: {
|
|
2358
|
+
tools: tools.map((t) => ({
|
|
2359
|
+
name: t.name,
|
|
2360
|
+
description: t.description,
|
|
2361
|
+
inputSchema: t.inputSchema
|
|
2362
|
+
}))
|
|
2170
2363
|
}
|
|
2171
|
-
}
|
|
2364
|
+
});
|
|
2365
|
+
return;
|
|
2172
2366
|
}
|
|
2173
|
-
|
|
2174
|
-
|
|
2175
|
-
|
|
2176
|
-
|
|
2177
|
-
|
|
2178
|
-
|
|
2179
|
-
|
|
2180
|
-
|
|
2181
|
-
|
|
2182
|
-
|
|
2183
|
-
|
|
2184
|
-
|
|
2185
|
-
|
|
2186
|
-
|
|
2187
|
-
usage: lastUsage,
|
|
2188
|
-
cacheReadTokens: u.cache_read_input_tokens ?? 0,
|
|
2189
|
-
cacheCreationTokens: u.cache_creation_input_tokens ?? 0,
|
|
2190
|
-
ephemeral1hTokens: u.cache_creation?.ephemeral_1h_input_tokens ?? 0,
|
|
2191
|
-
ephemeral5mTokens: u.cache_creation?.ephemeral_5m_input_tokens ?? 0,
|
|
2192
|
-
inputTokens: u.input_tokens ?? 0,
|
|
2193
|
-
outputTokens: u.output_tokens ?? 0,
|
|
2194
|
-
elapsedMs: Date.now() - t0
|
|
2195
|
-
};
|
|
2196
|
-
}
|
|
2197
|
-
/**
|
|
2198
|
-
* Like ask(), but instead of collecting the reply text it re-emits each NEW
|
|
2199
|
-
* raw JSONL transcript line via onLine (verbatim) until a terminal
|
|
2200
|
-
* stop_reason. Returns the terminal stop_reason + the last assistant usage.
|
|
2201
|
-
* Used by the opencode plugin transport shim, which feeds these raw lines
|
|
2202
|
-
* into the existing stream-json line handler unchanged.
|
|
2203
|
-
*/
|
|
2204
|
-
async tailTurn(prompt, onLine, perTurnTimeoutMs) {
|
|
2205
|
-
if (this.aborted) throw new Error("aborted");
|
|
2206
|
-
if (!this.proc || this.exited)
|
|
2207
|
-
throw new Error("session not started or already exited");
|
|
2208
|
-
const timeout = perTurnTimeoutMs ?? this.o.turnTimeoutMs;
|
|
2209
|
-
if (this.o.bracketedPaste) {
|
|
2210
|
-
this.proc.terminal.write("\x1B[200~" + prompt + "\x1B[201~");
|
|
2211
|
-
} else {
|
|
2212
|
-
this.proc.terminal.write(prompt);
|
|
2213
|
-
}
|
|
2214
|
-
await this.submitTurn();
|
|
2215
|
-
let lastUsage = null;
|
|
2216
|
-
let totalOutput = 0;
|
|
2217
|
-
let stopReason = null;
|
|
2218
|
-
const deadline = Date.now() + timeout;
|
|
2219
|
-
while (Date.now() < deadline) {
|
|
2220
|
-
await delay(this.o.pollMs);
|
|
2221
|
-
if (this.aborted) throw new Error("aborted mid-turn");
|
|
2222
|
-
const lines = this.readRawLines();
|
|
2223
|
-
const lastComplete = lines.length - 1;
|
|
2224
|
-
if (lastComplete <= this.cursor) {
|
|
2225
|
-
if (this.exited) {
|
|
2226
|
-
throw new Error(this.failureMessage("claude exited mid-turn", true));
|
|
2367
|
+
if (request.method === "tools/call") {
|
|
2368
|
+
const params = request.params ?? {};
|
|
2369
|
+
const toolName = String(params.name ?? "");
|
|
2370
|
+
const input = params.arguments ?? {};
|
|
2371
|
+
if (!tools.some((t) => t.name === toolName)) {
|
|
2372
|
+
writeJson(res, {
|
|
2373
|
+
jsonrpc: "2.0",
|
|
2374
|
+
id: requestId,
|
|
2375
|
+
result: {
|
|
2376
|
+
content: [{ type: "text", text: `Unknown proxy tool: ${toolName}` }],
|
|
2377
|
+
isError: true
|
|
2378
|
+
}
|
|
2379
|
+
});
|
|
2380
|
+
return;
|
|
2227
2381
|
}
|
|
2228
|
-
|
|
2382
|
+
const interceptor = interceptors?.get(toolName);
|
|
2383
|
+
if (interceptor) {
|
|
2384
|
+
let intercepted;
|
|
2385
|
+
try {
|
|
2386
|
+
intercepted = await interceptor(input);
|
|
2387
|
+
} catch (interceptorError) {
|
|
2388
|
+
const message = interceptorError instanceof Error ? interceptorError.message : String(interceptorError);
|
|
2389
|
+
log.warn("proxy-mcp interceptor failed", { toolName, error: message });
|
|
2390
|
+
intercepted = { kind: "error", message };
|
|
2391
|
+
}
|
|
2392
|
+
writeToolCallResult(res, requestId, intercepted);
|
|
2393
|
+
return;
|
|
2394
|
+
}
|
|
2395
|
+
const callId = crypto2.randomUUID();
|
|
2396
|
+
log.info("proxy-mcp tool call received", {
|
|
2397
|
+
callId,
|
|
2398
|
+
toolName,
|
|
2399
|
+
hasInput: input != null,
|
|
2400
|
+
sse: acceptsEventStream(req.headers.accept)
|
|
2401
|
+
});
|
|
2402
|
+
const channel = { closed: false };
|
|
2403
|
+
if (acceptsEventStream(req.headers.accept)) {
|
|
2404
|
+
sse = openEventStream(res);
|
|
2405
|
+
}
|
|
2406
|
+
res.once("close", () => {
|
|
2407
|
+
sse?.stop();
|
|
2408
|
+
if (res.writableFinished) return;
|
|
2409
|
+
channel.closed = true;
|
|
2410
|
+
log.notice("proxy-mcp client closed a tool call before its result", {
|
|
2411
|
+
callId,
|
|
2412
|
+
toolName
|
|
2413
|
+
});
|
|
2414
|
+
});
|
|
2415
|
+
let timer = null;
|
|
2416
|
+
const result = await new Promise(
|
|
2417
|
+
(resolve4, reject2) => {
|
|
2418
|
+
const entry = {
|
|
2419
|
+
id: callId,
|
|
2420
|
+
toolName,
|
|
2421
|
+
input,
|
|
2422
|
+
resolve: resolve4,
|
|
2423
|
+
reject: reject2,
|
|
2424
|
+
channel
|
|
2425
|
+
};
|
|
2426
|
+
pending.set(callId, entry);
|
|
2427
|
+
const deadlineMs = resolveProxyCallTimeoutMs(
|
|
2428
|
+
toolName,
|
|
2429
|
+
input,
|
|
2430
|
+
timeoutOverrides
|
|
2431
|
+
);
|
|
2432
|
+
timer = setTimeout(() => {
|
|
2433
|
+
if (!pending.has(callId)) return;
|
|
2434
|
+
pending.delete(callId);
|
|
2435
|
+
log.notice("proxy-mcp tool call timed out", {
|
|
2436
|
+
callId,
|
|
2437
|
+
toolName,
|
|
2438
|
+
deadlineMs
|
|
2439
|
+
});
|
|
2440
|
+
reject2(buildProxyTimeoutError(toolName, deadlineMs));
|
|
2441
|
+
}, deadlineMs);
|
|
2442
|
+
calls.emit("call", entry);
|
|
2443
|
+
}
|
|
2444
|
+
).finally(() => {
|
|
2445
|
+
if (timer) clearTimeout(timer);
|
|
2446
|
+
pending.delete(callId);
|
|
2447
|
+
});
|
|
2448
|
+
if (channel.closed) {
|
|
2449
|
+
log.notice("proxy-mcp dropping result for a closed tool call", {
|
|
2450
|
+
callId,
|
|
2451
|
+
toolName
|
|
2452
|
+
});
|
|
2453
|
+
return;
|
|
2454
|
+
}
|
|
2455
|
+
writeToolCallResult(res, requestId, result, sse);
|
|
2456
|
+
return;
|
|
2229
2457
|
}
|
|
2230
|
-
|
|
2231
|
-
|
|
2232
|
-
|
|
2233
|
-
|
|
2234
|
-
|
|
2458
|
+
writeJson(res, {
|
|
2459
|
+
jsonrpc: "2.0",
|
|
2460
|
+
id: requestId,
|
|
2461
|
+
error: { code: -32601, message: `Unknown method: ${request.method}` }
|
|
2462
|
+
});
|
|
2463
|
+
} catch (error) {
|
|
2464
|
+
const errorMessage = error instanceof Error ? error.message : String(error);
|
|
2465
|
+
const logFn = isExpectedCleanupError(errorMessage) ? log.notice : log.warn;
|
|
2466
|
+
logFn("proxy-mcp error handling request", {
|
|
2467
|
+
error: errorMessage
|
|
2468
|
+
});
|
|
2469
|
+
if (requestMethod === "tools/call") {
|
|
2235
2470
|
try {
|
|
2236
|
-
|
|
2471
|
+
writeToolCallResult(
|
|
2472
|
+
res,
|
|
2473
|
+
requestId,
|
|
2474
|
+
{ kind: "error", message: errorMessage },
|
|
2475
|
+
sse
|
|
2476
|
+
);
|
|
2237
2477
|
} catch {
|
|
2238
|
-
|
|
2239
|
-
|
|
2240
|
-
|
|
2241
|
-
|
|
2242
|
-
lastUsage = rec.message.usage;
|
|
2243
|
-
totalOutput += rec.message.usage.output_tokens ?? 0;
|
|
2244
|
-
}
|
|
2245
|
-
if (rec.message.stop_reason && TERMINAL_STOP.has(rec.message.stop_reason)) {
|
|
2246
|
-
stopReason = rec.message.stop_reason;
|
|
2478
|
+
try {
|
|
2479
|
+
res.statusCode = 500;
|
|
2480
|
+
res.end();
|
|
2481
|
+
} catch {
|
|
2247
2482
|
}
|
|
2248
2483
|
}
|
|
2484
|
+
return;
|
|
2249
2485
|
}
|
|
2250
|
-
this.cursor = lastComplete;
|
|
2251
|
-
if (stopReason) break;
|
|
2252
|
-
}
|
|
2253
|
-
let usage = lastUsage;
|
|
2254
|
-
if (lastUsage) {
|
|
2255
|
-
usage = { ...lastUsage, output_tokens: totalOutput };
|
|
2256
|
-
if (Array.isArray(lastUsage.iterations) && lastUsage.iterations.length > 0) {
|
|
2257
|
-
const iters = lastUsage.iterations.map((it) => ({ ...it }));
|
|
2258
|
-
iters[iters.length - 1] = {
|
|
2259
|
-
...iters[iters.length - 1],
|
|
2260
|
-
output_tokens: totalOutput
|
|
2261
|
-
};
|
|
2262
|
-
usage.iterations = iters;
|
|
2263
|
-
}
|
|
2264
|
-
}
|
|
2265
|
-
if (!stopReason) {
|
|
2266
|
-
throw new Error(
|
|
2267
|
-
this.failureMessage(
|
|
2268
|
-
`turn timed out after ${timeout}ms (no terminal assistant record)`
|
|
2269
|
-
)
|
|
2270
|
-
);
|
|
2271
|
-
}
|
|
2272
|
-
return { stopReason, usage };
|
|
2273
|
-
}
|
|
2274
|
-
dispose() {
|
|
2275
|
-
if (this.proc) {
|
|
2276
2486
|
try {
|
|
2277
|
-
|
|
2487
|
+
writeJson(res, {
|
|
2488
|
+
jsonrpc: "2.0",
|
|
2489
|
+
id: requestId,
|
|
2490
|
+
error: {
|
|
2491
|
+
code: -32603,
|
|
2492
|
+
message: error instanceof Error ? error.message : "Internal error"
|
|
2493
|
+
}
|
|
2494
|
+
});
|
|
2278
2495
|
} catch {
|
|
2496
|
+
try {
|
|
2497
|
+
res.statusCode = 500;
|
|
2498
|
+
res.end();
|
|
2499
|
+
} catch {
|
|
2500
|
+
}
|
|
2279
2501
|
}
|
|
2280
|
-
try {
|
|
2281
|
-
this.proc.kill();
|
|
2282
|
-
} catch {
|
|
2283
|
-
}
|
|
2284
|
-
try {
|
|
2285
|
-
this.proc.terminal.close();
|
|
2286
|
-
} catch {
|
|
2287
|
-
}
|
|
2288
|
-
}
|
|
2289
|
-
this.proc = null;
|
|
2290
|
-
}
|
|
2291
|
-
};
|
|
2292
|
-
|
|
2293
|
-
// src/claude-session-wrapper.ts
|
|
2294
|
-
function decodeUserEnvelope(chunk) {
|
|
2295
|
-
let parsed;
|
|
2296
|
-
try {
|
|
2297
|
-
parsed = JSON.parse(chunk);
|
|
2298
|
-
} catch {
|
|
2299
|
-
return chunk;
|
|
2300
|
-
}
|
|
2301
|
-
if (!parsed || parsed.type !== "user" || !parsed.message) return chunk;
|
|
2302
|
-
const content = parsed.message.content;
|
|
2303
|
-
if (typeof content === "string") return content;
|
|
2304
|
-
if (!Array.isArray(content)) return chunk;
|
|
2305
|
-
const parts = [];
|
|
2306
|
-
let dropped = 0;
|
|
2307
|
-
for (const block of content) {
|
|
2308
|
-
if (block?.type === "text" && typeof block.text === "string") {
|
|
2309
|
-
parts.push(block.text);
|
|
2310
|
-
} else if (block?.type === "tool_result") {
|
|
2311
|
-
const v = block.content;
|
|
2312
|
-
const text = typeof v === "string" ? v : Array.isArray(v) ? v.map((i) => i?.type === "text" ? i.text : "").filter(Boolean).join("\n") : "";
|
|
2313
|
-
parts.push(
|
|
2314
|
-
`[Tool result${block.tool_use_id ? ` ${block.tool_use_id}` : ""}]
|
|
2315
|
-
${text}`
|
|
2316
|
-
);
|
|
2317
|
-
} else {
|
|
2318
|
-
dropped++;
|
|
2319
2502
|
}
|
|
2320
|
-
}
|
|
2321
|
-
|
|
2322
|
-
|
|
2323
|
-
|
|
2503
|
+
});
|
|
2504
|
+
await new Promise((resolve4, reject2) => {
|
|
2505
|
+
server2.once("error", reject2);
|
|
2506
|
+
server2.listen(0, "127.0.0.1", () => {
|
|
2507
|
+
server2.off("error", reject2);
|
|
2508
|
+
resolve4();
|
|
2324
2509
|
});
|
|
2325
|
-
}
|
|
2326
|
-
return parts.join("\n\n");
|
|
2327
|
-
}
|
|
2328
|
-
function spawnInteractiveProcess(opts) {
|
|
2329
|
-
const extraArgs = [];
|
|
2330
|
-
if (opts.mcpConfigPaths && opts.mcpConfigPaths.length > 0) {
|
|
2331
|
-
extraArgs.push(
|
|
2332
|
-
"--mcp-config",
|
|
2333
|
-
...opts.mcpConfigPaths,
|
|
2334
|
-
"--strict-mcp-config"
|
|
2335
|
-
);
|
|
2336
|
-
}
|
|
2337
|
-
const flagSettings = {};
|
|
2338
|
-
if (opts.permissionsAllow && opts.permissionsAllow.length > 0) {
|
|
2339
|
-
flagSettings.permissions = { allow: opts.permissionsAllow };
|
|
2340
|
-
}
|
|
2341
|
-
if (opts.fastMode) {
|
|
2342
|
-
flagSettings.fastMode = true;
|
|
2343
|
-
}
|
|
2344
|
-
if (Object.keys(flagSettings).length > 0) {
|
|
2345
|
-
extraArgs.push("--settings", JSON.stringify(flagSettings));
|
|
2346
|
-
}
|
|
2347
|
-
if (opts.permissionMode === "bypassPermissions") {
|
|
2348
|
-
log.warn(
|
|
2349
|
-
"interactive permissionMode bypassPermissions ignored: Claude Code prompts for confirmation in the TUI"
|
|
2350
|
-
);
|
|
2351
|
-
} else if (opts.permissionMode) {
|
|
2352
|
-
extraArgs.push("--permission-mode", opts.permissionMode);
|
|
2353
|
-
}
|
|
2354
|
-
if (opts.systemPromptFile) {
|
|
2355
|
-
extraArgs.push("--append-system-prompt-file", opts.systemPromptFile);
|
|
2356
|
-
}
|
|
2357
|
-
const session = new ClaudeSession({
|
|
2358
|
-
cwd: opts.cwd,
|
|
2359
|
-
cliPath: opts.cliPath,
|
|
2360
|
-
configDir: opts.configDir,
|
|
2361
|
-
model: opts.model,
|
|
2362
|
-
// Default null = normal CLAUDE.md + settings load, matching what the
|
|
2363
|
-
// headless spawn does. "" (skip everything) is for fast e2e runs only.
|
|
2364
|
-
settingSources: opts.settingSources === void 0 ? null : opts.settingSources,
|
|
2365
|
-
extraArgs,
|
|
2366
|
-
ignoreAnthropicApiKey: opts.ignoreAnthropicApiKey
|
|
2367
2510
|
});
|
|
2368
|
-
|
|
2369
|
-
|
|
2370
|
-
|
|
2371
|
-
|
|
2372
|
-
|
|
2373
|
-
|
|
2374
|
-
|
|
2511
|
+
const addr = server2.address();
|
|
2512
|
+
if (!addr) {
|
|
2513
|
+
server2.close();
|
|
2514
|
+
throw new Error("Failed to bind proxy MCP server");
|
|
2515
|
+
}
|
|
2516
|
+
boundAuthority = `127.0.0.1:${addr.port}`;
|
|
2517
|
+
const url = `http://${boundAuthority}/mcp`;
|
|
2518
|
+
log.info("proxy-mcp server started", {
|
|
2519
|
+
url,
|
|
2520
|
+
tools: tools.map((t) => t.name)
|
|
2375
2521
|
});
|
|
2376
|
-
|
|
2377
|
-
const
|
|
2378
|
-
|
|
2379
|
-
|
|
2380
|
-
|
|
2381
|
-
|
|
2382
|
-
|
|
2383
|
-
|
|
2384
|
-
|
|
2385
|
-
|
|
2386
|
-
|
|
2387
|
-
|
|
2388
|
-
|
|
2389
|
-
|
|
2390
|
-
|
|
2391
|
-
|
|
2392
|
-
|
|
2393
|
-
|
|
2394
|
-
|
|
2395
|
-
|
|
2396
|
-
|
|
2397
|
-
|
|
2398
|
-
|
|
2399
|
-
|
|
2400
|
-
|
|
2401
|
-
|
|
2402
|
-
|
|
2403
|
-
|
|
2404
|
-
|
|
2405
|
-
|
|
2406
|
-
|
|
2407
|
-
|
|
2408
|
-
|
|
2409
|
-
|
|
2410
|
-
usage
|
|
2411
|
-
);
|
|
2412
|
-
} catch (err) {
|
|
2413
|
-
const e = err instanceof Error ? err : new Error(String(err));
|
|
2414
|
-
log.error("interactive turn failed", { error: e.message });
|
|
2415
|
-
emitResult(
|
|
2416
|
-
"error_during_execution",
|
|
2417
|
-
true,
|
|
2418
|
-
`Interactive transport failed: ${e.message}`
|
|
2419
|
-
);
|
|
2420
|
-
if (errorHandlers.size > 0) {
|
|
2421
|
-
for (const h of errorHandlers) h(e);
|
|
2422
|
-
} else {
|
|
2423
|
-
lineEmitter.emit("close");
|
|
2424
|
-
}
|
|
2425
|
-
}
|
|
2426
|
-
})();
|
|
2427
|
-
};
|
|
2428
|
-
const proc = {
|
|
2429
|
-
stdin: {
|
|
2430
|
-
write(chunk) {
|
|
2431
|
-
const raw = typeof chunk === "string" && chunk.endsWith("\n") ? chunk.slice(0, -1) : chunk;
|
|
2432
|
-
runTurn(decodeUserEnvelope(raw));
|
|
2433
|
-
return true;
|
|
2434
|
-
},
|
|
2435
|
-
end() {
|
|
2436
|
-
}
|
|
2437
|
-
},
|
|
2438
|
-
stdout: null,
|
|
2439
|
-
stderr: null,
|
|
2440
|
-
pid: -1,
|
|
2441
|
-
killed: false,
|
|
2442
|
-
on(event, fn) {
|
|
2443
|
-
if (event === "error") errorHandlers.add(fn);
|
|
2444
|
-
return proc;
|
|
2445
|
-
},
|
|
2446
|
-
once() {
|
|
2447
|
-
return proc;
|
|
2448
|
-
},
|
|
2449
|
-
off(event, fn) {
|
|
2450
|
-
if (event === "error") errorHandlers.delete(fn);
|
|
2451
|
-
return proc;
|
|
2522
|
+
let configFilePath = null;
|
|
2523
|
+
const api = {
|
|
2524
|
+
url,
|
|
2525
|
+
serverName: SERVER_NAME,
|
|
2526
|
+
tools,
|
|
2527
|
+
authToken,
|
|
2528
|
+
calls,
|
|
2529
|
+
configPath() {
|
|
2530
|
+
if (configFilePath) return configFilePath;
|
|
2531
|
+
const body = JSON.stringify(
|
|
2532
|
+
{
|
|
2533
|
+
mcpServers: {
|
|
2534
|
+
[SERVER_NAME]: {
|
|
2535
|
+
type: "http",
|
|
2536
|
+
url,
|
|
2537
|
+
// Claude CLI replays these headers on every request to this
|
|
2538
|
+
// server, which is what lets the handler above reject anyone
|
|
2539
|
+
// who did not read this 0600 file.
|
|
2540
|
+
headers: { Authorization: `Bearer ${authToken}` },
|
|
2541
|
+
timeout: resolveProxyClientCeilingMs(timeoutOverrides)
|
|
2542
|
+
}
|
|
2543
|
+
}
|
|
2544
|
+
},
|
|
2545
|
+
null,
|
|
2546
|
+
2
|
|
2547
|
+
);
|
|
2548
|
+
const hash = crypto2.createHash("sha256").update(body).digest("hex").slice(0, 12);
|
|
2549
|
+
const outPath = path4.join(
|
|
2550
|
+
pluginTmpDir(),
|
|
2551
|
+
`proxy-${hash}.json`
|
|
2552
|
+
);
|
|
2553
|
+
fs3.writeFileSync(outPath, body, { encoding: "utf8", mode: 384 });
|
|
2554
|
+
configFilePath = outPath;
|
|
2555
|
+
return outPath;
|
|
2452
2556
|
},
|
|
2453
|
-
|
|
2454
|
-
|
|
2455
|
-
|
|
2456
|
-
} catch {
|
|
2557
|
+
async close() {
|
|
2558
|
+
for (const entry of pending.values()) {
|
|
2559
|
+
entry.reject(new Error(SERVER_CLOSED_MESSAGE));
|
|
2457
2560
|
}
|
|
2458
|
-
|
|
2459
|
-
|
|
2460
|
-
|
|
2561
|
+
pending.clear();
|
|
2562
|
+
await new Promise((resolve4) => {
|
|
2563
|
+
server2.close(() => resolve4());
|
|
2564
|
+
});
|
|
2565
|
+
if (configFilePath) {
|
|
2566
|
+
try {
|
|
2567
|
+
fs3.unlinkSync(configFilePath);
|
|
2568
|
+
} catch {
|
|
2569
|
+
}
|
|
2570
|
+
configFilePath = null;
|
|
2461
2571
|
}
|
|
2462
|
-
proc.killed = true;
|
|
2463
|
-
return true;
|
|
2464
2572
|
}
|
|
2465
2573
|
};
|
|
2466
|
-
return
|
|
2467
|
-
proc,
|
|
2468
|
-
lineEmitter,
|
|
2469
|
-
proxyServer: null,
|
|
2470
|
-
mcpHash: void 0,
|
|
2471
|
-
systemPromptFile: opts.systemPromptFile
|
|
2472
|
-
};
|
|
2473
|
-
}
|
|
2474
|
-
|
|
2475
|
-
// src/compression-store.ts
|
|
2476
|
-
var MAX_COMPRESSION_ENTRIES = 32;
|
|
2477
|
-
var compressions = /* @__PURE__ */ new Map();
|
|
2478
|
-
function storeCompressionSummary(sessionKey2, summary) {
|
|
2479
|
-
compressions.set(sessionKey2, { summary, restartPending: true });
|
|
2480
|
-
while (compressions.size > MAX_COMPRESSION_ENTRIES) {
|
|
2481
|
-
const oldest = compressions.keys().next();
|
|
2482
|
-
if (oldest.done) break;
|
|
2483
|
-
compressions.delete(oldest.value);
|
|
2484
|
-
log.info("compression store evicted oldest entry", { sessionKey: oldest.value });
|
|
2485
|
-
}
|
|
2574
|
+
return api;
|
|
2486
2575
|
}
|
|
2487
|
-
function
|
|
2488
|
-
|
|
2576
|
+
function disallowedToolFlags(tools) {
|
|
2577
|
+
const nameMap = {
|
|
2578
|
+
bash: ["Bash"],
|
|
2579
|
+
read: ["Read"],
|
|
2580
|
+
write: ["Write"],
|
|
2581
|
+
edit: ["Edit", "MultiEdit"],
|
|
2582
|
+
glob: ["Glob"],
|
|
2583
|
+
grep: ["Grep"],
|
|
2584
|
+
webfetch: ["WebFetch"],
|
|
2585
|
+
task: ["Agent"],
|
|
2586
|
+
// `question` disables Claude Code's built-in `AskUserQuestion` so the
|
|
2587
|
+
// structured-questions path flows through opencode's native `question`
|
|
2588
|
+
// tool instead — same UI/permission/audit benefits as the other
|
|
2589
|
+
// proxies. Without this, the model can call both and the two paths
|
|
2590
|
+
// diverge (opencode's form vs the headless deny-and-render fallback).
|
|
2591
|
+
question: ["AskUserQuestion"]
|
|
2592
|
+
};
|
|
2593
|
+
const out = [];
|
|
2594
|
+
const seen = /* @__PURE__ */ new Set();
|
|
2595
|
+
for (const t of tools) {
|
|
2596
|
+
const mapped = nameMap[t.name.toLowerCase()];
|
|
2597
|
+
if (!mapped) continue;
|
|
2598
|
+
for (const claudeTool of mapped) {
|
|
2599
|
+
if (seen.has(claudeTool)) continue;
|
|
2600
|
+
seen.add(claudeTool);
|
|
2601
|
+
out.push(claudeTool);
|
|
2602
|
+
}
|
|
2603
|
+
}
|
|
2604
|
+
return out;
|
|
2489
2605
|
}
|
|
2490
|
-
function
|
|
2491
|
-
const
|
|
2492
|
-
|
|
2493
|
-
|
|
2494
|
-
|
|
2606
|
+
function resolveDisallowedTools(options) {
|
|
2607
|
+
const out = [];
|
|
2608
|
+
const seen = /* @__PURE__ */ new Set();
|
|
2609
|
+
const push = (name) => {
|
|
2610
|
+
const trimmed = name.trim();
|
|
2611
|
+
if (!trimmed || seen.has(trimmed)) return;
|
|
2612
|
+
seen.add(trimmed);
|
|
2613
|
+
out.push(trimmed);
|
|
2614
|
+
};
|
|
2615
|
+
for (const name of disallowedToolFlags(options.proxyTools ?? [])) push(name);
|
|
2616
|
+
for (const name of options.extraDisallowedTools ?? []) push(String(name));
|
|
2617
|
+
if (options.disableWebSearch) push("WebSearch");
|
|
2618
|
+
return out;
|
|
2495
2619
|
}
|
|
2496
|
-
function
|
|
2497
|
-
|
|
2620
|
+
function readBody(req) {
|
|
2621
|
+
return new Promise((resolve4, reject) => {
|
|
2622
|
+
const chunks = [];
|
|
2623
|
+
req.on("data", (chunk) => chunks.push(chunk));
|
|
2624
|
+
req.on("end", () => resolve4(Buffer.concat(chunks).toString("utf8")));
|
|
2625
|
+
req.on("error", reject);
|
|
2626
|
+
});
|
|
2627
|
+
}
|
|
2628
|
+
function writeToolCallResult(res, requestId, result, sse = null) {
|
|
2629
|
+
const text = result.kind === "error" ? result.message : result.text;
|
|
2630
|
+
const isError = result.kind === "error" || result.isError === true;
|
|
2631
|
+
const envelope = {
|
|
2632
|
+
jsonrpc: "2.0",
|
|
2633
|
+
id: requestId ?? null,
|
|
2634
|
+
result: {
|
|
2635
|
+
content: [{ type: "text", text }],
|
|
2636
|
+
isError
|
|
2637
|
+
}
|
|
2638
|
+
};
|
|
2639
|
+
if (sse) {
|
|
2640
|
+
sse.finish(envelope);
|
|
2641
|
+
return;
|
|
2642
|
+
}
|
|
2643
|
+
writeJson(res, envelope);
|
|
2498
2644
|
}
|
|
2645
|
+
function openEventStream(res) {
|
|
2646
|
+
res.statusCode = 200;
|
|
2647
|
+
res.setHeader("Content-Type", "text/event-stream");
|
|
2648
|
+
res.setHeader("Cache-Control", "no-cache, no-transform");
|
|
2649
|
+
res.setHeader("Connection", "keep-alive");
|
|
2650
|
+
res.flushHeaders();
|
|
2651
|
+
res.write(": open\n\n");
|
|
2652
|
+
let timer = setInterval(() => {
|
|
2653
|
+
if (res.writableEnded || res.destroyed) {
|
|
2654
|
+
stop();
|
|
2655
|
+
return;
|
|
2656
|
+
}
|
|
2657
|
+
res.write(": keepalive\n\n");
|
|
2658
|
+
}, SSE_KEEPALIVE_MS);
|
|
2659
|
+
timer.unref?.();
|
|
2660
|
+
const stop = () => {
|
|
2661
|
+
if (timer) {
|
|
2662
|
+
clearInterval(timer);
|
|
2663
|
+
timer = null;
|
|
2664
|
+
}
|
|
2665
|
+
};
|
|
2666
|
+
return {
|
|
2667
|
+
stop,
|
|
2668
|
+
finish(envelope) {
|
|
2669
|
+
stop();
|
|
2670
|
+
if (res.writableEnded || res.destroyed) return;
|
|
2671
|
+
res.end(`event: message
|
|
2672
|
+
data: ${JSON.stringify(envelope)}
|
|
2499
2673
|
|
|
2500
|
-
|
|
2501
|
-
|
|
2502
|
-
|
|
2503
|
-
import * as path4 from "path";
|
|
2504
|
-
import * as crypto2 from "crypto";
|
|
2505
|
-
import { EventEmitter as EventEmitter3 } from "events";
|
|
2506
|
-
var SERVER_CLOSED_MESSAGE = "proxy MCP server closed";
|
|
2507
|
-
function isExpectedCleanupError(message) {
|
|
2508
|
-
return message.includes("timed out after") && message.includes("waiting for opencode to resolve") || message.includes("rejecting as orphaned") || message.includes("was orphaned by a new user turn") || message.includes("stream was aborted") || message.includes(SERVER_CLOSED_MESSAGE);
|
|
2674
|
+
`);
|
|
2675
|
+
}
|
|
2676
|
+
};
|
|
2509
2677
|
}
|
|
2510
|
-
|
|
2511
|
-
|
|
2512
|
-
|
|
2513
|
-
|
|
2514
|
-
|
|
2515
|
-
|
|
2516
|
-
|
|
2517
|
-
question: 30 * 60 * 1e3
|
|
2518
|
-
// 30 min
|
|
2519
|
-
};
|
|
2520
|
-
var MAX_PROXY_TIMEOUT_MS = 2 ** 31 - 1;
|
|
2521
|
-
function resolveProxyCallTimeoutMs(toolName, input, overrides) {
|
|
2522
|
-
const key = toolName.toLowerCase();
|
|
2523
|
-
let ms = PROXY_PER_TOOL_DEFAULT_TIMEOUT_MS[key] ?? PROXY_DEFAULT_TIMEOUT_MS;
|
|
2524
|
-
if (overrides) {
|
|
2525
|
-
const ov = lookupCaseInsensitive(overrides, key);
|
|
2526
|
-
if (typeof ov === "number" && ov > 0) ms = ov;
|
|
2527
|
-
}
|
|
2528
|
-
if (key === "bash") {
|
|
2529
|
-
const requested = input?.timeout;
|
|
2530
|
-
if (typeof requested === "number" && requested > ms) ms = requested;
|
|
2531
|
-
}
|
|
2532
|
-
return Math.min(ms, MAX_PROXY_TIMEOUT_MS);
|
|
2678
|
+
function writeJson(res, body) {
|
|
2679
|
+
if (res.destroyed || res.writableEnded) return;
|
|
2680
|
+
const payload = JSON.stringify(body);
|
|
2681
|
+
res.statusCode = 200;
|
|
2682
|
+
res.setHeader("Content-Type", "application/json");
|
|
2683
|
+
res.setHeader("Content-Length", Buffer.byteLength(payload).toString());
|
|
2684
|
+
res.end(payload);
|
|
2533
2685
|
}
|
|
2534
|
-
|
|
2535
|
-
|
|
2536
|
-
|
|
2537
|
-
|
|
2538
|
-
|
|
2539
|
-
|
|
2686
|
+
|
|
2687
|
+
// src/proxy-broker.ts
|
|
2688
|
+
var pendingByCallId = /* @__PURE__ */ new Map();
|
|
2689
|
+
var callIdsBySession = /* @__PURE__ */ new Map();
|
|
2690
|
+
var emitter = new EventEmitter2();
|
|
2691
|
+
function eventName(sessionKey2) {
|
|
2692
|
+
return `pending:${sessionKey2}`;
|
|
2540
2693
|
}
|
|
2541
|
-
function
|
|
2542
|
-
let
|
|
2543
|
-
|
|
2544
|
-
|
|
2545
|
-
|
|
2546
|
-
if (overrides) {
|
|
2547
|
-
for (const v of Object.values(overrides)) {
|
|
2548
|
-
if (typeof v === "number" && v > ms) ms = v;
|
|
2549
|
-
}
|
|
2694
|
+
function indexAdd(sessionKey2, callId) {
|
|
2695
|
+
let s = callIdsBySession.get(sessionKey2);
|
|
2696
|
+
if (!s) {
|
|
2697
|
+
s = /* @__PURE__ */ new Set();
|
|
2698
|
+
callIdsBySession.set(sessionKey2, s);
|
|
2550
2699
|
}
|
|
2551
|
-
|
|
2700
|
+
s.add(callId);
|
|
2552
2701
|
}
|
|
2553
|
-
function
|
|
2554
|
-
const
|
|
2555
|
-
|
|
2556
|
-
|
|
2557
|
-
|
|
2558
|
-
base + " (the subagent). The subagent may still be running but its result is no longer reachable in this session. Do not declare the dispatch failed, and do not 'schedule a wake-up' or defer -- that mechanism does not apply here. If the result is required, re-dispatch or verify it directly now."
|
|
2559
|
-
);
|
|
2560
|
-
}
|
|
2561
|
-
return new Error(base);
|
|
2702
|
+
function indexRemove(sessionKey2, callId) {
|
|
2703
|
+
const s = callIdsBySession.get(sessionKey2);
|
|
2704
|
+
if (!s) return;
|
|
2705
|
+
s.delete(callId);
|
|
2706
|
+
if (s.size === 0) callIdsBySession.delete(sessionKey2);
|
|
2562
2707
|
}
|
|
2563
|
-
|
|
2564
|
-
|
|
2565
|
-
|
|
2566
|
-
|
|
2567
|
-
|
|
2568
|
-
function
|
|
2569
|
-
const
|
|
2570
|
-
if (
|
|
2571
|
-
|
|
2572
|
-
|
|
2573
|
-
|
|
2574
|
-
for (const raw of live.slice(start).split("\n")) {
|
|
2575
|
-
const match = /^-\s*([^:]+):\s*(.+)$/.exec(raw.trim());
|
|
2576
|
-
if (!match) continue;
|
|
2577
|
-
const name = match[1].trim();
|
|
2578
|
-
const blurb = match[2].trim();
|
|
2579
|
-
entries.push(
|
|
2580
|
-
`- ${name}: ${blurb.length > AGENT_BLURB_LIMIT ? `${blurb.slice(0, AGENT_BLURB_LIMIT).trimEnd()}\u2026` : blurb}`
|
|
2708
|
+
function onPendingProxyCall(sessionKey2, handler) {
|
|
2709
|
+
const name = eventName(sessionKey2);
|
|
2710
|
+
emitter.on(name, handler);
|
|
2711
|
+
return () => emitter.off(name, handler);
|
|
2712
|
+
}
|
|
2713
|
+
function queuePendingProxyCall(sessionKey2, call, timeoutOverrides) {
|
|
2714
|
+
const previous = pendingByCallId.get(call.id);
|
|
2715
|
+
if (previous) {
|
|
2716
|
+
clearTimeout(previous.timer);
|
|
2717
|
+
previous.reject(
|
|
2718
|
+
new Error(`Replaced pending proxy call ${call.id} with a fresh one`)
|
|
2581
2719
|
);
|
|
2720
|
+
pendingByCallId.delete(call.id);
|
|
2721
|
+
indexRemove(previous.sessionKey, call.id);
|
|
2582
2722
|
}
|
|
2583
|
-
|
|
2584
|
-
|
|
2585
|
-
|
|
2586
|
-
|
|
2587
|
-
function overlayTaskProxyDescription(tools, liveDescription) {
|
|
2588
|
-
const agentTypes = extractAgentTypeList(liveDescription);
|
|
2589
|
-
if (!agentTypes) return tools;
|
|
2590
|
-
return tools.map(
|
|
2591
|
-
(t) => t.name === "task" ? { ...t, description: `${agentTypes}
|
|
2592
|
-
|
|
2593
|
-
${t.description}` } : t
|
|
2723
|
+
const deadlineMs = resolveProxyCallTimeoutMs(
|
|
2724
|
+
call.toolName,
|
|
2725
|
+
call.input,
|
|
2726
|
+
timeoutOverrides
|
|
2594
2727
|
);
|
|
2728
|
+
const timer = setTimeout(() => {
|
|
2729
|
+
const current = pendingByCallId.get(call.id);
|
|
2730
|
+
if (!current) return;
|
|
2731
|
+
pendingByCallId.delete(call.id);
|
|
2732
|
+
indexRemove(current.sessionKey, call.id);
|
|
2733
|
+
current.reject(buildProxyTimeoutError(call.toolName, deadlineMs));
|
|
2734
|
+
log.notice("timed out pending proxy call", {
|
|
2735
|
+
sessionKey: current.sessionKey,
|
|
2736
|
+
toolCallId: call.id,
|
|
2737
|
+
toolName: call.toolName,
|
|
2738
|
+
deadlineMs
|
|
2739
|
+
});
|
|
2740
|
+
}, deadlineMs);
|
|
2741
|
+
const pending = {
|
|
2742
|
+
sessionKey: sessionKey2,
|
|
2743
|
+
toolCallId: call.id,
|
|
2744
|
+
toolName: call.toolName,
|
|
2745
|
+
input: call.input,
|
|
2746
|
+
channel: call.channel,
|
|
2747
|
+
createdAt: Date.now(),
|
|
2748
|
+
timer,
|
|
2749
|
+
resolve: call.resolve,
|
|
2750
|
+
reject: call.reject
|
|
2751
|
+
};
|
|
2752
|
+
pendingByCallId.set(call.id, pending);
|
|
2753
|
+
indexAdd(sessionKey2, call.id);
|
|
2754
|
+
emitter.emit(eventName(sessionKey2), pending);
|
|
2755
|
+
log.info("queued pending proxy call", {
|
|
2756
|
+
sessionKey: sessionKey2,
|
|
2757
|
+
toolCallId: call.id,
|
|
2758
|
+
toolName: call.toolName
|
|
2759
|
+
});
|
|
2760
|
+
return pending;
|
|
2595
2761
|
}
|
|
2596
|
-
function
|
|
2597
|
-
const
|
|
2598
|
-
if (
|
|
2599
|
-
return tools.map(
|
|
2600
|
-
(t) => t.name === "question" ? { ...t, description: `${live}
|
|
2601
|
-
|
|
2602
|
-
${QUESTION_PROXY_NOTE}` } : t
|
|
2603
|
-
);
|
|
2762
|
+
function markPendingProxyCallEmitted(toolCallId) {
|
|
2763
|
+
const pending = pendingByCallId.get(toolCallId);
|
|
2764
|
+
if (pending) pending.emitted = true;
|
|
2604
2765
|
}
|
|
2605
|
-
function
|
|
2606
|
-
|
|
2607
|
-
return tools.filter((t) => t.name !== "question");
|
|
2766
|
+
function isPendingProxyCallChannelClosed(call) {
|
|
2767
|
+
return call.channel?.closed === true;
|
|
2608
2768
|
}
|
|
2609
|
-
|
|
2610
|
-
|
|
2611
|
-
|
|
2612
|
-
|
|
2613
|
-
|
|
2614
|
-
|
|
2615
|
-
|
|
2616
|
-
|
|
2617
|
-
|
|
2618
|
-
|
|
2619
|
-
|
|
2620
|
-
|
|
2621
|
-
|
|
2622
|
-
|
|
2623
|
-
|
|
2624
|
-
|
|
2625
|
-
|
|
2626
|
-
|
|
2627
|
-
|
|
2628
|
-
|
|
2629
|
-
|
|
2630
|
-
|
|
2631
|
-
|
|
2632
|
-
|
|
2633
|
-
|
|
2634
|
-
|
|
2635
|
-
|
|
2636
|
-
|
|
2637
|
-
|
|
2638
|
-
|
|
2639
|
-
|
|
2640
|
-
|
|
2641
|
-
|
|
2642
|
-
|
|
2643
|
-
|
|
2644
|
-
|
|
2645
|
-
|
|
2646
|
-
|
|
2647
|
-
|
|
2648
|
-
|
|
2649
|
-
|
|
2650
|
-
|
|
2651
|
-
|
|
2652
|
-
|
|
2653
|
-
|
|
2654
|
-
|
|
2655
|
-
|
|
2656
|
-
|
|
2657
|
-
|
|
2658
|
-
|
|
2659
|
-
|
|
2660
|
-
|
|
2661
|
-
|
|
2662
|
-
|
|
2663
|
-
|
|
2664
|
-
|
|
2665
|
-
|
|
2666
|
-
|
|
2667
|
-
|
|
2668
|
-
|
|
2669
|
-
|
|
2670
|
-
|
|
2671
|
-
|
|
2672
|
-
|
|
2673
|
-
|
|
2769
|
+
function getPendingProxyCalls(sessionKey2) {
|
|
2770
|
+
const s = callIdsBySession.get(sessionKey2);
|
|
2771
|
+
if (!s || s.size === 0) return [];
|
|
2772
|
+
const out = [];
|
|
2773
|
+
for (const id of s) {
|
|
2774
|
+
const p = pendingByCallId.get(id);
|
|
2775
|
+
if (p) out.push(p);
|
|
2776
|
+
}
|
|
2777
|
+
return out;
|
|
2778
|
+
}
|
|
2779
|
+
function resolvePendingProxyCallById(toolCallId, result) {
|
|
2780
|
+
const pending = pendingByCallId.get(toolCallId);
|
|
2781
|
+
if (!pending) return false;
|
|
2782
|
+
pendingByCallId.delete(toolCallId);
|
|
2783
|
+
indexRemove(pending.sessionKey, toolCallId);
|
|
2784
|
+
clearTimeout(pending.timer);
|
|
2785
|
+
pending.resolve(result);
|
|
2786
|
+
log.info("resolved pending proxy call", {
|
|
2787
|
+
sessionKey: pending.sessionKey,
|
|
2788
|
+
toolCallId: pending.toolCallId,
|
|
2789
|
+
toolName: pending.toolName
|
|
2790
|
+
});
|
|
2791
|
+
return true;
|
|
2792
|
+
}
|
|
2793
|
+
function rejectPendingProxyCallById(toolCallId, error) {
|
|
2794
|
+
const pending = pendingByCallId.get(toolCallId);
|
|
2795
|
+
if (!pending) return false;
|
|
2796
|
+
pendingByCallId.delete(toolCallId);
|
|
2797
|
+
indexRemove(pending.sessionKey, toolCallId);
|
|
2798
|
+
clearTimeout(pending.timer);
|
|
2799
|
+
pending.reject(error);
|
|
2800
|
+
log.notice("rejected pending proxy call", {
|
|
2801
|
+
sessionKey: pending.sessionKey,
|
|
2802
|
+
toolCallId: pending.toolCallId,
|
|
2803
|
+
toolName: pending.toolName,
|
|
2804
|
+
error: error.message
|
|
2805
|
+
});
|
|
2806
|
+
return true;
|
|
2807
|
+
}
|
|
2808
|
+
function rejectAllPendingProxyCallsForSession(sessionKey2, error) {
|
|
2809
|
+
const s = callIdsBySession.get(sessionKey2);
|
|
2810
|
+
if (!s) return 0;
|
|
2811
|
+
const ids = [...s];
|
|
2812
|
+
let count = 0;
|
|
2813
|
+
for (const id of ids) {
|
|
2814
|
+
if (rejectPendingProxyCallById(id, error)) count++;
|
|
2815
|
+
}
|
|
2816
|
+
return count;
|
|
2817
|
+
}
|
|
2818
|
+
|
|
2819
|
+
// src/compression-store.ts
|
|
2820
|
+
var MAX_COMPRESSION_ENTRIES = 32;
|
|
2821
|
+
var compressions = /* @__PURE__ */ new Map();
|
|
2822
|
+
function storeCompressionSummary(sessionKey2, summary) {
|
|
2823
|
+
compressions.set(sessionKey2, { summary, restartPending: true });
|
|
2824
|
+
while (compressions.size > MAX_COMPRESSION_ENTRIES) {
|
|
2825
|
+
const oldest = compressions.keys().next();
|
|
2826
|
+
if (oldest.done) break;
|
|
2827
|
+
compressions.delete(oldest.value);
|
|
2828
|
+
log.info("compression store evicted oldest entry", { sessionKey: oldest.value });
|
|
2829
|
+
}
|
|
2830
|
+
}
|
|
2831
|
+
function getCompressionSummary(sessionKey2) {
|
|
2832
|
+
return compressions.get(sessionKey2)?.summary;
|
|
2833
|
+
}
|
|
2834
|
+
function consumeCompressionRestart(sessionKey2) {
|
|
2835
|
+
const state = compressions.get(sessionKey2);
|
|
2836
|
+
if (!state?.restartPending) return false;
|
|
2837
|
+
state.restartPending = false;
|
|
2838
|
+
return true;
|
|
2839
|
+
}
|
|
2840
|
+
function clearCompression(sessionKey2) {
|
|
2841
|
+
compressions.delete(sessionKey2);
|
|
2842
|
+
}
|
|
2843
|
+
|
|
2844
|
+
// src/session-manager.ts
|
|
2845
|
+
var UNATTENDED_LINE_CAP = 500;
|
|
2846
|
+
var UNATTENDED_BYTE_CAP = 2 * 1024 * 1024;
|
|
2847
|
+
function bufferUnattendedLine(ap, line) {
|
|
2848
|
+
const lines = ap.unattendedLines ??= [];
|
|
2849
|
+
lines.push(line);
|
|
2850
|
+
let bytes = 0;
|
|
2851
|
+
for (const kept of lines) bytes += Buffer.byteLength(kept);
|
|
2852
|
+
while (lines.length > 0 && (lines.length > UNATTENDED_LINE_CAP || bytes > UNATTENDED_BYTE_CAP)) {
|
|
2853
|
+
bytes -= Buffer.byteLength(lines.shift());
|
|
2854
|
+
ap.unattendedDropped = (ap.unattendedDropped ?? 0) + 1;
|
|
2855
|
+
}
|
|
2856
|
+
}
|
|
2857
|
+
function takeUnattendedLines(ap) {
|
|
2858
|
+
const lines = ap.unattendedLines ?? [];
|
|
2859
|
+
const dropped = ap.unattendedDropped ?? 0;
|
|
2860
|
+
ap.unattendedLines = [];
|
|
2861
|
+
ap.unattendedDropped = 0;
|
|
2862
|
+
return { lines, dropped };
|
|
2863
|
+
}
|
|
2864
|
+
var activeProcesses = /* @__PURE__ */ new Map();
|
|
2865
|
+
var claudeSessions = /* @__PURE__ */ new Map();
|
|
2866
|
+
var MAX_ACTIVE_PROCESSES = 16;
|
|
2867
|
+
var PROCESS_EXIT_TIMEOUT_MS = 1500;
|
|
2868
|
+
var PROCESS_FORCE_EXIT_TIMEOUT_MS = 500;
|
|
2869
|
+
function envFlagEnabled(value) {
|
|
2870
|
+
if (value === void 0) return false;
|
|
2871
|
+
const normalized = value.trim().toLowerCase();
|
|
2872
|
+
if (!normalized) return false;
|
|
2873
|
+
return !["0", "false", "no", "off"].includes(normalized);
|
|
2874
|
+
}
|
|
2875
|
+
function isClaudeThinkingDisabled() {
|
|
2876
|
+
return envFlagEnabled(process.env.CLAUDE_CODE_DISABLE_THINKING) || envFlagEnabled(process.env.CLAUDE_CODE_DISABLE_ADAPTIVE_THINKING);
|
|
2877
|
+
}
|
|
2878
|
+
function cliEffortLevel(effort) {
|
|
2879
|
+
return effort === "minimal" ? "low" : effort;
|
|
2880
|
+
}
|
|
2881
|
+
function claudeSpawnEnv(opts) {
|
|
2882
|
+
const env = {
|
|
2883
|
+
...process.env,
|
|
2884
|
+
TERM: "xterm-256color"
|
|
2885
|
+
};
|
|
2886
|
+
if (opts?.effort) {
|
|
2887
|
+
env.CLAUDE_CODE_EFFORT_LEVEL = cliEffortLevel(opts.effort);
|
|
2888
|
+
}
|
|
2889
|
+
if (opts?.ignoreAnthropicApiKey) {
|
|
2890
|
+
delete env.ANTHROPIC_API_KEY;
|
|
2891
|
+
delete env.ANTHROPIC_AUTH_TOKEN;
|
|
2892
|
+
}
|
|
2893
|
+
if (!isClaudeThinkingDisabled() && process.env.CLAUDE_CODE_SHOW_THINKING_SUMMARIES === void 0) {
|
|
2894
|
+
env.CLAUDE_CODE_SHOW_THINKING_SUMMARIES = "1";
|
|
2895
|
+
}
|
|
2896
|
+
return env;
|
|
2897
|
+
}
|
|
2898
|
+
function touch(key) {
|
|
2899
|
+
const existing = activeProcesses.get(key);
|
|
2900
|
+
if (existing) {
|
|
2901
|
+
activeProcesses.delete(key);
|
|
2902
|
+
activeProcesses.set(key, existing);
|
|
2903
|
+
}
|
|
2904
|
+
}
|
|
2905
|
+
function evictIfNeeded() {
|
|
2906
|
+
while (activeProcesses.size >= MAX_ACTIVE_PROCESSES) {
|
|
2907
|
+
const oldestKey = activeProcesses.keys().next().value;
|
|
2908
|
+
if (!oldestKey) break;
|
|
2909
|
+
log.info("evicting LRU claude process", { sessionKey: oldestKey });
|
|
2910
|
+
deleteActiveProcess(oldestKey);
|
|
2911
|
+
}
|
|
2912
|
+
}
|
|
2913
|
+
function getActiveProcess(key) {
|
|
2914
|
+
const ap = activeProcesses.get(key);
|
|
2915
|
+
if (ap) touch(key);
|
|
2916
|
+
return ap;
|
|
2917
|
+
}
|
|
2918
|
+
function setActiveProcess(key, ap) {
|
|
2919
|
+
activeProcesses.set(key, ap);
|
|
2920
|
+
}
|
|
2921
|
+
function detachActiveProcess(key) {
|
|
2922
|
+
const ap = activeProcesses.get(key);
|
|
2923
|
+
if (!ap) return void 0;
|
|
2924
|
+
activeProcesses.delete(key);
|
|
2925
|
+
void ap.proxyServer?.close();
|
|
2926
|
+
return ap;
|
|
2927
|
+
}
|
|
2928
|
+
function deleteActiveProcess(key) {
|
|
2929
|
+
const ap = detachActiveProcess(key);
|
|
2930
|
+
ap?.proc.kill();
|
|
2931
|
+
}
|
|
2932
|
+
function hasProcessExited(proc) {
|
|
2933
|
+
return proc.exitCode !== null || proc.signalCode !== null;
|
|
2934
|
+
}
|
|
2935
|
+
function waitForProcessExit(proc, timeoutMs) {
|
|
2936
|
+
if (hasProcessExited(proc)) return Promise.resolve(true);
|
|
2937
|
+
return new Promise((resolve4) => {
|
|
2938
|
+
const onExit = () => {
|
|
2939
|
+
clearTimeout(timer);
|
|
2940
|
+
resolve4(true);
|
|
2941
|
+
};
|
|
2942
|
+
const timer = setTimeout(() => {
|
|
2943
|
+
proc.off("exit", onExit);
|
|
2944
|
+
resolve4(hasProcessExited(proc));
|
|
2945
|
+
}, timeoutMs);
|
|
2946
|
+
proc.once("exit", onExit);
|
|
2947
|
+
});
|
|
2948
|
+
}
|
|
2949
|
+
async function deleteActiveProcessAndWait(key, options = {}) {
|
|
2950
|
+
const ap = detachActiveProcess(key);
|
|
2951
|
+
if (!ap || hasProcessExited(ap.proc)) return true;
|
|
2952
|
+
const gracefulExit = waitForProcessExit(
|
|
2953
|
+
ap.proc,
|
|
2954
|
+
options.exitTimeoutMs ?? PROCESS_EXIT_TIMEOUT_MS
|
|
2955
|
+
);
|
|
2956
|
+
ap.proc.kill();
|
|
2957
|
+
if (await gracefulExit) return true;
|
|
2958
|
+
const forcedExit = waitForProcessExit(
|
|
2959
|
+
ap.proc,
|
|
2960
|
+
options.forceExitTimeoutMs ?? PROCESS_FORCE_EXIT_TIMEOUT_MS
|
|
2961
|
+
);
|
|
2962
|
+
ap.proc.kill("SIGKILL");
|
|
2963
|
+
if (await forcedExit) return true;
|
|
2964
|
+
log.warn("claude process did not exit; starting a fresh session", {
|
|
2965
|
+
sessionKey: key
|
|
2966
|
+
});
|
|
2967
|
+
deleteClaudeSessionId(key);
|
|
2968
|
+
return false;
|
|
2969
|
+
}
|
|
2970
|
+
function getClaudeSessionId(key) {
|
|
2971
|
+
return claudeSessions.get(key);
|
|
2972
|
+
}
|
|
2973
|
+
function setClaudeSessionId(key, sessionId) {
|
|
2974
|
+
claudeSessions.set(key, sessionId);
|
|
2975
|
+
}
|
|
2976
|
+
function deleteClaudeSessionId(key) {
|
|
2977
|
+
clearExitPlanModeQuestions(key);
|
|
2978
|
+
const claudeSessionId = claudeSessions.get(key);
|
|
2979
|
+
if (claudeSessionId) clearLedger(claudeSessionId);
|
|
2980
|
+
claudeSessions.delete(key);
|
|
2981
|
+
}
|
|
2982
|
+
function effortSessionKey(baseKey, effort) {
|
|
2983
|
+
return effort ? `${baseKey}::effort=${effort}` : baseKey;
|
|
2984
|
+
}
|
|
2985
|
+
function invalidateOtherEffortSessions(baseKey, effort) {
|
|
2986
|
+
const levels = [
|
|
2987
|
+
void 0,
|
|
2988
|
+
"minimal",
|
|
2989
|
+
"low",
|
|
2990
|
+
"medium",
|
|
2991
|
+
"high",
|
|
2992
|
+
"xhigh",
|
|
2993
|
+
"max"
|
|
2994
|
+
];
|
|
2995
|
+
const staleKeys = levels.filter((level) => level !== effort).map((level) => effortSessionKey(baseKey, level));
|
|
2996
|
+
for (const key of staleKeys) {
|
|
2997
|
+
const active = activeProcesses.get(key);
|
|
2998
|
+
if (getPendingProxyCalls(key).length || hasExitPlanModeQuestions(key) || active?.pendingProxyCompletions?.size || active && (active.lineEmitter.listenerCount("line") > 0 || isSideQuestionPending(active))) {
|
|
2999
|
+
throw new Error(
|
|
3000
|
+
"Cannot change reasoning effort while the previous effort session has pending work. Finish that work at its original effort first."
|
|
3001
|
+
);
|
|
2674
3002
|
}
|
|
2675
|
-
}
|
|
2676
|
-
{
|
|
2677
|
-
|
|
2678
|
-
|
|
2679
|
-
|
|
2680
|
-
|
|
2681
|
-
|
|
2682
|
-
|
|
2683
|
-
|
|
2684
|
-
|
|
2685
|
-
|
|
2686
|
-
|
|
2687
|
-
|
|
2688
|
-
|
|
2689
|
-
|
|
2690
|
-
|
|
2691
|
-
|
|
2692
|
-
|
|
2693
|
-
|
|
2694
|
-
|
|
2695
|
-
|
|
2696
|
-
|
|
3003
|
+
}
|
|
3004
|
+
for (const key of staleKeys) {
|
|
3005
|
+
deleteActiveProcess(key);
|
|
3006
|
+
deleteClaudeSessionId(key);
|
|
3007
|
+
clearCompression(key);
|
|
3008
|
+
}
|
|
3009
|
+
}
|
|
3010
|
+
function spawnClaudeProcess(cliPath, cliArgs, cwd, sessionKey2, proxyServer, mcpHash, systemPromptFile, ignoreAnthropicApiKey, effort) {
|
|
3011
|
+
evictIfNeeded();
|
|
3012
|
+
log.info("spawning new claude process", {
|
|
3013
|
+
cliPath,
|
|
3014
|
+
cliArgs,
|
|
3015
|
+
cwd,
|
|
3016
|
+
sessionKey: sessionKey2,
|
|
3017
|
+
effort
|
|
3018
|
+
});
|
|
3019
|
+
const proc = spawn(cliPath, cliArgs, {
|
|
3020
|
+
cwd,
|
|
3021
|
+
stdio: ["pipe", "pipe", "pipe"],
|
|
3022
|
+
env: claudeSpawnEnv({ ignoreAnthropicApiKey, effort }),
|
|
3023
|
+
shell: process.platform === "win32"
|
|
3024
|
+
});
|
|
3025
|
+
const lineEmitter = new EventEmitter3();
|
|
3026
|
+
const ap = {
|
|
3027
|
+
proc,
|
|
3028
|
+
lineEmitter,
|
|
3029
|
+
proxyServer: proxyServer ?? null,
|
|
3030
|
+
mcpHash,
|
|
3031
|
+
systemPromptFile,
|
|
3032
|
+
effort,
|
|
3033
|
+
cliArgs: [...cliArgs],
|
|
3034
|
+
unattendedLines: [],
|
|
3035
|
+
unattendedDropped: 0
|
|
3036
|
+
};
|
|
3037
|
+
const rl = createInterface({ input: proc.stdout });
|
|
3038
|
+
rl.on("line", (line) => {
|
|
3039
|
+
if (dispatchSideQuestionResponse(ap, line)) return;
|
|
3040
|
+
if (lineEmitter.listenerCount("line") === 0) {
|
|
3041
|
+
bufferUnattendedLine(ap, line);
|
|
3042
|
+
return;
|
|
2697
3043
|
}
|
|
2698
|
-
|
|
2699
|
-
|
|
2700
|
-
|
|
2701
|
-
|
|
2702
|
-
|
|
2703
|
-
|
|
2704
|
-
|
|
2705
|
-
|
|
2706
|
-
|
|
2707
|
-
|
|
2708
|
-
|
|
2709
|
-
|
|
2710
|
-
|
|
2711
|
-
|
|
2712
|
-
|
|
2713
|
-
subagent_type: {
|
|
2714
|
-
type: "string",
|
|
2715
|
-
description: "The type of specialized agent to use for this task"
|
|
2716
|
-
},
|
|
2717
|
-
task_id: {
|
|
2718
|
-
type: "string",
|
|
2719
|
-
description: "Set this only if you mean to resume a previous task \u2014 pass the prior task_id to continue the same subagent session instead of creating a fresh one."
|
|
2720
|
-
},
|
|
2721
|
-
command: {
|
|
2722
|
-
type: "string",
|
|
2723
|
-
description: "The command that triggered this task"
|
|
2724
|
-
},
|
|
2725
|
-
background: {
|
|
2726
|
-
type: "boolean",
|
|
2727
|
-
description: "Run the task in the background when supported by opencode"
|
|
2728
|
-
}
|
|
2729
|
-
},
|
|
2730
|
-
required: ["description", "prompt", "subagent_type"]
|
|
3044
|
+
lineEmitter.emit("line", line);
|
|
3045
|
+
});
|
|
3046
|
+
rl.on("close", () => {
|
|
3047
|
+
lineEmitter.emit("close");
|
|
3048
|
+
});
|
|
3049
|
+
activeProcesses.set(sessionKey2, ap);
|
|
3050
|
+
proc.on("error", (err) => {
|
|
3051
|
+
log.error("claude process error", { sessionKey: sessionKey2, error: err.message });
|
|
3052
|
+
});
|
|
3053
|
+
proc.on("exit", (code, signal) => {
|
|
3054
|
+
log.info("claude process exited", { code, signal, sessionKey: sessionKey2 });
|
|
3055
|
+
void proxyServer?.close();
|
|
3056
|
+
if (systemPromptFile) {
|
|
3057
|
+
void unlink(systemPromptFile).catch(() => {
|
|
3058
|
+
});
|
|
2731
3059
|
}
|
|
2732
|
-
|
|
2733
|
-
|
|
2734
|
-
|
|
2735
|
-
|
|
2736
|
-
|
|
2737
|
-
|
|
2738
|
-
|
|
2739
|
-
|
|
2740
|
-
type: "array",
|
|
2741
|
-
description: "Questions to ask.",
|
|
2742
|
-
items: {
|
|
2743
|
-
type: "object",
|
|
2744
|
-
properties: {
|
|
2745
|
-
question: {
|
|
2746
|
-
type: "string",
|
|
2747
|
-
description: "Complete question."
|
|
2748
|
-
},
|
|
2749
|
-
header: {
|
|
2750
|
-
type: "string",
|
|
2751
|
-
description: "Very short label (max 30 chars)."
|
|
2752
|
-
},
|
|
2753
|
-
options: {
|
|
2754
|
-
type: "array",
|
|
2755
|
-
description: "Available choices.",
|
|
2756
|
-
items: {
|
|
2757
|
-
type: "object",
|
|
2758
|
-
properties: {
|
|
2759
|
-
label: {
|
|
2760
|
-
type: "string",
|
|
2761
|
-
description: "Display text (1-5 words, concise)."
|
|
2762
|
-
},
|
|
2763
|
-
description: {
|
|
2764
|
-
type: "string",
|
|
2765
|
-
description: "Explanation of choice."
|
|
2766
|
-
}
|
|
2767
|
-
},
|
|
2768
|
-
required: ["label", "description"]
|
|
2769
|
-
}
|
|
2770
|
-
},
|
|
2771
|
-
multiple: {
|
|
2772
|
-
type: "boolean",
|
|
2773
|
-
description: "Allow selecting multiple choices. Defaults to false."
|
|
2774
|
-
}
|
|
2775
|
-
},
|
|
2776
|
-
required: ["question", "header", "options"]
|
|
2777
|
-
}
|
|
2778
|
-
}
|
|
2779
|
-
},
|
|
2780
|
-
required: ["questions"]
|
|
3060
|
+
const ownsSessionKey = activeProcesses.get(sessionKey2) === ap;
|
|
3061
|
+
if (ownsSessionKey) activeProcesses.delete(sessionKey2);
|
|
3062
|
+
if (ownsSessionKey && code !== 0 && code !== null) {
|
|
3063
|
+
log.info("process exited with error, clearing session", {
|
|
3064
|
+
code,
|
|
3065
|
+
sessionKey: sessionKey2
|
|
3066
|
+
});
|
|
3067
|
+
claudeSessions.delete(sessionKey2);
|
|
2781
3068
|
}
|
|
2782
|
-
}
|
|
2783
|
-
{
|
|
2784
|
-
|
|
2785
|
-
|
|
2786
|
-
|
|
2787
|
-
|
|
2788
|
-
|
|
2789
|
-
|
|
2790
|
-
|
|
2791
|
-
|
|
2792
|
-
|
|
2793
|
-
}
|
|
2794
|
-
|
|
3069
|
+
});
|
|
3070
|
+
proc.stderr?.on("data", (data) => {
|
|
3071
|
+
const stderr = data.toString();
|
|
3072
|
+
log.debug("stderr", { data: stderr.slice(0, 200) });
|
|
3073
|
+
if (stderr.includes("No conversation found") || stderr.includes("Session ID") && (stderr.includes("already in use") || stderr.includes("not found") || stderr.includes("invalid"))) {
|
|
3074
|
+
if (activeProcesses.get(sessionKey2) === ap) {
|
|
3075
|
+
log.warn("claude session ID error, clearing session", {
|
|
3076
|
+
sessionKey: sessionKey2,
|
|
3077
|
+
error: stderr.slice(0, 200)
|
|
3078
|
+
});
|
|
3079
|
+
claudeSessions.delete(sessionKey2);
|
|
3080
|
+
} else {
|
|
3081
|
+
log.debug("ignoring session ID error from stale claude process", {
|
|
3082
|
+
sessionKey: sessionKey2
|
|
3083
|
+
});
|
|
3084
|
+
}
|
|
2795
3085
|
}
|
|
3086
|
+
});
|
|
3087
|
+
return ap;
|
|
3088
|
+
}
|
|
3089
|
+
function appendResumeIfNeeded(sessionKey2, cliArgs) {
|
|
3090
|
+
if (cliArgs.includes("--resume") || cliArgs.includes("--session-id")) {
|
|
3091
|
+
return cliArgs;
|
|
2796
3092
|
}
|
|
2797
|
-
|
|
2798
|
-
|
|
2799
|
-
|
|
2800
|
-
|
|
2801
|
-
|
|
2802
|
-
const
|
|
2803
|
-
|
|
2804
|
-
|
|
2805
|
-
|
|
2806
|
-
|
|
2807
|
-
|
|
2808
|
-
|
|
2809
|
-
return crypto2.timingSafeEqual(candidate, expectedAuth);
|
|
3093
|
+
const sid = claudeSessions.get(sessionKey2);
|
|
3094
|
+
if (!sid) return cliArgs;
|
|
3095
|
+
return [...cliArgs, "--resume", sid];
|
|
3096
|
+
}
|
|
3097
|
+
function respawnActiveProcess(sessionKey2, cliPath, cliArgs, cwd, ignoreAnthropicApiKey) {
|
|
3098
|
+
const old = activeProcesses.get(sessionKey2);
|
|
3099
|
+
if (!old) return void 0;
|
|
3100
|
+
activeProcesses.delete(sessionKey2);
|
|
3101
|
+
old.proc.removeAllListeners("exit");
|
|
3102
|
+
try {
|
|
3103
|
+
old.proc.kill();
|
|
3104
|
+
} catch {
|
|
2810
3105
|
}
|
|
2811
|
-
|
|
2812
|
-
|
|
2813
|
-
|
|
2814
|
-
|
|
2815
|
-
|
|
2816
|
-
|
|
2817
|
-
|
|
2818
|
-
|
|
2819
|
-
|
|
2820
|
-
|
|
2821
|
-
|
|
3106
|
+
const replacement = spawnClaudeProcess(
|
|
3107
|
+
cliPath,
|
|
3108
|
+
appendResumeIfNeeded(sessionKey2, old.cliArgs ?? cliArgs),
|
|
3109
|
+
cwd,
|
|
3110
|
+
sessionKey2,
|
|
3111
|
+
old.proxyServer,
|
|
3112
|
+
old.mcpHash,
|
|
3113
|
+
old.systemPromptFile,
|
|
3114
|
+
ignoreAnthropicApiKey,
|
|
3115
|
+
old.effort
|
|
3116
|
+
);
|
|
3117
|
+
replacement.pendingProxyCompletions = old.pendingProxyCompletions;
|
|
3118
|
+
delete old.pendingProxyCompletions;
|
|
3119
|
+
return replacement;
|
|
3120
|
+
}
|
|
3121
|
+
function buildCliArgs(opts) {
|
|
3122
|
+
const {
|
|
3123
|
+
sessionKey: sessionKey2,
|
|
3124
|
+
skipPermissions,
|
|
3125
|
+
includeSessionId = true,
|
|
3126
|
+
model,
|
|
3127
|
+
permissionMode,
|
|
3128
|
+
mcpConfig,
|
|
3129
|
+
strictMcpConfig,
|
|
3130
|
+
disallowedTools,
|
|
3131
|
+
appendSystemPromptFile,
|
|
3132
|
+
thinking,
|
|
3133
|
+
thinkingDisplay,
|
|
3134
|
+
fastMode,
|
|
3135
|
+
cliVersion
|
|
3136
|
+
} = opts;
|
|
3137
|
+
const args = [
|
|
3138
|
+
"--print",
|
|
3139
|
+
"--output-format",
|
|
3140
|
+
"stream-json",
|
|
3141
|
+
"--input-format",
|
|
3142
|
+
"stream-json",
|
|
3143
|
+
"--include-partial-messages",
|
|
3144
|
+
"--verbose"
|
|
3145
|
+
];
|
|
3146
|
+
if (model) {
|
|
3147
|
+
args.push("--model", model);
|
|
3148
|
+
}
|
|
3149
|
+
if (permissionMode) {
|
|
3150
|
+
args.push("--permission-mode", permissionMode);
|
|
3151
|
+
}
|
|
3152
|
+
if (includeSessionId) {
|
|
3153
|
+
const sessionId = claudeSessions.get(sessionKey2);
|
|
3154
|
+
if (sessionId && !activeProcesses.has(sessionKey2)) {
|
|
3155
|
+
args.push("--resume", sessionId);
|
|
3156
|
+
}
|
|
3157
|
+
}
|
|
3158
|
+
if (mcpConfig) {
|
|
3159
|
+
const configs = Array.isArray(mcpConfig) ? mcpConfig : [mcpConfig];
|
|
3160
|
+
const filtered = configs.filter((c) => typeof c === "string" && c.length > 0);
|
|
3161
|
+
if (filtered.length > 0) {
|
|
3162
|
+
args.push("--mcp-config", ...filtered);
|
|
3163
|
+
}
|
|
3164
|
+
}
|
|
3165
|
+
if (strictMcpConfig) {
|
|
3166
|
+
args.push("--strict-mcp-config");
|
|
3167
|
+
}
|
|
3168
|
+
if (disallowedTools && disallowedTools.length > 0) {
|
|
3169
|
+
args.push("--disallowedTools", ...disallowedTools);
|
|
3170
|
+
}
|
|
3171
|
+
if (thinking && cliSupportsThinking(cliVersion ?? null)) {
|
|
3172
|
+
args.push("--thinking", thinking);
|
|
3173
|
+
}
|
|
3174
|
+
if (thinkingDisplay && cliSupportsThinkingDisplay(cliVersion ?? null)) {
|
|
3175
|
+
args.push("--thinking-display", thinkingDisplay);
|
|
3176
|
+
}
|
|
3177
|
+
if (appendSystemPromptFile) {
|
|
3178
|
+
args.push("--append-system-prompt-file", appendSystemPromptFile);
|
|
3179
|
+
}
|
|
3180
|
+
if (fastMode && cliSupportsFastMode(cliVersion ?? null)) {
|
|
3181
|
+
args.push("--settings", JSON.stringify({ fastMode: true }));
|
|
3182
|
+
}
|
|
3183
|
+
if (skipPermissions) {
|
|
3184
|
+
args.push("--dangerously-skip-permissions");
|
|
3185
|
+
}
|
|
3186
|
+
return args;
|
|
3187
|
+
}
|
|
3188
|
+
function sessionKey(cwd, modelId) {
|
|
3189
|
+
return `${cwd}::${modelId}`;
|
|
3190
|
+
}
|
|
3191
|
+
|
|
3192
|
+
// src/claude-session-wrapper.ts
|
|
3193
|
+
import { EventEmitter as EventEmitter4 } from "events";
|
|
3194
|
+
import { unlink as unlink2 } from "fs/promises";
|
|
3195
|
+
|
|
3196
|
+
// src/claude-session-bun.ts
|
|
3197
|
+
import * as os3 from "os";
|
|
3198
|
+
import * as fs4 from "fs";
|
|
3199
|
+
import * as path5 from "path";
|
|
3200
|
+
import { execFileSync } from "child_process";
|
|
3201
|
+
import { randomUUID as randomUUID3 } from "crypto";
|
|
3202
|
+
function resolveClaude(cmd = "claude") {
|
|
3203
|
+
if (path5.isAbsolute(cmd) && fs4.existsSync(cmd)) return cmd;
|
|
3204
|
+
const viaBun = Bun.which(cmd);
|
|
3205
|
+
if (viaBun) return viaBun;
|
|
3206
|
+
const isWin = os3.platform() === "win32";
|
|
3207
|
+
try {
|
|
3208
|
+
const out = execFileSync(isWin ? "where" : "which", [cmd], {
|
|
3209
|
+
encoding: "utf8",
|
|
3210
|
+
stdio: ["ignore", "pipe", "ignore"]
|
|
2822
3211
|
});
|
|
2823
|
-
|
|
3212
|
+
const first = out.split(/\r?\n/).map((l) => l.trim()).filter(Boolean).find((p) => fs4.existsSync(p));
|
|
3213
|
+
if (first) return first;
|
|
3214
|
+
} catch {
|
|
2824
3215
|
}
|
|
2825
|
-
|
|
2826
|
-
|
|
2827
|
-
|
|
2828
|
-
|
|
2829
|
-
|
|
2830
|
-
|
|
2831
|
-
|
|
2832
|
-
|
|
2833
|
-
|
|
2834
|
-
|
|
2835
|
-
|
|
2836
|
-
|
|
2837
|
-
|
|
2838
|
-
|
|
2839
|
-
|
|
2840
|
-
|
|
2841
|
-
|
|
2842
|
-
|
|
2843
|
-
|
|
2844
|
-
|
|
2845
|
-
|
|
3216
|
+
throw new Error(`Could not resolve command on PATH: ${cmd}`);
|
|
3217
|
+
}
|
|
3218
|
+
function encodeCwd(cwd) {
|
|
3219
|
+
return path5.resolve(cwd).replace(/[^a-zA-Z0-9]/g, "-");
|
|
3220
|
+
}
|
|
3221
|
+
var TERMINAL_STOP = /* @__PURE__ */ new Set(["end_turn", "stop_sequence", "max_tokens"]);
|
|
3222
|
+
var delay = (ms) => new Promise((r) => setTimeout(r, ms));
|
|
3223
|
+
function resolveConfigDir(configDir) {
|
|
3224
|
+
const value = configDir ?? process.env.CLAUDE_CONFIG_DIR;
|
|
3225
|
+
if (!value) return path5.join(os3.homedir(), ".claude");
|
|
3226
|
+
if (value === "~") return os3.homedir();
|
|
3227
|
+
if (value.startsWith("~/") || value.startsWith("~\\")) {
|
|
3228
|
+
return path5.join(os3.homedir(), value.slice(2));
|
|
3229
|
+
}
|
|
3230
|
+
return path5.resolve(value);
|
|
3231
|
+
}
|
|
3232
|
+
var ClaudeSession = class {
|
|
3233
|
+
sessionId;
|
|
3234
|
+
cwd;
|
|
3235
|
+
configDir;
|
|
3236
|
+
jsonlPath;
|
|
3237
|
+
raw = "";
|
|
3238
|
+
proc = null;
|
|
3239
|
+
cursor = 0;
|
|
3240
|
+
// index into transcript split('\n')
|
|
3241
|
+
lastDataAt = 0;
|
|
3242
|
+
exited = false;
|
|
3243
|
+
exitCode = null;
|
|
3244
|
+
aborted = false;
|
|
3245
|
+
signal;
|
|
3246
|
+
o;
|
|
3247
|
+
constructor(opts = {}) {
|
|
3248
|
+
this.cwd = path5.resolve(opts.cwd ?? process.cwd());
|
|
3249
|
+
this.configDir = resolveConfigDir(opts.configDir);
|
|
3250
|
+
this.signal = opts.signal;
|
|
3251
|
+
this.sessionId = randomUUID3();
|
|
3252
|
+
this.jsonlPath = path5.join(
|
|
3253
|
+
this.configDir,
|
|
3254
|
+
"projects",
|
|
3255
|
+
encodeCwd(this.cwd),
|
|
3256
|
+
`${this.sessionId}.jsonl`
|
|
3257
|
+
);
|
|
3258
|
+
this.o = {
|
|
3259
|
+
cwd: this.cwd,
|
|
3260
|
+
cliPath: opts.cliPath,
|
|
3261
|
+
configDir: this.configDir,
|
|
3262
|
+
model: opts.model,
|
|
3263
|
+
settingSources: opts.settingSources,
|
|
3264
|
+
extraArgs: opts.extraArgs ?? [],
|
|
3265
|
+
ignoreAnthropicApiKey: opts.ignoreAnthropicApiKey,
|
|
3266
|
+
effort: opts.effort,
|
|
3267
|
+
cols: opts.cols ?? 200,
|
|
3268
|
+
rows: opts.rows ?? 50,
|
|
3269
|
+
bootMinMs: opts.bootMinMs ?? 3e3,
|
|
3270
|
+
bootQuietMs: opts.bootQuietMs ?? 1500,
|
|
3271
|
+
bootMaxMs: opts.bootMaxMs ?? 25e3,
|
|
3272
|
+
pollMs: opts.pollMs ?? 250,
|
|
3273
|
+
// Agentic turns (tool loops) routinely run for many minutes; a short
|
|
3274
|
+
// cap would surface as a mid-task error result. 30 min mirrors the
|
|
3275
|
+
// proxy-tool ceiling rather than a chat-reply expectation.
|
|
3276
|
+
turnTimeoutMs: opts.turnTimeoutMs ?? 18e5,
|
|
3277
|
+
bracketedPaste: opts.bracketedPaste ?? true,
|
|
3278
|
+
submitMinMs: opts.submitMinMs ?? 200,
|
|
3279
|
+
submitConfirmMs: opts.submitConfirmMs ?? 1500,
|
|
3280
|
+
submitMaxRetries: opts.submitMaxRetries ?? 8,
|
|
3281
|
+
debug: opts.debug ?? false
|
|
3282
|
+
};
|
|
3283
|
+
}
|
|
3284
|
+
async start() {
|
|
3285
|
+
if (this.signal?.aborted) throw new Error("aborted before start");
|
|
3286
|
+
this.signal?.addEventListener(
|
|
3287
|
+
"abort",
|
|
3288
|
+
() => {
|
|
3289
|
+
this.aborted = true;
|
|
3290
|
+
this.dispose();
|
|
3291
|
+
},
|
|
3292
|
+
{ once: true }
|
|
3293
|
+
);
|
|
3294
|
+
const claude = resolveClaude(this.o.cliPath ?? "claude");
|
|
3295
|
+
const args = ["--session-id", this.sessionId];
|
|
3296
|
+
if (this.o.model) args.push("--model", this.o.model);
|
|
3297
|
+
if (this.o.settingSources !== null && this.o.settingSources !== void 0) {
|
|
3298
|
+
args.push("--setting-sources", this.o.settingSources);
|
|
2846
3299
|
}
|
|
2847
|
-
|
|
2848
|
-
|
|
2849
|
-
|
|
2850
|
-
|
|
2851
|
-
|
|
2852
|
-
|
|
2853
|
-
|
|
2854
|
-
|
|
2855
|
-
|
|
2856
|
-
|
|
2857
|
-
|
|
2858
|
-
|
|
2859
|
-
}
|
|
2860
|
-
|
|
2861
|
-
|
|
2862
|
-
|
|
2863
|
-
|
|
2864
|
-
|
|
2865
|
-
|
|
2866
|
-
|
|
2867
|
-
|
|
2868
|
-
|
|
2869
|
-
id: requestId,
|
|
2870
|
-
result: {
|
|
2871
|
-
protocolVersion: PROTOCOL_VERSION,
|
|
2872
|
-
capabilities: { tools: {} },
|
|
2873
|
-
serverInfo: {
|
|
2874
|
-
name: SERVER_NAME,
|
|
2875
|
-
version: "0.1.0"
|
|
2876
|
-
}
|
|
2877
|
-
}
|
|
2878
|
-
});
|
|
2879
|
-
return;
|
|
2880
|
-
}
|
|
2881
|
-
if (request.method === "notifications/initialized") {
|
|
2882
|
-
res.statusCode = 204;
|
|
2883
|
-
res.end();
|
|
2884
|
-
return;
|
|
2885
|
-
}
|
|
2886
|
-
if (request.method === "tools/list") {
|
|
2887
|
-
writeJson(res, {
|
|
2888
|
-
jsonrpc: "2.0",
|
|
2889
|
-
id: requestId,
|
|
2890
|
-
result: {
|
|
2891
|
-
tools: tools.map((t) => ({
|
|
2892
|
-
name: t.name,
|
|
2893
|
-
description: t.description,
|
|
2894
|
-
inputSchema: t.inputSchema
|
|
2895
|
-
}))
|
|
2896
|
-
}
|
|
2897
|
-
});
|
|
2898
|
-
return;
|
|
2899
|
-
}
|
|
2900
|
-
if (request.method === "tools/call") {
|
|
2901
|
-
const params = request.params ?? {};
|
|
2902
|
-
const toolName = String(params.name ?? "");
|
|
2903
|
-
const input = params.arguments ?? {};
|
|
2904
|
-
if (!tools.some((t) => t.name === toolName)) {
|
|
2905
|
-
writeJson(res, {
|
|
2906
|
-
jsonrpc: "2.0",
|
|
2907
|
-
id: requestId,
|
|
2908
|
-
result: {
|
|
2909
|
-
content: [{ type: "text", text: `Unknown proxy tool: ${toolName}` }],
|
|
2910
|
-
isError: true
|
|
2911
|
-
}
|
|
2912
|
-
});
|
|
2913
|
-
return;
|
|
2914
|
-
}
|
|
2915
|
-
const interceptor = interceptors?.get(toolName);
|
|
2916
|
-
if (interceptor) {
|
|
2917
|
-
let intercepted;
|
|
2918
|
-
try {
|
|
2919
|
-
intercepted = await interceptor(input);
|
|
2920
|
-
} catch (interceptorError) {
|
|
2921
|
-
const message = interceptorError instanceof Error ? interceptorError.message : String(interceptorError);
|
|
2922
|
-
log.warn("proxy-mcp interceptor failed", { toolName, error: message });
|
|
2923
|
-
intercepted = { kind: "error", message };
|
|
2924
|
-
}
|
|
2925
|
-
writeToolCallResult(res, requestId, intercepted);
|
|
2926
|
-
return;
|
|
3300
|
+
if (this.o.extraArgs && this.o.extraArgs.length) args.push(...this.o.extraArgs);
|
|
3301
|
+
if (this.o.debug)
|
|
3302
|
+
process.stderr.write(`[session] spawn: ${claude} ${args.join(" ")}
|
|
3303
|
+
`);
|
|
3304
|
+
this.lastDataAt = Date.now();
|
|
3305
|
+
this.proc = Bun.spawn([claude, ...args], {
|
|
3306
|
+
cwd: this.cwd,
|
|
3307
|
+
env: {
|
|
3308
|
+
...process.env,
|
|
3309
|
+
CLAUDE_CONFIG_DIR: this.o.configDir,
|
|
3310
|
+
TERM: "xterm-256color",
|
|
3311
|
+
...this.o.ignoreAnthropicApiKey ? { ANTHROPIC_API_KEY: void 0, ANTHROPIC_AUTH_TOKEN: void 0 } : {},
|
|
3312
|
+
...this.o.effort ? { CLAUDE_CODE_EFFORT_LEVEL: this.o.effort } : {}
|
|
3313
|
+
},
|
|
3314
|
+
terminal: {
|
|
3315
|
+
cols: this.o.cols,
|
|
3316
|
+
rows: this.o.rows,
|
|
3317
|
+
data: (_term, d) => {
|
|
3318
|
+
this.lastDataAt = Date.now();
|
|
3319
|
+
const chunk = Buffer.from(d).toString("utf8");
|
|
3320
|
+
this.raw += chunk;
|
|
3321
|
+
if (this.o.debug) process.stdout.write(chunk);
|
|
2927
3322
|
}
|
|
2928
|
-
const callId = crypto2.randomUUID();
|
|
2929
|
-
log.info("proxy-mcp tool call received", {
|
|
2930
|
-
callId,
|
|
2931
|
-
toolName,
|
|
2932
|
-
hasInput: input != null
|
|
2933
|
-
});
|
|
2934
|
-
let timer = null;
|
|
2935
|
-
const result = await new Promise(
|
|
2936
|
-
(resolve4, reject2) => {
|
|
2937
|
-
const entry = {
|
|
2938
|
-
id: callId,
|
|
2939
|
-
toolName,
|
|
2940
|
-
input,
|
|
2941
|
-
resolve: resolve4,
|
|
2942
|
-
reject: reject2
|
|
2943
|
-
};
|
|
2944
|
-
pending.set(callId, entry);
|
|
2945
|
-
const deadlineMs = resolveProxyCallTimeoutMs(
|
|
2946
|
-
toolName,
|
|
2947
|
-
input,
|
|
2948
|
-
timeoutOverrides
|
|
2949
|
-
);
|
|
2950
|
-
timer = setTimeout(() => {
|
|
2951
|
-
if (!pending.has(callId)) return;
|
|
2952
|
-
pending.delete(callId);
|
|
2953
|
-
log.notice("proxy-mcp tool call timed out", {
|
|
2954
|
-
callId,
|
|
2955
|
-
toolName,
|
|
2956
|
-
deadlineMs
|
|
2957
|
-
});
|
|
2958
|
-
reject2(buildProxyTimeoutError(toolName, deadlineMs));
|
|
2959
|
-
}, deadlineMs);
|
|
2960
|
-
calls.emit("call", entry);
|
|
2961
|
-
}
|
|
2962
|
-
).finally(() => {
|
|
2963
|
-
if (timer) clearTimeout(timer);
|
|
2964
|
-
pending.delete(callId);
|
|
2965
|
-
});
|
|
2966
|
-
writeToolCallResult(res, requestId, result);
|
|
2967
|
-
return;
|
|
2968
3323
|
}
|
|
2969
|
-
|
|
2970
|
-
|
|
2971
|
-
|
|
2972
|
-
|
|
2973
|
-
|
|
2974
|
-
}
|
|
2975
|
-
|
|
2976
|
-
|
|
2977
|
-
|
|
2978
|
-
|
|
2979
|
-
|
|
2980
|
-
|
|
3324
|
+
});
|
|
3325
|
+
this.proc.exited.then((code) => {
|
|
3326
|
+
this.exitCode = typeof code === "number" ? code : null;
|
|
3327
|
+
this.exited = true;
|
|
3328
|
+
this.proc = null;
|
|
3329
|
+
}).catch(() => {
|
|
3330
|
+
this.exited = true;
|
|
3331
|
+
this.proc = null;
|
|
3332
|
+
});
|
|
3333
|
+
await this.waitForBoot();
|
|
3334
|
+
this.cursor = this.lineCount();
|
|
3335
|
+
}
|
|
3336
|
+
/** Wait until the TUI has been quiet for bootQuietMs (Ink ready), bounded by
|
|
3337
|
+
* bootMinMs..bootMaxMs. */
|
|
3338
|
+
async waitForBoot() {
|
|
3339
|
+
const start = Date.now();
|
|
3340
|
+
while (Date.now() - start < this.o.bootMaxMs) {
|
|
3341
|
+
await delay(150);
|
|
3342
|
+
if (this.aborted) throw new Error("aborted during boot");
|
|
3343
|
+
if (this.exited) {
|
|
3344
|
+
throw new Error(this.failureMessage("claude exited during boot", true));
|
|
3345
|
+
}
|
|
3346
|
+
const elapsed = Date.now() - start;
|
|
3347
|
+
const sinceData = Date.now() - this.lastDataAt;
|
|
3348
|
+
if (elapsed >= this.o.bootMinMs && sinceData >= this.o.bootQuietMs) return;
|
|
3349
|
+
}
|
|
3350
|
+
}
|
|
3351
|
+
/** Submit the freshly-injected prompt and confirm the turn was actually
|
|
3352
|
+
* accepted. A large bracketed paste collapses into a "[Pasted text]"
|
|
3353
|
+
* placeholder; an Enter sent while claude is still ingesting the paste is
|
|
3354
|
+
* silently dropped, so a single fixed-delay Enter races the paste and can
|
|
3355
|
+
* leave the prompt sitting unsubmitted (→ hang until turnTimeoutMs). Send
|
|
3356
|
+
* Enter, then poll for transcript growth past the cursor (the turn's records
|
|
3357
|
+
* are written on acceptance); resend Enter until accepted or the retry
|
|
3358
|
+
* budget is spent. Polling growth (not a blind delay) also stops us from
|
|
3359
|
+
* sending a stray Enter once the turn is in flight. */
|
|
3360
|
+
async submitTurn() {
|
|
3361
|
+
await delay(this.o.submitMinMs);
|
|
3362
|
+
for (let attempt = 0; attempt < this.o.submitMaxRetries; attempt++) {
|
|
3363
|
+
if (this.aborted || this.exited || !this.proc) return;
|
|
3364
|
+
this.proc.terminal.write("\r");
|
|
3365
|
+
const until = Date.now() + this.o.submitConfirmMs;
|
|
3366
|
+
while (Date.now() < until) {
|
|
3367
|
+
await delay(80);
|
|
3368
|
+
if (this.aborted || this.exited) return;
|
|
3369
|
+
if (this.lineCount() > this.cursor) return;
|
|
3370
|
+
}
|
|
3371
|
+
}
|
|
3372
|
+
}
|
|
3373
|
+
readRawLines() {
|
|
3374
|
+
try {
|
|
3375
|
+
return fs4.readFileSync(this.jsonlPath, "utf8").split("\n");
|
|
3376
|
+
} catch {
|
|
3377
|
+
return [];
|
|
3378
|
+
}
|
|
3379
|
+
}
|
|
3380
|
+
/** Count of complete lines (split('\n') minus the trailing/partial element). */
|
|
3381
|
+
lineCount() {
|
|
3382
|
+
const lines = this.readRawLines();
|
|
3383
|
+
return lines.length > 0 ? lines.length - 1 : 0;
|
|
3384
|
+
}
|
|
3385
|
+
rawTail(max = 600) {
|
|
3386
|
+
const clean = this.raw.replace(/\x1B(?:[@-Z\\-_]|\[[0-?]*[ -/]*[@-~])/g, "").replace(/\s+/g, " ").trim();
|
|
3387
|
+
return clean.length > max ? clean.slice(-max) : clean;
|
|
3388
|
+
}
|
|
3389
|
+
failureMessage(reason, includeRaw = false) {
|
|
3390
|
+
const parts = [
|
|
3391
|
+
`${reason} (sessionId=${this.sessionId}, jsonlPath=${this.jsonlPath}, exitCode=${this.exitCode ?? "unknown"})`
|
|
3392
|
+
];
|
|
3393
|
+
if (includeRaw) {
|
|
3394
|
+
const tail = this.rawTail();
|
|
3395
|
+
if (tail) parts.push(`terminalTail=${JSON.stringify(tail)}`);
|
|
3396
|
+
}
|
|
3397
|
+
return parts.join("; ");
|
|
3398
|
+
}
|
|
3399
|
+
/**
|
|
3400
|
+
* Inject a turn into the live session and return the assistant reply once a
|
|
3401
|
+
* terminal stop_reason is observed in the transcript.
|
|
3402
|
+
*/
|
|
3403
|
+
async ask(prompt, perTurnTimeoutMs) {
|
|
3404
|
+
if (this.aborted) throw new Error("aborted");
|
|
3405
|
+
if (!this.proc || this.exited)
|
|
3406
|
+
throw new Error("session not started or already exited");
|
|
3407
|
+
const timeout = perTurnTimeoutMs ?? this.o.turnTimeoutMs;
|
|
3408
|
+
const t0 = Date.now();
|
|
3409
|
+
if (this.o.bracketedPaste) {
|
|
3410
|
+
this.proc.terminal.write("\x1B[200~" + prompt + "\x1B[201~");
|
|
3411
|
+
} else {
|
|
3412
|
+
this.proc.terminal.write(prompt);
|
|
3413
|
+
}
|
|
3414
|
+
await this.submitTurn();
|
|
3415
|
+
const collected = [];
|
|
3416
|
+
let lastUsage = null;
|
|
3417
|
+
let stopReason = null;
|
|
3418
|
+
const deadline = Date.now() + timeout;
|
|
3419
|
+
while (Date.now() < deadline) {
|
|
3420
|
+
await delay(this.o.pollMs);
|
|
3421
|
+
if (this.aborted) throw new Error("aborted mid-turn");
|
|
3422
|
+
const lines = this.readRawLines();
|
|
3423
|
+
const lastComplete = lines.length - 1;
|
|
3424
|
+
if (lastComplete <= this.cursor) {
|
|
3425
|
+
if (this.exited) throw new Error(this.failureMessage("claude exited mid-turn", true));
|
|
3426
|
+
continue;
|
|
3427
|
+
}
|
|
3428
|
+
for (let i = this.cursor; i < lastComplete; i++) {
|
|
3429
|
+
const s = lines[i];
|
|
3430
|
+
if (!s || !s.trim()) continue;
|
|
3431
|
+
let rec;
|
|
2981
3432
|
try {
|
|
2982
|
-
|
|
2983
|
-
jsonrpc: "2.0",
|
|
2984
|
-
id: requestId,
|
|
2985
|
-
result: {
|
|
2986
|
-
content: [{ type: "text", text: errorMessage }],
|
|
2987
|
-
isError: true
|
|
2988
|
-
}
|
|
2989
|
-
});
|
|
3433
|
+
rec = JSON.parse(s);
|
|
2990
3434
|
} catch {
|
|
2991
|
-
|
|
2992
|
-
res.statusCode = 500;
|
|
2993
|
-
res.end();
|
|
2994
|
-
} catch {
|
|
2995
|
-
}
|
|
3435
|
+
continue;
|
|
2996
3436
|
}
|
|
2997
|
-
|
|
2998
|
-
|
|
2999
|
-
|
|
3000
|
-
|
|
3001
|
-
|
|
3002
|
-
|
|
3003
|
-
|
|
3004
|
-
|
|
3005
|
-
message: error instanceof Error ? error.message : "Internal error"
|
|
3437
|
+
if (rec.type === "assistant" && rec.message) {
|
|
3438
|
+
for (const b of rec.message.content ?? []) {
|
|
3439
|
+
if (b?.type === "text" && typeof b.text === "string")
|
|
3440
|
+
collected.push(b.text);
|
|
3441
|
+
}
|
|
3442
|
+
if (rec.message.usage) lastUsage = rec.message.usage;
|
|
3443
|
+
if (rec.message.stop_reason && TERMINAL_STOP.has(rec.message.stop_reason)) {
|
|
3444
|
+
stopReason = rec.message.stop_reason;
|
|
3006
3445
|
}
|
|
3007
|
-
});
|
|
3008
|
-
} catch {
|
|
3009
|
-
try {
|
|
3010
|
-
res.statusCode = 500;
|
|
3011
|
-
res.end();
|
|
3012
|
-
} catch {
|
|
3013
3446
|
}
|
|
3014
3447
|
}
|
|
3448
|
+
this.cursor = lastComplete;
|
|
3449
|
+
if (stopReason) break;
|
|
3015
3450
|
}
|
|
3016
|
-
|
|
3017
|
-
|
|
3018
|
-
|
|
3019
|
-
|
|
3020
|
-
|
|
3021
|
-
resolve4();
|
|
3022
|
-
});
|
|
3023
|
-
});
|
|
3024
|
-
const addr = server2.address();
|
|
3025
|
-
if (!addr) {
|
|
3026
|
-
server2.close();
|
|
3027
|
-
throw new Error("Failed to bind proxy MCP server");
|
|
3028
|
-
}
|
|
3029
|
-
boundAuthority = `127.0.0.1:${addr.port}`;
|
|
3030
|
-
const url = `http://${boundAuthority}/mcp`;
|
|
3031
|
-
log.info("proxy-mcp server started", {
|
|
3032
|
-
url,
|
|
3033
|
-
tools: tools.map((t) => t.name)
|
|
3034
|
-
});
|
|
3035
|
-
let configFilePath = null;
|
|
3036
|
-
const api = {
|
|
3037
|
-
url,
|
|
3038
|
-
serverName: SERVER_NAME,
|
|
3039
|
-
tools,
|
|
3040
|
-
authToken,
|
|
3041
|
-
calls,
|
|
3042
|
-
configPath() {
|
|
3043
|
-
if (configFilePath) return configFilePath;
|
|
3044
|
-
const body = JSON.stringify(
|
|
3045
|
-
{
|
|
3046
|
-
mcpServers: {
|
|
3047
|
-
[SERVER_NAME]: {
|
|
3048
|
-
type: "http",
|
|
3049
|
-
url,
|
|
3050
|
-
// Claude CLI replays these headers on every request to this
|
|
3051
|
-
// server, which is what lets the handler above reject anyone
|
|
3052
|
-
// who did not read this 0600 file.
|
|
3053
|
-
headers: { Authorization: `Bearer ${authToken}` },
|
|
3054
|
-
timeout: resolveProxyClientCeilingMs(timeoutOverrides)
|
|
3055
|
-
}
|
|
3056
|
-
}
|
|
3057
|
-
},
|
|
3058
|
-
null,
|
|
3059
|
-
2
|
|
3060
|
-
);
|
|
3061
|
-
const hash = crypto2.createHash("sha256").update(body).digest("hex").slice(0, 12);
|
|
3062
|
-
const outPath = path4.join(
|
|
3063
|
-
pluginTmpDir(),
|
|
3064
|
-
`proxy-${hash}.json`
|
|
3451
|
+
if (!stopReason) {
|
|
3452
|
+
throw new Error(
|
|
3453
|
+
this.failureMessage(
|
|
3454
|
+
`turn timed out after ${timeout}ms (no terminal assistant record; collected ${collected.length} text block(s))`
|
|
3455
|
+
)
|
|
3065
3456
|
);
|
|
3066
|
-
|
|
3067
|
-
|
|
3068
|
-
|
|
3069
|
-
|
|
3070
|
-
|
|
3071
|
-
|
|
3072
|
-
|
|
3457
|
+
}
|
|
3458
|
+
const u = lastUsage ?? {};
|
|
3459
|
+
return {
|
|
3460
|
+
text: collected.join("\n").trim(),
|
|
3461
|
+
stopReason,
|
|
3462
|
+
usage: lastUsage,
|
|
3463
|
+
cacheReadTokens: u.cache_read_input_tokens ?? 0,
|
|
3464
|
+
cacheCreationTokens: u.cache_creation_input_tokens ?? 0,
|
|
3465
|
+
ephemeral1hTokens: u.cache_creation?.ephemeral_1h_input_tokens ?? 0,
|
|
3466
|
+
ephemeral5mTokens: u.cache_creation?.ephemeral_5m_input_tokens ?? 0,
|
|
3467
|
+
inputTokens: u.input_tokens ?? 0,
|
|
3468
|
+
outputTokens: u.output_tokens ?? 0,
|
|
3469
|
+
elapsedMs: Date.now() - t0
|
|
3470
|
+
};
|
|
3471
|
+
}
|
|
3472
|
+
/**
|
|
3473
|
+
* Like ask(), but instead of collecting the reply text it re-emits each NEW
|
|
3474
|
+
* raw JSONL transcript line via onLine (verbatim) until a terminal
|
|
3475
|
+
* stop_reason. Returns the terminal stop_reason + the last assistant usage.
|
|
3476
|
+
* Used by the opencode plugin transport shim, which feeds these raw lines
|
|
3477
|
+
* into the existing stream-json line handler unchanged.
|
|
3478
|
+
*/
|
|
3479
|
+
async tailTurn(prompt, onLine, perTurnTimeoutMs) {
|
|
3480
|
+
if (this.aborted) throw new Error("aborted");
|
|
3481
|
+
if (!this.proc || this.exited)
|
|
3482
|
+
throw new Error("session not started or already exited");
|
|
3483
|
+
const timeout = perTurnTimeoutMs ?? this.o.turnTimeoutMs;
|
|
3484
|
+
if (this.o.bracketedPaste) {
|
|
3485
|
+
this.proc.terminal.write("\x1B[200~" + prompt + "\x1B[201~");
|
|
3486
|
+
} else {
|
|
3487
|
+
this.proc.terminal.write(prompt);
|
|
3488
|
+
}
|
|
3489
|
+
await this.submitTurn();
|
|
3490
|
+
let lastUsage = null;
|
|
3491
|
+
let totalOutput = 0;
|
|
3492
|
+
let stopReason = null;
|
|
3493
|
+
const deadline = Date.now() + timeout;
|
|
3494
|
+
while (Date.now() < deadline) {
|
|
3495
|
+
await delay(this.o.pollMs);
|
|
3496
|
+
if (this.aborted) throw new Error("aborted mid-turn");
|
|
3497
|
+
const lines = this.readRawLines();
|
|
3498
|
+
const lastComplete = lines.length - 1;
|
|
3499
|
+
if (lastComplete <= this.cursor) {
|
|
3500
|
+
if (this.exited) {
|
|
3501
|
+
throw new Error(this.failureMessage("claude exited mid-turn", true));
|
|
3502
|
+
}
|
|
3503
|
+
continue;
|
|
3073
3504
|
}
|
|
3074
|
-
|
|
3075
|
-
|
|
3076
|
-
|
|
3077
|
-
|
|
3078
|
-
|
|
3505
|
+
for (let i = this.cursor; i < lastComplete; i++) {
|
|
3506
|
+
const s = lines[i];
|
|
3507
|
+
if (!s || !s.trim()) continue;
|
|
3508
|
+
onLine(s);
|
|
3509
|
+
let rec;
|
|
3079
3510
|
try {
|
|
3080
|
-
|
|
3511
|
+
rec = JSON.parse(s);
|
|
3081
3512
|
} catch {
|
|
3513
|
+
continue;
|
|
3514
|
+
}
|
|
3515
|
+
if (rec.type === "assistant" && rec.message) {
|
|
3516
|
+
if (rec.message.usage) {
|
|
3517
|
+
lastUsage = rec.message.usage;
|
|
3518
|
+
totalOutput += rec.message.usage.output_tokens ?? 0;
|
|
3519
|
+
}
|
|
3520
|
+
if (rec.message.stop_reason && TERMINAL_STOP.has(rec.message.stop_reason)) {
|
|
3521
|
+
stopReason = rec.message.stop_reason;
|
|
3522
|
+
}
|
|
3082
3523
|
}
|
|
3083
|
-
configFilePath = null;
|
|
3084
3524
|
}
|
|
3525
|
+
this.cursor = lastComplete;
|
|
3526
|
+
if (stopReason) break;
|
|
3085
3527
|
}
|
|
3086
|
-
|
|
3087
|
-
|
|
3088
|
-
}
|
|
3089
|
-
|
|
3090
|
-
|
|
3091
|
-
|
|
3092
|
-
|
|
3093
|
-
|
|
3094
|
-
|
|
3095
|
-
|
|
3096
|
-
|
|
3097
|
-
|
|
3098
|
-
|
|
3099
|
-
|
|
3100
|
-
|
|
3101
|
-
|
|
3102
|
-
|
|
3103
|
-
|
|
3104
|
-
question: ["AskUserQuestion"]
|
|
3105
|
-
};
|
|
3106
|
-
const out = [];
|
|
3107
|
-
const seen = /* @__PURE__ */ new Set();
|
|
3108
|
-
for (const t of tools) {
|
|
3109
|
-
const mapped = nameMap[t.name.toLowerCase()];
|
|
3110
|
-
if (!mapped) continue;
|
|
3111
|
-
for (const claudeTool of mapped) {
|
|
3112
|
-
if (seen.has(claudeTool)) continue;
|
|
3113
|
-
seen.add(claudeTool);
|
|
3114
|
-
out.push(claudeTool);
|
|
3528
|
+
let usage = lastUsage;
|
|
3529
|
+
if (lastUsage) {
|
|
3530
|
+
usage = { ...lastUsage, output_tokens: totalOutput };
|
|
3531
|
+
if (Array.isArray(lastUsage.iterations) && lastUsage.iterations.length > 0) {
|
|
3532
|
+
const iters = lastUsage.iterations.map((it) => ({ ...it }));
|
|
3533
|
+
iters[iters.length - 1] = {
|
|
3534
|
+
...iters[iters.length - 1],
|
|
3535
|
+
output_tokens: totalOutput
|
|
3536
|
+
};
|
|
3537
|
+
usage.iterations = iters;
|
|
3538
|
+
}
|
|
3539
|
+
}
|
|
3540
|
+
if (!stopReason) {
|
|
3541
|
+
throw new Error(
|
|
3542
|
+
this.failureMessage(
|
|
3543
|
+
`turn timed out after ${timeout}ms (no terminal assistant record)`
|
|
3544
|
+
)
|
|
3545
|
+
);
|
|
3115
3546
|
}
|
|
3547
|
+
return { stopReason, usage };
|
|
3116
3548
|
}
|
|
3117
|
-
|
|
3118
|
-
|
|
3119
|
-
|
|
3120
|
-
|
|
3121
|
-
|
|
3122
|
-
|
|
3123
|
-
|
|
3124
|
-
|
|
3125
|
-
|
|
3126
|
-
|
|
3127
|
-
|
|
3128
|
-
|
|
3129
|
-
|
|
3130
|
-
|
|
3131
|
-
return out;
|
|
3132
|
-
}
|
|
3133
|
-
function readBody(req) {
|
|
3134
|
-
return new Promise((resolve4, reject) => {
|
|
3135
|
-
const chunks = [];
|
|
3136
|
-
req.on("data", (chunk) => chunks.push(chunk));
|
|
3137
|
-
req.on("end", () => resolve4(Buffer.concat(chunks).toString("utf8")));
|
|
3138
|
-
req.on("error", reject);
|
|
3139
|
-
});
|
|
3140
|
-
}
|
|
3141
|
-
function writeToolCallResult(res, requestId, result) {
|
|
3142
|
-
const text = result.kind === "error" ? result.message : result.text;
|
|
3143
|
-
const isError = result.kind === "error" || result.isError === true;
|
|
3144
|
-
writeJson(res, {
|
|
3145
|
-
jsonrpc: "2.0",
|
|
3146
|
-
id: requestId ?? null,
|
|
3147
|
-
result: {
|
|
3148
|
-
content: [{ type: "text", text }],
|
|
3149
|
-
isError
|
|
3549
|
+
dispose() {
|
|
3550
|
+
if (this.proc) {
|
|
3551
|
+
try {
|
|
3552
|
+
this.proc.terminal.write("");
|
|
3553
|
+
} catch {
|
|
3554
|
+
}
|
|
3555
|
+
try {
|
|
3556
|
+
this.proc.kill();
|
|
3557
|
+
} catch {
|
|
3558
|
+
}
|
|
3559
|
+
try {
|
|
3560
|
+
this.proc.terminal.close();
|
|
3561
|
+
} catch {
|
|
3562
|
+
}
|
|
3150
3563
|
}
|
|
3151
|
-
|
|
3152
|
-
}
|
|
3153
|
-
|
|
3154
|
-
const payload = JSON.stringify(body);
|
|
3155
|
-
res.statusCode = 200;
|
|
3156
|
-
res.setHeader("Content-Type", "application/json");
|
|
3157
|
-
res.setHeader("Content-Length", Buffer.byteLength(payload).toString());
|
|
3158
|
-
res.end(payload);
|
|
3159
|
-
}
|
|
3564
|
+
this.proc = null;
|
|
3565
|
+
}
|
|
3566
|
+
};
|
|
3160
3567
|
|
|
3161
|
-
// src/
|
|
3162
|
-
|
|
3163
|
-
|
|
3164
|
-
|
|
3165
|
-
|
|
3166
|
-
|
|
3167
|
-
|
|
3168
|
-
}
|
|
3169
|
-
function indexAdd(sessionKey2, callId) {
|
|
3170
|
-
let s = callIdsBySession.get(sessionKey2);
|
|
3171
|
-
if (!s) {
|
|
3172
|
-
s = /* @__PURE__ */ new Set();
|
|
3173
|
-
callIdsBySession.set(sessionKey2, s);
|
|
3568
|
+
// src/claude-session-wrapper.ts
|
|
3569
|
+
function decodeUserEnvelope(chunk) {
|
|
3570
|
+
let parsed;
|
|
3571
|
+
try {
|
|
3572
|
+
parsed = JSON.parse(chunk);
|
|
3573
|
+
} catch {
|
|
3574
|
+
return chunk;
|
|
3174
3575
|
}
|
|
3175
|
-
|
|
3176
|
-
|
|
3177
|
-
|
|
3178
|
-
|
|
3179
|
-
|
|
3180
|
-
|
|
3181
|
-
|
|
3182
|
-
|
|
3183
|
-
|
|
3184
|
-
|
|
3185
|
-
|
|
3186
|
-
|
|
3187
|
-
|
|
3188
|
-
|
|
3189
|
-
|
|
3190
|
-
|
|
3191
|
-
|
|
3192
|
-
|
|
3193
|
-
|
|
3194
|
-
);
|
|
3195
|
-
pendingByCallId.delete(call.id);
|
|
3196
|
-
indexRemove(previous.sessionKey, call.id);
|
|
3576
|
+
if (!parsed || parsed.type !== "user" || !parsed.message) return chunk;
|
|
3577
|
+
const content = parsed.message.content;
|
|
3578
|
+
if (typeof content === "string") return content;
|
|
3579
|
+
if (!Array.isArray(content)) return chunk;
|
|
3580
|
+
const parts = [];
|
|
3581
|
+
let dropped = 0;
|
|
3582
|
+
for (const block of content) {
|
|
3583
|
+
if (block?.type === "text" && typeof block.text === "string") {
|
|
3584
|
+
parts.push(block.text);
|
|
3585
|
+
} else if (block?.type === "tool_result") {
|
|
3586
|
+
const v = block.content;
|
|
3587
|
+
const text = typeof v === "string" ? v : Array.isArray(v) ? v.map((i) => i?.type === "text" ? i.text : "").filter(Boolean).join("\n") : "";
|
|
3588
|
+
parts.push(
|
|
3589
|
+
`[Tool result${block.tool_use_id ? ` ${block.tool_use_id}` : ""}]
|
|
3590
|
+
${text}`
|
|
3591
|
+
);
|
|
3592
|
+
} else {
|
|
3593
|
+
dropped++;
|
|
3594
|
+
}
|
|
3197
3595
|
}
|
|
3198
|
-
|
|
3199
|
-
|
|
3200
|
-
|
|
3201
|
-
timeoutOverrides
|
|
3202
|
-
);
|
|
3203
|
-
const timer = setTimeout(() => {
|
|
3204
|
-
const current = pendingByCallId.get(call.id);
|
|
3205
|
-
if (!current) return;
|
|
3206
|
-
pendingByCallId.delete(call.id);
|
|
3207
|
-
indexRemove(current.sessionKey, call.id);
|
|
3208
|
-
current.reject(buildProxyTimeoutError(call.toolName, deadlineMs));
|
|
3209
|
-
log.notice("timed out pending proxy call", {
|
|
3210
|
-
sessionKey: current.sessionKey,
|
|
3211
|
-
toolCallId: call.id,
|
|
3212
|
-
toolName: call.toolName,
|
|
3213
|
-
deadlineMs
|
|
3596
|
+
if (dropped > 0) {
|
|
3597
|
+
log.warn("interactive transport dropped non-text content blocks", {
|
|
3598
|
+
dropped
|
|
3214
3599
|
});
|
|
3215
|
-
}
|
|
3216
|
-
|
|
3217
|
-
sessionKey: sessionKey2,
|
|
3218
|
-
toolCallId: call.id,
|
|
3219
|
-
toolName: call.toolName,
|
|
3220
|
-
input: call.input,
|
|
3221
|
-
createdAt: Date.now(),
|
|
3222
|
-
timer,
|
|
3223
|
-
resolve: call.resolve,
|
|
3224
|
-
reject: call.reject
|
|
3225
|
-
};
|
|
3226
|
-
pendingByCallId.set(call.id, pending);
|
|
3227
|
-
indexAdd(sessionKey2, call.id);
|
|
3228
|
-
emitter.emit(eventName(sessionKey2), pending);
|
|
3229
|
-
log.info("queued pending proxy call", {
|
|
3230
|
-
sessionKey: sessionKey2,
|
|
3231
|
-
toolCallId: call.id,
|
|
3232
|
-
toolName: call.toolName
|
|
3233
|
-
});
|
|
3234
|
-
return pending;
|
|
3600
|
+
}
|
|
3601
|
+
return parts.join("\n\n");
|
|
3235
3602
|
}
|
|
3236
|
-
function
|
|
3237
|
-
const
|
|
3238
|
-
if (
|
|
3239
|
-
|
|
3240
|
-
|
|
3241
|
-
|
|
3242
|
-
|
|
3603
|
+
function spawnInteractiveProcess(opts) {
|
|
3604
|
+
const extraArgs = [];
|
|
3605
|
+
if (opts.mcpConfigPaths && opts.mcpConfigPaths.length > 0) {
|
|
3606
|
+
extraArgs.push(
|
|
3607
|
+
"--mcp-config",
|
|
3608
|
+
...opts.mcpConfigPaths,
|
|
3609
|
+
"--strict-mcp-config"
|
|
3610
|
+
);
|
|
3611
|
+
}
|
|
3612
|
+
const flagSettings = {};
|
|
3613
|
+
if (opts.permissionsAllow && opts.permissionsAllow.length > 0) {
|
|
3614
|
+
flagSettings.permissions = { allow: opts.permissionsAllow };
|
|
3243
3615
|
}
|
|
3244
|
-
|
|
3245
|
-
|
|
3246
|
-
|
|
3247
|
-
|
|
3248
|
-
|
|
3249
|
-
|
|
3250
|
-
|
|
3251
|
-
|
|
3252
|
-
|
|
3253
|
-
|
|
3254
|
-
|
|
3255
|
-
|
|
3256
|
-
|
|
3616
|
+
if (opts.fastMode) {
|
|
3617
|
+
flagSettings.fastMode = true;
|
|
3618
|
+
}
|
|
3619
|
+
if (Object.keys(flagSettings).length > 0) {
|
|
3620
|
+
extraArgs.push("--settings", JSON.stringify(flagSettings));
|
|
3621
|
+
}
|
|
3622
|
+
if (opts.permissionMode === "bypassPermissions") {
|
|
3623
|
+
log.warn(
|
|
3624
|
+
"interactive permissionMode bypassPermissions ignored: Claude Code prompts for confirmation in the TUI"
|
|
3625
|
+
);
|
|
3626
|
+
} else if (opts.permissionMode) {
|
|
3627
|
+
extraArgs.push("--permission-mode", opts.permissionMode);
|
|
3628
|
+
}
|
|
3629
|
+
if (opts.systemPromptFile) {
|
|
3630
|
+
extraArgs.push("--append-system-prompt-file", opts.systemPromptFile);
|
|
3631
|
+
}
|
|
3632
|
+
const session = new ClaudeSession({
|
|
3633
|
+
cwd: opts.cwd,
|
|
3634
|
+
cliPath: opts.cliPath,
|
|
3635
|
+
configDir: opts.configDir,
|
|
3636
|
+
model: opts.model,
|
|
3637
|
+
// Default null = normal CLAUDE.md + settings load, matching what the
|
|
3638
|
+
// headless spawn does. "" (skip everything) is for fast e2e runs only.
|
|
3639
|
+
settingSources: opts.settingSources === void 0 ? null : opts.settingSources,
|
|
3640
|
+
extraArgs,
|
|
3641
|
+
ignoreAnthropicApiKey: opts.ignoreAnthropicApiKey,
|
|
3642
|
+
effort: opts.effort ? cliEffortLevel(opts.effort) : void 0
|
|
3257
3643
|
});
|
|
3258
|
-
|
|
3259
|
-
|
|
3260
|
-
|
|
3261
|
-
|
|
3262
|
-
|
|
3263
|
-
|
|
3264
|
-
|
|
3265
|
-
|
|
3266
|
-
pending.reject(error);
|
|
3267
|
-
log.notice("rejected pending proxy call", {
|
|
3268
|
-
sessionKey: pending.sessionKey,
|
|
3269
|
-
toolCallId: pending.toolCallId,
|
|
3270
|
-
toolName: pending.toolName,
|
|
3271
|
-
error: error.message
|
|
3644
|
+
log.info("prepared interactive claude session", {
|
|
3645
|
+
cwd: opts.cwd,
|
|
3646
|
+
cliPath: opts.cliPath ?? "claude",
|
|
3647
|
+
configDir: session.configDir,
|
|
3648
|
+
model: opts.model,
|
|
3649
|
+
effort: opts.effort,
|
|
3650
|
+
sessionId: session.sessionId,
|
|
3651
|
+
jsonlPath: session.jsonlPath
|
|
3272
3652
|
});
|
|
3273
|
-
|
|
3274
|
-
|
|
3275
|
-
|
|
3276
|
-
const
|
|
3277
|
-
|
|
3278
|
-
|
|
3279
|
-
|
|
3280
|
-
|
|
3281
|
-
|
|
3282
|
-
|
|
3283
|
-
|
|
3653
|
+
const lineEmitter = new EventEmitter4();
|
|
3654
|
+
const errorHandlers = /* @__PURE__ */ new Set();
|
|
3655
|
+
let startPromise = null;
|
|
3656
|
+
const ensureStarted = () => {
|
|
3657
|
+
if (!startPromise) startPromise = session.start();
|
|
3658
|
+
return startPromise;
|
|
3659
|
+
};
|
|
3660
|
+
const emitResult = (subtype, isError, result, usage) => {
|
|
3661
|
+
lineEmitter.emit(
|
|
3662
|
+
"line",
|
|
3663
|
+
JSON.stringify({
|
|
3664
|
+
type: "result",
|
|
3665
|
+
subtype,
|
|
3666
|
+
is_error: isError,
|
|
3667
|
+
result,
|
|
3668
|
+
session_id: session.sessionId,
|
|
3669
|
+
usage: usage ?? {},
|
|
3670
|
+
total_cost_usd: null,
|
|
3671
|
+
duration_ms: 0
|
|
3672
|
+
})
|
|
3673
|
+
);
|
|
3674
|
+
};
|
|
3675
|
+
const runTurn = (userMsg) => {
|
|
3676
|
+
void (async () => {
|
|
3677
|
+
try {
|
|
3678
|
+
await ensureStarted();
|
|
3679
|
+
const { stopReason, usage } = await session.tailTurn(userMsg, (raw) => {
|
|
3680
|
+
lineEmitter.emit("line", raw);
|
|
3681
|
+
});
|
|
3682
|
+
const timedOut = !stopReason;
|
|
3683
|
+
emitResult(
|
|
3684
|
+
timedOut ? "error_during_execution" : stopReason,
|
|
3685
|
+
timedOut,
|
|
3686
|
+
timedOut ? "Interactive transport: the turn ended without a terminal stop_reason (turn timeout or claude exit). Output above may be incomplete." : void 0,
|
|
3687
|
+
usage
|
|
3688
|
+
);
|
|
3689
|
+
} catch (err) {
|
|
3690
|
+
const e = err instanceof Error ? err : new Error(String(err));
|
|
3691
|
+
log.error("interactive turn failed", { error: e.message });
|
|
3692
|
+
emitResult(
|
|
3693
|
+
"error_during_execution",
|
|
3694
|
+
true,
|
|
3695
|
+
`Interactive transport failed: ${e.message}`
|
|
3696
|
+
);
|
|
3697
|
+
if (errorHandlers.size > 0) {
|
|
3698
|
+
for (const h of errorHandlers) h(e);
|
|
3699
|
+
} else {
|
|
3700
|
+
lineEmitter.emit("close");
|
|
3701
|
+
}
|
|
3702
|
+
}
|
|
3703
|
+
})();
|
|
3704
|
+
};
|
|
3705
|
+
const proc = {
|
|
3706
|
+
stdin: {
|
|
3707
|
+
write(chunk) {
|
|
3708
|
+
const raw = typeof chunk === "string" && chunk.endsWith("\n") ? chunk.slice(0, -1) : chunk;
|
|
3709
|
+
runTurn(decodeUserEnvelope(raw));
|
|
3710
|
+
return true;
|
|
3711
|
+
},
|
|
3712
|
+
end() {
|
|
3713
|
+
}
|
|
3714
|
+
},
|
|
3715
|
+
stdout: null,
|
|
3716
|
+
stderr: null,
|
|
3717
|
+
pid: -1,
|
|
3718
|
+
killed: false,
|
|
3719
|
+
on(event, fn) {
|
|
3720
|
+
if (event === "error") errorHandlers.add(fn);
|
|
3721
|
+
return proc;
|
|
3722
|
+
},
|
|
3723
|
+
once() {
|
|
3724
|
+
return proc;
|
|
3725
|
+
},
|
|
3726
|
+
off(event, fn) {
|
|
3727
|
+
if (event === "error") errorHandlers.delete(fn);
|
|
3728
|
+
return proc;
|
|
3729
|
+
},
|
|
3730
|
+
kill() {
|
|
3731
|
+
try {
|
|
3732
|
+
session.dispose();
|
|
3733
|
+
} catch {
|
|
3734
|
+
}
|
|
3735
|
+
if (opts.systemPromptFile) {
|
|
3736
|
+
void unlink2(opts.systemPromptFile).catch(() => {
|
|
3737
|
+
});
|
|
3738
|
+
}
|
|
3739
|
+
proc.killed = true;
|
|
3740
|
+
return true;
|
|
3741
|
+
}
|
|
3742
|
+
};
|
|
3743
|
+
return {
|
|
3744
|
+
proc,
|
|
3745
|
+
lineEmitter,
|
|
3746
|
+
proxyServer: null,
|
|
3747
|
+
mcpHash: void 0,
|
|
3748
|
+
systemPromptFile: opts.systemPromptFile
|
|
3749
|
+
};
|
|
3284
3750
|
}
|
|
3285
3751
|
|
|
3286
3752
|
// src/claude-code-language-model.ts
|
|
3287
3753
|
import { readFileSync as readFileSync3, writeFileSync as writeFileSync3 } from "fs";
|
|
3288
3754
|
import { unlink as unlink3 } from "fs/promises";
|
|
3289
3755
|
import { homedir as homedir4, tmpdir as tmpdir2 } from "os";
|
|
3290
|
-
import { randomUUID as
|
|
3756
|
+
import { randomUUID as randomUUID4 } from "crypto";
|
|
3291
3757
|
import { dirname as dirname3, join as join6 } from "path";
|
|
3292
3758
|
var DEFAULT_COMPACTION_MODEL = "claude-haiku-4-5";
|
|
3293
3759
|
function resolveCompactionModel(configured) {
|
|
@@ -3479,9 +3945,25 @@ function makeAutoContinueMessage() {
|
|
|
3479
3945
|
}
|
|
3480
3946
|
});
|
|
3481
3947
|
}
|
|
3482
|
-
function
|
|
3948
|
+
function makeLateProxyResultMessage(entries) {
|
|
3949
|
+
const sections = entries.map(({ call, result }) => {
|
|
3950
|
+
const failed = result.kind === "error" || result.isError === true;
|
|
3951
|
+
const body = result.kind === "error" ? result.message : result.text;
|
|
3952
|
+
return `Your earlier \`${call.toolName}\` tool call (id ${call.toolCallId}) has ${failed ? "failed" : "completed"}, but delivery or continuation was interrupted. Treat the following as its ${failed ? "error" : "result"} and continue from there; do not re-run it.
|
|
3953
|
+
|
|
3954
|
+
${body}`;
|
|
3955
|
+
});
|
|
3956
|
+
return JSON.stringify({
|
|
3957
|
+
type: "user",
|
|
3958
|
+
message: {
|
|
3959
|
+
role: "user",
|
|
3960
|
+
content: [{ type: "text", text: sections.join("\n\n---\n\n") }]
|
|
3961
|
+
}
|
|
3962
|
+
});
|
|
3963
|
+
}
|
|
3964
|
+
function readPromptFileIfPresent(path8) {
|
|
3483
3965
|
try {
|
|
3484
|
-
const content = readFileSync3(
|
|
3966
|
+
const content = readFileSync3(path8, "utf8").trim();
|
|
3485
3967
|
return content || void 0;
|
|
3486
3968
|
} catch {
|
|
3487
3969
|
return void 0;
|
|
@@ -3584,10 +4066,10 @@ ${options.compressionSummary.trim()}`
|
|
|
3584
4066
|
if (includeMultiStepHint) parts.push(MULTI_STEP_TASK_HINT);
|
|
3585
4067
|
const content = parts.join("\n\n");
|
|
3586
4068
|
if (!content) return void 0;
|
|
3587
|
-
const
|
|
4069
|
+
const path8 = join6(tmpdir2(), `opencode-cc-sys-${randomUUID4()}.md`);
|
|
3588
4070
|
try {
|
|
3589
|
-
writeFileSync3(
|
|
3590
|
-
return
|
|
4071
|
+
writeFileSync3(path8, content, "utf8");
|
|
4072
|
+
return path8;
|
|
3591
4073
|
} catch (err) {
|
|
3592
4074
|
log.warn("failed to write system prompt file", { error: String(err) });
|
|
3593
4075
|
return void 0;
|
|
@@ -4164,11 +4646,26 @@ var ClaudeCodeLanguageModel = class {
|
|
|
4164
4646
|
};
|
|
4165
4647
|
}
|
|
4166
4648
|
async doGenerate(options) {
|
|
4649
|
+
if (!this.isCompactionCall(options) && this.requestScope(options) !== "no-tools" && parseSideQuestion(options.prompt)) {
|
|
4650
|
+
return this.doGenerateViaStream(options);
|
|
4651
|
+
}
|
|
4167
4652
|
const warnings = [];
|
|
4168
4653
|
const cwd = resolveSpawnCwd(this.config.cwd);
|
|
4169
4654
|
const scope = this.requestScope(options);
|
|
4170
4655
|
const affinity = this.sessionAffinity(options);
|
|
4171
|
-
const
|
|
4656
|
+
const effectiveModelId = resolveAgentModel(
|
|
4657
|
+
this.getOpencodeAgent(options.providerOptions),
|
|
4658
|
+
this.modelId
|
|
4659
|
+
);
|
|
4660
|
+
const reasoningEffort = resolveAgentEffort(
|
|
4661
|
+
this.getOpencodeAgent(options.providerOptions),
|
|
4662
|
+
this.getReasoningEffort(options.providerOptions)
|
|
4663
|
+
);
|
|
4664
|
+
const baseKey = sessionKey(
|
|
4665
|
+
cwd,
|
|
4666
|
+
`${effectiveModelId}::${scope}::${affinity}::context=${JSON.stringify([this.config.provider, this.getOpencodeAgent(options.providerOptions) ?? null])}`
|
|
4667
|
+
);
|
|
4668
|
+
const sk = effortSessionKey(baseKey, reasoningEffort);
|
|
4172
4669
|
const compactionMode = this.isCompactionCall(options);
|
|
4173
4670
|
if (scope === "tools" && (this.resolvedProxyTools() || this.config.proxyOpencodeMcpTools !== false && this.config.bridgeOpencodeMcp !== false)) {
|
|
4174
4671
|
return this.doGenerateViaStream(options);
|
|
@@ -4220,6 +4717,7 @@ var ClaudeCodeLanguageModel = class {
|
|
|
4220
4717
|
warnings
|
|
4221
4718
|
};
|
|
4222
4719
|
}
|
|
4720
|
+
invalidateOtherEffortSessions(baseKey, reasoningEffort);
|
|
4223
4721
|
const hasPriorConversation = options.prompt.filter((m) => m.role === "user" || m.role === "assistant").length > 1;
|
|
4224
4722
|
if (!hasPriorConversation) {
|
|
4225
4723
|
deleteClaudeSessionId(sk);
|
|
@@ -4228,8 +4726,7 @@ var ClaudeCodeLanguageModel = class {
|
|
|
4228
4726
|
}
|
|
4229
4727
|
const hasExistingSession = !!getClaudeSessionId(sk);
|
|
4230
4728
|
const includeHistoryContext = !hasExistingSession && hasPriorConversation;
|
|
4231
|
-
const
|
|
4232
|
-
const userMsg = consumeExitPlanModeQuestionResult(sk, options.prompt) ?? getClaudeUserMessage(options.prompt, includeHistoryContext, reasoningEffort);
|
|
4729
|
+
const userMsg = consumeExitPlanModeQuestionResult(sk, options.prompt) ?? getClaudeUserMessage(options.prompt, includeHistoryContext);
|
|
4233
4730
|
const [runtimeStatus, cliVersion, planModeQuestionActive] = await Promise.all([
|
|
4234
4731
|
getRuntimeMcpStatus(),
|
|
4235
4732
|
detectCliVersion(this.config.cliPath),
|
|
@@ -4243,7 +4740,7 @@ var ClaudeCodeLanguageModel = class {
|
|
|
4243
4740
|
// An existing summary still carries: it is this key's prior context.
|
|
4244
4741
|
{ compressEnabled: false, compressionSummary: getCompressionSummary(sk) }
|
|
4245
4742
|
);
|
|
4246
|
-
const { model: spawnModelId, fast: fastMode } = parseModelId(
|
|
4743
|
+
const { model: spawnModelId, fast: fastMode } = parseModelId(effectiveModelId);
|
|
4247
4744
|
const cliArgs = buildCliArgs({
|
|
4248
4745
|
sessionKey: sk,
|
|
4249
4746
|
skipPermissions: this.config.skipPermissions !== false,
|
|
@@ -4260,7 +4757,8 @@ var ClaudeCodeLanguageModel = class {
|
|
|
4260
4757
|
});
|
|
4261
4758
|
log.info("doGenerate starting", {
|
|
4262
4759
|
cwd,
|
|
4263
|
-
model:
|
|
4760
|
+
model: effectiveModelId,
|
|
4761
|
+
requestedModel: this.modelId,
|
|
4264
4762
|
textLength: userMsg.length,
|
|
4265
4763
|
includeHistoryContext
|
|
4266
4764
|
});
|
|
@@ -4270,7 +4768,8 @@ var ClaudeCodeLanguageModel = class {
|
|
|
4270
4768
|
cwd,
|
|
4271
4769
|
stdio: ["pipe", "pipe", "pipe"],
|
|
4272
4770
|
env: claudeSpawnEnv({
|
|
4273
|
-
ignoreAnthropicApiKey: this.config.ignoreAnthropicApiKey
|
|
4771
|
+
ignoreAnthropicApiKey: this.config.ignoreAnthropicApiKey,
|
|
4772
|
+
effort: reasoningEffort
|
|
4274
4773
|
}),
|
|
4275
4774
|
shell: process.platform === "win32"
|
|
4276
4775
|
});
|
|
@@ -4546,9 +5045,20 @@ ${plan}
|
|
|
4546
5045
|
const scope = this.requestScope(options);
|
|
4547
5046
|
const affinity = this.sessionAffinity(options);
|
|
4548
5047
|
const compactionMode = this.isCompactionCall(options);
|
|
4549
|
-
const effectiveModelId = compactionMode ? this.resolveCompactionModel() :
|
|
5048
|
+
const effectiveModelId = compactionMode ? this.resolveCompactionModel() : resolveAgentModel(
|
|
5049
|
+
this.getOpencodeAgent(options.providerOptions),
|
|
5050
|
+
this.modelId
|
|
5051
|
+
);
|
|
4550
5052
|
const { model: spawnModelId, fast: fastMode } = parseModelId(effectiveModelId);
|
|
4551
|
-
const
|
|
5053
|
+
const reasoningEffort = compactionMode ? void 0 : resolveAgentEffort(
|
|
5054
|
+
this.getOpencodeAgent(options.providerOptions),
|
|
5055
|
+
this.getReasoningEffort(options.providerOptions)
|
|
5056
|
+
);
|
|
5057
|
+
const baseKey = sessionKey(
|
|
5058
|
+
cwd,
|
|
5059
|
+
`${effectiveModelId}::${scope}::${affinity}::context=${JSON.stringify([this.config.provider, this.getOpencodeAgent(options.providerOptions) ?? null])}`
|
|
5060
|
+
);
|
|
5061
|
+
const sk = compactionMode ? sessionKey(cwd, `${effectiveModelId}::compaction::${affinity}`) : effortSessionKey(baseKey, reasoningEffort);
|
|
4552
5062
|
const toUsage = this.toUsage.bind(this);
|
|
4553
5063
|
const toFinishReason = this.toFinishReason.bind(this);
|
|
4554
5064
|
const handleControlRequest = this.handleControlRequest.bind(this);
|
|
@@ -4556,6 +5066,45 @@ ${plan}
|
|
|
4556
5066
|
const interactivePref = this.config.interactive ?? flagOn(process.env.CLAUDE_CODE_INTERACTIVE_TRANSPORT);
|
|
4557
5067
|
const useInteractive = interactivePref && typeof globalThis.Bun?.Terminal === "function";
|
|
4558
5068
|
const interactiveBypassRequested = this.config.interactiveBypass ?? flagOn(process.env.CLAUDE_CODE_INTERACTIVE_BYPASS);
|
|
5069
|
+
const aside = !compactionMode && scope !== "no-tools" ? parseSideQuestion(options.prompt) : null;
|
|
5070
|
+
if (aside) {
|
|
5071
|
+
const active = getActiveProcess(sk);
|
|
5072
|
+
const stream2 = new ReadableStream({
|
|
5073
|
+
async start(controller) {
|
|
5074
|
+
controller.enqueue({ type: "stream-start", warnings });
|
|
5075
|
+
try {
|
|
5076
|
+
if (aside.question && !active) {
|
|
5077
|
+
throw new Error("/btw needs an existing Claude Code session. Send a normal message with this model first.");
|
|
5078
|
+
}
|
|
5079
|
+
const answer = aside.question && active ? await requestSideQuestion(active, aside.question, {
|
|
5080
|
+
cliVersion: await detectCliVersion(cliPath),
|
|
5081
|
+
interactive: useInteractive,
|
|
5082
|
+
busy: getPendingProxyCalls(sk).length > 0 || !!active.pendingProxyCompletions?.size,
|
|
5083
|
+
abortSignal: options.abortSignal
|
|
5084
|
+
}) : { response: SIDE_QUESTION_USAGE, synthetic: true };
|
|
5085
|
+
const id = generateId();
|
|
5086
|
+
controller.enqueue({ type: "text-start", id });
|
|
5087
|
+
controller.enqueue({ type: "text-delta", id, delta: answer.response });
|
|
5088
|
+
controller.enqueue({ type: "text-end", id });
|
|
5089
|
+
controller.enqueue({
|
|
5090
|
+
type: "finish",
|
|
5091
|
+
finishReason: toFinishReason("stop"),
|
|
5092
|
+
usage: toUsage({}),
|
|
5093
|
+
providerMetadata: { "claude-code": { path: "side-question", synthetic: answer.synthetic, usageUnavailable: true } }
|
|
5094
|
+
});
|
|
5095
|
+
} catch (error) {
|
|
5096
|
+
controller.enqueue({ type: "error", error });
|
|
5097
|
+
} finally {
|
|
5098
|
+
controller.close();
|
|
5099
|
+
}
|
|
5100
|
+
}
|
|
5101
|
+
});
|
|
5102
|
+
return { stream: stream2, request: { body: { text: aside.question } } };
|
|
5103
|
+
}
|
|
5104
|
+
const existing = getActiveProcess(sk);
|
|
5105
|
+
if (existing && isSideQuestionPending(existing)) {
|
|
5106
|
+
throw new Error("Wait for /btw to finish before sending another message.");
|
|
5107
|
+
}
|
|
4559
5108
|
if (scope === "no-tools" && !compactionMode) {
|
|
4560
5109
|
log.info("doStream no-tools title stub", {
|
|
4561
5110
|
compactionMode,
|
|
@@ -4611,6 +5160,7 @@ ${plan}
|
|
|
4611
5160
|
});
|
|
4612
5161
|
return { stream: stream2, request: { body: { text: "" } } };
|
|
4613
5162
|
}
|
|
5163
|
+
if (!compactionMode) invalidateOtherEffortSessions(baseKey, reasoningEffort);
|
|
4614
5164
|
const hasPriorConversation = options.prompt.filter((m) => m.role === "user" || m.role === "assistant").length > 1;
|
|
4615
5165
|
if (!hasPriorConversation) {
|
|
4616
5166
|
deleteClaudeSessionId(sk);
|
|
@@ -4620,12 +5170,11 @@ ${plan}
|
|
|
4620
5170
|
const hasExistingSession = !!getClaudeSessionId(sk);
|
|
4621
5171
|
const hasActiveProcess = !!getActiveProcess(sk);
|
|
4622
5172
|
const includeHistoryContext = !hasExistingSession && !hasActiveProcess && hasPriorConversation;
|
|
4623
|
-
const reasoningEffort = this.getReasoningEffort(options.providerOptions);
|
|
4624
5173
|
const exitPlanModeQuestionResult = compactionMode ? null : consumeExitPlanModeQuestionResult(sk, options.prompt);
|
|
4625
5174
|
if (exitPlanModeQuestionResult) {
|
|
4626
5175
|
log.info("sending plan approval decision to claude", { sk });
|
|
4627
5176
|
}
|
|
4628
|
-
const userMsg = exitPlanModeQuestionResult ?? getClaudeUserMessage(options.prompt, includeHistoryContext,
|
|
5177
|
+
const userMsg = exitPlanModeQuestionResult ?? getClaudeUserMessage(options.prompt, includeHistoryContext, {
|
|
4629
5178
|
compactionMode
|
|
4630
5179
|
});
|
|
4631
5180
|
const resolvedProxy = compactionMode ? null : this.resolvedProxyTools();
|
|
@@ -4748,7 +5297,8 @@ ${plan}
|
|
|
4748
5297
|
mcpConfigPaths: mcp.paths,
|
|
4749
5298
|
permissionsAllow: allow,
|
|
4750
5299
|
systemPromptFile,
|
|
4751
|
-
ignoreAnthropicApiKey: self.config.ignoreAnthropicApiKey
|
|
5300
|
+
ignoreAnthropicApiKey: self.config.ignoreAnthropicApiKey,
|
|
5301
|
+
effort: reasoningEffort
|
|
4752
5302
|
});
|
|
4753
5303
|
ap.mcpHash = mcp.bridgedHash;
|
|
4754
5304
|
setActiveProcess(sk, ap);
|
|
@@ -4887,7 +5437,8 @@ ${plan}
|
|
|
4887
5437
|
spawnProxyServer,
|
|
4888
5438
|
spawnMcpHash,
|
|
4889
5439
|
spawnSystemPromptFile,
|
|
4890
|
-
self.config.ignoreAnthropicApiKey
|
|
5440
|
+
self.config.ignoreAnthropicApiKey,
|
|
5441
|
+
reasoningEffort
|
|
4891
5442
|
);
|
|
4892
5443
|
proc = ap.proc;
|
|
4893
5444
|
lineEmitter = ap.lineEmitter;
|
|
@@ -4917,10 +5468,13 @@ ${plan}
|
|
|
4917
5468
|
let hadThinkingTextFromStream = false;
|
|
4918
5469
|
let turnCompleted = false;
|
|
4919
5470
|
let controllerClosed = false;
|
|
5471
|
+
let unattendedTurnEnded = false;
|
|
5472
|
+
let watchdogMessage = userMsg;
|
|
4920
5473
|
let pendingProxyUnsubscribe = null;
|
|
4921
5474
|
let resultFallbackTimer = null;
|
|
4922
5475
|
let pendingResultCompletion = null;
|
|
4923
5476
|
let hasReceivedContent = false;
|
|
5477
|
+
let hasReceivedProgress = false;
|
|
4924
5478
|
let visibleTextSinceContinue = "";
|
|
4925
5479
|
let lastVisibleTextSinceContinue = "";
|
|
4926
5480
|
let hadReasoningSinceContinue = false;
|
|
@@ -4941,7 +5495,7 @@ ${plan}
|
|
|
4941
5495
|
};
|
|
4942
5496
|
const startResultFallback = (delayMs = 6e4) => {
|
|
4943
5497
|
clearFallbackTimer();
|
|
4944
|
-
if (!hasReceivedContent || controllerClosed) return;
|
|
5498
|
+
if (!hasReceivedContent && !hasReceivedProgress || controllerClosed) return;
|
|
4945
5499
|
resultFallbackTimer = setTimeout(() => {
|
|
4946
5500
|
if (controllerClosed) return;
|
|
4947
5501
|
log.warn("result fallback timer fired \u2014 closing stream without result event", {
|
|
@@ -4965,7 +5519,7 @@ ${plan}
|
|
|
4965
5519
|
};
|
|
4966
5520
|
const onStartWatchdogFire = () => {
|
|
4967
5521
|
startWatchdog = null;
|
|
4968
|
-
if (controllerClosed || hasReceivedContent) return;
|
|
5522
|
+
if (controllerClosed || hasReceivedContent || hasReceivedProgress) return;
|
|
4969
5523
|
if (respawnAttempted) {
|
|
4970
5524
|
log.error(
|
|
4971
5525
|
"claude process still silent after respawn; ending turn",
|
|
@@ -5028,25 +5582,46 @@ ${plan}
|
|
|
5028
5582
|
lineEmitter.on("close", closeHandler);
|
|
5029
5583
|
proc.on("error", procErrorHandler);
|
|
5030
5584
|
try {
|
|
5031
|
-
proc.stdin?.write(
|
|
5585
|
+
if (!deliverPendingCompletions(true)) proc.stdin?.write(watchdogMessage + "\n");
|
|
5032
5586
|
log.debug("re-sent user message after respawn", {
|
|
5033
|
-
textLength:
|
|
5587
|
+
textLength: watchdogMessage.length
|
|
5034
5588
|
});
|
|
5035
5589
|
} catch (err) {
|
|
5036
5590
|
log.error("failed to re-send envelope after respawn", {
|
|
5037
5591
|
error: err instanceof Error ? err.message : String(err)
|
|
5038
5592
|
});
|
|
5039
5593
|
}
|
|
5040
|
-
|
|
5041
|
-
onStartWatchdogFire,
|
|
5042
|
-
START_WATCHDOG_MS
|
|
5043
|
-
);
|
|
5594
|
+
armStartWatchdog();
|
|
5044
5595
|
};
|
|
5045
5596
|
const armStartWatchdog = () => {
|
|
5046
5597
|
clearStartWatchdog();
|
|
5047
5598
|
if (controllerClosed) return;
|
|
5048
5599
|
startWatchdog = setTimeout(onStartWatchdogFire, START_WATCHDOG_MS);
|
|
5049
5600
|
};
|
|
5601
|
+
const deliverPendingCompletions = (force = false) => {
|
|
5602
|
+
const pending = activeProcess?.pendingProxyCompletions;
|
|
5603
|
+
const entries = [...pending?.values() ?? []].filter(
|
|
5604
|
+
(entry) => force || entry.recoveryRequired || isPendingProxyCallChannelClosed(entry.call)
|
|
5605
|
+
);
|
|
5606
|
+
if (entries.length === 0) return false;
|
|
5607
|
+
endTextBlock();
|
|
5608
|
+
watchdogMessage = makeLateProxyResultMessage(entries);
|
|
5609
|
+
proc.stdin.write(watchdogMessage + "\n");
|
|
5610
|
+
for (const { call } of entries) pending.delete(call.toolCallId);
|
|
5611
|
+
log.warn("delivering proxy results after interrupted continuation", {
|
|
5612
|
+
sessionKey: sk,
|
|
5613
|
+
toolCallIds: entries.map(({ call }) => call.toolCallId),
|
|
5614
|
+
respawn: force
|
|
5615
|
+
});
|
|
5616
|
+
gotPartialEvents = false;
|
|
5617
|
+
hasReceivedContent = false;
|
|
5618
|
+
hasReceivedProgress = false;
|
|
5619
|
+
turnCompleted = false;
|
|
5620
|
+
resetAutoContinueWindow();
|
|
5621
|
+
clearFallbackTimer();
|
|
5622
|
+
armStartWatchdog();
|
|
5623
|
+
return true;
|
|
5624
|
+
};
|
|
5050
5625
|
const toolCallMap = /* @__PURE__ */ new Map();
|
|
5051
5626
|
const skipResultForIds = /* @__PURE__ */ new Set();
|
|
5052
5627
|
const toolCallsById = /* @__PURE__ */ new Map();
|
|
@@ -5071,6 +5646,7 @@ ${plan}
|
|
|
5071
5646
|
providerExecuted: false
|
|
5072
5647
|
});
|
|
5073
5648
|
skipResultForIds.add(call.toolCallId);
|
|
5649
|
+
markPendingProxyCallEmitted(call.toolCallId);
|
|
5074
5650
|
}
|
|
5075
5651
|
controller.enqueue({
|
|
5076
5652
|
type: "finish",
|
|
@@ -5181,6 +5757,10 @@ ${plan}
|
|
|
5181
5757
|
};
|
|
5182
5758
|
const completeResult = (msg) => {
|
|
5183
5759
|
if (controllerClosed) return;
|
|
5760
|
+
if (deliverPendingCompletions()) {
|
|
5761
|
+
if (drainBuffer.length > 0) drainNow();
|
|
5762
|
+
return;
|
|
5763
|
+
}
|
|
5184
5764
|
if (drainBuffer.length > 0) {
|
|
5185
5765
|
drainNow();
|
|
5186
5766
|
return;
|
|
@@ -5192,6 +5772,7 @@ ${plan}
|
|
|
5192
5772
|
count: pendingSiblings.length
|
|
5193
5773
|
});
|
|
5194
5774
|
}
|
|
5775
|
+
activeProcess?.pendingProxyCompletions?.clear();
|
|
5195
5776
|
const autoDecision = shouldAutoContinueIncompleteTurn(
|
|
5196
5777
|
autoContinueState,
|
|
5197
5778
|
{
|
|
@@ -5278,10 +5859,15 @@ ${plan}
|
|
|
5278
5859
|
if (!line.trim()) return;
|
|
5279
5860
|
if (controllerClosed) return;
|
|
5280
5861
|
startResultFallback();
|
|
5281
|
-
clearStartWatchdog();
|
|
5282
5862
|
try {
|
|
5283
5863
|
const outer = JSON.parse(line);
|
|
5284
5864
|
const msg = outer.type === "stream_event" && outer.event ? { ...outer.event, session_id: outer.session_id } : outer;
|
|
5865
|
+
const modelProgress = msg.type === "assistant" && !!msg.message?.content?.length || msg.type === "content_block_start" && msg.content_block?.type === "tool_use" || msg.type === "content_block_delta" && (msg.delta?.type === "text_delta" && !!msg.delta.text || msg.delta?.type === "thinking_delta" && !!msg.delta.thinking);
|
|
5866
|
+
if (modelProgress) {
|
|
5867
|
+
hasReceivedProgress = true;
|
|
5868
|
+
clearStartWatchdog();
|
|
5869
|
+
startResultFallback();
|
|
5870
|
+
}
|
|
5285
5871
|
if (outer.type === "stream_event") {
|
|
5286
5872
|
gotPartialEvents = true;
|
|
5287
5873
|
}
|
|
@@ -5795,6 +6381,9 @@ ${plan}
|
|
|
5795
6381
|
if (msg.session_id) {
|
|
5796
6382
|
setClaudeSessionId(sk, msg.session_id);
|
|
5797
6383
|
}
|
|
6384
|
+
if (deliverPendingCompletions()) {
|
|
6385
|
+
return;
|
|
6386
|
+
}
|
|
5798
6387
|
if (!currentTextId && msg.is_error && typeof msg.result === "string" && msg.result.trim().length > 0) {
|
|
5799
6388
|
const errId = startTextBlock();
|
|
5800
6389
|
controller.enqueue({
|
|
@@ -5924,6 +6513,55 @@ ${plan}
|
|
|
5924
6513
|
} catch {
|
|
5925
6514
|
}
|
|
5926
6515
|
};
|
|
6516
|
+
if (activeProcess) {
|
|
6517
|
+
const unattended = takeUnattendedLines(activeProcess);
|
|
6518
|
+
if (unattended.lines.length > 0 || unattended.dropped > 0) {
|
|
6519
|
+
log.notice("replaying stdout the child emitted between turns", {
|
|
6520
|
+
sessionKey: sk,
|
|
6521
|
+
lines: unattended.lines.length,
|
|
6522
|
+
dropped: unattended.dropped
|
|
6523
|
+
});
|
|
6524
|
+
let partialText = false;
|
|
6525
|
+
{
|
|
6526
|
+
if (unattended.dropped > 0) {
|
|
6527
|
+
const id = startTextBlock();
|
|
6528
|
+
controller.enqueue({
|
|
6529
|
+
type: "text-delta",
|
|
6530
|
+
id,
|
|
6531
|
+
delta: `> _${unattended.dropped} lines of output emitted between turns were dropped._
|
|
6532
|
+
|
|
6533
|
+
`
|
|
6534
|
+
});
|
|
6535
|
+
}
|
|
6536
|
+
for (const line of unattended.lines) {
|
|
6537
|
+
try {
|
|
6538
|
+
const outer = JSON.parse(line);
|
|
6539
|
+
const msg = outer.type === "stream_event" && outer.event ? outer.event : outer;
|
|
6540
|
+
let text = "";
|
|
6541
|
+
if (msg.type === "content_block_delta" && msg.delta?.type === "text_delta") {
|
|
6542
|
+
text = msg.delta.text ?? "";
|
|
6543
|
+
partialText = true;
|
|
6544
|
+
} else if (msg.type === "assistant") {
|
|
6545
|
+
if (!partialText) text = (msg.message?.content ?? []).filter((part) => part.type === "text").map((part) => part.text ?? "").join("");
|
|
6546
|
+
partialText = false;
|
|
6547
|
+
} else if (msg.type === "result") {
|
|
6548
|
+
unattendedTurnEnded = true;
|
|
6549
|
+
for (const entry of activeProcess.pendingProxyCompletions?.values() ?? []) {
|
|
6550
|
+
if (isPendingProxyCallChannelClosed(entry.call)) entry.recoveryRequired = true;
|
|
6551
|
+
}
|
|
6552
|
+
if (outer.session_id) setClaudeSessionId(sk, outer.session_id);
|
|
6553
|
+
if (msg.is_error && msg.result) text = msg.result;
|
|
6554
|
+
}
|
|
6555
|
+
if (text) controller.enqueue({ type: "text-delta", id: startTextBlock(), delta: text });
|
|
6556
|
+
} catch {
|
|
6557
|
+
}
|
|
6558
|
+
}
|
|
6559
|
+
}
|
|
6560
|
+
endTextBlock();
|
|
6561
|
+
clearFallbackTimer();
|
|
6562
|
+
hasReceivedContent = false;
|
|
6563
|
+
}
|
|
6564
|
+
}
|
|
5927
6565
|
lineEmitter.on("line", lineHandler);
|
|
5928
6566
|
lineEmitter.on("close", closeHandler);
|
|
5929
6567
|
pendingProxyUnsubscribe = onPendingProxyCall(sk, (call) => {
|
|
@@ -5993,11 +6631,21 @@ ${plan}
|
|
|
5993
6631
|
if (hasMatchedPendingResults) {
|
|
5994
6632
|
for (const { call, result } of previousPendingProxyMatches) {
|
|
5995
6633
|
if (result) {
|
|
6634
|
+
const channelClosed = isPendingProxyCallChannelClosed(call);
|
|
5996
6635
|
log.info("resolving pending proxy call from tool result prompt", {
|
|
5997
6636
|
sessionKey: sk,
|
|
5998
6637
|
toolCallId: call.toolCallId,
|
|
5999
|
-
toolName: call.toolName
|
|
6638
|
+
toolName: call.toolName,
|
|
6639
|
+
channelClosed
|
|
6000
6640
|
});
|
|
6641
|
+
const completions = activeProcess.pendingProxyCompletions ??= /* @__PURE__ */ new Map();
|
|
6642
|
+
if (!completions.has(call.toolCallId)) {
|
|
6643
|
+
completions.set(call.toolCallId, {
|
|
6644
|
+
call,
|
|
6645
|
+
result,
|
|
6646
|
+
recoveryRequired: channelClosed || unattendedTurnEnded
|
|
6647
|
+
});
|
|
6648
|
+
}
|
|
6001
6649
|
resolvePendingProxyCallById(call.toolCallId, result);
|
|
6002
6650
|
} else {
|
|
6003
6651
|
log.info(
|
|
@@ -6010,6 +6658,22 @@ ${plan}
|
|
|
6010
6658
|
);
|
|
6011
6659
|
}
|
|
6012
6660
|
}
|
|
6661
|
+
if (unattendedTurnEnded) deliverPendingCompletions();
|
|
6662
|
+
const unemitted = getPendingProxyCalls(sk).filter(
|
|
6663
|
+
(call) => !call.emitted
|
|
6664
|
+
);
|
|
6665
|
+
if (unemitted.length > 0) {
|
|
6666
|
+
log.notice("draining proxy calls queued between turns", {
|
|
6667
|
+
sessionKey: sk,
|
|
6668
|
+
toolCallIds: unemitted.map((call) => call.toolCallId)
|
|
6669
|
+
});
|
|
6670
|
+
drainBuffer.push(...unemitted);
|
|
6671
|
+
drainNow();
|
|
6672
|
+
return;
|
|
6673
|
+
}
|
|
6674
|
+
if (getPendingProxyCalls(sk).length === 0) {
|
|
6675
|
+
armStartWatchdog();
|
|
6676
|
+
}
|
|
6013
6677
|
return;
|
|
6014
6678
|
}
|
|
6015
6679
|
if (previousPendingProxyCalls.length > 0) {
|
|
@@ -6053,7 +6717,7 @@ ${plan}
|
|
|
6053
6717
|
|
|
6054
6718
|
// src/accounts.ts
|
|
6055
6719
|
import { chmod, lstat, mkdir, readlink, symlink, writeFile } from "fs/promises";
|
|
6056
|
-
import
|
|
6720
|
+
import path6 from "path";
|
|
6057
6721
|
var BASE_PROVIDER_ID = "claude-code";
|
|
6058
6722
|
var DEFAULT_ACCOUNT = "default";
|
|
6059
6723
|
var SHARED_CAPABILITY_ITEMS = [
|
|
@@ -6091,7 +6755,7 @@ function expandHome(value) {
|
|
|
6091
6755
|
const home = process.env.HOME ?? process.env.USERPROFILE;
|
|
6092
6756
|
if (value === "~") return home ?? value;
|
|
6093
6757
|
if (value.startsWith("~/") || value.startsWith("~\\")) {
|
|
6094
|
-
return home ?
|
|
6758
|
+
return home ? path6.join(home, value.slice(2)) : value;
|
|
6095
6759
|
}
|
|
6096
6760
|
return value;
|
|
6097
6761
|
}
|
|
@@ -6123,8 +6787,8 @@ async function ensureSharedCapabilities(targetRoot) {
|
|
|
6123
6787
|
}
|
|
6124
6788
|
}
|
|
6125
6789
|
async function ensureSharedCapabilityItem(sourceRoot, targetRoot, item) {
|
|
6126
|
-
const source =
|
|
6127
|
-
const target =
|
|
6790
|
+
const source = path6.join(sourceRoot, item);
|
|
6791
|
+
const target = path6.join(targetRoot, item);
|
|
6128
6792
|
let sourceStat;
|
|
6129
6793
|
try {
|
|
6130
6794
|
sourceStat = await lstat(source);
|
|
@@ -6135,8 +6799,8 @@ async function ensureSharedCapabilityItem(sourceRoot, targetRoot, item) {
|
|
|
6135
6799
|
const targetStat = await lstat(target);
|
|
6136
6800
|
if (targetStat.isSymbolicLink()) {
|
|
6137
6801
|
const current = await readlink(target);
|
|
6138
|
-
const resolvedCurrent =
|
|
6139
|
-
const resolvedSource =
|
|
6802
|
+
const resolvedCurrent = path6.resolve(path6.dirname(target), current);
|
|
6803
|
+
const resolvedSource = path6.resolve(source);
|
|
6140
6804
|
if (resolvedCurrent === resolvedSource) return;
|
|
6141
6805
|
}
|
|
6142
6806
|
log.warn("shared Claude capability already exists; leaving untouched", {
|
|
@@ -6151,11 +6815,11 @@ async function ensureSharedCapabilityItem(sourceRoot, targetRoot, item) {
|
|
|
6151
6815
|
await symlink(source, target, type);
|
|
6152
6816
|
}
|
|
6153
6817
|
async function writeAccountWrapper(account, baseCliPath, configDir) {
|
|
6154
|
-
const cacheRoot =
|
|
6818
|
+
const cacheRoot = path6.join(
|
|
6155
6819
|
process.env.XDG_CACHE_HOME ?? expandHome("~/.cache"),
|
|
6156
6820
|
"opencode-claude-code-plugin"
|
|
6157
6821
|
);
|
|
6158
|
-
const wrapperPath =
|
|
6822
|
+
const wrapperPath = path6.join(cacheRoot, `claude-${account}`);
|
|
6159
6823
|
const suffix = `@${account}`;
|
|
6160
6824
|
await mkdir(cacheRoot, { recursive: true });
|
|
6161
6825
|
const script = `#!/usr/bin/env bash
|
|
@@ -6307,15 +6971,15 @@ function cleanupOne(cacheRoot, ourDir) {
|
|
|
6307
6971
|
// src/startup-diagnostics.ts
|
|
6308
6972
|
import { execFile as execFile2 } from "child_process";
|
|
6309
6973
|
import * as fs5 from "fs";
|
|
6310
|
-
import * as
|
|
6974
|
+
import * as path7 from "path";
|
|
6311
6975
|
import { promisify as promisify2 } from "util";
|
|
6312
6976
|
import { fileURLToPath as fileURLToPath2 } from "url";
|
|
6313
6977
|
var cachedPluginVersion;
|
|
6314
6978
|
function pluginVersion() {
|
|
6315
6979
|
if (cachedPluginVersion) return cachedPluginVersion;
|
|
6316
6980
|
try {
|
|
6317
|
-
const here =
|
|
6318
|
-
const raw = fs5.readFileSync(
|
|
6981
|
+
const here = path7.dirname(fileURLToPath2(import.meta.url));
|
|
6982
|
+
const raw = fs5.readFileSync(path7.join(here, "..", "package.json"), "utf8");
|
|
6319
6983
|
const version = JSON.parse(raw).version;
|
|
6320
6984
|
cachedPluginVersion = typeof version === "string" ? version : "unknown";
|
|
6321
6985
|
} catch {
|
|
@@ -6339,7 +7003,7 @@ var opencodeVersionProbe;
|
|
|
6339
7003
|
function detectOpencodeVersion(execPath = process.execPath) {
|
|
6340
7004
|
if (opencodeVersionProbe) return opencodeVersionProbe;
|
|
6341
7005
|
opencodeVersionProbe = (async () => {
|
|
6342
|
-
if (!
|
|
7006
|
+
if (!path7.basename(execPath).toLowerCase().includes("opencode")) {
|
|
6343
7007
|
log.debug("skipping opencode version probe: execPath is not opencode", { execPath });
|
|
6344
7008
|
return void 0;
|
|
6345
7009
|
}
|
|
@@ -6445,6 +7109,13 @@ var DEFAULT_PROXY_TOOL_NAMES = [
|
|
|
6445
7109
|
"WebFetch",
|
|
6446
7110
|
"Task"
|
|
6447
7111
|
];
|
|
7112
|
+
function registerSideQuestionCommand(config) {
|
|
7113
|
+
config.command ??= {};
|
|
7114
|
+
config.command.btw ??= {
|
|
7115
|
+
template: "/btw $ARGUMENTS",
|
|
7116
|
+
description: "Ask a side question in the live Claude Code session without changing its context"
|
|
7117
|
+
};
|
|
7118
|
+
}
|
|
6448
7119
|
function warnIfAnthropicApiKey(ignore) {
|
|
6449
7120
|
if (warnedAnthropicApiKey) return;
|
|
6450
7121
|
if (!process.env.ANTHROPIC_API_KEY && !process.env.ANTHROPIC_AUTH_TOKEN) return;
|
|
@@ -6520,6 +7191,7 @@ function pluginEntrypoint() {
|
|
|
6520
7191
|
function cleanProviderOptions(options = {}) {
|
|
6521
7192
|
const result = { ...options };
|
|
6522
7193
|
delete result.accounts;
|
|
7194
|
+
delete result.defaultSubagentModel;
|
|
6523
7195
|
return result;
|
|
6524
7196
|
}
|
|
6525
7197
|
function defaultModelsForProvider(providerModels, providerID = PROVIDER_ID2, modelSuffix) {
|
|
@@ -6656,6 +7328,37 @@ async function expandAccountProviders(config) {
|
|
|
6656
7328
|
}
|
|
6657
7329
|
return expandedCount > 0;
|
|
6658
7330
|
}
|
|
7331
|
+
async function buildAgentRegistry(config) {
|
|
7332
|
+
const options = config.provider?.[PROVIDER_ID2]?.options;
|
|
7333
|
+
const configured = options?.defaultSubagentModel;
|
|
7334
|
+
setDefaultSubagentModel(
|
|
7335
|
+
typeof configured === "string" ? configured : void 0
|
|
7336
|
+
);
|
|
7337
|
+
const records = await readAgentMarkdownRecords(
|
|
7338
|
+
agentDirectories(
|
|
7339
|
+
process.env.HOME ?? process.env.USERPROFILE,
|
|
7340
|
+
getOpencodeProjectDirectory()
|
|
7341
|
+
)
|
|
7342
|
+
);
|
|
7343
|
+
for (const [name, agent] of Object.entries(config.agent ?? {})) {
|
|
7344
|
+
const bag = agent.options ?? {};
|
|
7345
|
+
const pick = (key) => {
|
|
7346
|
+
const value = agent[key] ?? bag[key];
|
|
7347
|
+
return typeof value === "string" ? value : void 0;
|
|
7348
|
+
};
|
|
7349
|
+
records[name] = {
|
|
7350
|
+
mode: pick("mode") ?? records[name]?.mode,
|
|
7351
|
+
model: pick("model") ?? records[name]?.model,
|
|
7352
|
+
forceModel: pick("forceModel") ?? records[name]?.forceModel,
|
|
7353
|
+
reasoningEffort: pick("reasoningEffort") ?? records[name]?.reasoningEffort
|
|
7354
|
+
};
|
|
7355
|
+
}
|
|
7356
|
+
setAgentRegistry(records);
|
|
7357
|
+
log.debug("agent registry built", {
|
|
7358
|
+
agents: Object.keys(records).length,
|
|
7359
|
+
defaultSubagentModel: getDefaultSubagentModel()
|
|
7360
|
+
});
|
|
7361
|
+
}
|
|
6659
7362
|
var server = async (input) => {
|
|
6660
7363
|
cleanupStaleUnscopedInstall();
|
|
6661
7364
|
const opencodeVersion = pickOpencodeVersion(input);
|
|
@@ -6665,7 +7368,9 @@ var server = async (input) => {
|
|
|
6665
7368
|
setOpencodeProjectDirectory(pickOpencodeDirectory(input));
|
|
6666
7369
|
return {
|
|
6667
7370
|
config: async (config) => {
|
|
7371
|
+
registerSideQuestionCommand(config);
|
|
6668
7372
|
config.provider ??= {};
|
|
7373
|
+
await buildAgentRegistry(config);
|
|
6669
7374
|
const expanded = await expandAccountProviders(config);
|
|
6670
7375
|
if (expanded) {
|
|
6671
7376
|
logStartupDiagnostics(
|
|
@@ -6736,6 +7441,10 @@ export {
|
|
|
6736
7441
|
configModelsForProvider,
|
|
6737
7442
|
createClaudeCode,
|
|
6738
7443
|
index_default as default,
|
|
6739
|
-
defaultModels
|
|
7444
|
+
defaultModels,
|
|
7445
|
+
getAgentRegistry,
|
|
7446
|
+
getDefaultSubagentModel,
|
|
7447
|
+
registerSideQuestionCommand,
|
|
7448
|
+
resolveAgentModel
|
|
6740
7449
|
};
|
|
6741
7450
|
//# sourceMappingURL=index.js.map
|