@threadbase-sh/streamer 1.16.1 → 1.17.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/dist/cli.cjs +529 -6
- package/dist/cli.cjs.map +1 -1
- package/dist/index.cjs +513 -3
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +57 -1
- package/dist/index.d.ts +57 -1
- package/dist/index.js +513 -3
- package/dist/index.js.map +1 -1
- package/package.json +1 -1
package/dist/cli.cjs
CHANGED
|
@@ -124790,12 +124790,23 @@ async function tryBind(start, offset) {
|
|
|
124790
124790
|
if (free) return port;
|
|
124791
124791
|
return tryBind(start, offset + 1);
|
|
124792
124792
|
}
|
|
124793
|
+
function isPidAlive2(pid) {
|
|
124794
|
+
try {
|
|
124795
|
+
process.kill(pid, 0);
|
|
124796
|
+
return true;
|
|
124797
|
+
} catch {
|
|
124798
|
+
return false;
|
|
124799
|
+
}
|
|
124800
|
+
}
|
|
124793
124801
|
function takeoverProd(opts) {
|
|
124794
124802
|
const existing = readMarker();
|
|
124795
124803
|
if (existing) {
|
|
124796
|
-
|
|
124797
|
-
|
|
124798
|
-
|
|
124804
|
+
if (isPidAlive2(existing.devPid)) {
|
|
124805
|
+
throw new Error(
|
|
124806
|
+
`prod is already suspended by dev pid ${existing.devPid} (since ${existing.suspendedAt}). Stop that dev session first, or run 'tb-streamer prod doctor'.`
|
|
124807
|
+
);
|
|
124808
|
+
}
|
|
124809
|
+
log6.info(`stale marker found (pid ${existing.devPid} is gone) \u2014 clearing and proceeding`);
|
|
124799
124810
|
}
|
|
124800
124811
|
getSupervisor().bootoutAgent();
|
|
124801
124812
|
writeMarker({
|
|
@@ -133470,6 +133481,10 @@ var createSessionRoutes = (deps) => {
|
|
|
133470
133481
|
await deps.handleSendInput(c.req.param("id"), c.env.incoming, c.env.outgoing);
|
|
133471
133482
|
return alreadyHandled6();
|
|
133472
133483
|
});
|
|
133484
|
+
app.post("/:id/answer", async (c) => {
|
|
133485
|
+
await deps.handleSendAnswer(c.req.param("id"), c.env.incoming, c.env.outgoing);
|
|
133486
|
+
return alreadyHandled6();
|
|
133487
|
+
});
|
|
133473
133488
|
app.post("/:id/files", async (c) => {
|
|
133474
133489
|
await deps.handleUploadFile(c.req.param("id"), c.env.incoming, c.env.outgoing);
|
|
133475
133490
|
return alreadyHandled6();
|
|
@@ -135452,6 +135467,164 @@ var import_crypto5 = require("crypto");
|
|
|
135452
135467
|
var import_fs11 = require("fs");
|
|
135453
135468
|
var import_path12 = require("path");
|
|
135454
135469
|
init_logger();
|
|
135470
|
+
|
|
135471
|
+
// src/services/questions/detectPermissionGate.ts
|
|
135472
|
+
var OSC_777_RE = /\x1b\]777;notify;Claude Code;[^\x07\x1b]*/;
|
|
135473
|
+
function hasPermissionOsc(rawData) {
|
|
135474
|
+
return OSC_777_RE.test(rawData);
|
|
135475
|
+
}
|
|
135476
|
+
var OPTION_RE = /^\s*(❯)?\s*(\d+)\.\s+(.+?)\s*$/;
|
|
135477
|
+
var FOOTER_RE = /Enter to select|Esc to cancel|↑|↓|to navigate|to cancel/i;
|
|
135478
|
+
var BOX_ONLY_RE = /^[\s│─┌┐└┘├┤┬┴┼╭╮╰╯╱╲=_-]+$/;
|
|
135479
|
+
var PROMPT_ARROW_RE = /^[\s]*[❯›>]\s*$/;
|
|
135480
|
+
function stripGutter(line) {
|
|
135481
|
+
return line.replace(/^\s*[│|]\s?/, "").replace(/\s*[│|]\s*$/, "");
|
|
135482
|
+
}
|
|
135483
|
+
function scrapePermissionGate(lines) {
|
|
135484
|
+
const options = [];
|
|
135485
|
+
let cursor;
|
|
135486
|
+
let firstOptionLine = -1;
|
|
135487
|
+
for (let i = 0; i < lines.length; i++) {
|
|
135488
|
+
const m2 = OPTION_RE.exec(stripGutter(lines[i]));
|
|
135489
|
+
if (!m2) continue;
|
|
135490
|
+
const index = Number.parseInt(m2[2], 10);
|
|
135491
|
+
if (!Number.isFinite(index)) continue;
|
|
135492
|
+
if (firstOptionLine === -1) firstOptionLine = i;
|
|
135493
|
+
if (m2[1]) cursor = index;
|
|
135494
|
+
options.push({ index, label: m2[3] });
|
|
135495
|
+
}
|
|
135496
|
+
if (options.length === 0) return null;
|
|
135497
|
+
let prompt;
|
|
135498
|
+
for (let i = firstOptionLine - 1; i >= 0; i--) {
|
|
135499
|
+
const t2 = stripGutter(lines[i]).trim();
|
|
135500
|
+
if (t2.length === 0) continue;
|
|
135501
|
+
if (BOX_ONLY_RE.test(lines[i].trim()) || FOOTER_RE.test(t2) || PROMPT_ARROW_RE.test(t2)) continue;
|
|
135502
|
+
prompt = t2 || void 0;
|
|
135503
|
+
break;
|
|
135504
|
+
}
|
|
135505
|
+
return { ...prompt ? { prompt } : {}, options, ...cursor !== void 0 ? { cursor } : {} };
|
|
135506
|
+
}
|
|
135507
|
+
|
|
135508
|
+
// src/services/questions/detectQuestionFromScreen.ts
|
|
135509
|
+
var ASK_FOOTER_RE = /Enter to select/i;
|
|
135510
|
+
var ESC_FOOTER_RE = /Esc to cancel|to navigate/i;
|
|
135511
|
+
var OPTION_RE2 = /^\s*(?:❯)?\s*(\d+)\.\s+(.+?)\s*$/;
|
|
135512
|
+
var QUESTION_RE = /\?\s*$/;
|
|
135513
|
+
var BOX_ONLY_RE2 = /^[\s│─┌┐└┘├┤┬┴┼╭╮╰╯╱╲=_-]+$/;
|
|
135514
|
+
var PERMISSION_LABEL_RE = /^(Yes|No)\b/i;
|
|
135515
|
+
function stripBoxGutter(line) {
|
|
135516
|
+
return line.replace(/^\s*[│|]\s?/, "").replace(/\s*[│|]\s*$/, "");
|
|
135517
|
+
}
|
|
135518
|
+
function detectQuestionFromScreen(lines) {
|
|
135519
|
+
let footerIdx = -1;
|
|
135520
|
+
for (let i = lines.length - 1; i >= 0; i--) {
|
|
135521
|
+
if (ASK_FOOTER_RE.test(lines[i])) {
|
|
135522
|
+
footerIdx = i;
|
|
135523
|
+
break;
|
|
135524
|
+
}
|
|
135525
|
+
}
|
|
135526
|
+
if (footerIdx === -1) return null;
|
|
135527
|
+
const options = [];
|
|
135528
|
+
let firstOptionIdx = -1;
|
|
135529
|
+
for (let i = footerIdx - 1; i >= 0; i--) {
|
|
135530
|
+
const line = lines[i];
|
|
135531
|
+
const inner = stripBoxGutter(line);
|
|
135532
|
+
const trimmed = inner.trim();
|
|
135533
|
+
if (trimmed.length === 0) {
|
|
135534
|
+
if (options.length === 0) continue;
|
|
135535
|
+
break;
|
|
135536
|
+
}
|
|
135537
|
+
if (BOX_ONLY_RE2.test(line.trim())) continue;
|
|
135538
|
+
if (ESC_FOOTER_RE.test(line) && options.length === 0) continue;
|
|
135539
|
+
if (options.length > 0 && QUESTION_RE.test(trimmed) && !OPTION_RE2.test(inner)) break;
|
|
135540
|
+
const m2 = OPTION_RE2.exec(inner);
|
|
135541
|
+
if (m2) {
|
|
135542
|
+
const label = m2[2].trim();
|
|
135543
|
+
if (PERMISSION_LABEL_RE.test(label)) return null;
|
|
135544
|
+
options.unshift({ label, description: "" });
|
|
135545
|
+
firstOptionIdx = i;
|
|
135546
|
+
}
|
|
135547
|
+
}
|
|
135548
|
+
if (options.length < 2 || firstOptionIdx === -1) return null;
|
|
135549
|
+
let question;
|
|
135550
|
+
for (let i = firstOptionIdx - 1; i >= 0; i--) {
|
|
135551
|
+
const raw2 = stripBoxGutter(lines[i]);
|
|
135552
|
+
const trimmed = raw2.trim();
|
|
135553
|
+
if (trimmed.length === 0) continue;
|
|
135554
|
+
if (BOX_ONLY_RE2.test(lines[i].trim())) continue;
|
|
135555
|
+
if (QUESTION_RE.test(trimmed)) {
|
|
135556
|
+
question = trimmed;
|
|
135557
|
+
}
|
|
135558
|
+
break;
|
|
135559
|
+
}
|
|
135560
|
+
if (!question) return null;
|
|
135561
|
+
return {
|
|
135562
|
+
questions: [{ question, header: "", multiSelect: false, options }]
|
|
135563
|
+
};
|
|
135564
|
+
}
|
|
135565
|
+
function questionContentKey(questions) {
|
|
135566
|
+
return questions.map((q2) => `${q2.question} ${q2.options.map((o) => o.label).join(",")}`).join("::");
|
|
135567
|
+
}
|
|
135568
|
+
|
|
135569
|
+
// src/services/questions/detectShellPrompt.ts
|
|
135570
|
+
var ENTER = "\r";
|
|
135571
|
+
var YN_RE = /[[(]\s*y\s*\/\s*n\s*[\])]/i;
|
|
135572
|
+
var PRESS_ENTER_RE = /press\s+(enter|return|any key)/i;
|
|
135573
|
+
var CONTINUE_RE = /\bcontinue\b\s*\??\s*$/i;
|
|
135574
|
+
var NUMBERED_RE = /^\s*(?:❯|>)?\s*(\d+)[.)]\s+(.+?)\s*$/;
|
|
135575
|
+
var CLAUDE_CHROME_RE = /Enter to select|Esc to cancel|╭|╰|│.*│/;
|
|
135576
|
+
var BOX_ONLY_RE3 = /^[\s│─┌┐└┘├┤┬┴┼╭╮╰╯╱╲=_-]+$/;
|
|
135577
|
+
function lastNonBlank(lines) {
|
|
135578
|
+
for (let i = lines.length - 1; i >= 0; i--) {
|
|
135579
|
+
const t2 = lines[i].trim();
|
|
135580
|
+
if (t2.length > 0) return { text: t2, idx: i };
|
|
135581
|
+
}
|
|
135582
|
+
return null;
|
|
135583
|
+
}
|
|
135584
|
+
function detectShellPrompt(lines) {
|
|
135585
|
+
if (lines.some((l) => CLAUDE_CHROME_RE.test(l))) return null;
|
|
135586
|
+
const last = lastNonBlank(lines);
|
|
135587
|
+
if (!last) return null;
|
|
135588
|
+
if (YN_RE.test(last.text)) {
|
|
135589
|
+
return {
|
|
135590
|
+
prompt: last.text,
|
|
135591
|
+
options: [
|
|
135592
|
+
{ index: 1, label: "Yes", answerKeys: `y${ENTER}` },
|
|
135593
|
+
{ index: 2, label: "No", answerKeys: `n${ENTER}` }
|
|
135594
|
+
]
|
|
135595
|
+
};
|
|
135596
|
+
}
|
|
135597
|
+
if (NUMBERED_RE.test(last.text)) {
|
|
135598
|
+
const options = [];
|
|
135599
|
+
for (let i = 0; i <= last.idx; i++) {
|
|
135600
|
+
const m2 = NUMBERED_RE.exec(lines[i]);
|
|
135601
|
+
if (!m2) continue;
|
|
135602
|
+
const num = Number.parseInt(m2[1], 10);
|
|
135603
|
+
if (!Number.isFinite(num)) continue;
|
|
135604
|
+
options.push({ index: num, label: m2[2].trim(), answerKeys: `${num}${ENTER}` });
|
|
135605
|
+
}
|
|
135606
|
+
if (options.length >= 2) {
|
|
135607
|
+
const firstRow = lines.findIndex((l) => NUMBERED_RE.test(l));
|
|
135608
|
+
let prompt = "";
|
|
135609
|
+
for (let i = firstRow - 1; i >= 0; i--) {
|
|
135610
|
+
const t2 = lines[i].trim();
|
|
135611
|
+
if (t2.length === 0 || BOX_ONLY_RE3.test(t2)) continue;
|
|
135612
|
+
prompt = t2;
|
|
135613
|
+
break;
|
|
135614
|
+
}
|
|
135615
|
+
return { prompt: prompt || "Select an option", options };
|
|
135616
|
+
}
|
|
135617
|
+
}
|
|
135618
|
+
if (PRESS_ENTER_RE.test(last.text) || CONTINUE_RE.test(last.text)) {
|
|
135619
|
+
return {
|
|
135620
|
+
prompt: last.text,
|
|
135621
|
+
options: [{ index: 1, label: "Continue", answerKeys: ENTER }]
|
|
135622
|
+
};
|
|
135623
|
+
}
|
|
135624
|
+
return null;
|
|
135625
|
+
}
|
|
135626
|
+
|
|
135627
|
+
// src/pty-manager.ts
|
|
135455
135628
|
var OUTPUT_BUFFER_MAX = 65536;
|
|
135456
135629
|
var PTY_COLS = 120;
|
|
135457
135630
|
var PTY_ROWS = 40;
|
|
@@ -135501,6 +135674,21 @@ var PTYManager = class {
|
|
|
135501
135674
|
onOutput;
|
|
135502
135675
|
onStatusChange;
|
|
135503
135676
|
onReady;
|
|
135677
|
+
onPermissionChange;
|
|
135678
|
+
onLiveQuestion;
|
|
135679
|
+
onLiveQuestionGone;
|
|
135680
|
+
// Per-session permission-gate state. True between an OSC 777 (gate open) and
|
|
135681
|
+
// the next prompt-ready without a fresh 777 (gate closed). Prevents
|
|
135682
|
+
// re-broadcasting open/close on every chunk.
|
|
135683
|
+
permissionOpen = /* @__PURE__ */ new Set();
|
|
135684
|
+
// Content key of the last AskUserQuestion broadcast from the rendered screen,
|
|
135685
|
+
// per session — de-dupes the same menu firing on consecutive repaints.
|
|
135686
|
+
lastScreenQuestionKey = /* @__PURE__ */ new Map();
|
|
135687
|
+
// Content key of the last unstructured shell prompt (detectShellPrompt)
|
|
135688
|
+
// broadcast per session — present between open and resolve so we can clear it
|
|
135689
|
+
// on a prompt-ready/marker return and de-dupe consecutive repaints. Modelled
|
|
135690
|
+
// on permissionOpen but keyed by content (a shell prompt has no OSC trigger).
|
|
135691
|
+
shellPromptOpen = /* @__PURE__ */ new Map();
|
|
135504
135692
|
// Tracks sessions (both fresh and resume) whose PTY has spawned but Claude
|
|
135505
135693
|
// hasn't yet reached an interactive prompt — i.e. onReady hasn't fired.
|
|
135506
135694
|
pendingReady = /* @__PURE__ */ new Set();
|
|
@@ -135523,6 +135711,9 @@ var PTYManager = class {
|
|
|
135523
135711
|
this.onOutput = options.onOutput;
|
|
135524
135712
|
this.onStatusChange = options.onStatusChange;
|
|
135525
135713
|
this.onReady = options.onReady;
|
|
135714
|
+
this.onPermissionChange = options.onPermissionChange;
|
|
135715
|
+
this.onLiveQuestion = options.onLiveQuestion;
|
|
135716
|
+
this.onLiveQuestionGone = options.onLiveQuestionGone;
|
|
135526
135717
|
this.log = options.logger ?? getLogger("pty");
|
|
135527
135718
|
}
|
|
135528
135719
|
// Resume an existing Claude conversation. sessionId is the JSONL UUID.
|
|
@@ -135765,6 +135956,9 @@ var PTYManager = class {
|
|
|
135765
135956
|
this.pendingReady.delete(sessionId);
|
|
135766
135957
|
this.queuedInputs.delete(sessionId);
|
|
135767
135958
|
this.firstChunkAt.delete(sessionId);
|
|
135959
|
+
this.permissionOpen.delete(sessionId);
|
|
135960
|
+
this.lastScreenQuestionKey.delete(sessionId);
|
|
135961
|
+
this.shellPromptOpen.delete(sessionId);
|
|
135768
135962
|
try {
|
|
135769
135963
|
session.process.kill("SIGINT");
|
|
135770
135964
|
} catch {
|
|
@@ -135824,6 +136018,9 @@ var PTYManager = class {
|
|
|
135824
136018
|
this.firstChunkAt.clear();
|
|
135825
136019
|
this.chunkIndex.clear();
|
|
135826
136020
|
this.lastChunkAt.clear();
|
|
136021
|
+
this.permissionOpen.clear();
|
|
136022
|
+
this.lastScreenQuestionKey.clear();
|
|
136023
|
+
this.shellPromptOpen.clear();
|
|
135827
136024
|
}
|
|
135828
136025
|
handleOutput(sessionId, data) {
|
|
135829
136026
|
const session = this.sessions.get(sessionId);
|
|
@@ -135867,6 +136064,87 @@ var PTYManager = class {
|
|
|
135867
136064
|
this.markReady(sessionId, session, "fallback:timeout");
|
|
135868
136065
|
}
|
|
135869
136066
|
this.onOutput?.(sessionId, data);
|
|
136067
|
+
this.detectLivePrompts(sessionId, data, stripped).catch((err) => {
|
|
136068
|
+
this.log.warn("[pty.prompt_detect] failed", {
|
|
136069
|
+
event: "pty.prompt_detect_failed",
|
|
136070
|
+
sessionId,
|
|
136071
|
+
err
|
|
136072
|
+
});
|
|
136073
|
+
});
|
|
136074
|
+
}
|
|
136075
|
+
// Detect permission gates (OSC 777 + scraped options) and AskUserQuestion
|
|
136076
|
+
// menus from the rendered screen, firing the additive callbacks. Async because
|
|
136077
|
+
// reading the rendered buffer needs the xterm write queue flushed. Pure
|
|
136078
|
+
// detection lives in services/questions/*; this only orchestrates triggers,
|
|
136079
|
+
// per-session debounce, and the callbacks.
|
|
136080
|
+
async detectLivePrompts(sessionId, rawData, stripped) {
|
|
136081
|
+
const session = this.sessions.get(sessionId);
|
|
136082
|
+
if (!session) return;
|
|
136083
|
+
const oscPermission = hasPermissionOsc(rawData);
|
|
136084
|
+
const hasAskFooter = /Enter to select/i.test(stripped);
|
|
136085
|
+
const hasPromptMarker = CLAUDE_PROMPT_MARKERS.some((m2) => stripped.includes(m2));
|
|
136086
|
+
const hasShellPromptHint = /[[(]\s*y\s*\/\s*n\s*[\])]|press\s+(enter|return|any key)|\bcontinue\b\s*\?|^\s*(?:❯|>)?\s*\d+[.)]\s+\S/im.test(
|
|
136087
|
+
stripped
|
|
136088
|
+
);
|
|
136089
|
+
if (!oscPermission && !hasAskFooter && !hasShellPromptHint && !this.permissionOpen.has(sessionId) && !this.shellPromptOpen.has(sessionId) && !this.lastScreenQuestionKey.has(sessionId)) {
|
|
136090
|
+
return;
|
|
136091
|
+
}
|
|
136092
|
+
const lines = await this.getOutputLines(sessionId, 60);
|
|
136093
|
+
const askFooterOnScreen = lines.some((l) => /Enter to select/i.test(l));
|
|
136094
|
+
if (oscPermission || hasAskFooter || askFooterOnScreen) {
|
|
136095
|
+
this.log.debug?.(`[pty.prompt_detect] ${sessionId.slice(0, 8)} trigger`, {
|
|
136096
|
+
event: "pty.prompt_detect",
|
|
136097
|
+
sessionId,
|
|
136098
|
+
oscPermission,
|
|
136099
|
+
hasAskFooter,
|
|
136100
|
+
askFooterOnScreen,
|
|
136101
|
+
permGate: oscPermission && !askFooterOnScreen ? scrapePermissionGate(lines) : void 0,
|
|
136102
|
+
askQuestion: askFooterOnScreen ? detectQuestionFromScreen(lines) : void 0,
|
|
136103
|
+
renderedTail: lines.slice(-25)
|
|
136104
|
+
});
|
|
136105
|
+
}
|
|
136106
|
+
if (oscPermission && !askFooterOnScreen) {
|
|
136107
|
+
const gate = scrapePermissionGate(lines);
|
|
136108
|
+
this.permissionOpen.add(sessionId);
|
|
136109
|
+
this.onPermissionChange?.(sessionId, gate ?? { options: [] });
|
|
136110
|
+
} else if (this.permissionOpen.has(sessionId) && !askFooterOnScreen) {
|
|
136111
|
+
const gate = scrapePermissionGate(lines);
|
|
136112
|
+
if (gate) {
|
|
136113
|
+
this.onPermissionChange?.(sessionId, gate);
|
|
136114
|
+
} else if (hasPromptMarker) {
|
|
136115
|
+
this.permissionOpen.delete(sessionId);
|
|
136116
|
+
this.onPermissionChange?.(sessionId, null);
|
|
136117
|
+
}
|
|
136118
|
+
}
|
|
136119
|
+
if (askFooterOnScreen) {
|
|
136120
|
+
const detected = detectQuestionFromScreen(lines);
|
|
136121
|
+
if (detected) {
|
|
136122
|
+
const key = questionContentKey(detected.questions);
|
|
136123
|
+
if (this.lastScreenQuestionKey.get(sessionId) !== key) {
|
|
136124
|
+
this.lastScreenQuestionKey.set(sessionId, key);
|
|
136125
|
+
this.onLiveQuestion?.(sessionId, detected.questions);
|
|
136126
|
+
}
|
|
136127
|
+
}
|
|
136128
|
+
} else if (this.lastScreenQuestionKey.has(sessionId) && hasPromptMarker) {
|
|
136129
|
+
this.lastScreenQuestionKey.delete(sessionId);
|
|
136130
|
+
this.onLiveQuestionGone?.(sessionId);
|
|
136131
|
+
}
|
|
136132
|
+
if (!oscPermission && !askFooterOnScreen && !this.permissionOpen.has(sessionId)) {
|
|
136133
|
+
const shell = detectShellPrompt(lines);
|
|
136134
|
+
if (shell) {
|
|
136135
|
+
const key = `${shell.prompt}\0${shell.options.map((o) => o.label).join("\0")}`;
|
|
136136
|
+
if (this.shellPromptOpen.get(sessionId) !== key) {
|
|
136137
|
+
this.shellPromptOpen.set(sessionId, key);
|
|
136138
|
+
this.onPermissionChange?.(sessionId, {
|
|
136139
|
+
prompt: shell.prompt,
|
|
136140
|
+
options: shell.options
|
|
136141
|
+
});
|
|
136142
|
+
}
|
|
136143
|
+
} else if (this.shellPromptOpen.has(sessionId) && hasPromptMarker) {
|
|
136144
|
+
this.shellPromptOpen.delete(sessionId);
|
|
136145
|
+
this.onPermissionChange?.(sessionId, null);
|
|
136146
|
+
}
|
|
136147
|
+
}
|
|
135870
136148
|
}
|
|
135871
136149
|
// Transition a session from "running" to "waiting_input", clear pendingReady,
|
|
135872
136150
|
// and flush any queued input. Idempotent: callers can invoke at any chunk.
|
|
@@ -135905,6 +136183,9 @@ var PTYManager = class {
|
|
|
135905
136183
|
this.sessions.delete(sessionId);
|
|
135906
136184
|
this.queuedInputs.delete(sessionId);
|
|
135907
136185
|
this.firstChunkAt.delete(sessionId);
|
|
136186
|
+
this.permissionOpen.delete(sessionId);
|
|
136187
|
+
this.lastScreenQuestionKey.delete(sessionId);
|
|
136188
|
+
this.shellPromptOpen.delete(sessionId);
|
|
135908
136189
|
}
|
|
135909
136190
|
};
|
|
135910
136191
|
function toPublicSession(s3) {
|
|
@@ -137830,6 +138111,129 @@ function pruneAgentConversations(cache) {
|
|
|
137830
138111
|
return { scanned: rows.length, pruned, missing };
|
|
137831
138112
|
}
|
|
137832
138113
|
|
|
138114
|
+
// src/services/questions/detectAskUserQuestion.ts
|
|
138115
|
+
function normalizeContent2(raw2) {
|
|
138116
|
+
if (Array.isArray(raw2)) return raw2;
|
|
138117
|
+
if (typeof raw2 === "string") return [{ type: "text", text: raw2 }];
|
|
138118
|
+
return [];
|
|
138119
|
+
}
|
|
138120
|
+
function coerceOptions(raw2) {
|
|
138121
|
+
if (!Array.isArray(raw2)) return null;
|
|
138122
|
+
const out = [];
|
|
138123
|
+
for (const o of raw2) {
|
|
138124
|
+
if (o && typeof o === "object" && typeof o.label === "string") {
|
|
138125
|
+
const opt = o;
|
|
138126
|
+
out.push({
|
|
138127
|
+
label: opt.label,
|
|
138128
|
+
description: typeof opt.description === "string" ? opt.description : "",
|
|
138129
|
+
...typeof opt.preview === "string" ? { preview: opt.preview } : {}
|
|
138130
|
+
});
|
|
138131
|
+
}
|
|
138132
|
+
}
|
|
138133
|
+
return out.length > 0 ? out : null;
|
|
138134
|
+
}
|
|
138135
|
+
function coerceQuestions(raw2) {
|
|
138136
|
+
if (!Array.isArray(raw2)) return null;
|
|
138137
|
+
const out = [];
|
|
138138
|
+
for (const q2 of raw2) {
|
|
138139
|
+
if (!q2 || typeof q2 !== "object") continue;
|
|
138140
|
+
const qq = q2;
|
|
138141
|
+
const options = coerceOptions(qq.options);
|
|
138142
|
+
if (typeof qq.question !== "string" || !options) continue;
|
|
138143
|
+
out.push({
|
|
138144
|
+
question: qq.question,
|
|
138145
|
+
header: typeof qq.header === "string" ? qq.header : "",
|
|
138146
|
+
multiSelect: qq.multiSelect === true,
|
|
138147
|
+
options
|
|
138148
|
+
});
|
|
138149
|
+
}
|
|
138150
|
+
return out.length > 0 ? out : null;
|
|
138151
|
+
}
|
|
138152
|
+
function detectAskUserQuestion(rawLine) {
|
|
138153
|
+
let parsed;
|
|
138154
|
+
try {
|
|
138155
|
+
parsed = JSON.parse(rawLine);
|
|
138156
|
+
} catch {
|
|
138157
|
+
return null;
|
|
138158
|
+
}
|
|
138159
|
+
const blocks = normalizeContent2(parsed.message?.content ?? parsed.content);
|
|
138160
|
+
for (const b2 of blocks) {
|
|
138161
|
+
if (b2.type === "tool_use" && b2.name === "AskUserQuestion" && typeof b2.id === "string") {
|
|
138162
|
+
const input = b2.input;
|
|
138163
|
+
const questions = coerceQuestions(input?.questions);
|
|
138164
|
+
if (questions) return { toolUseId: b2.id, questions };
|
|
138165
|
+
}
|
|
138166
|
+
}
|
|
138167
|
+
return null;
|
|
138168
|
+
}
|
|
138169
|
+
|
|
138170
|
+
// src/services/questions/questionBroadcast.ts
|
|
138171
|
+
function questionsFromLines(sessionId, lines) {
|
|
138172
|
+
const messages = [];
|
|
138173
|
+
const pending = [];
|
|
138174
|
+
for (const line of lines) {
|
|
138175
|
+
const detected = detectAskUserQuestion(line);
|
|
138176
|
+
if (detected) {
|
|
138177
|
+
messages.push({
|
|
138178
|
+
type: "question",
|
|
138179
|
+
sessionId,
|
|
138180
|
+
toolUseId: detected.toolUseId,
|
|
138181
|
+
questions: detected.questions
|
|
138182
|
+
});
|
|
138183
|
+
pending.push(detected);
|
|
138184
|
+
}
|
|
138185
|
+
}
|
|
138186
|
+
return { messages, pending };
|
|
138187
|
+
}
|
|
138188
|
+
function shouldBroadcastQuestion(args) {
|
|
138189
|
+
const alreadyShown = args.lastContentKey === args.newContentKey;
|
|
138190
|
+
if (!alreadyShown) return true;
|
|
138191
|
+
return args.priorToolUseId !== args.newToolUseId;
|
|
138192
|
+
}
|
|
138193
|
+
|
|
138194
|
+
// src/services/questions/answersToKeystrokes.ts
|
|
138195
|
+
var DOWN = "\x1B[B";
|
|
138196
|
+
var ENTER2 = "\r";
|
|
138197
|
+
var UnknownOptionError = class extends Error {
|
|
138198
|
+
constructor(question, value) {
|
|
138199
|
+
super(`No option labelled "${value}" for question "${question}"`);
|
|
138200
|
+
this.question = question;
|
|
138201
|
+
this.value = value;
|
|
138202
|
+
this.name = "UnknownOptionError";
|
|
138203
|
+
}
|
|
138204
|
+
question;
|
|
138205
|
+
value;
|
|
138206
|
+
};
|
|
138207
|
+
function answersToKeystrokes(questions, answers) {
|
|
138208
|
+
let out = "";
|
|
138209
|
+
for (const q2 of questions) {
|
|
138210
|
+
const raw2 = answers[q2.question];
|
|
138211
|
+
if (raw2 === void 0) {
|
|
138212
|
+
throw new Error(`Missing answer for question "${q2.question}"`);
|
|
138213
|
+
}
|
|
138214
|
+
const label = Array.isArray(raw2) ? raw2[0] : raw2;
|
|
138215
|
+
const target = q2.options.findIndex((o) => o.label === label);
|
|
138216
|
+
if (target < 0) throw new UnknownOptionError(q2.question, label);
|
|
138217
|
+
out += DOWN.repeat(target) + ENTER2;
|
|
138218
|
+
}
|
|
138219
|
+
return out;
|
|
138220
|
+
}
|
|
138221
|
+
|
|
138222
|
+
// src/services/questions/resolveAnswer.ts
|
|
138223
|
+
function resolveAnswer(pending, body) {
|
|
138224
|
+
if (!pending) return { ok: false, reason: "no_pending_question" };
|
|
138225
|
+
if (typeof body.toolUseId !== "string" || body.toolUseId !== pending.toolUseId) {
|
|
138226
|
+
return { ok: false, reason: "tool_use_mismatch" };
|
|
138227
|
+
}
|
|
138228
|
+
const answers = body.answers ?? {};
|
|
138229
|
+
try {
|
|
138230
|
+
return { ok: true, keys: answersToKeystrokes(pending.questions, answers) };
|
|
138231
|
+
} catch (e) {
|
|
138232
|
+
if (e instanceof UnknownOptionError) return { ok: false, reason: "unknown_option" };
|
|
138233
|
+
throw e;
|
|
138234
|
+
}
|
|
138235
|
+
}
|
|
138236
|
+
|
|
137833
138237
|
// src/agent/dedupe.ts
|
|
137834
138238
|
function createProgressDedupeLRU(capacity) {
|
|
137835
138239
|
if (!Number.isFinite(capacity) || capacity < 1) {
|
|
@@ -138286,6 +138690,16 @@ var StreamerServer = class {
|
|
|
138286
138690
|
fileWatcher;
|
|
138287
138691
|
sessionFileMap = /* @__PURE__ */ new Map();
|
|
138288
138692
|
// sessionId → JSONL filePath
|
|
138693
|
+
pendingQuestions = /* @__PURE__ */ new Map();
|
|
138694
|
+
// Content key of the AskUserQuestion currently broadcast for a session (from
|
|
138695
|
+
// either the rendered screen or JSONL), used to de-dupe the two paths: when
|
|
138696
|
+
// the screen detection fires first, the later JSONL flush of the same question
|
|
138697
|
+
// is suppressed. Cleared alongside pendingQuestions.
|
|
138698
|
+
pendingQuestionKey = /* @__PURE__ */ new Map();
|
|
138699
|
+
// Per-session permission gate currently open (scraped via OSC 777). Parallel
|
|
138700
|
+
// to pendingQuestions; mobile answers it by sending the option index via
|
|
138701
|
+
// /input { keys }. Cleared when the gate closes.
|
|
138702
|
+
pendingPermission = /* @__PURE__ */ new Map();
|
|
138289
138703
|
scanner = null;
|
|
138290
138704
|
scannerReady = null;
|
|
138291
138705
|
// Set by onConversationChanged while a scan is in-flight; getScanner() does
|
|
@@ -138381,6 +138795,28 @@ var StreamerServer = class {
|
|
|
138381
138795
|
this.cache?.updateFromLines(filePath, lines);
|
|
138382
138796
|
for (const [sessionId, watchedPath] of this.sessionFileMap) {
|
|
138383
138797
|
if (watchedPath === filePath) {
|
|
138798
|
+
const priorToolUseId = this.pendingQuestions.get(sessionId)?.toolUseId;
|
|
138799
|
+
const { messages, pending } = questionsFromLines(sessionId, lines);
|
|
138800
|
+
for (const p2 of pending) {
|
|
138801
|
+
this.pendingQuestions.set(sessionId, p2);
|
|
138802
|
+
const t2 = setTimeout(() => {
|
|
138803
|
+
if (this.pendingQuestions.get(sessionId)?.toolUseId === p2.toolUseId) {
|
|
138804
|
+
this.cancelPendingQuestion(sessionId);
|
|
138805
|
+
}
|
|
138806
|
+
}, 6e4);
|
|
138807
|
+
t2.unref();
|
|
138808
|
+
}
|
|
138809
|
+
for (const m2 of messages) {
|
|
138810
|
+
const key = questionContentKey(m2.questions);
|
|
138811
|
+
const broadcast = shouldBroadcastQuestion({
|
|
138812
|
+
newContentKey: key,
|
|
138813
|
+
lastContentKey: this.pendingQuestionKey.get(sessionId),
|
|
138814
|
+
newToolUseId: m2.toolUseId,
|
|
138815
|
+
priorToolUseId
|
|
138816
|
+
});
|
|
138817
|
+
this.pendingQuestionKey.set(sessionId, key);
|
|
138818
|
+
if (broadcast) this.wsHub.broadcast(m2);
|
|
138819
|
+
}
|
|
138384
138820
|
this.wsHub.broadcast({ type: "conversation_events", sessionId, lines });
|
|
138385
138821
|
for (const line of lines) {
|
|
138386
138822
|
this.wsHub.broadcast({ type: "conversation_event", sessionId, line });
|
|
@@ -138412,6 +138848,19 @@ var StreamerServer = class {
|
|
|
138412
138848
|
onOutput: (sessionId, data) => {
|
|
138413
138849
|
this.wsHub.broadcast({ type: "terminal_output", sessionId, data });
|
|
138414
138850
|
},
|
|
138851
|
+
onPermissionChange: (sessionId, gate) => {
|
|
138852
|
+
this.handlePermissionChange(sessionId, gate);
|
|
138853
|
+
},
|
|
138854
|
+
onLiveQuestion: (sessionId, questions) => {
|
|
138855
|
+
this.handleLiveQuestion(sessionId, questions);
|
|
138856
|
+
},
|
|
138857
|
+
onLiveQuestionGone: (sessionId) => {
|
|
138858
|
+
this.pendingQuestionKey.delete(sessionId);
|
|
138859
|
+
const pq = this.pendingQuestions.get(sessionId);
|
|
138860
|
+
if (pq?.toolUseId.startsWith("screen:")) {
|
|
138861
|
+
this.cancelPendingQuestion(sessionId);
|
|
138862
|
+
}
|
|
138863
|
+
},
|
|
138415
138864
|
onReady: (session) => {
|
|
138416
138865
|
const resp = this.sessionStore.get(session.id, this.ptyAttachedIds());
|
|
138417
138866
|
if (resp) this.wsHub.broadcast({ type: "session_ready", session: resp });
|
|
@@ -138449,7 +138898,9 @@ var StreamerServer = class {
|
|
|
138449
138898
|
if (filePath) {
|
|
138450
138899
|
this.fileWatcher.unwatch(filePath);
|
|
138451
138900
|
this.sessionFileMap.delete(session.id);
|
|
138901
|
+
this.cancelPendingQuestion(session.id);
|
|
138452
138902
|
}
|
|
138903
|
+
this.pendingPermission.delete(session.id);
|
|
138453
138904
|
}
|
|
138454
138905
|
const resp = this.sessionStore.get(session.id, this.ptyAttachedIds());
|
|
138455
138906
|
if (resp) {
|
|
@@ -138500,6 +138951,7 @@ var StreamerServer = class {
|
|
|
138500
138951
|
handleGetSession: (id, res) => this.handleGetSession(id, res),
|
|
138501
138952
|
handleGetOutput: (id, res) => this.handleGetOutput(id, res),
|
|
138502
138953
|
handleSendInput: (id, req, res) => this.handleSendInput(id, req, res),
|
|
138954
|
+
handleSendAnswer: (id, req, res) => this.handleSendAnswer(id, req, res),
|
|
138503
138955
|
handleCancel: (id, res) => this.handleCancel(id, res),
|
|
138504
138956
|
handleStopSession: (id, res) => this.handleStopSession(id, res),
|
|
138505
138957
|
handleSetSessionName: (id, req, res) => this.handleSetSessionName(id, req, res),
|
|
@@ -138635,16 +139087,28 @@ var StreamerServer = class {
|
|
|
138635
139087
|
if (existing) clearTimeout(existing);
|
|
138636
139088
|
const timer = setTimeout(() => {
|
|
138637
139089
|
this.ptyGraceTimers.delete(sessionId);
|
|
138638
|
-
this.sessionSubscribers.delete(sessionId);
|
|
138639
139090
|
if (this.ptyManager.hasSession(sessionId)) {
|
|
139091
|
+
const resp = this.sessionStore.get(sessionId, this.ptyAttachedIds());
|
|
139092
|
+
if (resp?.status === "running") {
|
|
139093
|
+
this.log.info(
|
|
139094
|
+
`[grace] session ${sessionId} still running, deferring hold`,
|
|
139095
|
+
{ sessionId, event: "pty.grace_defer" },
|
|
139096
|
+
"pino"
|
|
139097
|
+
);
|
|
139098
|
+
this.startGraceTimer(sessionId, delayMs);
|
|
139099
|
+
return;
|
|
139100
|
+
}
|
|
139101
|
+
this.sessionSubscribers.delete(sessionId);
|
|
138640
139102
|
this.log.info(
|
|
138641
139103
|
`[grace] killing idle PTY for ${sessionId}`,
|
|
138642
139104
|
{ sessionId, event: "pty.grace_kill" },
|
|
138643
139105
|
"pino"
|
|
138644
139106
|
);
|
|
138645
139107
|
this.ptyManager.putOnHold(sessionId);
|
|
138646
|
-
const
|
|
138647
|
-
if (
|
|
139108
|
+
const held = this.sessionStore.get(sessionId, this.ptyAttachedIds());
|
|
139109
|
+
if (held) this.wsHub.broadcast({ type: "session_update", session: held });
|
|
139110
|
+
} else {
|
|
139111
|
+
this.sessionSubscribers.delete(sessionId);
|
|
138648
139112
|
}
|
|
138649
139113
|
}, delayMs);
|
|
138650
139114
|
this.ptyGraceTimers.set(sessionId, timer);
|
|
@@ -139623,6 +140087,65 @@ var StreamerServer = class {
|
|
|
139623
140087
|
json2(res, 400, { error: message });
|
|
139624
140088
|
}
|
|
139625
140089
|
}
|
|
140090
|
+
cancelPendingQuestion(sessionId) {
|
|
140091
|
+
const pq = this.pendingQuestions.get(sessionId);
|
|
140092
|
+
if (!pq) return;
|
|
140093
|
+
this.pendingQuestions.delete(sessionId);
|
|
140094
|
+
this.pendingQuestionKey.delete(sessionId);
|
|
140095
|
+
this.wsHub.broadcast({ type: "question_cancelled", sessionId, toolUseId: pq.toolUseId });
|
|
140096
|
+
}
|
|
140097
|
+
// Live AskUserQuestion detected from the rendered screen (ahead of JSONL).
|
|
140098
|
+
// Broadcasts the `question` event immediately and records the content key so
|
|
140099
|
+
// the later JSONL flush of the same question is de-duped. We synthesize a
|
|
140100
|
+
// screen-scoped toolUseId; the JSONL path overwrites pendingQuestions with the
|
|
140101
|
+
// real toolUseId when it lands, so answering works once JSONL catches up.
|
|
140102
|
+
handleLiveQuestion(sessionId, questions) {
|
|
140103
|
+
const key = questionContentKey(questions);
|
|
140104
|
+
if (this.pendingQuestionKey.get(sessionId) === key) return;
|
|
140105
|
+
const toolUseId = `screen:${sessionId}:${key.length}`;
|
|
140106
|
+
this.pendingQuestions.set(sessionId, { toolUseId, questions });
|
|
140107
|
+
this.pendingQuestionKey.set(sessionId, key);
|
|
140108
|
+
this.wsHub.broadcast({ type: "question", sessionId, toolUseId, questions });
|
|
140109
|
+
}
|
|
140110
|
+
// Permission gate opened/closed (OSC 777 + scraped options). Broadcasts the
|
|
140111
|
+
// additive `permission` / `permission_cancelled` events. Mobile answers by
|
|
140112
|
+
// sending the chosen option index via /input { keys } (e.g. "2\r").
|
|
140113
|
+
handlePermissionChange(sessionId, gate) {
|
|
140114
|
+
if (gate === null) {
|
|
140115
|
+
if (!this.pendingPermission.has(sessionId)) return;
|
|
140116
|
+
this.pendingPermission.delete(sessionId);
|
|
140117
|
+
this.wsHub.broadcast({ type: "permission_cancelled", sessionId });
|
|
140118
|
+
return;
|
|
140119
|
+
}
|
|
140120
|
+
this.pendingPermission.set(sessionId, gate);
|
|
140121
|
+
this.wsHub.broadcast({
|
|
140122
|
+
type: "permission",
|
|
140123
|
+
sessionId,
|
|
140124
|
+
...gate.prompt ? { prompt: gate.prompt } : {},
|
|
140125
|
+
options: gate.options,
|
|
140126
|
+
...gate.cursor !== void 0 ? { cursor: gate.cursor } : {}
|
|
140127
|
+
});
|
|
140128
|
+
}
|
|
140129
|
+
async handleSendAnswer(sessionId, req, res) {
|
|
140130
|
+
const body = await readBody(req);
|
|
140131
|
+
const pending = this.pendingQuestions.get(sessionId);
|
|
140132
|
+
const resolution = resolveAnswer(pending, body);
|
|
140133
|
+
if (!resolution.ok) {
|
|
140134
|
+
json2(res, 400, { ok: false, reason: resolution.reason });
|
|
140135
|
+
return;
|
|
140136
|
+
}
|
|
140137
|
+
const toolUseId = pending?.toolUseId ?? "";
|
|
140138
|
+
try {
|
|
140139
|
+
this.ptyManager.sendKeys(sessionId, resolution.keys);
|
|
140140
|
+
} catch (err) {
|
|
140141
|
+
const message = err instanceof Error ? err.message : "Failed to send answer";
|
|
140142
|
+
json2(res, 400, { ok: false, reason: message });
|
|
140143
|
+
return;
|
|
140144
|
+
}
|
|
140145
|
+
this.pendingQuestions.delete(sessionId);
|
|
140146
|
+
this.wsHub.broadcast({ type: "question_cancelled", sessionId, toolUseId });
|
|
140147
|
+
json2(res, 200, { ok: true });
|
|
140148
|
+
}
|
|
139626
140149
|
async handleUploadFile(sessionId, req, res) {
|
|
139627
140150
|
const session = this.sessionStore.get(sessionId, this.ptyAttachedIds());
|
|
139628
140151
|
if (!session) {
|