@threadbase-sh/streamer 1.29.2 → 1.31.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/index.cjs CHANGED
@@ -215,7 +215,7 @@ function verifySignature(rawBody, signature, secret) {
215
215
  }
216
216
  }
217
217
  function isWithinSkew(timestampHeader, skewSeconds) {
218
- if (!timestampHeader) return false;
218
+ if (!timestampHeader) return true;
219
219
  const t = Number(timestampHeader);
220
220
  if (!Number.isFinite(t)) return false;
221
221
  const now = Math.floor(Date.now() / 1e3);
@@ -552,13 +552,13 @@ async function createPool(config) {
552
552
  }
553
553
 
554
554
  // src/live-session-manager.ts
555
- var import_path6 = require("path");
555
+ var import_path7 = require("path");
556
556
 
557
557
  // src/codex-pty-runner.ts
558
558
  var import_headless = require("@xterm/headless");
559
559
  var import_crypto2 = require("crypto");
560
- var import_fs4 = require("fs");
561
- var import_path4 = require("path");
560
+ var import_fs5 = require("fs");
561
+ var import_path5 = require("path");
562
562
 
563
563
  // src/logger.ts
564
564
  var import_pino = __toESM(require("pino"), 1);
@@ -732,13 +732,80 @@ function isProviderResumable(_provider, availabilityResumable) {
732
732
  return availabilityResumable;
733
733
  }
734
734
 
735
+ // src/services/questions/codexGateAnswers.ts
736
+ var import_fs4 = require("fs");
737
+ var import_os3 = require("os");
738
+ var import_path4 = require("path");
739
+ function gateAnswersPath() {
740
+ const dir = process.env.THREADBASE_CONFIG_DIR ?? (0, import_path4.join)((0, import_os3.homedir)(), ".threadbase");
741
+ return (0, import_path4.join)(dir, "gate-answers.json");
742
+ }
743
+ function loadGateAnswers() {
744
+ try {
745
+ const parsed = JSON.parse((0, import_fs4.readFileSync)(gateAnswersPath(), "utf-8"));
746
+ return parsed && typeof parsed === "object" ? parsed : {};
747
+ } catch {
748
+ return {};
749
+ }
750
+ }
751
+ function saveGateAnswer(key, value) {
752
+ const path = gateAnswersPath();
753
+ (0, import_fs4.mkdirSync)((0, import_path4.dirname)(path), { recursive: true });
754
+ (0, import_fs4.writeFileSync)(path, `${JSON.stringify({ ...loadGateAnswers(), [key]: value }, null, 2)}
755
+ `);
756
+ }
757
+ function rememberedGateDigit(gate) {
758
+ const answers = loadGateAnswers();
759
+ if (gate === "hooks") {
760
+ if (answers.codexHooksGate === "trust_all") return "2";
761
+ if (answers.codexHooksGate === "continue_untrusted") return "3";
762
+ return null;
763
+ }
764
+ return answers.codexTrustGate === "yes" ? "1" : null;
765
+ }
766
+
767
+ // src/utils/debounce.ts
768
+ function debounce(fn, waitMs) {
769
+ let timer = null;
770
+ let lastArgs = null;
771
+ const run2 = () => {
772
+ timer = null;
773
+ if (lastArgs) {
774
+ const args = lastArgs;
775
+ lastArgs = null;
776
+ fn(...args);
777
+ }
778
+ };
779
+ const debounced = (...args) => {
780
+ lastArgs = args;
781
+ if (timer) clearTimeout(timer);
782
+ timer = setTimeout(run2, waitMs);
783
+ };
784
+ debounced.cancel = () => {
785
+ if (timer) clearTimeout(timer);
786
+ timer = null;
787
+ lastArgs = null;
788
+ };
789
+ debounced.flush = () => {
790
+ if (timer) {
791
+ clearTimeout(timer);
792
+ run2();
793
+ }
794
+ };
795
+ return debounced;
796
+ }
797
+
735
798
  // src/codex-pty-runner.ts
736
799
  var OUTPUT_BUFFER_MAX = 65536;
800
+ var INPUT_HISTORY_MAX = 50;
737
801
  var PTY_COLS = 120;
738
802
  var PTY_ROWS = 40;
739
803
  var SCREEN_SCROLLBACK = 1e3;
740
804
  var CODEX_PROMPT_READY_TEXT = "Ready";
741
805
  var CODEX_TRUST_GATE_REGEX = /trust the contents/i;
806
+ var CODEX_HOOKS_GATE_REGEX = /hooks need review/i;
807
+ var QUIET_DETECT_MS = 500;
808
+ var CODEX_READY_FALLBACK_MS = 8e3;
742
809
  var SUBMIT_BYTES = "\r";
743
810
  var CODEX_SUBMIT_DELAY_MS = 16;
744
811
  function digestBytes(s) {
@@ -746,6 +813,40 @@ function digestBytes(s) {
746
813
  if (escaped.length <= 200) return escaped;
747
814
  return `${escaped.slice(0, 100)}\u2026[${escaped.length - 200}B omitted]\u2026${escaped.slice(-100)}`;
748
815
  }
816
+ function gateCard(gate, lines) {
817
+ if (gate === "hooks") {
818
+ const countLine = lines.find((l) => /new or changed/i.test(l))?.trim();
819
+ return {
820
+ prompt: [
821
+ "Hooks need review",
822
+ countLine,
823
+ "Hooks can run outside the sandbox after you trust them."
824
+ ].filter(Boolean).join(" \u2014 "),
825
+ options: [
826
+ { index: 2, label: "Trust all and continue", answerKeys: "2\r" },
827
+ { index: 3, label: "Continue without trusting (hooks won't run)", answerKeys: "3\r" },
828
+ {
829
+ index: 4,
830
+ label: "Trust all and continue (remember for all projects)",
831
+ answerKeys: "4\r"
832
+ },
833
+ {
834
+ index: 5,
835
+ label: "Continue without trusting (remember for all projects)",
836
+ answerKeys: "5\r"
837
+ }
838
+ ]
839
+ };
840
+ }
841
+ return {
842
+ prompt: lines.find((l) => CODEX_TRUST_GATE_REGEX.test(l))?.trim() ?? "Do you trust the contents of this directory?",
843
+ options: [
844
+ { index: 1, label: "Yes, continue", answerKeys: "1\r" },
845
+ { index: 2, label: "No, quit", answerKeys: "2\r" },
846
+ { index: 3, label: "Yes, continue (remember for all projects)", answerKeys: "3\r" }
847
+ ]
848
+ };
849
+ }
749
850
  var pty = null;
750
851
  async function loadPty() {
751
852
  if (pty) return pty;
@@ -772,11 +873,12 @@ var CodexPtyRunner = class {
772
873
  onOutput;
773
874
  onStatusChange;
774
875
  onReady;
775
- // Accepted for shape-compatibility with PTYManagerOptions; Codex has no
776
- // detected equivalent yet (Phase 0) never invoked.
876
+ // Broadcasts Codex's blocking startup gates (directory trust, hooks review)
877
+ // as question cards; null dismisses the card once the gate leaves the screen.
777
878
  onPermissionChange;
778
879
  onLiveQuestion;
779
880
  onLiveQuestionGone;
881
+ onUserMessage;
780
882
  log;
781
883
  // Tracks sessions whose PTY has spawned but Codex hasn't yet reached its
782
884
  // "Ready" status bar — i.e. onReady hasn't fired.
@@ -784,8 +886,18 @@ var CodexPtyRunner = class {
784
886
  // Inputs received via sendInput() while the session was still pendingReady.
785
887
  // Flushed in arrival order once Codex reaches Ready.
786
888
  queuedInputs = /* @__PURE__ */ new Map();
787
- // Per-session debounce so the directory-trust gate's \r is only written once.
788
- trustGateAnswered = /* @__PURE__ */ new Set();
889
+ // Gate currently on a session's screen (card broadcast, unanswered). While
890
+ // set, queued-input flushes are held — a flushed digit would CONFIRM a
891
+ // dialog option — and sendKeys() intercepts remember-variant digits.
892
+ openGate = /* @__PURE__ */ new Map();
893
+ // `${sessionId}:${gate}` once a gate has been actioned (auto-answered or
894
+ // card broadcast) — dedupes repaints of the same dialog.
895
+ gateActioned = /* @__PURE__ */ new Set();
896
+ // Per-session trailing debounce re-armed on every chunk; on quiet, re-runs
897
+ // screen detection so a blocked/truncated boot still reaches ready.
898
+ quietCheckers = /* @__PURE__ */ new Map();
899
+ // Per-session flat backstop from spawn (CODEX_READY_FALLBACK_MS).
900
+ readyFallbackTimers = /* @__PURE__ */ new Map();
789
901
  // In-flight start()/startFresh() calls keyed by sessionId. A second
790
902
  // concurrent resume for the same session (double-tap, client retry) awaits
791
903
  // the first call's promise instead of spawning a duplicate PTY (CRITICAL #3).
@@ -797,6 +909,7 @@ var CodexPtyRunner = class {
797
909
  this.onPermissionChange = options.onPermissionChange;
798
910
  this.onLiveQuestion = options.onLiveQuestion;
799
911
  this.onLiveQuestionGone = options.onLiveQuestionGone;
912
+ this.onUserMessage = options.onUserMessage;
800
913
  this.log = options.logger ?? getLogger("codex-pty");
801
914
  }
802
915
  // Resume an existing Codex session. sessionId is the Codex-persisted
@@ -815,7 +928,7 @@ var CodexPtyRunner = class {
815
928
  }
816
929
  async doStart(sessionId, options) {
817
930
  const nodePty = await loadPty();
818
- const projectName = options.projectName ?? (0, import_path4.basename)(options.projectPath);
931
+ const projectName = options.projectName ?? (0, import_path5.basename)(options.projectPath);
819
932
  const proc = nodePty.spawn(
820
933
  resolveCodexExe(),
821
934
  ["resume", sessionId, "--cd", options.projectPath, "--no-alt-screen"],
@@ -840,10 +953,12 @@ var CodexPtyRunner = class {
840
953
  lastOutput: "",
841
954
  process: proc,
842
955
  outputBuffer: Buffer.alloc(0),
843
- screen: createScreen()
956
+ screen: createScreen(),
957
+ inputHistory: []
844
958
  };
845
959
  this.sessions.set(sessionId, session);
846
960
  this.pendingReady.add(sessionId);
961
+ this.armReadyFallback(sessionId);
847
962
  proc.onData((data) => {
848
963
  this.handleOutput(sessionId, data);
849
964
  });
@@ -860,7 +975,7 @@ var CodexPtyRunner = class {
860
975
  async startFresh(options) {
861
976
  const nodePty = await loadPty();
862
977
  const sessionId = (0, import_crypto2.randomUUID)();
863
- const projectName = options.projectName ?? (0, import_path4.basename)(options.projectPath);
978
+ const projectName = options.projectName ?? (0, import_path5.basename)(options.projectPath);
864
979
  const args = ["--cd", options.projectPath, "--no-alt-screen"];
865
980
  if (options.systemPrompt) {
866
981
  args.push(options.systemPrompt);
@@ -885,10 +1000,12 @@ var CodexPtyRunner = class {
885
1000
  lastOutput: "",
886
1001
  process: proc,
887
1002
  outputBuffer: Buffer.alloc(0),
888
- screen: createScreen()
1003
+ screen: createScreen(),
1004
+ inputHistory: []
889
1005
  };
890
1006
  this.sessions.set(sessionId, session);
891
1007
  this.pendingReady.add(sessionId);
1008
+ this.armReadyFallback(sessionId);
892
1009
  proc.onData((data) => {
893
1010
  this.handleOutput(sessionId, data);
894
1011
  });
@@ -898,6 +1015,21 @@ var CodexPtyRunner = class {
898
1015
  });
899
1016
  return toPublicSession(session);
900
1017
  }
1018
+ // Flat backstop: if neither the "Ready" marker nor the quiet-checker settled
1019
+ // the session within CODEX_READY_FALLBACK_MS of spawn, mark it ready anyway
1020
+ // so start requests resolve and mobile can watch the boot live. unref() so a
1021
+ // pending timer never holds the process open.
1022
+ armReadyFallback(sessionId) {
1023
+ const timer = setTimeout(() => {
1024
+ this.readyFallbackTimers.delete(sessionId);
1025
+ const session = this.sessions.get(sessionId);
1026
+ if (session?.status === "running" && this.pendingReady.has(sessionId)) {
1027
+ this.markReady(sessionId, session, "fallback:timeout");
1028
+ }
1029
+ }, CODEX_READY_FALLBACK_MS);
1030
+ timer.unref?.();
1031
+ this.readyFallbackTimers.set(sessionId, timer);
1032
+ }
901
1033
  // Write raw key bytes directly to the PTY, same as PTYManager.sendKeys.
902
1034
  sendKeys(sessionId, keys) {
903
1035
  const session = this.sessions.get(sessionId);
@@ -909,13 +1041,47 @@ var CodexPtyRunner = class {
909
1041
  session.status = "running";
910
1042
  this.onStatusChange?.(toPublicSession(session));
911
1043
  }
1044
+ const gate = this.openGate.get(sessionId);
1045
+ const digit = gate ? /^([0-9])\r?$/.exec(keys)?.[1] : void 0;
1046
+ const out = gate && digit ? this.resolveGateAnswer(sessionId, gate, digit) : keys;
912
1047
  this.log.info(
913
- `[codex.keys.write] ${sessionId.slice(0, 8)} bytes=${keys.length} digest=${digestBytes(keys)}`,
914
- { event: "codex.keys_write", sessionId, byteLen: keys.length }
1048
+ `[codex.keys.write] ${sessionId.slice(0, 8)} bytes=${out.length} digest=${digestBytes(out)}`,
1049
+ { event: "codex.keys_write", sessionId, byteLen: out.length }
915
1050
  );
916
- session.process.write(keys);
1051
+ session.process.write(out);
917
1052
  session.lastActivityAt = /* @__PURE__ */ new Date();
918
1053
  }
1054
+ // Map a gate-card digit to the PTY bytes that answer the real dialog,
1055
+ // persisting the choice when the digit was a synthetic "remember for all
1056
+ // projects" option (those numbers don't exist on the actual dialog and must
1057
+ // never reach codex). The trailing \r mobile sends is dropped: a digit alone
1058
+ // selects AND confirms (live-probe verified), and a stray Enter would land
1059
+ // on whatever screen follows.
1060
+ resolveGateAnswer(sessionId, gate, digit) {
1061
+ let real = digit;
1062
+ let remembered = false;
1063
+ if (gate === "hooks" && digit === "4") {
1064
+ saveGateAnswer("codexHooksGate", "trust_all");
1065
+ real = "2";
1066
+ remembered = true;
1067
+ } else if (gate === "hooks" && digit === "5") {
1068
+ saveGateAnswer("codexHooksGate", "continue_untrusted");
1069
+ real = "3";
1070
+ remembered = true;
1071
+ } else if (gate === "trust" && digit === "3") {
1072
+ saveGateAnswer("codexTrustGate", "yes");
1073
+ real = "1";
1074
+ remembered = true;
1075
+ }
1076
+ this.log.info(`[codex.gate_answer] ${sessionId.slice(0, 8)} ${gate} digit=${real}`, {
1077
+ event: "codex.gate_answer",
1078
+ sessionId,
1079
+ gate,
1080
+ digit: real,
1081
+ remembered
1082
+ });
1083
+ return real;
1084
+ }
919
1085
  sendInput(sessionId, input) {
920
1086
  const session = this.sessions.get(sessionId);
921
1087
  if (!session) throw new Error(`Session not found: ${sessionId}`);
@@ -953,6 +1119,7 @@ var CodexPtyRunner = class {
953
1119
  // confirmed Codex accepts plain keystrokes), then submit \r after a short
954
1120
  // delay so Codex's TUI gets an event-loop tick to process the input first.
955
1121
  writeSubmit(sessionId, session, input, path, promptCount) {
1122
+ this.recordUserMessage(session, input);
956
1123
  this.log.info(
957
1124
  `[codex.input.write] ${sessionId.slice(0, 8)} promptCount=${promptCount} bytes=${input.length} digest=${digestBytes(input)}`,
958
1125
  {
@@ -985,8 +1152,12 @@ var CodexPtyRunner = class {
985
1152
  }, CODEX_SUBMIT_DELAY_MS);
986
1153
  }
987
1154
  // Drain any inputs sent while the session was still pendingReady, writing
988
- // them in arrival order now that Codex is Ready.
1155
+ // them in arrival order now that Codex is Ready. No-op while a gate dialog
1156
+ // is open (a flushed digit would confirm a dialog option) or while still
1157
+ // pendingReady (markReady drains it) — the gate-close path re-drives it for
1158
+ // the ready-with-gate-open case.
989
1159
  flushQueuedInputs(sessionId) {
1160
+ if (this.openGate.has(sessionId) || this.pendingReady.has(sessionId)) return;
990
1161
  const queue = this.queuedInputs.get(sessionId);
991
1162
  if (!queue || queue.length === 0) return;
992
1163
  this.queuedInputs.delete(sessionId);
@@ -1031,7 +1202,7 @@ var CodexPtyRunner = class {
1031
1202
  if (!session) return;
1032
1203
  this.pendingReady.delete(sessionId);
1033
1204
  this.queuedInputs.delete(sessionId);
1034
- this.trustGateAnswered.delete(sessionId);
1205
+ this.clearSessionDetectors(sessionId);
1035
1206
  try {
1036
1207
  session.process.kill("SIGINT");
1037
1208
  } catch {
@@ -1042,6 +1213,21 @@ var CodexPtyRunner = class {
1042
1213
  this.sessions.delete(sessionId);
1043
1214
  this.onStatusChange?.(toPublicSession(session));
1044
1215
  }
1216
+ // Drop a session's detection state: quiet-checker, ready-fallback timer,
1217
+ // gate bookkeeping — and dismiss a still-open gate card so mobile doesn't
1218
+ // keep rendering a question for a dead PTY.
1219
+ clearSessionDetectors(sessionId) {
1220
+ this.quietCheckers.get(sessionId)?.cancel();
1221
+ this.quietCheckers.delete(sessionId);
1222
+ const timer = this.readyFallbackTimers.get(sessionId);
1223
+ if (timer) clearTimeout(timer);
1224
+ this.readyFallbackTimers.delete(sessionId);
1225
+ if (this.openGate.delete(sessionId)) {
1226
+ this.onPermissionChange?.(sessionId, null);
1227
+ }
1228
+ this.gateActioned.delete(`${sessionId}:hooks`);
1229
+ this.gateActioned.delete(`${sessionId}:trust`);
1230
+ }
1045
1231
  getOutput(sessionId) {
1046
1232
  const session = this.sessions.get(sessionId);
1047
1233
  if (!session) throw new Error(`Session not found: ${sessionId}`);
@@ -1063,6 +1249,19 @@ var CodexPtyRunner = class {
1063
1249
  }
1064
1250
  return lines.slice(-maxLines);
1065
1251
  }
1252
+ getInputHistory(sessionId) {
1253
+ return this.sessions.get(sessionId)?.inputHistory ?? [];
1254
+ }
1255
+ // Record a submitted user message as ground truth and fire onUserMessage.
1256
+ // Called from writeSubmit (direct and flush paths) — never from sendKeys.
1257
+ recordUserMessage(session, text) {
1258
+ const ts = Date.now();
1259
+ session.inputHistory.push({ text, ts });
1260
+ if (session.inputHistory.length > INPUT_HISTORY_MAX) {
1261
+ session.inputHistory.shift();
1262
+ }
1263
+ this.onUserMessage?.(session.id, text, ts);
1264
+ }
1066
1265
  getSession(sessionId) {
1067
1266
  const session = this.sessions.get(sessionId);
1068
1267
  return session ? toPublicSession(session) : null;
@@ -1081,10 +1280,19 @@ var CodexPtyRunner = class {
1081
1280
  }
1082
1281
  session.screen.dispose();
1083
1282
  }
1283
+ for (const sessionId of Array.from(this.quietCheckers.keys())) {
1284
+ this.clearSessionDetectors(sessionId);
1285
+ }
1286
+ for (const timer of this.readyFallbackTimers.values()) {
1287
+ clearTimeout(timer);
1288
+ }
1084
1289
  this.sessions.clear();
1085
1290
  this.pendingReady.clear();
1086
1291
  this.queuedInputs.clear();
1087
- this.trustGateAnswered.clear();
1292
+ this.openGate.clear();
1293
+ this.gateActioned.clear();
1294
+ this.quietCheckers.clear();
1295
+ this.readyFallbackTimers.clear();
1088
1296
  }
1089
1297
  handleOutput(sessionId, data) {
1090
1298
  const session = this.sessions.get(sessionId);
@@ -1099,44 +1307,92 @@ var CodexPtyRunner = class {
1099
1307
  session.screen.write(data);
1100
1308
  session.lastOutput = stripAnsi(data);
1101
1309
  this.onOutput?.(sessionId, data);
1102
- this.detectReady(sessionId, session).catch((err) => {
1310
+ this.detectScreenState(sessionId, "chunk").catch((err) => {
1103
1311
  this.log.warn("[codex.ready_detect] failed", {
1104
1312
  event: "codex.ready_detect_failed",
1105
1313
  sessionId,
1106
1314
  err
1107
1315
  });
1108
1316
  });
1317
+ let quiet = this.quietCheckers.get(sessionId);
1318
+ if (!quiet) {
1319
+ quiet = debounce(() => {
1320
+ this.detectScreenState(sessionId, "quiet").catch((err) => {
1321
+ this.log.warn("[codex.ready_detect] failed", {
1322
+ event: "codex.ready_detect_failed",
1323
+ sessionId,
1324
+ err
1325
+ });
1326
+ });
1327
+ }, QUIET_DETECT_MS);
1328
+ this.quietCheckers.set(sessionId, quiet);
1329
+ }
1330
+ quiet();
1109
1331
  }
1110
- // Renders the session's headless screen and checks for the directory-trust
1111
- // gate (answered once, debounced) and the "Ready" status-bar text. Only
1112
- // transitions to waiting_input / fires onReady when the rendered status
1113
- // line literally contains "Ready" `›` alone (visible during "Starting")
1114
- // is NOT a valid readiness signal (Phase 0).
1115
- async detectReady(sessionId, session) {
1116
- if (session.status !== "running" || !this.pendingReady.has(sessionId)) return;
1332
+ // Renders the session's headless screen and drives both detections:
1333
+ // - Gates (directory trust, hooks review) checked on EVERY pass,
1334
+ // independent of pendingReady, so a gate appearing after ready is still
1335
+ // surfaced and a gate leaving the screen closes its card.
1336
+ // - Readiness the "Ready" status-bar marker while pendingReady, plus the
1337
+ // quiet path: after QUIET_DETECT_MS of PTY silence a still-pending
1338
+ // session is marked ready anyway (`›` alone is NOT a marker — Phase 0 —
1339
+ // but a quiet boot screen is more useful to the user live than a
1340
+ // spinner, and "Ready" may be truncated off the 120-col status bar).
1341
+ async detectScreenState(sessionId, trigger) {
1342
+ const session = this.sessions.get(sessionId);
1343
+ if (!session || session.status === "idle") return;
1117
1344
  const lines = await this.getOutputLines(sessionId, PTY_ROWS);
1118
1345
  const screenText = lines.join("\n");
1119
- if (CODEX_TRUST_GATE_REGEX.test(screenText)) {
1120
- if (!this.trustGateAnswered.has(sessionId)) {
1121
- this.trustGateAnswered.add(sessionId);
1122
- this.log.info(`[codex.trust_gate] ${sessionId.slice(0, 8)} auto-answering`, {
1123
- event: "codex.trust_gate",
1124
- sessionId
1125
- });
1126
- session.process.write("\r");
1127
- }
1128
- return;
1346
+ const gate = CODEX_HOOKS_GATE_REGEX.test(screenText) ? "hooks" : CODEX_TRUST_GATE_REGEX.test(screenText) ? "trust" : null;
1347
+ if (gate) {
1348
+ this.handleGate(sessionId, session, gate, lines);
1349
+ } else if (this.openGate.delete(sessionId)) {
1350
+ this.onPermissionChange?.(sessionId, null);
1351
+ this.flushQueuedInputs(sessionId);
1129
1352
  }
1353
+ if (session.status !== "running" || !this.pendingReady.has(sessionId)) return;
1130
1354
  const lastNonBlank2 = [...lines].reverse().find((l) => l.trim() !== "") ?? "";
1131
- if (!lastNonBlank2.includes(CODEX_PROMPT_READY_TEXT)) return;
1132
- this.markReady(sessionId, session);
1355
+ if (lastNonBlank2.includes(CODEX_PROMPT_READY_TEXT)) {
1356
+ this.markReady(sessionId, session, `marker:${CODEX_PROMPT_READY_TEXT}`);
1357
+ } else if (trigger === "quiet") {
1358
+ this.markReady(sessionId, session, "quiet:timeout");
1359
+ }
1360
+ }
1361
+ // Answer a gate from the persisted remember-store, or surface it as a
1362
+ // question card over the permission transport. Actioned once per session and
1363
+ // gate type — repaints of the same dialog neither re-write nor re-broadcast.
1364
+ handleGate(sessionId, session, gate, lines) {
1365
+ const key = `${sessionId}:${gate}`;
1366
+ if (this.gateActioned.has(key)) return;
1367
+ this.gateActioned.add(key);
1368
+ const remembered = rememberedGateDigit(gate);
1369
+ if (remembered) {
1370
+ this.log.info(`[codex.gate_auto_answer] ${sessionId.slice(0, 8)} ${gate} \u2192 ${remembered}`, {
1371
+ event: "codex.gate_auto_answer",
1372
+ sessionId,
1373
+ gate,
1374
+ digit: remembered
1375
+ });
1376
+ session.process.write(remembered);
1377
+ return;
1378
+ }
1379
+ this.openGate.set(sessionId, gate);
1380
+ const card = gateCard(gate, lines);
1381
+ this.log.info(`[codex.gate_prompt] ${sessionId.slice(0, 8)} ${gate}`, {
1382
+ event: "codex.gate_prompt",
1383
+ sessionId,
1384
+ gate,
1385
+ prompt: card.prompt
1386
+ });
1387
+ this.onPermissionChange?.(sessionId, card);
1133
1388
  }
1134
- markReady(sessionId, session) {
1389
+ markReady(sessionId, session, reason) {
1135
1390
  session.lastActivityAt = /* @__PURE__ */ new Date();
1136
1391
  session.status = "waiting_input";
1137
- this.log.info(`[codex.ready] ${sessionId.slice(0, 8)}`, {
1392
+ this.log.info(`[codex.ready] ${sessionId.slice(0, 8)} ${reason}`, {
1138
1393
  event: "codex.ready",
1139
- sessionId
1394
+ sessionId,
1395
+ reason
1140
1396
  });
1141
1397
  this.onStatusChange?.(toPublicSession(session));
1142
1398
  if (this.pendingReady.has(sessionId)) {
@@ -1152,7 +1408,7 @@ var CodexPtyRunner = class {
1152
1408
  session.status = "idle";
1153
1409
  const elapsedMs = session.completedAt.getTime() - session.startedAt.getTime();
1154
1410
  if (exitCode !== 0 && elapsedMs < 2e3 && session.lastOutput === "") {
1155
- if (!(0, import_fs4.existsSync)(session.projectPath)) {
1411
+ if (!(0, import_fs5.existsSync)(session.projectPath)) {
1156
1412
  session.failureReason = `Project directory not found: ${session.projectPath}`;
1157
1413
  } else {
1158
1414
  session.failureReason = `Codex process exited immediately (code ${exitCode}).`;
@@ -1162,7 +1418,7 @@ var CodexPtyRunner = class {
1162
1418
  session.screen.dispose();
1163
1419
  this.sessions.delete(sessionId);
1164
1420
  this.queuedInputs.delete(sessionId);
1165
- this.trustGateAnswered.delete(sessionId);
1421
+ this.clearSessionDetectors(sessionId);
1166
1422
  }
1167
1423
  };
1168
1424
  function toPublicSession(s) {
@@ -1189,8 +1445,8 @@ function stripAnsi(str) {
1189
1445
  // src/pty-manager.ts
1190
1446
  var import_headless2 = require("@xterm/headless");
1191
1447
  var import_crypto3 = require("crypto");
1192
- var import_fs5 = require("fs");
1193
- var import_path5 = require("path");
1448
+ var import_fs6 = require("fs");
1449
+ var import_path6 = require("path");
1194
1450
 
1195
1451
  // src/services/questions/detectPermissionGate.ts
1196
1452
  var OSC_777_RE = /\x1b\]777;notify;Claude Code;[^\x07\x1b]*/;
@@ -1345,9 +1601,15 @@ function detectShellPrompt(lines) {
1345
1601
  ]
1346
1602
  };
1347
1603
  }
1348
- if (NUMBERED_RE.test(last.text)) {
1604
+ const lastNumberedIdx = (() => {
1605
+ for (let i = last.idx; i >= 0; i--) {
1606
+ if (NUMBERED_RE.test(lines[i])) return i;
1607
+ }
1608
+ return -1;
1609
+ })();
1610
+ if (lastNumberedIdx >= 0) {
1349
1611
  const options = [];
1350
- for (let i = 0; i <= last.idx; i++) {
1612
+ for (let i = 0; i <= lastNumberedIdx; i++) {
1351
1613
  const m = NUMBERED_RE.exec(lines[i]);
1352
1614
  if (!m) continue;
1353
1615
  const num = Number.parseInt(m[1], 10);
@@ -1375,45 +1637,15 @@ function detectShellPrompt(lines) {
1375
1637
  return null;
1376
1638
  }
1377
1639
 
1378
- // src/utils/debounce.ts
1379
- function debounce(fn, waitMs) {
1380
- let timer = null;
1381
- let lastArgs = null;
1382
- const run2 = () => {
1383
- timer = null;
1384
- if (lastArgs) {
1385
- const args = lastArgs;
1386
- lastArgs = null;
1387
- fn(...args);
1388
- }
1389
- };
1390
- const debounced = (...args) => {
1391
- lastArgs = args;
1392
- if (timer) clearTimeout(timer);
1393
- timer = setTimeout(run2, waitMs);
1394
- };
1395
- debounced.cancel = () => {
1396
- if (timer) clearTimeout(timer);
1397
- timer = null;
1398
- lastArgs = null;
1399
- };
1400
- debounced.flush = () => {
1401
- if (timer) {
1402
- clearTimeout(timer);
1403
- run2();
1404
- }
1405
- };
1406
- return debounced;
1407
- }
1408
-
1409
1640
  // src/pty-manager.ts
1410
1641
  var OUTPUT_BUFFER_MAX2 = 65536;
1642
+ var INPUT_HISTORY_MAX2 = 50;
1411
1643
  var PTY_COLS2 = 120;
1412
1644
  var PTY_ROWS2 = 40;
1413
1645
  var SCREEN_SCROLLBACK2 = 1e3;
1414
1646
  var CLAUDE_PROMPT_MARKERS = ["\u256D", "\u276F"];
1415
1647
  var PROMPT_MARKER_FALLBACK_MS = 1e4;
1416
- var QUIET_DETECT_MS = 500;
1648
+ var QUIET_DETECT_MS2 = 500;
1417
1649
  function buildPasteBytes(input) {
1418
1650
  return `\x1B[200~${input}\x1B[201~`;
1419
1651
  }
@@ -1466,6 +1698,7 @@ var PTYManager = class {
1466
1698
  onPermissionChange;
1467
1699
  onLiveQuestion;
1468
1700
  onLiveQuestionGone;
1701
+ onUserMessage;
1469
1702
  // Per-session permission-gate state. True between an OSC 777 (gate open) and
1470
1703
  // the next prompt-ready without a fresh 777 (gate closed). Prevents
1471
1704
  // re-broadcasting open/close on every chunk.
@@ -1511,6 +1744,7 @@ var PTYManager = class {
1511
1744
  this.onPermissionChange = options.onPermissionChange;
1512
1745
  this.onLiveQuestion = options.onLiveQuestion;
1513
1746
  this.onLiveQuestionGone = options.onLiveQuestionGone;
1747
+ this.onUserMessage = options.onUserMessage;
1514
1748
  this.log = options.logger ?? getLogger("pty");
1515
1749
  }
1516
1750
  // Resume an existing Claude conversation. sessionId is the JSONL UUID.
@@ -1541,7 +1775,7 @@ var PTYManager = class {
1541
1775
  }
1542
1776
  async doStart(sessionId, options) {
1543
1777
  const nodePty = await loadPty2();
1544
- const projectName = options.projectName ?? (0, import_path5.basename)(options.projectPath);
1778
+ const projectName = options.projectName ?? (0, import_path6.basename)(options.projectPath);
1545
1779
  const proc = nodePty.spawn(
1546
1780
  resolveClaudeExe(),
1547
1781
  [
@@ -1573,7 +1807,8 @@ var PTYManager = class {
1573
1807
  lastOutput: "",
1574
1808
  process: proc,
1575
1809
  outputBuffer: Buffer.alloc(0),
1576
- screen: createScreen2()
1810
+ screen: createScreen2(),
1811
+ inputHistory: []
1577
1812
  };
1578
1813
  this.sessions.set(sessionId, session);
1579
1814
  this.pendingReady.add(sessionId);
@@ -1592,7 +1827,7 @@ var PTYManager = class {
1592
1827
  async startFresh(options) {
1593
1828
  const nodePty = await loadPty2();
1594
1829
  const sessionId = (0, import_crypto3.randomUUID)();
1595
- const projectName = options.projectName ?? (0, import_path5.basename)(options.projectPath);
1830
+ const projectName = options.projectName ?? (0, import_path6.basename)(options.projectPath);
1596
1831
  const args = [
1597
1832
  "--permission-mode",
1598
1833
  options.permissionMode ?? "acceptEdits",
@@ -1624,7 +1859,8 @@ var PTYManager = class {
1624
1859
  lastOutput: "",
1625
1860
  process: proc,
1626
1861
  outputBuffer: Buffer.alloc(0),
1627
- screen: createScreen2()
1862
+ screen: createScreen2(),
1863
+ inputHistory: []
1628
1864
  };
1629
1865
  this.sessions.set(sessionId, session);
1630
1866
  this.pendingReady.add(sessionId);
@@ -1704,6 +1940,7 @@ var PTYManager = class {
1704
1940
  // step gives the TUI as many extra ticks as it needs, capped at
1705
1941
  // SUBMIT_MAX_WAIT_MS so a silent/wedged PTY still gets its \r eventually.
1706
1942
  writeSubmit(sessionId, session, input, path, promptCount) {
1943
+ this.recordUserMessage(session, input);
1707
1944
  const pasteBytes = buildPasteBytes(input);
1708
1945
  this.log.info(
1709
1946
  `[pty.input.write] ${sessionId.slice(0, 8)} promptCount=${promptCount} bytes=${pasteBytes.length} digest=${digestBytes2(pasteBytes)}`,
@@ -1834,6 +2071,20 @@ var PTYManager = class {
1834
2071
  }
1835
2072
  return lines.slice(-maxLines);
1836
2073
  }
2074
+ getInputHistory(sessionId) {
2075
+ return this.sessions.get(sessionId)?.inputHistory ?? [];
2076
+ }
2077
+ // Record a submitted user message as ground truth and fire onUserMessage.
2078
+ // Called from writeSubmit (both direct and flush paths) — never from
2079
+ // sendKeys, so raw keystrokes aren't logged as messages.
2080
+ recordUserMessage(session, text) {
2081
+ const ts = Date.now();
2082
+ session.inputHistory.push({ text, ts });
2083
+ if (session.inputHistory.length > INPUT_HISTORY_MAX2) {
2084
+ session.inputHistory.shift();
2085
+ }
2086
+ this.onUserMessage?.(session.id, text, ts);
2087
+ }
1837
2088
  getSession(sessionId) {
1838
2089
  const session = this.sessions.get(sessionId);
1839
2090
  return session ? toPublicSession2(session) : null;
@@ -1913,7 +2164,7 @@ var PTYManager = class {
1913
2164
  });
1914
2165
  let quiet = this.quietCheckers.get(sessionId);
1915
2166
  if (!quiet) {
1916
- quiet = debounce(() => this.handleQuiet(sessionId), QUIET_DETECT_MS);
2167
+ quiet = debounce(() => this.handleQuiet(sessionId), QUIET_DETECT_MS2);
1917
2168
  this.quietCheckers.set(sessionId, quiet);
1918
2169
  }
1919
2170
  quiet();
@@ -2036,7 +2287,7 @@ var PTYManager = class {
2036
2287
  session.status = "idle";
2037
2288
  const elapsedMs = session.completedAt.getTime() - session.startedAt.getTime();
2038
2289
  if (exitCode !== 0 && elapsedMs < 2e3 && session.lastOutput === "") {
2039
- if (!(0, import_fs5.existsSync)(session.projectPath)) {
2290
+ if (!(0, import_fs6.existsSync)(session.projectPath)) {
2040
2291
  session.failureReason = `Project directory not found: ${session.projectPath}`;
2041
2292
  } else {
2042
2293
  session.failureReason = `Process exited immediately (code ${exitCode}). Check that the Claude binary is installed and accessible.`;
@@ -2130,6 +2381,9 @@ var LiveSessionManager = class {
2130
2381
  getOutputLines(sessionId, maxLines) {
2131
2382
  return this.runnerFor(sessionId).getOutputLines(sessionId, maxLines);
2132
2383
  }
2384
+ getInputHistory(sessionId) {
2385
+ return this.runnerFor(sessionId).getInputHistory(sessionId);
2386
+ }
2133
2387
  getSession(sessionId) {
2134
2388
  for (const runner of this.runners.values()) {
2135
2389
  const session = runner.getSession(sessionId);
@@ -2164,7 +2418,7 @@ var LiveSessionManager = class {
2164
2418
  const runner = this.runners.get(provider);
2165
2419
  if (runner) return runner;
2166
2420
  const err = new Error(
2167
- `Live ${provider} sessions are not implemented yet for ${(0, import_path6.basename)(projectPath)}`
2421
+ `Live ${provider} sessions are not implemented yet for ${(0, import_path7.basename)(projectPath)}`
2168
2422
  );
2169
2423
  err.statusCode = 501;
2170
2424
  throw err;
@@ -2173,10 +2427,10 @@ var LiveSessionManager = class {
2173
2427
 
2174
2428
  // src/process-discovery.ts
2175
2429
  var import_child_process2 = require("child_process");
2176
- var import_os3 = require("os");
2177
- var import_path7 = require("path");
2430
+ var import_os4 = require("os");
2431
+ var import_path8 = require("path");
2178
2432
  async function discoverClaudeProcesses() {
2179
- if ((0, import_os3.platform)() === "win32") return discoverWindows();
2433
+ if ((0, import_os4.platform)() === "win32") return discoverWindows();
2180
2434
  return discoverUnix();
2181
2435
  }
2182
2436
  async function discoverUnix() {
@@ -2193,7 +2447,7 @@ async function discoverUnix() {
2193
2447
  return {
2194
2448
  pid,
2195
2449
  projectPath: cwd,
2196
- projectName: (0, import_path7.basename)(cwd),
2450
+ projectName: (0, import_path8.basename)(cwd),
2197
2451
  branch: await readGitBranch(cwd),
2198
2452
  conversationId,
2199
2453
  startedAt
@@ -2215,7 +2469,7 @@ async function discoverWindows() {
2215
2469
  return {
2216
2470
  pid,
2217
2471
  projectPath: info.cwd,
2218
- projectName: (0, import_path7.basename)(info.cwd),
2472
+ projectName: (0, import_path8.basename)(info.cwd),
2219
2473
  branch: await readGitBranch(info.cwd),
2220
2474
  conversationId: extractResumeId(info.args),
2221
2475
  startedAt: info.startedAt
@@ -2296,7 +2550,7 @@ async function getProcessInfoWindows(pid) {
2296
2550
  const startedAt = /* @__PURE__ */ new Date(`${year}-${month}-${day}T${hour}:${min}:${sec}`);
2297
2551
  if (Number.isNaN(startedAt.getTime())) return null;
2298
2552
  const exePath = parts[3] ?? "";
2299
- const cwd = exePath ? (0, import_path7.dirname)(exePath) : "";
2553
+ const cwd = exePath ? (0, import_path8.dirname)(exePath) : "";
2300
2554
  return { cwd, args, startedAt };
2301
2555
  } catch {
2302
2556
  return null;
@@ -2319,11 +2573,11 @@ var import_node_ws = require("@hono/node-ws");
2319
2573
  var import_client = require("@temporalio/client");
2320
2574
  var import_scanner3 = require("@threadbase-sh/scanner");
2321
2575
  var import_events = require("events");
2322
- var import_fs12 = require("fs");
2576
+ var import_fs13 = require("fs");
2323
2577
  var import_promises7 = require("fs/promises");
2324
2578
  var import_http = require("http");
2325
- var import_os6 = require("os");
2326
- var import_path13 = require("path");
2579
+ var import_os7 = require("os");
2580
+ var import_path14 = require("path");
2327
2581
  var import_readline = require("readline");
2328
2582
 
2329
2583
  // node_modules/nanoid/index.js
@@ -2582,14 +2836,15 @@ async function handleStartAgentSession(body, deps) {
2582
2836
  }
2583
2837
 
2584
2838
  // src/api/app.ts
2585
- var import_hono11 = require("hono");
2839
+ var import_hono12 = require("hono");
2586
2840
 
2587
2841
  // src/api/middleware/auth.middleware.ts
2588
2842
  function isLocalRequest(remoteAddr) {
2589
2843
  const addr = remoteAddr ?? "";
2590
2844
  return addr === "127.0.0.1" || addr === "::1" || addr === "::ffff:127.0.0.1";
2591
2845
  }
2592
- var PUBLIC_PATHS = /* @__PURE__ */ new Set(["/healthz", "/ws"]);
2846
+ var PUBLIC_PATHS = /* @__PURE__ */ new Set(["/healthz"]);
2847
+ var LOCAL_ONLY_PATHS = /* @__PURE__ */ new Set(["/api/logs", "/api/logs/meta"]);
2593
2848
  var PUBLIC_POST_PATHS = /* @__PURE__ */ new Set(["/api/pair/exchange", "/api/__update"]);
2594
2849
  var PUBLIC_POST_PREFIXES = ["/internal/sessions/"];
2595
2850
  var authMiddleware = (deps) => async (c, next) => {
@@ -2600,8 +2855,12 @@ var authMiddleware = (deps) => async (c, next) => {
2600
2855
  await next();
2601
2856
  return;
2602
2857
  }
2858
+ const remoteAddr = c.env.incoming?.socket?.remoteAddress;
2859
+ if (LOCAL_ONLY_PATHS.has(path) && isLocalRequest(remoteAddr)) {
2860
+ await next();
2861
+ return;
2862
+ }
2603
2863
  if (deps.localNoAuth) {
2604
- const remoteAddr = c.env.incoming?.socket?.remoteAddress;
2605
2864
  if (isLocalRequest(remoteAddr)) {
2606
2865
  await next();
2607
2866
  return;
@@ -2773,16 +3032,145 @@ var createHealthRoutes = () => {
2773
3032
  return app;
2774
3033
  };
2775
3034
 
3035
+ // src/api/routes/logs.routes.ts
3036
+ var import_node_fs3 = require("fs");
3037
+ var import_node_path5 = require("path");
3038
+ var import_hono5 = require("hono");
3039
+
3040
+ // src/lifecycle/constants.ts
3041
+ var import_node_os = require("os");
3042
+ var import_node_path4 = require("path");
3043
+ var TASK_NAME = process.env.THREADBASE_TASK_NAME ?? "Threadbase";
3044
+ function installDir() {
3045
+ return process.env.THREADBASE_INSTALL_DIR ?? (0, import_node_path4.join)((0, import_node_os.homedir)(), ".threadbase");
3046
+ }
3047
+
3048
+ // src/api/routes/logs.routes.ts
3049
+ var logger2 = getLogger("logs-api");
3050
+ function resolveLogPath(source) {
3051
+ return (0, import_node_path5.join)(installDir(), "logs", `${source}.log`);
3052
+ }
3053
+ function pickDefaultSource() {
3054
+ for (const source of ["stdout", "stderr", "dev"]) {
3055
+ const p = resolveLogPath(source);
3056
+ if ((0, import_node_fs3.existsSync)(p) && (0, import_node_fs3.statSync)(p).size > 0) return source;
3057
+ }
3058
+ return "stdout";
3059
+ }
3060
+ function readLogLines(filePath, sinceOffset, limit) {
3061
+ if (!(0, import_node_fs3.existsSync)(filePath)) {
3062
+ return { lines: [], offset: 0, total: 0 };
3063
+ }
3064
+ const fd = (0, import_node_fs3.openSync)(filePath, "r");
3065
+ try {
3066
+ const { size } = (0, import_node_fs3.fstatSync)(fd);
3067
+ if (size === 0) return { lines: [], offset: 0, total: 0 };
3068
+ const maxBytes = Math.min(size, 2 * 1024 * 1024);
3069
+ const start = size - maxBytes;
3070
+ const buf = Buffer.alloc(maxBytes);
3071
+ (0, import_node_fs3.readSync)(fd, buf, 0, maxBytes, start);
3072
+ let text = buf.toString("utf8");
3073
+ if (start > 0) {
3074
+ const firstNl = text.indexOf("\n");
3075
+ if (firstNl >= 0) text = text.slice(firstNl + 1);
3076
+ }
3077
+ const allLines = text.split("\n").filter((line) => line.trim() && !line.startsWith("==="));
3078
+ let lines;
3079
+ let newOffset;
3080
+ if (sinceOffset > 0 && sinceOffset < allLines.length) {
3081
+ lines = allLines.slice(sinceOffset, sinceOffset + limit);
3082
+ newOffset = sinceOffset + lines.length;
3083
+ } else if (sinceOffset >= allLines.length && sinceOffset > 0) {
3084
+ lines = [];
3085
+ newOffset = allLines.length;
3086
+ } else {
3087
+ lines = allLines.slice(-limit);
3088
+ newOffset = allLines.length;
3089
+ }
3090
+ return { lines, offset: newOffset, total: allLines.length };
3091
+ } finally {
3092
+ (0, import_node_fs3.closeSync)(fd);
3093
+ }
3094
+ }
3095
+ function createLogsRoutes() {
3096
+ const app = new import_hono5.Hono();
3097
+ app.get("/", (c) => {
3098
+ try {
3099
+ const sourceParam = (c.req.query("source") || "").toLowerCase();
3100
+ const source = sourceParam === "stdout" || sourceParam === "stderr" || sourceParam === "dev" ? sourceParam : pickDefaultSource();
3101
+ const logPath = resolveLogPath(source);
3102
+ const sinceOffset = parseInt(c.req.query("since") || "0", 10);
3103
+ const limit = Math.min(parseInt(c.req.query("limit") || "100", 10) || 100, 1e3);
3104
+ if (!(0, import_node_fs3.existsSync)(logPath)) {
3105
+ return c.json({
3106
+ logs: [],
3107
+ message: `No log file found for source=${source}`,
3108
+ offset: 0,
3109
+ total: 0,
3110
+ source
3111
+ });
3112
+ }
3113
+ const { lines, offset, total } = readLogLines(logPath, sinceOffset, limit);
3114
+ const stats = (0, import_node_fs3.statSync)(logPath);
3115
+ return c.json({
3116
+ logs: lines,
3117
+ offset,
3118
+ total,
3119
+ hasMore: offset < total,
3120
+ source,
3121
+ fileSize: stats.size,
3122
+ fileModified: stats.mtime.toISOString()
3123
+ });
3124
+ } catch (error) {
3125
+ logger2.error("Failed to read logs", { error: String(error) });
3126
+ return c.json(
3127
+ {
3128
+ error: "Failed to read logs",
3129
+ logs: [],
3130
+ offset: 0,
3131
+ total: 0
3132
+ },
3133
+ 500
3134
+ );
3135
+ }
3136
+ });
3137
+ app.get("/meta", (c) => {
3138
+ try {
3139
+ const sources = ["stdout", "stderr", "dev"].map((source) => {
3140
+ const logPath = resolveLogPath(source);
3141
+ if (!(0, import_node_fs3.existsSync)(logPath)) {
3142
+ return { source, exists: false, total: 0, fileSize: 0 };
3143
+ }
3144
+ const stats = (0, import_node_fs3.statSync)(logPath);
3145
+ return {
3146
+ source,
3147
+ exists: true,
3148
+ fileSize: stats.size,
3149
+ fileModified: stats.mtime.toISOString()
3150
+ };
3151
+ });
3152
+ return c.json({
3153
+ defaultSource: pickDefaultSource(),
3154
+ sources
3155
+ });
3156
+ } catch (error) {
3157
+ logger2.error("Failed to read log metadata", { error: String(error) });
3158
+ return c.json({ error: "Failed to read log metadata", exists: false }, 500);
3159
+ }
3160
+ });
3161
+ return app;
3162
+ }
3163
+
2776
3164
  // src/api/routes/misc.routes.ts
2777
3165
  var import_node_child_process = require("child_process");
2778
3166
  var import_node_crypto2 = require("crypto");
2779
- var import_hono5 = require("hono");
2780
- var import_os4 = require("os");
3167
+ var import_hono6 = require("hono");
3168
+ var import_os5 = require("os");
2781
3169
 
2782
3170
  // src/config/update-config.ts
2783
- var import_node_fs3 = require("fs");
2784
- var import_node_os = require("os");
2785
- var import_node_path4 = require("path");
3171
+ var import_node_fs4 = require("fs");
3172
+ var import_node_os2 = require("os");
3173
+ var import_node_path6 = require("path");
2786
3174
  var import_yaml = require("yaml");
2787
3175
 
2788
3176
  // src/schemas/updateConfig.schema.ts
@@ -2798,12 +3186,12 @@ var UpdateConfigSchema = import_zod.z.object({
2798
3186
  }).strict();
2799
3187
 
2800
3188
  // src/config/update-config.ts
2801
- var DEFAULT_CONFIG_PATH = (0, import_node_path4.join)((0, import_node_os.homedir)(), ".threadbase", "update.yaml");
3189
+ var DEFAULT_CONFIG_PATH = (0, import_node_path6.join)((0, import_node_os2.homedir)(), ".threadbase", "update.yaml");
2802
3190
  function loadUpdateConfig(opts = {}) {
2803
3191
  const path = opts.path ?? DEFAULT_CONFIG_PATH;
2804
3192
  let raw;
2805
3193
  try {
2806
- raw = (0, import_node_fs3.readFileSync)(path, "utf-8");
3194
+ raw = (0, import_node_fs4.readFileSync)(path, "utf-8");
2807
3195
  } catch (err) {
2808
3196
  if (err.code === "ENOENT") return null;
2809
3197
  throw err;
@@ -2850,12 +3238,12 @@ function verifyWebhookSignature(body, header, secret) {
2850
3238
  }
2851
3239
  var clientLog = getLogger("client");
2852
3240
  var createMiscRoutes = (deps) => {
2853
- const app = new import_hono5.Hono();
3241
+ const app = new import_hono6.Hono();
2854
3242
  app.get("/api/info", (c) => {
2855
3243
  const ptyIds = deps.ptyAttachedIds();
2856
3244
  return c.json({
2857
3245
  version: getVersion(),
2858
- machineName: (0, import_os4.hostname)(),
3246
+ machineName: (0, import_os5.hostname)(),
2859
3247
  platform: process.platform,
2860
3248
  activeSessions: deps.sessionStore.list(ptyIds).filter((s) => s.status === "running").length,
2861
3249
  publicUrl: deps.publicUrl
@@ -2926,11 +3314,11 @@ var createMiscRoutes = (deps) => {
2926
3314
  };
2927
3315
 
2928
3316
  // src/api/routes/pair.routes.ts
2929
- var import_hono6 = require("hono");
3317
+ var import_hono7 = require("hono");
2930
3318
  var ALREADY_HANDLED3 = 597;
2931
3319
  var alreadyHandled3 = () => new Response(null, { status: ALREADY_HANDLED3 });
2932
3320
  var createPairRoutes = (deps) => {
2933
- const app = new import_hono6.Hono();
3321
+ const app = new import_hono7.Hono();
2934
3322
  app.post("/start", (c) => {
2935
3323
  deps.handlePairStart(c.env.outgoing);
2936
3324
  return alreadyHandled3();
@@ -2943,11 +3331,11 @@ var createPairRoutes = (deps) => {
2943
3331
  };
2944
3332
 
2945
3333
  // src/api/routes/projects.routes.ts
2946
- var import_hono7 = require("hono");
3334
+ var import_hono8 = require("hono");
2947
3335
  var ALREADY_HANDLED4 = 597;
2948
3336
  var alreadyHandled4 = () => new Response(null, { status: ALREADY_HANDLED4 });
2949
3337
  var createProjectRoutes = (deps) => {
2950
- const app = new import_hono7.Hono();
3338
+ const app = new import_hono8.Hono();
2951
3339
  app.get("/", (c) => {
2952
3340
  const url = new URL(c.req.url);
2953
3341
  deps.handleListProjects(url, c.env.outgoing);
@@ -2962,11 +3350,11 @@ var createProjectRoutes = (deps) => {
2962
3350
  };
2963
3351
 
2964
3352
  // src/api/routes/scanner.routes.ts
2965
- var import_hono8 = require("hono");
3353
+ var import_hono9 = require("hono");
2966
3354
  var ALREADY_HANDLED5 = 597;
2967
3355
  var alreadyHandled5 = () => new Response(null, { status: ALREADY_HANDLED5 });
2968
3356
  var createScannerRoutes = (deps) => {
2969
- const app = new import_hono8.Hono();
3357
+ const app = new import_hono9.Hono();
2970
3358
  app.get("/api/search", async (c) => {
2971
3359
  const url = new URL(c.req.url);
2972
3360
  await deps.handleSearch(url, c.env.outgoing);
@@ -2976,11 +3364,11 @@ var createScannerRoutes = (deps) => {
2976
3364
  };
2977
3365
 
2978
3366
  // src/api/routes/sessions.routes.ts
2979
- var import_hono9 = require("hono");
3367
+ var import_hono10 = require("hono");
2980
3368
  var ALREADY_HANDLED6 = 597;
2981
3369
  var alreadyHandled6 = () => new Response(null, { status: ALREADY_HANDLED6 });
2982
3370
  var createSessionRoutes = (deps) => {
2983
- const app = new import_hono9.Hono();
3371
+ const app = new import_hono10.Hono();
2984
3372
  app.get("/count", (c) => {
2985
3373
  deps.handleSessionsCount(c.env.outgoing);
2986
3374
  return alreadyHandled6();
@@ -3047,21 +3435,19 @@ var createSessionRoutes = (deps) => {
3047
3435
  };
3048
3436
 
3049
3437
  // src/api/routes/ws.routes.ts
3050
- var import_hono10 = require("hono");
3438
+ var import_hono11 = require("hono");
3051
3439
  var createWsRoutes = (deps, upgradeWebSocket) => {
3052
- const app = new import_hono10.Hono();
3440
+ const app = new import_hono11.Hono();
3053
3441
  app.get(
3054
3442
  "/ws",
3055
- upgradeWebSocket((c) => {
3056
- const key = c.req.query("key");
3057
- const preAuthed = typeof key === "string" && validateApiKey(key, deps.apiKey);
3443
+ upgradeWebSocket(() => {
3058
3444
  let openWs = null;
3059
3445
  return {
3060
3446
  onOpen(_evt, ws) {
3061
3447
  const raw = ws.raw;
3062
3448
  if (!raw) return;
3063
3449
  openWs = raw;
3064
- deps.handleWsOpen(raw, preAuthed);
3450
+ deps.handleWsOpen(raw);
3065
3451
  },
3066
3452
  onMessage(evt, _ws) {
3067
3453
  if (openWs) deps.handleWsMessage(openWs, evt.data);
@@ -3077,7 +3463,7 @@ var createWsRoutes = (deps, upgradeWebSocket) => {
3077
3463
 
3078
3464
  // src/api/app.ts
3079
3465
  var createHonoApp = (deps, upgradeWebSocket) => {
3080
- const app = new import_hono11.Hono();
3466
+ const app = new import_hono12.Hono();
3081
3467
  const httpLog = getLogger("http");
3082
3468
  app.use("*", async (c, next) => {
3083
3469
  const start = Date.now();
@@ -3106,6 +3492,7 @@ var createHonoApp = (deps, upgradeWebSocket) => {
3106
3492
  app.route("/api", createBrowseRoutes(deps));
3107
3493
  app.route("/", createScannerRoutes(deps));
3108
3494
  app.route("/internal", createProgressRoutes(deps));
3495
+ app.route("/api/logs", createLogsRoutes());
3109
3496
  if (upgradeWebSocket) {
3110
3497
  app.route("/", createWsRoutes(deps, upgradeWebSocket));
3111
3498
  }
@@ -3114,7 +3501,7 @@ var createHonoApp = (deps, upgradeWebSocket) => {
3114
3501
 
3115
3502
  // src/browse.ts
3116
3503
  var import_promises2 = require("fs/promises");
3117
- var import_path8 = require("path");
3504
+ var import_path9 = require("path");
3118
3505
  var BrowsePathNotFoundError = class extends Error {
3119
3506
  constructor(message) {
3120
3507
  super(message);
@@ -3122,15 +3509,15 @@ var BrowsePathNotFoundError = class extends Error {
3122
3509
  }
3123
3510
  };
3124
3511
  async function resolveBrowsePath(browseRoot, relativePath) {
3125
- const normalizedRoot = (0, import_path8.resolve)(browseRoot);
3512
+ const normalizedRoot = (0, import_path9.resolve)(browseRoot);
3126
3513
  let sanitized;
3127
3514
  if (process.platform !== "win32" && relativePath.startsWith("/") && relativePath.length > 1 && relativePath.includes("/", 1)) {
3128
3515
  sanitized = relativePath;
3129
3516
  } else {
3130
3517
  sanitized = relativePath.replace(/^[/\\]+/, "");
3131
3518
  }
3132
- const target = sanitized ? (0, import_path8.resolve)(normalizedRoot, sanitized) : normalizedRoot;
3133
- const rootPrefix = normalizedRoot.endsWith(import_path8.sep) ? normalizedRoot : `${normalizedRoot}${import_path8.sep}`;
3519
+ const target = sanitized ? (0, import_path9.resolve)(normalizedRoot, sanitized) : normalizedRoot;
3520
+ const rootPrefix = normalizedRoot.endsWith(import_path9.sep) ? normalizedRoot : `${normalizedRoot}${import_path9.sep}`;
3134
3521
  if (!target.startsWith(rootPrefix) && target !== normalizedRoot) {
3135
3522
  throw new Error("Path outside browse root");
3136
3523
  }
@@ -3152,7 +3539,7 @@ async function createDirectory(parentAbsolutePath, name) {
3152
3539
  if (name.includes("/") || name.includes("\\") || name === ".." || name === ".") {
3153
3540
  throw new Error("Invalid directory name");
3154
3541
  }
3155
- const target = (0, import_path8.join)(parentAbsolutePath, name);
3542
+ const target = (0, import_path9.join)(parentAbsolutePath, name);
3156
3543
  try {
3157
3544
  const s = await (0, import_promises2.stat)(target);
3158
3545
  if (s.isDirectory()) throw new Error("Directory already exists");
@@ -3166,19 +3553,19 @@ async function createDirectory(parentAbsolutePath, name) {
3166
3553
  // src/conversation-cache.ts
3167
3554
  var import_scanner2 = require("@threadbase-sh/scanner");
3168
3555
  var import_better_sqlite3 = __toESM(require("better-sqlite3"), 1);
3169
- var import_fs8 = require("fs");
3556
+ var import_fs9 = require("fs");
3170
3557
  var import_promises3 = require("fs/promises");
3171
- var import_path10 = require("path");
3558
+ var import_path11 = require("path");
3172
3559
  var import_promises4 = require("timers/promises");
3173
3560
 
3174
3561
  // src/db/sqlite-migrate.ts
3175
- var import_fs6 = require("fs");
3176
- var import_path9 = require("path");
3562
+ var import_fs7 = require("fs");
3563
+ var import_path10 = require("path");
3177
3564
  var import_url2 = require("url");
3178
3565
  var import_meta2 = {};
3179
3566
  function getMigrationsDir2() {
3180
3567
  if (typeof import_meta2 !== "undefined" && import_meta2.url) {
3181
- return (0, import_path9.dirname)((0, import_url2.fileURLToPath)(import_meta2.url));
3568
+ return (0, import_path10.dirname)((0, import_url2.fileURLToPath)(import_meta2.url));
3182
3569
  }
3183
3570
  return __dirname;
3184
3571
  }
@@ -3190,8 +3577,8 @@ CREATE TABLE IF NOT EXISTS schema_migrations (
3190
3577
  `;
3191
3578
  function runSqliteMigrations(db, migrationsDir) {
3192
3579
  db.exec(SCHEMA_MIGRATIONS_SQL);
3193
- const dir = migrationsDir ?? (0, import_path9.join)(getMigrationsDir2(), "migrations");
3194
- const files = (0, import_fs6.readdirSync)(dir).filter((f) => f.endsWith(".sql")).sort();
3580
+ const dir = migrationsDir ?? (0, import_path10.join)(getMigrationsDir2(), "migrations");
3581
+ const files = (0, import_fs7.readdirSync)(dir).filter((f) => f.endsWith(".sql")).sort();
3195
3582
  const appliedRows = db.prepare("SELECT id FROM schema_migrations").all();
3196
3583
  const appliedSet = new Set(appliedRows.map((r) => r.id));
3197
3584
  const recordApplied = db.prepare("INSERT INTO schema_migrations (id, applied_at) VALUES (?, ?)");
@@ -3202,7 +3589,7 @@ function runSqliteMigrations(db, migrationsDir) {
3202
3589
  skipped.push(file);
3203
3590
  continue;
3204
3591
  }
3205
- const sql = (0, import_fs6.readFileSync)((0, import_path9.join)(dir, file), "utf-8");
3592
+ const sql = (0, import_fs7.readFileSync)((0, import_path10.join)(dir, file), "utf-8");
3206
3593
  const tx = db.transaction(() => {
3207
3594
  db.exec(sql);
3208
3595
  recordApplied.run(file, (/* @__PURE__ */ new Date()).toISOString());
@@ -3214,7 +3601,7 @@ function runSqliteMigrations(db, migrationsDir) {
3214
3601
  }
3215
3602
 
3216
3603
  // src/services/conversations/isAgentConversation.ts
3217
- var import_fs7 = require("fs");
3604
+ var import_fs8 = require("fs");
3218
3605
  var DEFAULT_AGENT_ENTRYPOINTS = /* @__PURE__ */ new Set(["sdk-cli", "claude-vscode"]);
3219
3606
  var CHUNK_BYTES = 64 * 1024;
3220
3607
  var ENTRYPOINT_PROBE = `"entrypoint":`;
@@ -3236,12 +3623,12 @@ function isAgentFile(filePath, entrypoints = DEFAULT_AGENT_ENTRYPOINTS) {
3236
3623
  if (cached2 !== void 0) return cached2;
3237
3624
  let fd;
3238
3625
  try {
3239
- fd = (0, import_fs7.openSync)(filePath, "r");
3626
+ fd = (0, import_fs8.openSync)(filePath, "r");
3240
3627
  } catch {
3241
3628
  return false;
3242
3629
  }
3243
3630
  try {
3244
- const fileSize = (0, import_fs7.statSync)(filePath).size;
3631
+ const fileSize = (0, import_fs8.statSync)(filePath).size;
3245
3632
  if (fileSize === 0) {
3246
3633
  fileDecisionCache.set(key, false);
3247
3634
  return false;
@@ -3252,7 +3639,7 @@ function isAgentFile(filePath, entrypoints = DEFAULT_AGENT_ENTRYPOINTS) {
3252
3639
  let carry = "";
3253
3640
  while (offset < fileSize) {
3254
3641
  const toRead = Math.min(CHUNK_BYTES, fileSize - offset);
3255
- const got = (0, import_fs7.readSync)(fd, buf, 0, toRead, offset);
3642
+ const got = (0, import_fs8.readSync)(fd, buf, 0, toRead, offset);
3256
3643
  if (got <= 0) break;
3257
3644
  const chunk = carry + buf.toString("utf8", 0, got);
3258
3645
  for (const marker of markers) {
@@ -3273,7 +3660,7 @@ function isAgentFile(filePath, entrypoints = DEFAULT_AGENT_ENTRYPOINTS) {
3273
3660
  } catch {
3274
3661
  return false;
3275
3662
  } finally {
3276
- (0, import_fs7.closeSync)(fd);
3663
+ (0, import_fs8.closeSync)(fd);
3277
3664
  }
3278
3665
  }
3279
3666
  function parseAgentEntrypointsEnv(raw) {
@@ -3810,7 +4197,7 @@ var ConversationCache = class _ConversationCache {
3810
4197
  if (!fileState) return null;
3811
4198
  let stat3;
3812
4199
  try {
3813
- stat3 = (0, import_fs8.statSync)(filePath);
4200
+ stat3 = (0, import_fs9.statSync)(filePath);
3814
4201
  } catch {
3815
4202
  return null;
3816
4203
  }
@@ -3831,17 +4218,17 @@ var ConversationCache = class _ConversationCache {
3831
4218
  );
3832
4219
  if (rows.length === 0) return { messages: [], total, fromIndex: from };
3833
4220
  const messages = [];
3834
- const fd = (0, import_fs8.openSync)(filePath, "r");
4221
+ const fd = (0, import_fs9.openSync)(filePath, "r");
3835
4222
  try {
3836
4223
  const state = (0, import_scanner2.createJsonlParseState)();
3837
4224
  for (const row of rows) {
3838
4225
  const buf = Buffer.alloc(row.byte_length);
3839
- (0, import_fs8.readSync)(fd, buf, 0, row.byte_length, row.byte_offset);
4226
+ (0, import_fs9.readSync)(fd, buf, 0, row.byte_length, row.byte_offset);
3840
4227
  const msg = (0, import_scanner2.parseJsonlLine)(buf.toString("utf-8"), state);
3841
4228
  if (msg) messages.push(msg);
3842
4229
  }
3843
4230
  } finally {
3844
- (0, import_fs8.closeSync)(fd);
4231
+ (0, import_fs9.closeSync)(fd);
3845
4232
  }
3846
4233
  return { messages, total, fromIndex: from };
3847
4234
  }
@@ -3869,14 +4256,14 @@ var ConversationCache = class _ConversationCache {
3869
4256
  isAgentFileCached(filePath) {
3870
4257
  let s;
3871
4258
  try {
3872
- s = (0, import_fs8.statSync)(filePath);
4259
+ s = (0, import_fs9.statSync)(filePath);
3873
4260
  } catch {
3874
4261
  return false;
3875
4262
  }
3876
4263
  return this.classifyAgentFile(filePath, s.mtimeMs, s.size);
3877
4264
  }
3878
4265
  static open(dbPath, tailSize = 10, migrationsDir, options) {
3879
- (0, import_fs8.mkdirSync)((0, import_path10.dirname)(dbPath), { recursive: true });
4266
+ (0, import_fs9.mkdirSync)((0, import_path11.dirname)(dbPath), { recursive: true });
3880
4267
  const db = new import_better_sqlite3.default(dbPath);
3881
4268
  db.pragma("journal_mode = WAL");
3882
4269
  db.pragma("foreign_keys = ON");
@@ -4057,7 +4444,7 @@ var ConversationCache = class _ConversationCache {
4057
4444
  let mtimeMs = null;
4058
4445
  let fileSize = null;
4059
4446
  try {
4060
- const s = (0, import_fs8.statSync)(m.filePath);
4447
+ const s = (0, import_fs9.statSync)(m.filePath);
4061
4448
  mtimeMs = s.mtimeMs;
4062
4449
  fileSize = s.size;
4063
4450
  } catch {
@@ -4116,8 +4503,8 @@ var ConversationCache = class _ConversationCache {
4116
4503
  let fileSize;
4117
4504
  let fd;
4118
4505
  try {
4119
- fileSize = (0, import_fs8.statSync)(filePath).size;
4120
- fd = (0, import_fs8.openSync)(filePath, "r");
4506
+ fileSize = (0, import_fs9.statSync)(filePath).size;
4507
+ fd = (0, import_fs9.openSync)(filePath, "r");
4121
4508
  } catch {
4122
4509
  return false;
4123
4510
  }
@@ -4130,7 +4517,7 @@ var ConversationCache = class _ConversationCache {
4130
4517
  while (pos > 0 && lines.length < this.tailSize * 4) {
4131
4518
  const toRead = Math.min(CHUNK, pos);
4132
4519
  pos -= toRead;
4133
- (0, import_fs8.readSync)(fd, buf, 0, toRead, pos);
4520
+ (0, import_fs9.readSync)(fd, buf, 0, toRead, pos);
4134
4521
  const chunk = buf.subarray(0, toRead).toString("utf8");
4135
4522
  const combined = chunk + partial;
4136
4523
  const parts = combined.split("\n");
@@ -4141,7 +4528,7 @@ var ConversationCache = class _ConversationCache {
4141
4528
  }
4142
4529
  if (partial) lines.push(partial);
4143
4530
  } finally {
4144
- (0, import_fs8.closeSync)(fd);
4531
+ (0, import_fs9.closeSync)(fd);
4145
4532
  }
4146
4533
  const msgs = [];
4147
4534
  for (let i = 0; i < lines.length && msgs.length < this.tailSize; i++) {
@@ -4343,7 +4730,7 @@ var ConversationCache = class _ConversationCache {
4343
4730
  * `handleGetConversation` can still serve the cached tail even when the
4344
4731
  * JSONL has been deleted.
4345
4732
  */
4346
- pruneGhostFiles(exists = import_fs8.existsSync) {
4733
+ pruneGhostFiles(exists = import_fs9.existsSync) {
4347
4734
  const rows = this.stmts.allFilePaths.all();
4348
4735
  const ghosts = [];
4349
4736
  const prune = this.db.transaction((ids) => {
@@ -4398,7 +4785,7 @@ var ConversationCache = class _ConversationCache {
4398
4785
  * Returns the removed IDs.
4399
4786
  */
4400
4787
  reconcileDeletions(livePaths, opts) {
4401
- const exists = opts?.exists ?? import_fs8.existsSync;
4788
+ const exists = opts?.exists ?? import_fs9.existsSync;
4402
4789
  const rows = this.stmts.allFilePaths.all();
4403
4790
  const removed = [];
4404
4791
  const drop = this.db.transaction((ids) => {
@@ -4627,23 +5014,23 @@ async function recordUpload(pool2, instanceId, row) {
4627
5014
  }
4628
5015
 
4629
5016
  // src/handlers/handleListProjects.ts
4630
- var import_fs9 = require("fs");
4631
- var import_os5 = require("os");
4632
- var import_path11 = require("path");
5017
+ var import_fs10 = require("fs");
5018
+ var import_os6 = require("os");
5019
+ var import_path12 = require("path");
4633
5020
  function decodeProjectPath(dirName) {
4634
5021
  return dirName.replace(/-/g, "/");
4635
5022
  }
4636
5023
  function handleListProjects(url, res) {
4637
5024
  const limit = Math.max(1, parseInt(url.searchParams.get("limit") ?? "50", 10) || 50);
4638
5025
  const offset = Math.max(0, parseInt(url.searchParams.get("offset") ?? "0", 10) || 0);
4639
- const projectsDir = (0, import_path11.join)((0, import_os5.homedir)(), ".claude", "projects");
5026
+ const projectsDir = (0, import_path12.join)((0, import_os6.homedir)(), ".claude", "projects");
4640
5027
  let entries;
4641
5028
  try {
4642
- entries = (0, import_fs9.readdirSync)(projectsDir).map((dirName) => {
4643
- const fullPath = (0, import_path11.join)(projectsDir, dirName);
5029
+ entries = (0, import_fs10.readdirSync)(projectsDir).map((dirName) => {
5030
+ const fullPath = (0, import_path12.join)(projectsDir, dirName);
4644
5031
  let mtime = 0;
4645
5032
  try {
4646
- mtime = (0, import_fs9.statSync)(fullPath).mtimeMs;
5033
+ mtime = (0, import_fs10.statSync)(fullPath).mtimeMs;
4647
5034
  } catch {
4648
5035
  }
4649
5036
  const path = decodeProjectPath(dirName);
@@ -4738,7 +5125,7 @@ function seal(plaintext, recipientPublicKeyBase64) {
4738
5125
 
4739
5126
  // src/services/conversations/conversationWatcher.ts
4740
5127
  var import_chokidar = __toESM(require("chokidar"), 1);
4741
- var import_fs10 = require("fs");
5128
+ var import_fs11 = require("fs");
4742
5129
  var import_promises5 = require("fs/promises");
4743
5130
  var ConversationWatcher = class {
4744
5131
  files = /* @__PURE__ */ new Map();
@@ -4761,7 +5148,7 @@ var ConversationWatcher = class {
4761
5148
  if (this.files.has(filePath)) return;
4762
5149
  let offset;
4763
5150
  try {
4764
- offset = (0, import_fs10.statSync)(filePath).size;
5151
+ offset = (0, import_fs11.statSync)(filePath).size;
4765
5152
  } catch {
4766
5153
  offset = 0;
4767
5154
  }
@@ -4936,14 +5323,14 @@ function findSearchTarget(messages, query) {
4936
5323
  }
4937
5324
 
4938
5325
  // src/services/conversations/pruneAgentConversations.ts
4939
- var import_fs11 = require("fs");
5326
+ var import_fs12 = require("fs");
4940
5327
  function pruneAgentConversations(cache) {
4941
5328
  const db = cache.getDatabase();
4942
5329
  const rows = db.prepare("SELECT id, file_path FROM conversation_meta").all();
4943
5330
  let pruned = 0;
4944
5331
  let missing = 0;
4945
5332
  for (const row of rows) {
4946
- if (!(0, import_fs11.existsSync)(row.file_path)) {
5333
+ if (!(0, import_fs12.existsSync)(row.file_path)) {
4947
5334
  missing += 1;
4948
5335
  continue;
4949
5336
  }
@@ -4966,13 +5353,6 @@ function deriveProjectChatTitle(input) {
4966
5353
  return `Untitled \xB7 ${input.id.slice(0, 8)}`;
4967
5354
  }
4968
5355
 
4969
- // src/services/questions/permissionAnswerKeys.ts
4970
- var ANSWER_KEYS_ALLOWLIST = /^(?:\r|[yn]\r|\x03|\d+\r)$/;
4971
- function sanitizeAnswerKeys(keys) {
4972
- if (keys === void 0) return void 0;
4973
- return ANSWER_KEYS_ALLOWLIST.test(keys) ? keys : void 0;
4974
- }
4975
-
4976
5356
  // src/services/questions/detectAskUserQuestion.ts
4977
5357
  function normalizeContent2(raw) {
4978
5358
  if (Array.isArray(raw)) return raw;
@@ -5286,7 +5666,7 @@ function discoveredToResponse(d, conversationId) {
5286
5666
  var import_crypto8 = require("crypto");
5287
5667
  var import_promises6 = require("fs/promises");
5288
5668
  var import_heic_convert = __toESM(require("heic-convert"), 1);
5289
- var import_path12 = require("path");
5669
+ var import_path13 = require("path");
5290
5670
  var UPLOAD_DIR_NAME = ".threadbase-uploads";
5291
5671
  var MAX_BYTES = 25 * 1024 * 1024;
5292
5672
  var HEIC_MIMES = /* @__PURE__ */ new Set(["image/heic", "image/heif"]);
@@ -5319,9 +5699,9 @@ async function saveUploadFile(input) {
5319
5699
  }
5320
5700
  const id = `up_${(0, import_crypto8.randomBytes)(8).toString("hex")}`;
5321
5701
  const safeName = sanitizeFilename(originalName) || `file${MIME_TO_EXT[mimeType] ?? ""}`;
5322
- const dir = (0, import_path12.join)(input.projectPath, UPLOAD_DIR_NAME, input.sessionId);
5702
+ const dir = (0, import_path13.join)(input.projectPath, UPLOAD_DIR_NAME, input.sessionId);
5323
5703
  await (0, import_promises6.mkdir)(dir, { recursive: true });
5324
- const filePath = (0, import_path12.join)(dir, `${Date.now()}-${id}-${safeName}`);
5704
+ const filePath = (0, import_path13.join)(dir, `${Date.now()}-${id}-${safeName}`);
5325
5705
  await (0, import_promises6.writeFile)(filePath, buffer);
5326
5706
  return {
5327
5707
  id,
@@ -5577,10 +5957,8 @@ var WSHub = class {
5577
5957
  var BROWSE_SYSTEM_PROMPT = (browseRoot) => `You are working within the project boundary: ${browseRoot}. Do not read, write, or execute commands that access files or directories outside this boundary.`;
5578
5958
  var DEFAULT_SYSTEM_PROMPT = "When presenting options or choices to the user, limit the options to at most 3.";
5579
5959
  var DEFAULT_PTY_GRACE_PERIOD_MS = 27e4;
5580
- var DEFAULT_WS_AUTH_TIMEOUT_MS = 5e3;
5581
- var WS_CLOSE_UNAUTHORIZED = 4401;
5582
5960
  var REFRESH_TTL_MS = 2e3;
5583
- var START_READY_TIMEOUT_MS = 15e3;
5961
+ var START_READY_TIMEOUT_MS = 1e4;
5584
5962
  function parseIncludeAgentsEnv(raw) {
5585
5963
  if (raw === void 0) return false;
5586
5964
  const v = raw.trim().toLowerCase();
@@ -5669,14 +6047,6 @@ var StreamerServer = class {
5669
6047
  clientIdToWs = /* @__PURE__ */ new Map();
5670
6048
  // Reverse map for cleanup on close
5671
6049
  wsToClientId = /* @__PURE__ */ new Map();
5672
- // M1 — WS auth. Sockets that have authenticated (via ?key= at upgrade OR a
5673
- // { type: "auth", token } first message). Only authed sockets are added to
5674
- // the hub and receive broadcasts.
5675
- // Plan: https://github.com/RonenMars/threadbase-streamer/blob/a251353bfa417bd48ce3f15086bc336a2c622629/docs/plans/2026-06-24-security-hardening.md#L40
5676
- wsAuthed = /* @__PURE__ */ new Set();
5677
- // Keyless sockets awaiting their first-message auth handshake → close timer.
5678
- wsAuthPending = /* @__PURE__ */ new Map();
5679
- wsAuthTimeoutMs;
5680
6050
  cache = null;
5681
6051
  projectsRepo = null;
5682
6052
  conversationsRepo = null;
@@ -5714,12 +6084,11 @@ var StreamerServer = class {
5714
6084
  this.disableDb = config.disableDb ?? false;
5715
6085
  this.scannerPersistenceDisabled = config.scannerPersistent === false;
5716
6086
  this.scanProfiles = config.scanProfiles;
5717
- this.codexRoots = config.codexRoots ?? [(0, import_path13.join)((0, import_os6.homedir)(), ".codex", "sessions")];
6087
+ this.codexRoots = config.codexRoots ?? [(0, import_path14.join)((0, import_os7.homedir)(), ".codex", "sessions")];
5718
6088
  this.ptyGracePeriodMs = config.ptyGracePeriodMs ?? DEFAULT_PTY_GRACE_PERIOD_MS;
5719
- this.wsAuthTimeoutMs = config.wsAuthTimeoutMs ?? DEFAULT_WS_AUTH_TIMEOUT_MS;
5720
6089
  this.defaultSystemPrompt = config.defaultSystemPrompt ?? DEFAULT_SYSTEM_PROMPT;
5721
6090
  this.defaultPermissionMode = config.defaultPermissionMode ?? loadDefaultPermissionMode() ?? "acceptEdits";
5722
- this.cacheDir = config.cacheDir ?? loadCacheDir() ?? (0, import_path13.join)((0, import_os6.homedir)(), ".threadbase", "cache");
6091
+ this.cacheDir = config.cacheDir ?? loadCacheDir() ?? (0, import_path14.join)((0, import_os7.homedir)(), ".threadbase", "cache");
5723
6092
  this.tailSize = config.tailSize ?? loadTailSize() ?? 10;
5724
6093
  this.directoryDebounceMs = parseDirScanDebounceEnv(process.env.THREADBASE_DIR_SCAN_DEBOUNCE_MS) ?? config.directoryScanDebounceMs ?? 1e3;
5725
6094
  this.markScannerStaleDebounced = debounce(() => {
@@ -5760,7 +6129,7 @@ var StreamerServer = class {
5760
6129
  const seqs = cache.extendMessageIndex(
5761
6130
  filePath,
5762
6131
  spans,
5763
- (0, import_fs12.statSync)(filePath),
6132
+ (0, import_fs13.statSync)(filePath),
5764
6133
  readFrom,
5765
6134
  endOffset
5766
6135
  );
@@ -5845,6 +6214,9 @@ var StreamerServer = class {
5845
6214
  onOutput: (sessionId, data) => {
5846
6215
  this.wsHub.broadcast({ type: "terminal_output", sessionId, data });
5847
6216
  },
6217
+ onUserMessage: (sessionId, text, ts) => {
6218
+ this.wsHub.broadcast({ type: "user_message", sessionId, text, ts });
6219
+ },
5848
6220
  onPermissionChange: (sessionId, gate) => {
5849
6221
  this.handlePermissionChange(sessionId, gate);
5850
6222
  },
@@ -5921,7 +6293,7 @@ var StreamerServer = class {
5921
6293
  temporalClient,
5922
6294
  taskQueue: agentConfig.temporal.taskQueue
5923
6295
  });
5924
- const conversationsBaseDir = agentConfig.conversationsDir || (0, import_path13.join)((0, import_path13.dirname)(this.cacheDir), "conversations");
6296
+ const conversationsBaseDir = agentConfig.conversationsDir || (0, import_path14.join)((0, import_path14.dirname)(this.cacheDir), "conversations");
5925
6297
  conversationWriter = createConversationWriter({
5926
6298
  baseDir: conversationsBaseDir
5927
6299
  });
@@ -5974,39 +6346,17 @@ var StreamerServer = class {
5974
6346
  handlePairExchange: (req, res) => this.handlePairExchange(req, res),
5975
6347
  handleBrowse: (url, res) => this.handleBrowse(url, res),
5976
6348
  handleMkdir: (req, res) => this.handleMkdir(req, res),
5977
- handleWsOpen: (ws, preAuthed) => {
5978
- if (preAuthed) {
5979
- this.completeWsAuth(ws);
5980
- return;
6349
+ handleWsOpen: (ws) => {
6350
+ this.wsHub.addClient(ws);
6351
+ const sessions = this.sessionStore.list(this.ptyAttachedIds());
6352
+ ws.send(JSON.stringify({ type: "session_list", sessions }));
6353
+ if (this.cacheReady) {
6354
+ ws.send(JSON.stringify({ type: "cache_ready" }));
5981
6355
  }
5982
- const timer = setTimeout(() => {
5983
- this.wsAuthPending.delete(ws);
5984
- try {
5985
- ws.close(WS_CLOSE_UNAUTHORIZED, "auth timeout");
5986
- } catch {
5987
- }
5988
- }, this.wsAuthTimeoutMs);
5989
- this.wsAuthPending.set(ws, timer);
5990
6356
  },
5991
6357
  handleWsMessage: async (ws, raw) => {
5992
6358
  try {
5993
6359
  const msg = JSON.parse(String(raw));
5994
- if (!this.wsAuthed.has(ws)) {
5995
- if (msg.type === "auth" && typeof msg.token === "string") {
5996
- const t = this.wsAuthPending.get(ws);
5997
- if (t) clearTimeout(t);
5998
- this.wsAuthPending.delete(ws);
5999
- if (validateApiKey(msg.token, this.apiKey)) {
6000
- this.completeWsAuth(ws);
6001
- } else {
6002
- try {
6003
- ws.close(WS_CLOSE_UNAUTHORIZED, "unauthorized");
6004
- } catch {
6005
- }
6006
- }
6007
- }
6008
- return;
6009
- }
6010
6360
  if (msg.type === "register" && typeof msg.clientId === "string") {
6011
6361
  const oldClientId = this.wsToClientId.get(ws);
6012
6362
  if (oldClientId) this.clientIdToWs.delete(oldClientId);
@@ -6017,24 +6367,56 @@ var StreamerServer = class {
6017
6367
  this.addSessionSubscriber(msg.sessionId, ws);
6018
6368
  if (this.ptyManager.hasSession(msg.sessionId)) {
6019
6369
  const lines = await this.ptyManager.getOutputLines(msg.sessionId, 200);
6020
- ws.send(JSON.stringify({ type: "terminal_replay", sessionId: msg.sessionId, lines }));
6370
+ const userMessages = this.ptyManager.getInputHistory(msg.sessionId);
6371
+ ws.send(
6372
+ JSON.stringify({
6373
+ type: "terminal_replay",
6374
+ sessionId: msg.sessionId,
6375
+ lines,
6376
+ userMessages
6377
+ })
6378
+ );
6379
+ }
6380
+ const pendingGate = this.pendingPermission.get(msg.sessionId);
6381
+ if (pendingGate) {
6382
+ this.log.info(`[ws.replay_permission] ${msg.sessionId.slice(0, 8)}`, {
6383
+ event: "ws.replay_permission",
6384
+ sessionId: msg.sessionId
6385
+ });
6386
+ ws.send(
6387
+ JSON.stringify({
6388
+ type: "permission",
6389
+ sessionId: msg.sessionId,
6390
+ ...pendingGate.prompt ? { prompt: pendingGate.prompt } : {},
6391
+ ...pendingGate.detail ? { detail: pendingGate.detail } : {},
6392
+ options: pendingGate.options,
6393
+ ...pendingGate.cursor !== void 0 ? { cursor: pendingGate.cursor } : {}
6394
+ })
6395
+ );
6396
+ }
6397
+ const pendingQuestion = this.pendingQuestions.get(msg.sessionId);
6398
+ if (pendingQuestion) {
6399
+ this.log.info(`[ws.replay_question] ${msg.sessionId.slice(0, 8)}`, {
6400
+ event: "ws.replay_question",
6401
+ sessionId: msg.sessionId
6402
+ });
6403
+ ws.send(
6404
+ JSON.stringify({
6405
+ type: "question",
6406
+ sessionId: msg.sessionId,
6407
+ toolUseId: pendingQuestion.toolUseId,
6408
+ questions: pendingQuestion.questions
6409
+ })
6410
+ );
6021
6411
  }
6022
6412
  }
6023
6413
  if (msg.type === "hold_session" && typeof msg.sessionId === "string") {
6024
- if (this.sessionSubscribers.get(msg.sessionId)?.has(ws)) {
6025
- this.startGraceTimer(msg.sessionId, 0);
6026
- }
6414
+ this.startGraceTimer(msg.sessionId, 0);
6027
6415
  }
6028
6416
  } catch {
6029
6417
  }
6030
6418
  },
6031
6419
  handleWsClose: (ws) => {
6032
- const pendingTimer = this.wsAuthPending.get(ws);
6033
- if (pendingTimer) {
6034
- clearTimeout(pendingTimer);
6035
- this.wsAuthPending.delete(ws);
6036
- }
6037
- this.wsAuthed.delete(ws);
6038
6420
  const clientId = this.wsToClientId.get(ws);
6039
6421
  if (clientId) {
6040
6422
  this.clientIdToWs.delete(clientId);
@@ -6104,20 +6486,6 @@ var StreamerServer = class {
6104
6486
  this.wsHub.broadcast(payload);
6105
6487
  }
6106
6488
  }
6107
- // M1: finalize a WebSocket auth (via ?key= at upgrade or a first-message
6108
- // handshake) — register it with the hub and send the initial snapshot. Only
6109
- // authed sockets reach this, so no unauthenticated client ever receives a
6110
- // broadcast.
6111
- // Plan: https://github.com/RonenMars/threadbase-streamer/blob/a251353bfa417bd48ce3f15086bc336a2c622629/docs/plans/2026-06-24-security-hardening.md#L40
6112
- completeWsAuth(ws) {
6113
- this.wsAuthed.add(ws);
6114
- this.wsHub.addClient(ws);
6115
- const sessions = this.sessionStore.list(this.ptyAttachedIds());
6116
- ws.send(JSON.stringify({ type: "session_list", sessions }));
6117
- if (this.cacheReady) {
6118
- ws.send(JSON.stringify({ type: "cache_ready" }));
6119
- }
6120
- }
6121
6489
  addSessionSubscriber(sessionId, ws) {
6122
6490
  let subs = this.sessionSubscribers.get(sessionId);
6123
6491
  if (!subs) {
@@ -6189,7 +6557,7 @@ var StreamerServer = class {
6189
6557
  });
6190
6558
  try {
6191
6559
  this.cache = ConversationCache.open(
6192
- (0, import_path13.join)(this.cacheDir, "cache.db"),
6560
+ (0, import_path14.join)(this.cacheDir, "cache.db"),
6193
6561
  this.tailSize,
6194
6562
  void 0,
6195
6563
  {
@@ -6215,11 +6583,11 @@ var StreamerServer = class {
6215
6583
  if (this.scanProfiles && this.scanProfiles.length > 0) {
6216
6584
  for (const profile of this.scanProfiles) {
6217
6585
  if (profile.enabled) {
6218
- this.fileWatcher.watchDirectory((0, import_path13.join)(profile.configDir, "projects"));
6586
+ this.fileWatcher.watchDirectory((0, import_path14.join)(profile.configDir, "projects"));
6219
6587
  }
6220
6588
  }
6221
6589
  } else {
6222
- this.fileWatcher.watchDirectory((0, import_path13.join)((0, import_os6.homedir)(), ".claude", "projects"));
6590
+ this.fileWatcher.watchDirectory((0, import_path14.join)((0, import_os7.homedir)(), ".claude", "projects"));
6223
6591
  }
6224
6592
  } catch (err) {
6225
6593
  const message = err instanceof Error ? err.message : String(err);
@@ -6404,9 +6772,6 @@ var StreamerServer = class {
6404
6772
  this.ptyManager.dispose();
6405
6773
  this.fileWatcher.dispose();
6406
6774
  this.wsHub.dispose();
6407
- for (const timer of this.wsAuthPending.values()) clearTimeout(timer);
6408
- this.wsAuthPending.clear();
6409
- this.wsAuthed.clear();
6410
6775
  this.pairTokens.dispose();
6411
6776
  if (this.dbPool) {
6412
6777
  await this.dbPool.end();
@@ -6804,17 +7169,17 @@ var StreamerServer = class {
6804
7169
  return scanner;
6805
7170
  }
6806
7171
  findJsonlPath(uuid) {
6807
- const projectsDir = (0, import_path13.join)((0, import_os6.homedir)(), ".claude", "projects");
6808
- if (!(0, import_fs12.existsSync)(projectsDir)) return null;
7172
+ const projectsDir = (0, import_path14.join)((0, import_os7.homedir)(), ".claude", "projects");
7173
+ if (!(0, import_fs13.existsSync)(projectsDir)) return null;
6809
7174
  const filename = `${uuid}.jsonl`;
6810
- for (const dir of (0, import_fs12.readdirSync)(projectsDir)) {
6811
- const fp = (0, import_path13.join)(projectsDir, dir, filename);
6812
- if ((0, import_fs12.existsSync)(fp)) return fp;
6813
- const projectDir = (0, import_path13.join)(projectsDir, dir);
7175
+ for (const dir of (0, import_fs13.readdirSync)(projectsDir)) {
7176
+ const fp = (0, import_path14.join)(projectsDir, dir, filename);
7177
+ if ((0, import_fs13.existsSync)(fp)) return fp;
7178
+ const projectDir = (0, import_path14.join)(projectsDir, dir);
6814
7179
  try {
6815
- for (const sub of (0, import_fs12.readdirSync)(projectDir)) {
6816
- const subagentPath = (0, import_path13.join)(projectDir, sub, "subagents", filename);
6817
- if ((0, import_fs12.existsSync)(subagentPath)) return subagentPath;
7180
+ for (const sub of (0, import_fs13.readdirSync)(projectDir)) {
7181
+ const subagentPath = (0, import_path14.join)(projectDir, sub, "subagents", filename);
7182
+ if ((0, import_fs13.existsSync)(subagentPath)) return subagentPath;
6818
7183
  }
6819
7184
  } catch {
6820
7185
  }
@@ -6823,7 +7188,7 @@ var StreamerServer = class {
6823
7188
  }
6824
7189
  async readCwdFromJsonl(filePath) {
6825
7190
  return new Promise((resolve2) => {
6826
- const rl = (0, import_readline.createInterface)({ input: (0, import_fs12.createReadStream)(filePath), crlfDelay: Infinity });
7191
+ const rl = (0, import_readline.createInterface)({ input: (0, import_fs13.createReadStream)(filePath), crlfDelay: Infinity });
6827
7192
  let found = false;
6828
7193
  rl.on("line", (line) => {
6829
7194
  if (found) return;
@@ -6961,7 +7326,7 @@ var StreamerServer = class {
6961
7326
  if (!conv.filePath) return false;
6962
7327
  let mtimeMs = null;
6963
7328
  try {
6964
- mtimeMs = (0, import_fs12.statSync)(conv.filePath).mtimeMs;
7329
+ mtimeMs = (0, import_fs13.statSync)(conv.filePath).mtimeMs;
6965
7330
  } catch {
6966
7331
  return false;
6967
7332
  }
@@ -7329,7 +7694,7 @@ var StreamerServer = class {
7329
7694
  handleGetSession(sessionId, res) {
7330
7695
  const session = this.sessionStore.get(sessionId, this.ptyAttachedIds());
7331
7696
  if (session) {
7332
- if (!(0, import_fs12.existsSync)(session.projectPath)) {
7697
+ if (!(0, import_fs13.existsSync)(session.projectPath)) {
7333
7698
  session.failureReason = `Project directory not found: ${session.projectPath}`;
7334
7699
  }
7335
7700
  json(res, 200, session);
@@ -7533,16 +7898,17 @@ var StreamerServer = class {
7533
7898
  return;
7534
7899
  }
7535
7900
  this.pendingPermission.set(sessionId, gate);
7536
- const safeOptions = gate.options.map((o) => {
7537
- const answerKeys = sanitizeAnswerKeys(o.answerKeys);
7538
- return answerKeys === void 0 ? { index: o.index, label: o.label } : { ...o, answerKeys };
7539
- });
7901
+ const subscriberCount = this.sessionSubscribers.get(sessionId)?.size ?? 0;
7902
+ this.log.info(
7903
+ `[ws.broadcast_permission] ${sessionId.slice(0, 8)} subscribers=${subscriberCount}`,
7904
+ { event: "ws.broadcast_permission", sessionId, subscriberCount }
7905
+ );
7540
7906
  this.wsHub.broadcast({
7541
7907
  type: "permission",
7542
7908
  sessionId,
7543
7909
  ...gate.prompt ? { prompt: gate.prompt } : {},
7544
7910
  ...gate.detail ? { detail: gate.detail } : {},
7545
- options: safeOptions,
7911
+ options: gate.options,
7546
7912
  ...gate.cursor !== void 0 ? { cursor: gate.cursor } : {}
7547
7913
  });
7548
7914
  }
@@ -7729,7 +8095,7 @@ var StreamerServer = class {
7729
8095
  sessionStore: this.sessionStore,
7730
8096
  // biome-ignore lint/style/noNonNullAssertion: agentClient is set when agentConfig.enabled is true
7731
8097
  agentClient: this.agentClient,
7732
- conversationsDir: this.cacheDir ? (0, import_path13.join)((0, import_path13.dirname)(this.cacheDir), "conversations") : "",
8098
+ conversationsDir: this.cacheDir ? (0, import_path14.join)((0, import_path14.dirname)(this.cacheDir), "conversations") : "",
7733
8099
  agentConfig: this.agentConfig
7734
8100
  });
7735
8101
  json(res, result.status, result.body);
@@ -7870,9 +8236,9 @@ var StreamerServer = class {
7870
8236
  // was passed to Claude via --session-id so the filename matches from the start.
7871
8237
  watchForJsonl(sessionId, projectPath) {
7872
8238
  const encoded = projectPath.replace(/[/\\:.]/g, "-");
7873
- const projectsDir = (0, import_path13.join)((0, import_os6.homedir)(), ".claude", "projects", encoded);
8239
+ const projectsDir = (0, import_path14.join)((0, import_os7.homedir)(), ".claude", "projects", encoded);
7874
8240
  const expectedFile = `${sessionId}.jsonl`;
7875
- const filePath = (0, import_path13.join)(projectsDir, expectedFile);
8241
+ const filePath = (0, import_path14.join)(projectsDir, expectedFile);
7876
8242
  const deadline = Date.now() + 12e4;
7877
8243
  let watcher = null;
7878
8244
  const cleanup = () => {
@@ -7890,12 +8256,12 @@ var StreamerServer = class {
7890
8256
  cleanup();
7891
8257
  return;
7892
8258
  }
7893
- let resolvedFilePath = (0, import_fs12.existsSync)(filePath) ? filePath : null;
7894
- if (!resolvedFilePath && (0, import_fs12.existsSync)(projectsDir)) {
8259
+ let resolvedFilePath = (0, import_fs13.existsSync)(filePath) ? filePath : null;
8260
+ if (!resolvedFilePath && (0, import_fs13.existsSync)(projectsDir)) {
7895
8261
  try {
7896
8262
  const now = Date.now();
7897
- const recent = (0, import_fs12.readdirSync)(projectsDir).filter((f) => f.endsWith(".jsonl")).map((f) => ({ f, mtime: (0, import_fs12.statSync)((0, import_path13.join)(projectsDir, f)).mtimeMs })).filter(({ mtime }) => now - mtime < 5e3).sort((a, b) => b.mtime - a.mtime)[0];
7898
- if (recent) resolvedFilePath = (0, import_path13.join)(projectsDir, recent.f);
8263
+ const recent = (0, import_fs13.readdirSync)(projectsDir).filter((f) => f.endsWith(".jsonl")).map((f) => ({ f, mtime: (0, import_fs13.statSync)((0, import_path14.join)(projectsDir, f)).mtimeMs })).filter(({ mtime }) => now - mtime < 5e3).sort((a, b) => b.mtime - a.mtime)[0];
8264
+ if (recent) resolvedFilePath = (0, import_path14.join)(projectsDir, recent.f);
7899
8265
  } catch {
7900
8266
  }
7901
8267
  }
@@ -7904,7 +8270,7 @@ var StreamerServer = class {
7904
8270
  this.sessionFileMap.set(sessionId, resolvedFilePath);
7905
8271
  this.fileWatcher.watch(resolvedFilePath);
7906
8272
  try {
7907
- const existing = (0, import_fs12.readFileSync)(resolvedFilePath, "utf8").split("\n").filter(Boolean);
8273
+ const existing = (0, import_fs13.readFileSync)(resolvedFilePath, "utf8").split("\n").filter(Boolean);
7908
8274
  if (existing.length > 0) {
7909
8275
  this.broadcastConversationLines(sessionId, existing);
7910
8276
  }
@@ -7927,7 +8293,7 @@ var StreamerServer = class {
7927
8293
  if (this.sessionFileMap.has(sessionId)) return;
7928
8294
  try {
7929
8295
  require("fs").mkdirSync(projectsDir, { recursive: true });
7930
- watcher = (0, import_fs12.watch)(projectsDir, tryWire);
8296
+ watcher = (0, import_fs13.watch)(projectsDir, tryWire);
7931
8297
  watcher.on("error", cleanup);
7932
8298
  } catch {
7933
8299
  }
@@ -7943,7 +8309,7 @@ var StreamerServer = class {
7943
8309
  watchForCodexRollout(sessionId, projectPath) {
7944
8310
  const deadline = Date.now() + 12e4;
7945
8311
  const now = /* @__PURE__ */ new Date();
7946
- const dateDir = (0, import_path13.join)(
8312
+ const dateDir = (0, import_path14.join)(
7947
8313
  String(now.getFullYear()),
7948
8314
  String(now.getMonth() + 1).padStart(2, "0"),
7949
8315
  String(now.getDate()).padStart(2, "0")
@@ -7956,7 +8322,7 @@ var StreamerServer = class {
7956
8322
  };
7957
8323
  const matchesProjectPath = (candidatePath) => {
7958
8324
  try {
7959
- const firstLine = (0, import_fs12.readFileSync)(candidatePath, "utf8").split("\n", 1)[0];
8325
+ const firstLine = (0, import_fs13.readFileSync)(candidatePath, "utf8").split("\n", 1)[0];
7960
8326
  if (!firstLine) return null;
7961
8327
  const parsed = JSON.parse(firstLine);
7962
8328
  if (parsed?.type !== "session_meta") return null;
@@ -7984,18 +8350,18 @@ var StreamerServer = class {
7984
8350
  this.sessionStore.listManaged().filter((s) => s.id !== sessionId && s.boundConversationId != null).map((s) => s.boundConversationId)
7985
8351
  );
7986
8352
  for (const root of this.codexRoots) {
7987
- const sessionsDir = (0, import_path13.join)(root, dateDir);
7988
- if (!(0, import_fs12.existsSync)(sessionsDir)) continue;
8353
+ const sessionsDir = (0, import_path14.join)(root, dateDir);
8354
+ if (!(0, import_fs13.existsSync)(sessionsDir)) continue;
7989
8355
  let candidateFiles;
7990
8356
  try {
7991
- candidateFiles = (0, import_fs12.readdirSync)(sessionsDir).filter((f) => f.endsWith(".jsonl"));
8357
+ candidateFiles = (0, import_fs13.readdirSync)(sessionsDir).filter((f) => f.endsWith(".jsonl"));
7992
8358
  } catch {
7993
8359
  continue;
7994
8360
  }
7995
8361
  const nowMs = Date.now();
7996
- const recentCandidates = candidateFiles.map((f) => ({ f, mtime: (0, import_fs12.statSync)((0, import_path13.join)(sessionsDir, f)).mtimeMs })).filter(({ mtime }) => nowMs - mtime < 1e4).sort((a, b) => b.mtime - a.mtime);
8362
+ const recentCandidates = candidateFiles.map((f) => ({ f, mtime: (0, import_fs13.statSync)((0, import_path14.join)(sessionsDir, f)).mtimeMs })).filter(({ mtime }) => nowMs - mtime < 1e4).sort((a, b) => b.mtime - a.mtime);
7997
8363
  for (const { f } of recentCandidates) {
7998
- const candidatePath = (0, import_path13.join)(sessionsDir, f);
8364
+ const candidatePath = (0, import_path14.join)(sessionsDir, f);
7999
8365
  const match = matchesProjectPath(candidatePath);
8000
8366
  if (!match) continue;
8001
8367
  if (boundElsewhere.has(match.id)) continue;
@@ -8005,7 +8371,7 @@ var StreamerServer = class {
8005
8371
  this.sessionFileMap.set(sessionId, candidatePath);
8006
8372
  this.fileWatcher.watch(candidatePath);
8007
8373
  try {
8008
- const existing = (0, import_fs12.readFileSync)(candidatePath, "utf8").split("\n").filter(Boolean);
8374
+ const existing = (0, import_fs13.readFileSync)(candidatePath, "utf8").split("\n").filter(Boolean);
8009
8375
  if (existing.length > 0) {
8010
8376
  this.broadcastConversationLines(sessionId, existing);
8011
8377
  }
@@ -8127,7 +8493,7 @@ var StreamerServer = class {
8127
8493
  };
8128
8494
  function classifyResumability(cwd) {
8129
8495
  if (!cwd) return { resumable: true };
8130
- if ((0, import_fs12.existsSync)(cwd)) return { resumable: true };
8496
+ if ((0, import_fs13.existsSync)(cwd)) return { resumable: true };
8131
8497
  const ranInWorktree = /\/\.worktrees\//.test(cwd) || /\/\.claude\/worktrees\//.test(cwd);
8132
8498
  return {
8133
8499
  resumable: false,