@threadbase-sh/streamer 1.16.0 → 1.17.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/cli.cjs +536 -10
- package/dist/cli.cjs.map +1 -1
- package/dist/index.cjs +534 -10
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +58 -1
- package/dist/index.d.ts +58 -1
- package/dist/index.js +534 -10
- package/dist/index.js.map +1 -1
- package/package.json +1 -1
package/dist/cli.cjs
CHANGED
|
@@ -133470,6 +133470,10 @@ var createSessionRoutes = (deps) => {
|
|
|
133470
133470
|
await deps.handleSendInput(c.req.param("id"), c.env.incoming, c.env.outgoing);
|
|
133471
133471
|
return alreadyHandled6();
|
|
133472
133472
|
});
|
|
133473
|
+
app.post("/:id/answer", async (c) => {
|
|
133474
|
+
await deps.handleSendAnswer(c.req.param("id"), c.env.incoming, c.env.outgoing);
|
|
133475
|
+
return alreadyHandled6();
|
|
133476
|
+
});
|
|
133473
133477
|
app.post("/:id/files", async (c) => {
|
|
133474
133478
|
await deps.handleUploadFile(c.req.param("id"), c.env.incoming, c.env.outgoing);
|
|
133475
133479
|
return alreadyHandled6();
|
|
@@ -135452,6 +135456,164 @@ var import_crypto5 = require("crypto");
|
|
|
135452
135456
|
var import_fs11 = require("fs");
|
|
135453
135457
|
var import_path12 = require("path");
|
|
135454
135458
|
init_logger();
|
|
135459
|
+
|
|
135460
|
+
// src/services/questions/detectPermissionGate.ts
|
|
135461
|
+
var OSC_777_RE = /\x1b\]777;notify;Claude Code;[^\x07\x1b]*/;
|
|
135462
|
+
function hasPermissionOsc(rawData) {
|
|
135463
|
+
return OSC_777_RE.test(rawData);
|
|
135464
|
+
}
|
|
135465
|
+
var OPTION_RE = /^\s*(❯)?\s*(\d+)\.\s+(.+?)\s*$/;
|
|
135466
|
+
var FOOTER_RE = /Enter to select|Esc to cancel|↑|↓|to navigate|to cancel/i;
|
|
135467
|
+
var BOX_ONLY_RE = /^[\s│─┌┐└┘├┤┬┴┼╭╮╰╯╱╲=_-]+$/;
|
|
135468
|
+
var PROMPT_ARROW_RE = /^[\s]*[❯›>]\s*$/;
|
|
135469
|
+
function stripGutter(line) {
|
|
135470
|
+
return line.replace(/^\s*[│|]\s?/, "").replace(/\s*[│|]\s*$/, "");
|
|
135471
|
+
}
|
|
135472
|
+
function scrapePermissionGate(lines) {
|
|
135473
|
+
const options = [];
|
|
135474
|
+
let cursor;
|
|
135475
|
+
let firstOptionLine = -1;
|
|
135476
|
+
for (let i = 0; i < lines.length; i++) {
|
|
135477
|
+
const m2 = OPTION_RE.exec(stripGutter(lines[i]));
|
|
135478
|
+
if (!m2) continue;
|
|
135479
|
+
const index = Number.parseInt(m2[2], 10);
|
|
135480
|
+
if (!Number.isFinite(index)) continue;
|
|
135481
|
+
if (firstOptionLine === -1) firstOptionLine = i;
|
|
135482
|
+
if (m2[1]) cursor = index;
|
|
135483
|
+
options.push({ index, label: m2[3] });
|
|
135484
|
+
}
|
|
135485
|
+
if (options.length === 0) return null;
|
|
135486
|
+
let prompt;
|
|
135487
|
+
for (let i = firstOptionLine - 1; i >= 0; i--) {
|
|
135488
|
+
const t2 = stripGutter(lines[i]).trim();
|
|
135489
|
+
if (t2.length === 0) continue;
|
|
135490
|
+
if (BOX_ONLY_RE.test(lines[i].trim()) || FOOTER_RE.test(t2) || PROMPT_ARROW_RE.test(t2)) continue;
|
|
135491
|
+
prompt = t2 || void 0;
|
|
135492
|
+
break;
|
|
135493
|
+
}
|
|
135494
|
+
return { ...prompt ? { prompt } : {}, options, ...cursor !== void 0 ? { cursor } : {} };
|
|
135495
|
+
}
|
|
135496
|
+
|
|
135497
|
+
// src/services/questions/detectQuestionFromScreen.ts
|
|
135498
|
+
var ASK_FOOTER_RE = /Enter to select/i;
|
|
135499
|
+
var ESC_FOOTER_RE = /Esc to cancel|to navigate/i;
|
|
135500
|
+
var OPTION_RE2 = /^\s*(?:❯)?\s*(\d+)\.\s+(.+?)\s*$/;
|
|
135501
|
+
var QUESTION_RE = /\?\s*$/;
|
|
135502
|
+
var BOX_ONLY_RE2 = /^[\s│─┌┐└┘├┤┬┴┼╭╮╰╯╱╲=_-]+$/;
|
|
135503
|
+
var PERMISSION_LABEL_RE = /^(Yes|No)\b/i;
|
|
135504
|
+
function stripBoxGutter(line) {
|
|
135505
|
+
return line.replace(/^\s*[│|]\s?/, "").replace(/\s*[│|]\s*$/, "");
|
|
135506
|
+
}
|
|
135507
|
+
function detectQuestionFromScreen(lines) {
|
|
135508
|
+
let footerIdx = -1;
|
|
135509
|
+
for (let i = lines.length - 1; i >= 0; i--) {
|
|
135510
|
+
if (ASK_FOOTER_RE.test(lines[i])) {
|
|
135511
|
+
footerIdx = i;
|
|
135512
|
+
break;
|
|
135513
|
+
}
|
|
135514
|
+
}
|
|
135515
|
+
if (footerIdx === -1) return null;
|
|
135516
|
+
const options = [];
|
|
135517
|
+
let firstOptionIdx = -1;
|
|
135518
|
+
for (let i = footerIdx - 1; i >= 0; i--) {
|
|
135519
|
+
const line = lines[i];
|
|
135520
|
+
const inner = stripBoxGutter(line);
|
|
135521
|
+
const trimmed = inner.trim();
|
|
135522
|
+
if (trimmed.length === 0) {
|
|
135523
|
+
if (options.length === 0) continue;
|
|
135524
|
+
break;
|
|
135525
|
+
}
|
|
135526
|
+
if (BOX_ONLY_RE2.test(line.trim())) continue;
|
|
135527
|
+
if (ESC_FOOTER_RE.test(line) && options.length === 0) continue;
|
|
135528
|
+
if (options.length > 0 && QUESTION_RE.test(trimmed) && !OPTION_RE2.test(inner)) break;
|
|
135529
|
+
const m2 = OPTION_RE2.exec(inner);
|
|
135530
|
+
if (m2) {
|
|
135531
|
+
const label = m2[2].trim();
|
|
135532
|
+
if (PERMISSION_LABEL_RE.test(label)) return null;
|
|
135533
|
+
options.unshift({ label, description: "" });
|
|
135534
|
+
firstOptionIdx = i;
|
|
135535
|
+
}
|
|
135536
|
+
}
|
|
135537
|
+
if (options.length < 2 || firstOptionIdx === -1) return null;
|
|
135538
|
+
let question;
|
|
135539
|
+
for (let i = firstOptionIdx - 1; i >= 0; i--) {
|
|
135540
|
+
const raw2 = stripBoxGutter(lines[i]);
|
|
135541
|
+
const trimmed = raw2.trim();
|
|
135542
|
+
if (trimmed.length === 0) continue;
|
|
135543
|
+
if (BOX_ONLY_RE2.test(lines[i].trim())) continue;
|
|
135544
|
+
if (QUESTION_RE.test(trimmed)) {
|
|
135545
|
+
question = trimmed;
|
|
135546
|
+
}
|
|
135547
|
+
break;
|
|
135548
|
+
}
|
|
135549
|
+
if (!question) return null;
|
|
135550
|
+
return {
|
|
135551
|
+
questions: [{ question, header: "", multiSelect: false, options }]
|
|
135552
|
+
};
|
|
135553
|
+
}
|
|
135554
|
+
function questionContentKey(questions) {
|
|
135555
|
+
return questions.map((q2) => `${q2.question} ${q2.options.map((o) => o.label).join(",")}`).join("::");
|
|
135556
|
+
}
|
|
135557
|
+
|
|
135558
|
+
// src/services/questions/detectShellPrompt.ts
|
|
135559
|
+
var ENTER = "\r";
|
|
135560
|
+
var YN_RE = /[[(]\s*y\s*\/\s*n\s*[\])]/i;
|
|
135561
|
+
var PRESS_ENTER_RE = /press\s+(enter|return|any key)/i;
|
|
135562
|
+
var CONTINUE_RE = /\bcontinue\b\s*\??\s*$/i;
|
|
135563
|
+
var NUMBERED_RE = /^\s*(?:❯|>)?\s*(\d+)[.)]\s+(.+?)\s*$/;
|
|
135564
|
+
var CLAUDE_CHROME_RE = /Enter to select|Esc to cancel|╭|╰|│.*│/;
|
|
135565
|
+
var BOX_ONLY_RE3 = /^[\s│─┌┐└┘├┤┬┴┼╭╮╰╯╱╲=_-]+$/;
|
|
135566
|
+
function lastNonBlank(lines) {
|
|
135567
|
+
for (let i = lines.length - 1; i >= 0; i--) {
|
|
135568
|
+
const t2 = lines[i].trim();
|
|
135569
|
+
if (t2.length > 0) return { text: t2, idx: i };
|
|
135570
|
+
}
|
|
135571
|
+
return null;
|
|
135572
|
+
}
|
|
135573
|
+
function detectShellPrompt(lines) {
|
|
135574
|
+
if (lines.some((l) => CLAUDE_CHROME_RE.test(l))) return null;
|
|
135575
|
+
const last = lastNonBlank(lines);
|
|
135576
|
+
if (!last) return null;
|
|
135577
|
+
if (YN_RE.test(last.text)) {
|
|
135578
|
+
return {
|
|
135579
|
+
prompt: last.text,
|
|
135580
|
+
options: [
|
|
135581
|
+
{ index: 1, label: "Yes", answerKeys: `y${ENTER}` },
|
|
135582
|
+
{ index: 2, label: "No", answerKeys: `n${ENTER}` }
|
|
135583
|
+
]
|
|
135584
|
+
};
|
|
135585
|
+
}
|
|
135586
|
+
if (NUMBERED_RE.test(last.text)) {
|
|
135587
|
+
const options = [];
|
|
135588
|
+
for (let i = 0; i <= last.idx; i++) {
|
|
135589
|
+
const m2 = NUMBERED_RE.exec(lines[i]);
|
|
135590
|
+
if (!m2) continue;
|
|
135591
|
+
const num = Number.parseInt(m2[1], 10);
|
|
135592
|
+
if (!Number.isFinite(num)) continue;
|
|
135593
|
+
options.push({ index: num, label: m2[2].trim(), answerKeys: `${num}${ENTER}` });
|
|
135594
|
+
}
|
|
135595
|
+
if (options.length >= 2) {
|
|
135596
|
+
const firstRow = lines.findIndex((l) => NUMBERED_RE.test(l));
|
|
135597
|
+
let prompt = "";
|
|
135598
|
+
for (let i = firstRow - 1; i >= 0; i--) {
|
|
135599
|
+
const t2 = lines[i].trim();
|
|
135600
|
+
if (t2.length === 0 || BOX_ONLY_RE3.test(t2)) continue;
|
|
135601
|
+
prompt = t2;
|
|
135602
|
+
break;
|
|
135603
|
+
}
|
|
135604
|
+
return { prompt: prompt || "Select an option", options };
|
|
135605
|
+
}
|
|
135606
|
+
}
|
|
135607
|
+
if (PRESS_ENTER_RE.test(last.text) || CONTINUE_RE.test(last.text)) {
|
|
135608
|
+
return {
|
|
135609
|
+
prompt: last.text,
|
|
135610
|
+
options: [{ index: 1, label: "Continue", answerKeys: ENTER }]
|
|
135611
|
+
};
|
|
135612
|
+
}
|
|
135613
|
+
return null;
|
|
135614
|
+
}
|
|
135615
|
+
|
|
135616
|
+
// src/pty-manager.ts
|
|
135455
135617
|
var OUTPUT_BUFFER_MAX = 65536;
|
|
135456
135618
|
var PTY_COLS = 120;
|
|
135457
135619
|
var PTY_ROWS = 40;
|
|
@@ -135501,6 +135663,21 @@ var PTYManager = class {
|
|
|
135501
135663
|
onOutput;
|
|
135502
135664
|
onStatusChange;
|
|
135503
135665
|
onReady;
|
|
135666
|
+
onPermissionChange;
|
|
135667
|
+
onLiveQuestion;
|
|
135668
|
+
onLiveQuestionGone;
|
|
135669
|
+
// Per-session permission-gate state. True between an OSC 777 (gate open) and
|
|
135670
|
+
// the next prompt-ready without a fresh 777 (gate closed). Prevents
|
|
135671
|
+
// re-broadcasting open/close on every chunk.
|
|
135672
|
+
permissionOpen = /* @__PURE__ */ new Set();
|
|
135673
|
+
// Content key of the last AskUserQuestion broadcast from the rendered screen,
|
|
135674
|
+
// per session — de-dupes the same menu firing on consecutive repaints.
|
|
135675
|
+
lastScreenQuestionKey = /* @__PURE__ */ new Map();
|
|
135676
|
+
// Content key of the last unstructured shell prompt (detectShellPrompt)
|
|
135677
|
+
// broadcast per session — present between open and resolve so we can clear it
|
|
135678
|
+
// on a prompt-ready/marker return and de-dupe consecutive repaints. Modelled
|
|
135679
|
+
// on permissionOpen but keyed by content (a shell prompt has no OSC trigger).
|
|
135680
|
+
shellPromptOpen = /* @__PURE__ */ new Map();
|
|
135504
135681
|
// Tracks sessions (both fresh and resume) whose PTY has spawned but Claude
|
|
135505
135682
|
// hasn't yet reached an interactive prompt — i.e. onReady hasn't fired.
|
|
135506
135683
|
pendingReady = /* @__PURE__ */ new Set();
|
|
@@ -135523,6 +135700,9 @@ var PTYManager = class {
|
|
|
135523
135700
|
this.onOutput = options.onOutput;
|
|
135524
135701
|
this.onStatusChange = options.onStatusChange;
|
|
135525
135702
|
this.onReady = options.onReady;
|
|
135703
|
+
this.onPermissionChange = options.onPermissionChange;
|
|
135704
|
+
this.onLiveQuestion = options.onLiveQuestion;
|
|
135705
|
+
this.onLiveQuestionGone = options.onLiveQuestionGone;
|
|
135526
135706
|
this.log = options.logger ?? getLogger("pty");
|
|
135527
135707
|
}
|
|
135528
135708
|
// Resume an existing Claude conversation. sessionId is the JSONL UUID.
|
|
@@ -135765,6 +135945,9 @@ var PTYManager = class {
|
|
|
135765
135945
|
this.pendingReady.delete(sessionId);
|
|
135766
135946
|
this.queuedInputs.delete(sessionId);
|
|
135767
135947
|
this.firstChunkAt.delete(sessionId);
|
|
135948
|
+
this.permissionOpen.delete(sessionId);
|
|
135949
|
+
this.lastScreenQuestionKey.delete(sessionId);
|
|
135950
|
+
this.shellPromptOpen.delete(sessionId);
|
|
135768
135951
|
try {
|
|
135769
135952
|
session.process.kill("SIGINT");
|
|
135770
135953
|
} catch {
|
|
@@ -135824,6 +136007,9 @@ var PTYManager = class {
|
|
|
135824
136007
|
this.firstChunkAt.clear();
|
|
135825
136008
|
this.chunkIndex.clear();
|
|
135826
136009
|
this.lastChunkAt.clear();
|
|
136010
|
+
this.permissionOpen.clear();
|
|
136011
|
+
this.lastScreenQuestionKey.clear();
|
|
136012
|
+
this.shellPromptOpen.clear();
|
|
135827
136013
|
}
|
|
135828
136014
|
handleOutput(sessionId, data) {
|
|
135829
136015
|
const session = this.sessions.get(sessionId);
|
|
@@ -135867,6 +136053,87 @@ var PTYManager = class {
|
|
|
135867
136053
|
this.markReady(sessionId, session, "fallback:timeout");
|
|
135868
136054
|
}
|
|
135869
136055
|
this.onOutput?.(sessionId, data);
|
|
136056
|
+
this.detectLivePrompts(sessionId, data, stripped).catch((err) => {
|
|
136057
|
+
this.log.warn("[pty.prompt_detect] failed", {
|
|
136058
|
+
event: "pty.prompt_detect_failed",
|
|
136059
|
+
sessionId,
|
|
136060
|
+
err
|
|
136061
|
+
});
|
|
136062
|
+
});
|
|
136063
|
+
}
|
|
136064
|
+
// Detect permission gates (OSC 777 + scraped options) and AskUserQuestion
|
|
136065
|
+
// menus from the rendered screen, firing the additive callbacks. Async because
|
|
136066
|
+
// reading the rendered buffer needs the xterm write queue flushed. Pure
|
|
136067
|
+
// detection lives in services/questions/*; this only orchestrates triggers,
|
|
136068
|
+
// per-session debounce, and the callbacks.
|
|
136069
|
+
async detectLivePrompts(sessionId, rawData, stripped) {
|
|
136070
|
+
const session = this.sessions.get(sessionId);
|
|
136071
|
+
if (!session) return;
|
|
136072
|
+
const oscPermission = hasPermissionOsc(rawData);
|
|
136073
|
+
const hasAskFooter = /Enter to select/i.test(stripped);
|
|
136074
|
+
const hasPromptMarker = CLAUDE_PROMPT_MARKERS.some((m2) => stripped.includes(m2));
|
|
136075
|
+
const hasShellPromptHint = /[[(]\s*y\s*\/\s*n\s*[\])]|press\s+(enter|return|any key)|\bcontinue\b\s*\?|^\s*(?:❯|>)?\s*\d+[.)]\s+\S/im.test(
|
|
136076
|
+
stripped
|
|
136077
|
+
);
|
|
136078
|
+
if (!oscPermission && !hasAskFooter && !hasShellPromptHint && !this.permissionOpen.has(sessionId) && !this.shellPromptOpen.has(sessionId) && !this.lastScreenQuestionKey.has(sessionId)) {
|
|
136079
|
+
return;
|
|
136080
|
+
}
|
|
136081
|
+
const lines = await this.getOutputLines(sessionId, 60);
|
|
136082
|
+
const askFooterOnScreen = lines.some((l) => /Enter to select/i.test(l));
|
|
136083
|
+
if (oscPermission || hasAskFooter || askFooterOnScreen) {
|
|
136084
|
+
this.log.debug?.(`[pty.prompt_detect] ${sessionId.slice(0, 8)} trigger`, {
|
|
136085
|
+
event: "pty.prompt_detect",
|
|
136086
|
+
sessionId,
|
|
136087
|
+
oscPermission,
|
|
136088
|
+
hasAskFooter,
|
|
136089
|
+
askFooterOnScreen,
|
|
136090
|
+
permGate: oscPermission && !askFooterOnScreen ? scrapePermissionGate(lines) : void 0,
|
|
136091
|
+
askQuestion: askFooterOnScreen ? detectQuestionFromScreen(lines) : void 0,
|
|
136092
|
+
renderedTail: lines.slice(-25)
|
|
136093
|
+
});
|
|
136094
|
+
}
|
|
136095
|
+
if (oscPermission && !askFooterOnScreen) {
|
|
136096
|
+
const gate = scrapePermissionGate(lines);
|
|
136097
|
+
this.permissionOpen.add(sessionId);
|
|
136098
|
+
this.onPermissionChange?.(sessionId, gate ?? { options: [] });
|
|
136099
|
+
} else if (this.permissionOpen.has(sessionId) && !askFooterOnScreen) {
|
|
136100
|
+
const gate = scrapePermissionGate(lines);
|
|
136101
|
+
if (gate) {
|
|
136102
|
+
this.onPermissionChange?.(sessionId, gate);
|
|
136103
|
+
} else if (hasPromptMarker) {
|
|
136104
|
+
this.permissionOpen.delete(sessionId);
|
|
136105
|
+
this.onPermissionChange?.(sessionId, null);
|
|
136106
|
+
}
|
|
136107
|
+
}
|
|
136108
|
+
if (askFooterOnScreen) {
|
|
136109
|
+
const detected = detectQuestionFromScreen(lines);
|
|
136110
|
+
if (detected) {
|
|
136111
|
+
const key = questionContentKey(detected.questions);
|
|
136112
|
+
if (this.lastScreenQuestionKey.get(sessionId) !== key) {
|
|
136113
|
+
this.lastScreenQuestionKey.set(sessionId, key);
|
|
136114
|
+
this.onLiveQuestion?.(sessionId, detected.questions);
|
|
136115
|
+
}
|
|
136116
|
+
}
|
|
136117
|
+
} else if (this.lastScreenQuestionKey.has(sessionId) && hasPromptMarker) {
|
|
136118
|
+
this.lastScreenQuestionKey.delete(sessionId);
|
|
136119
|
+
this.onLiveQuestionGone?.(sessionId);
|
|
136120
|
+
}
|
|
136121
|
+
if (!oscPermission && !askFooterOnScreen && !this.permissionOpen.has(sessionId)) {
|
|
136122
|
+
const shell = detectShellPrompt(lines);
|
|
136123
|
+
if (shell) {
|
|
136124
|
+
const key = `${shell.prompt}\0${shell.options.map((o) => o.label).join("\0")}`;
|
|
136125
|
+
if (this.shellPromptOpen.get(sessionId) !== key) {
|
|
136126
|
+
this.shellPromptOpen.set(sessionId, key);
|
|
136127
|
+
this.onPermissionChange?.(sessionId, {
|
|
136128
|
+
prompt: shell.prompt,
|
|
136129
|
+
options: shell.options
|
|
136130
|
+
});
|
|
136131
|
+
}
|
|
136132
|
+
} else if (this.shellPromptOpen.has(sessionId) && hasPromptMarker) {
|
|
136133
|
+
this.shellPromptOpen.delete(sessionId);
|
|
136134
|
+
this.onPermissionChange?.(sessionId, null);
|
|
136135
|
+
}
|
|
136136
|
+
}
|
|
135870
136137
|
}
|
|
135871
136138
|
// Transition a session from "running" to "waiting_input", clear pendingReady,
|
|
135872
136139
|
// and flush any queued input. Idempotent: callers can invoke at any chunk.
|
|
@@ -135905,6 +136172,9 @@ var PTYManager = class {
|
|
|
135905
136172
|
this.sessions.delete(sessionId);
|
|
135906
136173
|
this.queuedInputs.delete(sessionId);
|
|
135907
136174
|
this.firstChunkAt.delete(sessionId);
|
|
136175
|
+
this.permissionOpen.delete(sessionId);
|
|
136176
|
+
this.lastScreenQuestionKey.delete(sessionId);
|
|
136177
|
+
this.shellPromptOpen.delete(sessionId);
|
|
135908
136178
|
}
|
|
135909
136179
|
};
|
|
135910
136180
|
function toPublicSession(s3) {
|
|
@@ -137830,6 +138100,129 @@ function pruneAgentConversations(cache) {
|
|
|
137830
138100
|
return { scanned: rows.length, pruned, missing };
|
|
137831
138101
|
}
|
|
137832
138102
|
|
|
138103
|
+
// src/services/questions/detectAskUserQuestion.ts
|
|
138104
|
+
function normalizeContent2(raw2) {
|
|
138105
|
+
if (Array.isArray(raw2)) return raw2;
|
|
138106
|
+
if (typeof raw2 === "string") return [{ type: "text", text: raw2 }];
|
|
138107
|
+
return [];
|
|
138108
|
+
}
|
|
138109
|
+
function coerceOptions(raw2) {
|
|
138110
|
+
if (!Array.isArray(raw2)) return null;
|
|
138111
|
+
const out = [];
|
|
138112
|
+
for (const o of raw2) {
|
|
138113
|
+
if (o && typeof o === "object" && typeof o.label === "string") {
|
|
138114
|
+
const opt = o;
|
|
138115
|
+
out.push({
|
|
138116
|
+
label: opt.label,
|
|
138117
|
+
description: typeof opt.description === "string" ? opt.description : "",
|
|
138118
|
+
...typeof opt.preview === "string" ? { preview: opt.preview } : {}
|
|
138119
|
+
});
|
|
138120
|
+
}
|
|
138121
|
+
}
|
|
138122
|
+
return out.length > 0 ? out : null;
|
|
138123
|
+
}
|
|
138124
|
+
function coerceQuestions(raw2) {
|
|
138125
|
+
if (!Array.isArray(raw2)) return null;
|
|
138126
|
+
const out = [];
|
|
138127
|
+
for (const q2 of raw2) {
|
|
138128
|
+
if (!q2 || typeof q2 !== "object") continue;
|
|
138129
|
+
const qq = q2;
|
|
138130
|
+
const options = coerceOptions(qq.options);
|
|
138131
|
+
if (typeof qq.question !== "string" || !options) continue;
|
|
138132
|
+
out.push({
|
|
138133
|
+
question: qq.question,
|
|
138134
|
+
header: typeof qq.header === "string" ? qq.header : "",
|
|
138135
|
+
multiSelect: qq.multiSelect === true,
|
|
138136
|
+
options
|
|
138137
|
+
});
|
|
138138
|
+
}
|
|
138139
|
+
return out.length > 0 ? out : null;
|
|
138140
|
+
}
|
|
138141
|
+
function detectAskUserQuestion(rawLine) {
|
|
138142
|
+
let parsed;
|
|
138143
|
+
try {
|
|
138144
|
+
parsed = JSON.parse(rawLine);
|
|
138145
|
+
} catch {
|
|
138146
|
+
return null;
|
|
138147
|
+
}
|
|
138148
|
+
const blocks = normalizeContent2(parsed.message?.content ?? parsed.content);
|
|
138149
|
+
for (const b2 of blocks) {
|
|
138150
|
+
if (b2.type === "tool_use" && b2.name === "AskUserQuestion" && typeof b2.id === "string") {
|
|
138151
|
+
const input = b2.input;
|
|
138152
|
+
const questions = coerceQuestions(input?.questions);
|
|
138153
|
+
if (questions) return { toolUseId: b2.id, questions };
|
|
138154
|
+
}
|
|
138155
|
+
}
|
|
138156
|
+
return null;
|
|
138157
|
+
}
|
|
138158
|
+
|
|
138159
|
+
// src/services/questions/questionBroadcast.ts
|
|
138160
|
+
function questionsFromLines(sessionId, lines) {
|
|
138161
|
+
const messages = [];
|
|
138162
|
+
const pending = [];
|
|
138163
|
+
for (const line of lines) {
|
|
138164
|
+
const detected = detectAskUserQuestion(line);
|
|
138165
|
+
if (detected) {
|
|
138166
|
+
messages.push({
|
|
138167
|
+
type: "question",
|
|
138168
|
+
sessionId,
|
|
138169
|
+
toolUseId: detected.toolUseId,
|
|
138170
|
+
questions: detected.questions
|
|
138171
|
+
});
|
|
138172
|
+
pending.push(detected);
|
|
138173
|
+
}
|
|
138174
|
+
}
|
|
138175
|
+
return { messages, pending };
|
|
138176
|
+
}
|
|
138177
|
+
function shouldBroadcastQuestion(args) {
|
|
138178
|
+
const alreadyShown = args.lastContentKey === args.newContentKey;
|
|
138179
|
+
if (!alreadyShown) return true;
|
|
138180
|
+
return args.priorToolUseId !== args.newToolUseId;
|
|
138181
|
+
}
|
|
138182
|
+
|
|
138183
|
+
// src/services/questions/answersToKeystrokes.ts
|
|
138184
|
+
var DOWN = "\x1B[B";
|
|
138185
|
+
var ENTER2 = "\r";
|
|
138186
|
+
var UnknownOptionError = class extends Error {
|
|
138187
|
+
constructor(question, value) {
|
|
138188
|
+
super(`No option labelled "${value}" for question "${question}"`);
|
|
138189
|
+
this.question = question;
|
|
138190
|
+
this.value = value;
|
|
138191
|
+
this.name = "UnknownOptionError";
|
|
138192
|
+
}
|
|
138193
|
+
question;
|
|
138194
|
+
value;
|
|
138195
|
+
};
|
|
138196
|
+
function answersToKeystrokes(questions, answers) {
|
|
138197
|
+
let out = "";
|
|
138198
|
+
for (const q2 of questions) {
|
|
138199
|
+
const raw2 = answers[q2.question];
|
|
138200
|
+
if (raw2 === void 0) {
|
|
138201
|
+
throw new Error(`Missing answer for question "${q2.question}"`);
|
|
138202
|
+
}
|
|
138203
|
+
const label = Array.isArray(raw2) ? raw2[0] : raw2;
|
|
138204
|
+
const target = q2.options.findIndex((o) => o.label === label);
|
|
138205
|
+
if (target < 0) throw new UnknownOptionError(q2.question, label);
|
|
138206
|
+
out += DOWN.repeat(target) + ENTER2;
|
|
138207
|
+
}
|
|
138208
|
+
return out;
|
|
138209
|
+
}
|
|
138210
|
+
|
|
138211
|
+
// src/services/questions/resolveAnswer.ts
|
|
138212
|
+
function resolveAnswer(pending, body) {
|
|
138213
|
+
if (!pending) return { ok: false, reason: "no_pending_question" };
|
|
138214
|
+
if (typeof body.toolUseId !== "string" || body.toolUseId !== pending.toolUseId) {
|
|
138215
|
+
return { ok: false, reason: "tool_use_mismatch" };
|
|
138216
|
+
}
|
|
138217
|
+
const answers = body.answers ?? {};
|
|
138218
|
+
try {
|
|
138219
|
+
return { ok: true, keys: answersToKeystrokes(pending.questions, answers) };
|
|
138220
|
+
} catch (e) {
|
|
138221
|
+
if (e instanceof UnknownOptionError) return { ok: false, reason: "unknown_option" };
|
|
138222
|
+
throw e;
|
|
138223
|
+
}
|
|
138224
|
+
}
|
|
138225
|
+
|
|
137833
138226
|
// src/agent/dedupe.ts
|
|
137834
138227
|
function createProgressDedupeLRU(capacity) {
|
|
137835
138228
|
if (!Number.isFinite(capacity) || capacity < 1) {
|
|
@@ -138286,6 +138679,16 @@ var StreamerServer = class {
|
|
|
138286
138679
|
fileWatcher;
|
|
138287
138680
|
sessionFileMap = /* @__PURE__ */ new Map();
|
|
138288
138681
|
// sessionId → JSONL filePath
|
|
138682
|
+
pendingQuestions = /* @__PURE__ */ new Map();
|
|
138683
|
+
// Content key of the AskUserQuestion currently broadcast for a session (from
|
|
138684
|
+
// either the rendered screen or JSONL), used to de-dupe the two paths: when
|
|
138685
|
+
// the screen detection fires first, the later JSONL flush of the same question
|
|
138686
|
+
// is suppressed. Cleared alongside pendingQuestions.
|
|
138687
|
+
pendingQuestionKey = /* @__PURE__ */ new Map();
|
|
138688
|
+
// Per-session permission gate currently open (scraped via OSC 777). Parallel
|
|
138689
|
+
// to pendingQuestions; mobile answers it by sending the option index via
|
|
138690
|
+
// /input { keys }. Cleared when the gate closes.
|
|
138691
|
+
pendingPermission = /* @__PURE__ */ new Map();
|
|
138289
138692
|
scanner = null;
|
|
138290
138693
|
scannerReady = null;
|
|
138291
138694
|
// Set by onConversationChanged while a scan is in-flight; getScanner() does
|
|
@@ -138381,6 +138784,28 @@ var StreamerServer = class {
|
|
|
138381
138784
|
this.cache?.updateFromLines(filePath, lines);
|
|
138382
138785
|
for (const [sessionId, watchedPath] of this.sessionFileMap) {
|
|
138383
138786
|
if (watchedPath === filePath) {
|
|
138787
|
+
const priorToolUseId = this.pendingQuestions.get(sessionId)?.toolUseId;
|
|
138788
|
+
const { messages, pending } = questionsFromLines(sessionId, lines);
|
|
138789
|
+
for (const p2 of pending) {
|
|
138790
|
+
this.pendingQuestions.set(sessionId, p2);
|
|
138791
|
+
const t2 = setTimeout(() => {
|
|
138792
|
+
if (this.pendingQuestions.get(sessionId)?.toolUseId === p2.toolUseId) {
|
|
138793
|
+
this.cancelPendingQuestion(sessionId);
|
|
138794
|
+
}
|
|
138795
|
+
}, 6e4);
|
|
138796
|
+
t2.unref();
|
|
138797
|
+
}
|
|
138798
|
+
for (const m2 of messages) {
|
|
138799
|
+
const key = questionContentKey(m2.questions);
|
|
138800
|
+
const broadcast = shouldBroadcastQuestion({
|
|
138801
|
+
newContentKey: key,
|
|
138802
|
+
lastContentKey: this.pendingQuestionKey.get(sessionId),
|
|
138803
|
+
newToolUseId: m2.toolUseId,
|
|
138804
|
+
priorToolUseId
|
|
138805
|
+
});
|
|
138806
|
+
this.pendingQuestionKey.set(sessionId, key);
|
|
138807
|
+
if (broadcast) this.wsHub.broadcast(m2);
|
|
138808
|
+
}
|
|
138384
138809
|
this.wsHub.broadcast({ type: "conversation_events", sessionId, lines });
|
|
138385
138810
|
for (const line of lines) {
|
|
138386
138811
|
this.wsHub.broadcast({ type: "conversation_event", sessionId, line });
|
|
@@ -138412,6 +138837,19 @@ var StreamerServer = class {
|
|
|
138412
138837
|
onOutput: (sessionId, data) => {
|
|
138413
138838
|
this.wsHub.broadcast({ type: "terminal_output", sessionId, data });
|
|
138414
138839
|
},
|
|
138840
|
+
onPermissionChange: (sessionId, gate) => {
|
|
138841
|
+
this.handlePermissionChange(sessionId, gate);
|
|
138842
|
+
},
|
|
138843
|
+
onLiveQuestion: (sessionId, questions) => {
|
|
138844
|
+
this.handleLiveQuestion(sessionId, questions);
|
|
138845
|
+
},
|
|
138846
|
+
onLiveQuestionGone: (sessionId) => {
|
|
138847
|
+
this.pendingQuestionKey.delete(sessionId);
|
|
138848
|
+
const pq = this.pendingQuestions.get(sessionId);
|
|
138849
|
+
if (pq?.toolUseId.startsWith("screen:")) {
|
|
138850
|
+
this.cancelPendingQuestion(sessionId);
|
|
138851
|
+
}
|
|
138852
|
+
},
|
|
138415
138853
|
onReady: (session) => {
|
|
138416
138854
|
const resp = this.sessionStore.get(session.id, this.ptyAttachedIds());
|
|
138417
138855
|
if (resp) this.wsHub.broadcast({ type: "session_ready", session: resp });
|
|
@@ -138449,7 +138887,9 @@ var StreamerServer = class {
|
|
|
138449
138887
|
if (filePath) {
|
|
138450
138888
|
this.fileWatcher.unwatch(filePath);
|
|
138451
138889
|
this.sessionFileMap.delete(session.id);
|
|
138890
|
+
this.cancelPendingQuestion(session.id);
|
|
138452
138891
|
}
|
|
138892
|
+
this.pendingPermission.delete(session.id);
|
|
138453
138893
|
}
|
|
138454
138894
|
const resp = this.sessionStore.get(session.id, this.ptyAttachedIds());
|
|
138455
138895
|
if (resp) {
|
|
@@ -138500,6 +138940,7 @@ var StreamerServer = class {
|
|
|
138500
138940
|
handleGetSession: (id, res) => this.handleGetSession(id, res),
|
|
138501
138941
|
handleGetOutput: (id, res) => this.handleGetOutput(id, res),
|
|
138502
138942
|
handleSendInput: (id, req, res) => this.handleSendInput(id, req, res),
|
|
138943
|
+
handleSendAnswer: (id, req, res) => this.handleSendAnswer(id, req, res),
|
|
138503
138944
|
handleCancel: (id, res) => this.handleCancel(id, res),
|
|
138504
138945
|
handleStopSession: (id, res) => this.handleStopSession(id, res),
|
|
138505
138946
|
handleSetSessionName: (id, req, res) => this.handleSetSessionName(id, req, res),
|
|
@@ -138635,16 +139076,28 @@ var StreamerServer = class {
|
|
|
138635
139076
|
if (existing) clearTimeout(existing);
|
|
138636
139077
|
const timer = setTimeout(() => {
|
|
138637
139078
|
this.ptyGraceTimers.delete(sessionId);
|
|
138638
|
-
this.sessionSubscribers.delete(sessionId);
|
|
138639
139079
|
if (this.ptyManager.hasSession(sessionId)) {
|
|
139080
|
+
const resp = this.sessionStore.get(sessionId, this.ptyAttachedIds());
|
|
139081
|
+
if (resp?.status === "running") {
|
|
139082
|
+
this.log.info(
|
|
139083
|
+
`[grace] session ${sessionId} still running, deferring hold`,
|
|
139084
|
+
{ sessionId, event: "pty.grace_defer" },
|
|
139085
|
+
"pino"
|
|
139086
|
+
);
|
|
139087
|
+
this.startGraceTimer(sessionId, delayMs);
|
|
139088
|
+
return;
|
|
139089
|
+
}
|
|
139090
|
+
this.sessionSubscribers.delete(sessionId);
|
|
138640
139091
|
this.log.info(
|
|
138641
139092
|
`[grace] killing idle PTY for ${sessionId}`,
|
|
138642
139093
|
{ sessionId, event: "pty.grace_kill" },
|
|
138643
139094
|
"pino"
|
|
138644
139095
|
);
|
|
138645
139096
|
this.ptyManager.putOnHold(sessionId);
|
|
138646
|
-
const
|
|
138647
|
-
if (
|
|
139097
|
+
const held = this.sessionStore.get(sessionId, this.ptyAttachedIds());
|
|
139098
|
+
if (held) this.wsHub.broadcast({ type: "session_update", session: held });
|
|
139099
|
+
} else {
|
|
139100
|
+
this.sessionSubscribers.delete(sessionId);
|
|
138648
139101
|
}
|
|
138649
139102
|
}, delayMs);
|
|
138650
139103
|
this.ptyGraceTimers.set(sessionId, timer);
|
|
@@ -139010,22 +139463,36 @@ var StreamerServer = class {
|
|
|
139010
139463
|
async handleConversationsCount(url2, res) {
|
|
139011
139464
|
const project = url2.searchParams.get("project") ?? void 0;
|
|
139012
139465
|
const bustCache = url2.searchParams.get("refresh") === "1";
|
|
139013
|
-
if (
|
|
139014
|
-
this.cache?.invalidate();
|
|
139015
|
-
this.scanner = null;
|
|
139016
|
-
this.scannerReady = null;
|
|
139017
|
-
}
|
|
139018
|
-
if (this.cache && !bustCache) {
|
|
139466
|
+
if (this.cache) {
|
|
139019
139467
|
const { total } = this.cache.listConversations({ project, limit: 0, offset: 0 });
|
|
139020
139468
|
json2(res, 200, { total });
|
|
139469
|
+
if (bustCache) this.refreshCountInBackground();
|
|
139021
139470
|
return;
|
|
139022
139471
|
}
|
|
139023
|
-
const scanner = await this.getScanner();
|
|
139472
|
+
const scanner = await this.getScanner(true);
|
|
139024
139473
|
let metas = [...scanner.getMetadataCache().values()];
|
|
139025
139474
|
metas = applyIncludeFilter(metas, "conversations");
|
|
139026
139475
|
if (project) metas = applyProjectFilter(metas, project);
|
|
139027
139476
|
json2(res, 200, { total: metas.length });
|
|
139028
139477
|
}
|
|
139478
|
+
// Fire-and-forget full rescan that reconciles the SQLite cache from disk so a
|
|
139479
|
+
// later count reflects new/removed conversations. Never awaited by the request
|
|
139480
|
+
// path — refresh=1 returns the cached total synchronously and this catches up.
|
|
139481
|
+
refreshCountInBackground() {
|
|
139482
|
+
void (async () => {
|
|
139483
|
+
try {
|
|
139484
|
+
const scanner = await this.getFreshScanner();
|
|
139485
|
+
if (this.cache) {
|
|
139486
|
+
this.cache.upsertFromScannerMeta([...scanner.getMetadataCache().values()]);
|
|
139487
|
+
}
|
|
139488
|
+
} catch (err) {
|
|
139489
|
+
this.log.warn(
|
|
139490
|
+
`Background count refresh failed: ${err instanceof Error ? err.message : String(err)}`,
|
|
139491
|
+
{ event: "count.refresh_failed" }
|
|
139492
|
+
);
|
|
139493
|
+
}
|
|
139494
|
+
})();
|
|
139495
|
+
}
|
|
139029
139496
|
handleSessionsCount(res) {
|
|
139030
139497
|
json2(res, 200, { total: this.sessionStore.list(this.ptyAttachedIds()).length });
|
|
139031
139498
|
}
|
|
@@ -139609,6 +140076,65 @@ var StreamerServer = class {
|
|
|
139609
140076
|
json2(res, 400, { error: message });
|
|
139610
140077
|
}
|
|
139611
140078
|
}
|
|
140079
|
+
cancelPendingQuestion(sessionId) {
|
|
140080
|
+
const pq = this.pendingQuestions.get(sessionId);
|
|
140081
|
+
if (!pq) return;
|
|
140082
|
+
this.pendingQuestions.delete(sessionId);
|
|
140083
|
+
this.pendingQuestionKey.delete(sessionId);
|
|
140084
|
+
this.wsHub.broadcast({ type: "question_cancelled", sessionId, toolUseId: pq.toolUseId });
|
|
140085
|
+
}
|
|
140086
|
+
// Live AskUserQuestion detected from the rendered screen (ahead of JSONL).
|
|
140087
|
+
// Broadcasts the `question` event immediately and records the content key so
|
|
140088
|
+
// the later JSONL flush of the same question is de-duped. We synthesize a
|
|
140089
|
+
// screen-scoped toolUseId; the JSONL path overwrites pendingQuestions with the
|
|
140090
|
+
// real toolUseId when it lands, so answering works once JSONL catches up.
|
|
140091
|
+
handleLiveQuestion(sessionId, questions) {
|
|
140092
|
+
const key = questionContentKey(questions);
|
|
140093
|
+
if (this.pendingQuestionKey.get(sessionId) === key) return;
|
|
140094
|
+
const toolUseId = `screen:${sessionId}:${key.length}`;
|
|
140095
|
+
this.pendingQuestions.set(sessionId, { toolUseId, questions });
|
|
140096
|
+
this.pendingQuestionKey.set(sessionId, key);
|
|
140097
|
+
this.wsHub.broadcast({ type: "question", sessionId, toolUseId, questions });
|
|
140098
|
+
}
|
|
140099
|
+
// Permission gate opened/closed (OSC 777 + scraped options). Broadcasts the
|
|
140100
|
+
// additive `permission` / `permission_cancelled` events. Mobile answers by
|
|
140101
|
+
// sending the chosen option index via /input { keys } (e.g. "2\r").
|
|
140102
|
+
handlePermissionChange(sessionId, gate) {
|
|
140103
|
+
if (gate === null) {
|
|
140104
|
+
if (!this.pendingPermission.has(sessionId)) return;
|
|
140105
|
+
this.pendingPermission.delete(sessionId);
|
|
140106
|
+
this.wsHub.broadcast({ type: "permission_cancelled", sessionId });
|
|
140107
|
+
return;
|
|
140108
|
+
}
|
|
140109
|
+
this.pendingPermission.set(sessionId, gate);
|
|
140110
|
+
this.wsHub.broadcast({
|
|
140111
|
+
type: "permission",
|
|
140112
|
+
sessionId,
|
|
140113
|
+
...gate.prompt ? { prompt: gate.prompt } : {},
|
|
140114
|
+
options: gate.options,
|
|
140115
|
+
...gate.cursor !== void 0 ? { cursor: gate.cursor } : {}
|
|
140116
|
+
});
|
|
140117
|
+
}
|
|
140118
|
+
async handleSendAnswer(sessionId, req, res) {
|
|
140119
|
+
const body = await readBody(req);
|
|
140120
|
+
const pending = this.pendingQuestions.get(sessionId);
|
|
140121
|
+
const resolution = resolveAnswer(pending, body);
|
|
140122
|
+
if (!resolution.ok) {
|
|
140123
|
+
json2(res, 400, { ok: false, reason: resolution.reason });
|
|
140124
|
+
return;
|
|
140125
|
+
}
|
|
140126
|
+
const toolUseId = pending?.toolUseId ?? "";
|
|
140127
|
+
try {
|
|
140128
|
+
this.ptyManager.sendKeys(sessionId, resolution.keys);
|
|
140129
|
+
} catch (err) {
|
|
140130
|
+
const message = err instanceof Error ? err.message : "Failed to send answer";
|
|
140131
|
+
json2(res, 400, { ok: false, reason: message });
|
|
140132
|
+
return;
|
|
140133
|
+
}
|
|
140134
|
+
this.pendingQuestions.delete(sessionId);
|
|
140135
|
+
this.wsHub.broadcast({ type: "question_cancelled", sessionId, toolUseId });
|
|
140136
|
+
json2(res, 200, { ok: true });
|
|
140137
|
+
}
|
|
139612
140138
|
async handleUploadFile(sessionId, req, res) {
|
|
139613
140139
|
const session = this.sessionStore.get(sessionId, this.ptyAttachedIds());
|
|
139614
140140
|
if (!session) {
|