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