@threadbase-sh/streamer 1.29.2 → 1.30.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
@@ -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,6 +732,69 @@ 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;
737
800
  var PTY_COLS = 120;
@@ -739,6 +802,9 @@ var PTY_ROWS = 40;
739
802
  var SCREEN_SCROLLBACK = 1e3;
740
803
  var CODEX_PROMPT_READY_TEXT = "Ready";
741
804
  var CODEX_TRUST_GATE_REGEX = /trust the contents/i;
805
+ var CODEX_HOOKS_GATE_REGEX = /hooks need review/i;
806
+ var QUIET_DETECT_MS = 500;
807
+ var CODEX_READY_FALLBACK_MS = 8e3;
742
808
  var SUBMIT_BYTES = "\r";
743
809
  var CODEX_SUBMIT_DELAY_MS = 16;
744
810
  function digestBytes(s) {
@@ -746,6 +812,40 @@ function digestBytes(s) {
746
812
  if (escaped.length <= 200) return escaped;
747
813
  return `${escaped.slice(0, 100)}\u2026[${escaped.length - 200}B omitted]\u2026${escaped.slice(-100)}`;
748
814
  }
815
+ function gateCard(gate, lines) {
816
+ if (gate === "hooks") {
817
+ const countLine = lines.find((l) => /new or changed/i.test(l))?.trim();
818
+ return {
819
+ prompt: [
820
+ "Hooks need review",
821
+ countLine,
822
+ "Hooks can run outside the sandbox after you trust them."
823
+ ].filter(Boolean).join(" \u2014 "),
824
+ options: [
825
+ { index: 2, label: "Trust all and continue", answerKeys: "2\r" },
826
+ { index: 3, label: "Continue without trusting (hooks won't run)", answerKeys: "3\r" },
827
+ {
828
+ index: 4,
829
+ label: "Trust all and continue (remember for all projects)",
830
+ answerKeys: "4\r"
831
+ },
832
+ {
833
+ index: 5,
834
+ label: "Continue without trusting (remember for all projects)",
835
+ answerKeys: "5\r"
836
+ }
837
+ ]
838
+ };
839
+ }
840
+ return {
841
+ prompt: lines.find((l) => CODEX_TRUST_GATE_REGEX.test(l))?.trim() ?? "Do you trust the contents of this directory?",
842
+ options: [
843
+ { index: 1, label: "Yes, continue", answerKeys: "1\r" },
844
+ { index: 2, label: "No, quit", answerKeys: "2\r" },
845
+ { index: 3, label: "Yes, continue (remember for all projects)", answerKeys: "3\r" }
846
+ ]
847
+ };
848
+ }
749
849
  var pty = null;
750
850
  async function loadPty() {
751
851
  if (pty) return pty;
@@ -772,8 +872,8 @@ var CodexPtyRunner = class {
772
872
  onOutput;
773
873
  onStatusChange;
774
874
  onReady;
775
- // Accepted for shape-compatibility with PTYManagerOptions; Codex has no
776
- // detected equivalent yet (Phase 0) never invoked.
875
+ // Broadcasts Codex's blocking startup gates (directory trust, hooks review)
876
+ // as question cards; null dismisses the card once the gate leaves the screen.
777
877
  onPermissionChange;
778
878
  onLiveQuestion;
779
879
  onLiveQuestionGone;
@@ -784,8 +884,18 @@ var CodexPtyRunner = class {
784
884
  // Inputs received via sendInput() while the session was still pendingReady.
785
885
  // Flushed in arrival order once Codex reaches Ready.
786
886
  queuedInputs = /* @__PURE__ */ new Map();
787
- // Per-session debounce so the directory-trust gate's \r is only written once.
788
- trustGateAnswered = /* @__PURE__ */ new Set();
887
+ // Gate currently on a session's screen (card broadcast, unanswered). While
888
+ // set, queued-input flushes are held — a flushed digit would CONFIRM a
889
+ // dialog option — and sendKeys() intercepts remember-variant digits.
890
+ openGate = /* @__PURE__ */ new Map();
891
+ // `${sessionId}:${gate}` once a gate has been actioned (auto-answered or
892
+ // card broadcast) — dedupes repaints of the same dialog.
893
+ gateActioned = /* @__PURE__ */ new Set();
894
+ // Per-session trailing debounce re-armed on every chunk; on quiet, re-runs
895
+ // screen detection so a blocked/truncated boot still reaches ready.
896
+ quietCheckers = /* @__PURE__ */ new Map();
897
+ // Per-session flat backstop from spawn (CODEX_READY_FALLBACK_MS).
898
+ readyFallbackTimers = /* @__PURE__ */ new Map();
789
899
  // In-flight start()/startFresh() calls keyed by sessionId. A second
790
900
  // concurrent resume for the same session (double-tap, client retry) awaits
791
901
  // the first call's promise instead of spawning a duplicate PTY (CRITICAL #3).
@@ -815,7 +925,7 @@ var CodexPtyRunner = class {
815
925
  }
816
926
  async doStart(sessionId, options) {
817
927
  const nodePty = await loadPty();
818
- const projectName = options.projectName ?? (0, import_path4.basename)(options.projectPath);
928
+ const projectName = options.projectName ?? (0, import_path5.basename)(options.projectPath);
819
929
  const proc = nodePty.spawn(
820
930
  resolveCodexExe(),
821
931
  ["resume", sessionId, "--cd", options.projectPath, "--no-alt-screen"],
@@ -844,6 +954,7 @@ var CodexPtyRunner = class {
844
954
  };
845
955
  this.sessions.set(sessionId, session);
846
956
  this.pendingReady.add(sessionId);
957
+ this.armReadyFallback(sessionId);
847
958
  proc.onData((data) => {
848
959
  this.handleOutput(sessionId, data);
849
960
  });
@@ -860,7 +971,7 @@ var CodexPtyRunner = class {
860
971
  async startFresh(options) {
861
972
  const nodePty = await loadPty();
862
973
  const sessionId = (0, import_crypto2.randomUUID)();
863
- const projectName = options.projectName ?? (0, import_path4.basename)(options.projectPath);
974
+ const projectName = options.projectName ?? (0, import_path5.basename)(options.projectPath);
864
975
  const args = ["--cd", options.projectPath, "--no-alt-screen"];
865
976
  if (options.systemPrompt) {
866
977
  args.push(options.systemPrompt);
@@ -889,6 +1000,7 @@ var CodexPtyRunner = class {
889
1000
  };
890
1001
  this.sessions.set(sessionId, session);
891
1002
  this.pendingReady.add(sessionId);
1003
+ this.armReadyFallback(sessionId);
892
1004
  proc.onData((data) => {
893
1005
  this.handleOutput(sessionId, data);
894
1006
  });
@@ -898,6 +1010,21 @@ var CodexPtyRunner = class {
898
1010
  });
899
1011
  return toPublicSession(session);
900
1012
  }
1013
+ // Flat backstop: if neither the "Ready" marker nor the quiet-checker settled
1014
+ // the session within CODEX_READY_FALLBACK_MS of spawn, mark it ready anyway
1015
+ // so start requests resolve and mobile can watch the boot live. unref() so a
1016
+ // pending timer never holds the process open.
1017
+ armReadyFallback(sessionId) {
1018
+ const timer = setTimeout(() => {
1019
+ this.readyFallbackTimers.delete(sessionId);
1020
+ const session = this.sessions.get(sessionId);
1021
+ if (session?.status === "running" && this.pendingReady.has(sessionId)) {
1022
+ this.markReady(sessionId, session, "fallback:timeout");
1023
+ }
1024
+ }, CODEX_READY_FALLBACK_MS);
1025
+ timer.unref?.();
1026
+ this.readyFallbackTimers.set(sessionId, timer);
1027
+ }
901
1028
  // Write raw key bytes directly to the PTY, same as PTYManager.sendKeys.
902
1029
  sendKeys(sessionId, keys) {
903
1030
  const session = this.sessions.get(sessionId);
@@ -909,13 +1036,47 @@ var CodexPtyRunner = class {
909
1036
  session.status = "running";
910
1037
  this.onStatusChange?.(toPublicSession(session));
911
1038
  }
1039
+ const gate = this.openGate.get(sessionId);
1040
+ const digit = gate ? /^([0-9])\r?$/.exec(keys)?.[1] : void 0;
1041
+ const out = gate && digit ? this.resolveGateAnswer(sessionId, gate, digit) : keys;
912
1042
  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 }
1043
+ `[codex.keys.write] ${sessionId.slice(0, 8)} bytes=${out.length} digest=${digestBytes(out)}`,
1044
+ { event: "codex.keys_write", sessionId, byteLen: out.length }
915
1045
  );
916
- session.process.write(keys);
1046
+ session.process.write(out);
917
1047
  session.lastActivityAt = /* @__PURE__ */ new Date();
918
1048
  }
1049
+ // Map a gate-card digit to the PTY bytes that answer the real dialog,
1050
+ // persisting the choice when the digit was a synthetic "remember for all
1051
+ // projects" option (those numbers don't exist on the actual dialog and must
1052
+ // never reach codex). The trailing \r mobile sends is dropped: a digit alone
1053
+ // selects AND confirms (live-probe verified), and a stray Enter would land
1054
+ // on whatever screen follows.
1055
+ resolveGateAnswer(sessionId, gate, digit) {
1056
+ let real = digit;
1057
+ let remembered = false;
1058
+ if (gate === "hooks" && digit === "4") {
1059
+ saveGateAnswer("codexHooksGate", "trust_all");
1060
+ real = "2";
1061
+ remembered = true;
1062
+ } else if (gate === "hooks" && digit === "5") {
1063
+ saveGateAnswer("codexHooksGate", "continue_untrusted");
1064
+ real = "3";
1065
+ remembered = true;
1066
+ } else if (gate === "trust" && digit === "3") {
1067
+ saveGateAnswer("codexTrustGate", "yes");
1068
+ real = "1";
1069
+ remembered = true;
1070
+ }
1071
+ this.log.info(`[codex.gate_answer] ${sessionId.slice(0, 8)} ${gate} digit=${real}`, {
1072
+ event: "codex.gate_answer",
1073
+ sessionId,
1074
+ gate,
1075
+ digit: real,
1076
+ remembered
1077
+ });
1078
+ return real;
1079
+ }
919
1080
  sendInput(sessionId, input) {
920
1081
  const session = this.sessions.get(sessionId);
921
1082
  if (!session) throw new Error(`Session not found: ${sessionId}`);
@@ -985,8 +1146,12 @@ var CodexPtyRunner = class {
985
1146
  }, CODEX_SUBMIT_DELAY_MS);
986
1147
  }
987
1148
  // Drain any inputs sent while the session was still pendingReady, writing
988
- // them in arrival order now that Codex is Ready.
1149
+ // them in arrival order now that Codex is Ready. No-op while a gate dialog
1150
+ // is open (a flushed digit would confirm a dialog option) or while still
1151
+ // pendingReady (markReady drains it) — the gate-close path re-drives it for
1152
+ // the ready-with-gate-open case.
989
1153
  flushQueuedInputs(sessionId) {
1154
+ if (this.openGate.has(sessionId) || this.pendingReady.has(sessionId)) return;
990
1155
  const queue = this.queuedInputs.get(sessionId);
991
1156
  if (!queue || queue.length === 0) return;
992
1157
  this.queuedInputs.delete(sessionId);
@@ -1031,7 +1196,7 @@ var CodexPtyRunner = class {
1031
1196
  if (!session) return;
1032
1197
  this.pendingReady.delete(sessionId);
1033
1198
  this.queuedInputs.delete(sessionId);
1034
- this.trustGateAnswered.delete(sessionId);
1199
+ this.clearSessionDetectors(sessionId);
1035
1200
  try {
1036
1201
  session.process.kill("SIGINT");
1037
1202
  } catch {
@@ -1042,6 +1207,21 @@ var CodexPtyRunner = class {
1042
1207
  this.sessions.delete(sessionId);
1043
1208
  this.onStatusChange?.(toPublicSession(session));
1044
1209
  }
1210
+ // Drop a session's detection state: quiet-checker, ready-fallback timer,
1211
+ // gate bookkeeping — and dismiss a still-open gate card so mobile doesn't
1212
+ // keep rendering a question for a dead PTY.
1213
+ clearSessionDetectors(sessionId) {
1214
+ this.quietCheckers.get(sessionId)?.cancel();
1215
+ this.quietCheckers.delete(sessionId);
1216
+ const timer = this.readyFallbackTimers.get(sessionId);
1217
+ if (timer) clearTimeout(timer);
1218
+ this.readyFallbackTimers.delete(sessionId);
1219
+ if (this.openGate.delete(sessionId)) {
1220
+ this.onPermissionChange?.(sessionId, null);
1221
+ }
1222
+ this.gateActioned.delete(`${sessionId}:hooks`);
1223
+ this.gateActioned.delete(`${sessionId}:trust`);
1224
+ }
1045
1225
  getOutput(sessionId) {
1046
1226
  const session = this.sessions.get(sessionId);
1047
1227
  if (!session) throw new Error(`Session not found: ${sessionId}`);
@@ -1081,10 +1261,19 @@ var CodexPtyRunner = class {
1081
1261
  }
1082
1262
  session.screen.dispose();
1083
1263
  }
1264
+ for (const sessionId of Array.from(this.quietCheckers.keys())) {
1265
+ this.clearSessionDetectors(sessionId);
1266
+ }
1267
+ for (const timer of this.readyFallbackTimers.values()) {
1268
+ clearTimeout(timer);
1269
+ }
1084
1270
  this.sessions.clear();
1085
1271
  this.pendingReady.clear();
1086
1272
  this.queuedInputs.clear();
1087
- this.trustGateAnswered.clear();
1273
+ this.openGate.clear();
1274
+ this.gateActioned.clear();
1275
+ this.quietCheckers.clear();
1276
+ this.readyFallbackTimers.clear();
1088
1277
  }
1089
1278
  handleOutput(sessionId, data) {
1090
1279
  const session = this.sessions.get(sessionId);
@@ -1099,44 +1288,92 @@ var CodexPtyRunner = class {
1099
1288
  session.screen.write(data);
1100
1289
  session.lastOutput = stripAnsi(data);
1101
1290
  this.onOutput?.(sessionId, data);
1102
- this.detectReady(sessionId, session).catch((err) => {
1291
+ this.detectScreenState(sessionId, "chunk").catch((err) => {
1103
1292
  this.log.warn("[codex.ready_detect] failed", {
1104
1293
  event: "codex.ready_detect_failed",
1105
1294
  sessionId,
1106
1295
  err
1107
1296
  });
1108
1297
  });
1298
+ let quiet = this.quietCheckers.get(sessionId);
1299
+ if (!quiet) {
1300
+ quiet = debounce(() => {
1301
+ this.detectScreenState(sessionId, "quiet").catch((err) => {
1302
+ this.log.warn("[codex.ready_detect] failed", {
1303
+ event: "codex.ready_detect_failed",
1304
+ sessionId,
1305
+ err
1306
+ });
1307
+ });
1308
+ }, QUIET_DETECT_MS);
1309
+ this.quietCheckers.set(sessionId, quiet);
1310
+ }
1311
+ quiet();
1109
1312
  }
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;
1313
+ // Renders the session's headless screen and drives both detections:
1314
+ // - Gates (directory trust, hooks review) checked on EVERY pass,
1315
+ // independent of pendingReady, so a gate appearing after ready is still
1316
+ // surfaced and a gate leaving the screen closes its card.
1317
+ // - Readiness the "Ready" status-bar marker while pendingReady, plus the
1318
+ // quiet path: after QUIET_DETECT_MS of PTY silence a still-pending
1319
+ // session is marked ready anyway (`›` alone is NOT a marker — Phase 0 —
1320
+ // but a quiet boot screen is more useful to the user live than a
1321
+ // spinner, and "Ready" may be truncated off the 120-col status bar).
1322
+ async detectScreenState(sessionId, trigger) {
1323
+ const session = this.sessions.get(sessionId);
1324
+ if (!session || session.status === "idle") return;
1117
1325
  const lines = await this.getOutputLines(sessionId, PTY_ROWS);
1118
1326
  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;
1327
+ const gate = CODEX_HOOKS_GATE_REGEX.test(screenText) ? "hooks" : CODEX_TRUST_GATE_REGEX.test(screenText) ? "trust" : null;
1328
+ if (gate) {
1329
+ this.handleGate(sessionId, session, gate, lines);
1330
+ } else if (this.openGate.delete(sessionId)) {
1331
+ this.onPermissionChange?.(sessionId, null);
1332
+ this.flushQueuedInputs(sessionId);
1129
1333
  }
1334
+ if (session.status !== "running" || !this.pendingReady.has(sessionId)) return;
1130
1335
  const lastNonBlank2 = [...lines].reverse().find((l) => l.trim() !== "") ?? "";
1131
- if (!lastNonBlank2.includes(CODEX_PROMPT_READY_TEXT)) return;
1132
- this.markReady(sessionId, session);
1336
+ if (lastNonBlank2.includes(CODEX_PROMPT_READY_TEXT)) {
1337
+ this.markReady(sessionId, session, `marker:${CODEX_PROMPT_READY_TEXT}`);
1338
+ } else if (trigger === "quiet") {
1339
+ this.markReady(sessionId, session, "quiet:timeout");
1340
+ }
1341
+ }
1342
+ // Answer a gate from the persisted remember-store, or surface it as a
1343
+ // question card over the permission transport. Actioned once per session and
1344
+ // gate type — repaints of the same dialog neither re-write nor re-broadcast.
1345
+ handleGate(sessionId, session, gate, lines) {
1346
+ const key = `${sessionId}:${gate}`;
1347
+ if (this.gateActioned.has(key)) return;
1348
+ this.gateActioned.add(key);
1349
+ const remembered = rememberedGateDigit(gate);
1350
+ if (remembered) {
1351
+ this.log.info(`[codex.gate_auto_answer] ${sessionId.slice(0, 8)} ${gate} \u2192 ${remembered}`, {
1352
+ event: "codex.gate_auto_answer",
1353
+ sessionId,
1354
+ gate,
1355
+ digit: remembered
1356
+ });
1357
+ session.process.write(remembered);
1358
+ return;
1359
+ }
1360
+ this.openGate.set(sessionId, gate);
1361
+ const card = gateCard(gate, lines);
1362
+ this.log.info(`[codex.gate_prompt] ${sessionId.slice(0, 8)} ${gate}`, {
1363
+ event: "codex.gate_prompt",
1364
+ sessionId,
1365
+ gate,
1366
+ prompt: card.prompt
1367
+ });
1368
+ this.onPermissionChange?.(sessionId, card);
1133
1369
  }
1134
- markReady(sessionId, session) {
1370
+ markReady(sessionId, session, reason) {
1135
1371
  session.lastActivityAt = /* @__PURE__ */ new Date();
1136
1372
  session.status = "waiting_input";
1137
- this.log.info(`[codex.ready] ${sessionId.slice(0, 8)}`, {
1373
+ this.log.info(`[codex.ready] ${sessionId.slice(0, 8)} ${reason}`, {
1138
1374
  event: "codex.ready",
1139
- sessionId
1375
+ sessionId,
1376
+ reason
1140
1377
  });
1141
1378
  this.onStatusChange?.(toPublicSession(session));
1142
1379
  if (this.pendingReady.has(sessionId)) {
@@ -1152,7 +1389,7 @@ var CodexPtyRunner = class {
1152
1389
  session.status = "idle";
1153
1390
  const elapsedMs = session.completedAt.getTime() - session.startedAt.getTime();
1154
1391
  if (exitCode !== 0 && elapsedMs < 2e3 && session.lastOutput === "") {
1155
- if (!(0, import_fs4.existsSync)(session.projectPath)) {
1392
+ if (!(0, import_fs5.existsSync)(session.projectPath)) {
1156
1393
  session.failureReason = `Project directory not found: ${session.projectPath}`;
1157
1394
  } else {
1158
1395
  session.failureReason = `Codex process exited immediately (code ${exitCode}).`;
@@ -1162,7 +1399,7 @@ var CodexPtyRunner = class {
1162
1399
  session.screen.dispose();
1163
1400
  this.sessions.delete(sessionId);
1164
1401
  this.queuedInputs.delete(sessionId);
1165
- this.trustGateAnswered.delete(sessionId);
1402
+ this.clearSessionDetectors(sessionId);
1166
1403
  }
1167
1404
  };
1168
1405
  function toPublicSession(s) {
@@ -1189,8 +1426,8 @@ function stripAnsi(str) {
1189
1426
  // src/pty-manager.ts
1190
1427
  var import_headless2 = require("@xterm/headless");
1191
1428
  var import_crypto3 = require("crypto");
1192
- var import_fs5 = require("fs");
1193
- var import_path5 = require("path");
1429
+ var import_fs6 = require("fs");
1430
+ var import_path6 = require("path");
1194
1431
 
1195
1432
  // src/services/questions/detectPermissionGate.ts
1196
1433
  var OSC_777_RE = /\x1b\]777;notify;Claude Code;[^\x07\x1b]*/;
@@ -1345,9 +1582,15 @@ function detectShellPrompt(lines) {
1345
1582
  ]
1346
1583
  };
1347
1584
  }
1348
- if (NUMBERED_RE.test(last.text)) {
1585
+ const lastNumberedIdx = (() => {
1586
+ for (let i = last.idx; i >= 0; i--) {
1587
+ if (NUMBERED_RE.test(lines[i])) return i;
1588
+ }
1589
+ return -1;
1590
+ })();
1591
+ if (lastNumberedIdx >= 0) {
1349
1592
  const options = [];
1350
- for (let i = 0; i <= last.idx; i++) {
1593
+ for (let i = 0; i <= lastNumberedIdx; i++) {
1351
1594
  const m = NUMBERED_RE.exec(lines[i]);
1352
1595
  if (!m) continue;
1353
1596
  const num = Number.parseInt(m[1], 10);
@@ -1375,37 +1618,6 @@ function detectShellPrompt(lines) {
1375
1618
  return null;
1376
1619
  }
1377
1620
 
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
1621
  // src/pty-manager.ts
1410
1622
  var OUTPUT_BUFFER_MAX2 = 65536;
1411
1623
  var PTY_COLS2 = 120;
@@ -1413,7 +1625,7 @@ var PTY_ROWS2 = 40;
1413
1625
  var SCREEN_SCROLLBACK2 = 1e3;
1414
1626
  var CLAUDE_PROMPT_MARKERS = ["\u256D", "\u276F"];
1415
1627
  var PROMPT_MARKER_FALLBACK_MS = 1e4;
1416
- var QUIET_DETECT_MS = 500;
1628
+ var QUIET_DETECT_MS2 = 500;
1417
1629
  function buildPasteBytes(input) {
1418
1630
  return `\x1B[200~${input}\x1B[201~`;
1419
1631
  }
@@ -1541,7 +1753,7 @@ var PTYManager = class {
1541
1753
  }
1542
1754
  async doStart(sessionId, options) {
1543
1755
  const nodePty = await loadPty2();
1544
- const projectName = options.projectName ?? (0, import_path5.basename)(options.projectPath);
1756
+ const projectName = options.projectName ?? (0, import_path6.basename)(options.projectPath);
1545
1757
  const proc = nodePty.spawn(
1546
1758
  resolveClaudeExe(),
1547
1759
  [
@@ -1592,7 +1804,7 @@ var PTYManager = class {
1592
1804
  async startFresh(options) {
1593
1805
  const nodePty = await loadPty2();
1594
1806
  const sessionId = (0, import_crypto3.randomUUID)();
1595
- const projectName = options.projectName ?? (0, import_path5.basename)(options.projectPath);
1807
+ const projectName = options.projectName ?? (0, import_path6.basename)(options.projectPath);
1596
1808
  const args = [
1597
1809
  "--permission-mode",
1598
1810
  options.permissionMode ?? "acceptEdits",
@@ -1913,7 +2125,7 @@ var PTYManager = class {
1913
2125
  });
1914
2126
  let quiet = this.quietCheckers.get(sessionId);
1915
2127
  if (!quiet) {
1916
- quiet = debounce(() => this.handleQuiet(sessionId), QUIET_DETECT_MS);
2128
+ quiet = debounce(() => this.handleQuiet(sessionId), QUIET_DETECT_MS2);
1917
2129
  this.quietCheckers.set(sessionId, quiet);
1918
2130
  }
1919
2131
  quiet();
@@ -2036,7 +2248,7 @@ var PTYManager = class {
2036
2248
  session.status = "idle";
2037
2249
  const elapsedMs = session.completedAt.getTime() - session.startedAt.getTime();
2038
2250
  if (exitCode !== 0 && elapsedMs < 2e3 && session.lastOutput === "") {
2039
- if (!(0, import_fs5.existsSync)(session.projectPath)) {
2251
+ if (!(0, import_fs6.existsSync)(session.projectPath)) {
2040
2252
  session.failureReason = `Project directory not found: ${session.projectPath}`;
2041
2253
  } else {
2042
2254
  session.failureReason = `Process exited immediately (code ${exitCode}). Check that the Claude binary is installed and accessible.`;
@@ -2164,7 +2376,7 @@ var LiveSessionManager = class {
2164
2376
  const runner = this.runners.get(provider);
2165
2377
  if (runner) return runner;
2166
2378
  const err = new Error(
2167
- `Live ${provider} sessions are not implemented yet for ${(0, import_path6.basename)(projectPath)}`
2379
+ `Live ${provider} sessions are not implemented yet for ${(0, import_path7.basename)(projectPath)}`
2168
2380
  );
2169
2381
  err.statusCode = 501;
2170
2382
  throw err;
@@ -2173,10 +2385,10 @@ var LiveSessionManager = class {
2173
2385
 
2174
2386
  // src/process-discovery.ts
2175
2387
  var import_child_process2 = require("child_process");
2176
- var import_os3 = require("os");
2177
- var import_path7 = require("path");
2388
+ var import_os4 = require("os");
2389
+ var import_path8 = require("path");
2178
2390
  async function discoverClaudeProcesses() {
2179
- if ((0, import_os3.platform)() === "win32") return discoverWindows();
2391
+ if ((0, import_os4.platform)() === "win32") return discoverWindows();
2180
2392
  return discoverUnix();
2181
2393
  }
2182
2394
  async function discoverUnix() {
@@ -2193,7 +2405,7 @@ async function discoverUnix() {
2193
2405
  return {
2194
2406
  pid,
2195
2407
  projectPath: cwd,
2196
- projectName: (0, import_path7.basename)(cwd),
2408
+ projectName: (0, import_path8.basename)(cwd),
2197
2409
  branch: await readGitBranch(cwd),
2198
2410
  conversationId,
2199
2411
  startedAt
@@ -2215,7 +2427,7 @@ async function discoverWindows() {
2215
2427
  return {
2216
2428
  pid,
2217
2429
  projectPath: info.cwd,
2218
- projectName: (0, import_path7.basename)(info.cwd),
2430
+ projectName: (0, import_path8.basename)(info.cwd),
2219
2431
  branch: await readGitBranch(info.cwd),
2220
2432
  conversationId: extractResumeId(info.args),
2221
2433
  startedAt: info.startedAt
@@ -2296,7 +2508,7 @@ async function getProcessInfoWindows(pid) {
2296
2508
  const startedAt = /* @__PURE__ */ new Date(`${year}-${month}-${day}T${hour}:${min}:${sec}`);
2297
2509
  if (Number.isNaN(startedAt.getTime())) return null;
2298
2510
  const exePath = parts[3] ?? "";
2299
- const cwd = exePath ? (0, import_path7.dirname)(exePath) : "";
2511
+ const cwd = exePath ? (0, import_path8.dirname)(exePath) : "";
2300
2512
  return { cwd, args, startedAt };
2301
2513
  } catch {
2302
2514
  return null;
@@ -2319,11 +2531,11 @@ var import_node_ws = require("@hono/node-ws");
2319
2531
  var import_client = require("@temporalio/client");
2320
2532
  var import_scanner3 = require("@threadbase-sh/scanner");
2321
2533
  var import_events = require("events");
2322
- var import_fs12 = require("fs");
2534
+ var import_fs13 = require("fs");
2323
2535
  var import_promises7 = require("fs/promises");
2324
2536
  var import_http = require("http");
2325
- var import_os6 = require("os");
2326
- var import_path13 = require("path");
2537
+ var import_os7 = require("os");
2538
+ var import_path14 = require("path");
2327
2539
  var import_readline = require("readline");
2328
2540
 
2329
2541
  // node_modules/nanoid/index.js
@@ -2582,7 +2794,7 @@ async function handleStartAgentSession(body, deps) {
2582
2794
  }
2583
2795
 
2584
2796
  // src/api/app.ts
2585
- var import_hono11 = require("hono");
2797
+ var import_hono12 = require("hono");
2586
2798
 
2587
2799
  // src/api/middleware/auth.middleware.ts
2588
2800
  function isLocalRequest(remoteAddr) {
@@ -2590,6 +2802,7 @@ function isLocalRequest(remoteAddr) {
2590
2802
  return addr === "127.0.0.1" || addr === "::1" || addr === "::ffff:127.0.0.1";
2591
2803
  }
2592
2804
  var PUBLIC_PATHS = /* @__PURE__ */ new Set(["/healthz", "/ws"]);
2805
+ var LOCAL_ONLY_PATHS = /* @__PURE__ */ new Set(["/api/logs", "/api/logs/meta"]);
2593
2806
  var PUBLIC_POST_PATHS = /* @__PURE__ */ new Set(["/api/pair/exchange", "/api/__update"]);
2594
2807
  var PUBLIC_POST_PREFIXES = ["/internal/sessions/"];
2595
2808
  var authMiddleware = (deps) => async (c, next) => {
@@ -2600,8 +2813,12 @@ var authMiddleware = (deps) => async (c, next) => {
2600
2813
  await next();
2601
2814
  return;
2602
2815
  }
2816
+ const remoteAddr = c.env.incoming?.socket?.remoteAddress;
2817
+ if (LOCAL_ONLY_PATHS.has(path) && isLocalRequest(remoteAddr)) {
2818
+ await next();
2819
+ return;
2820
+ }
2603
2821
  if (deps.localNoAuth) {
2604
- const remoteAddr = c.env.incoming?.socket?.remoteAddress;
2605
2822
  if (isLocalRequest(remoteAddr)) {
2606
2823
  await next();
2607
2824
  return;
@@ -2773,16 +2990,145 @@ var createHealthRoutes = () => {
2773
2990
  return app;
2774
2991
  };
2775
2992
 
2993
+ // src/api/routes/logs.routes.ts
2994
+ var import_node_fs3 = require("fs");
2995
+ var import_node_path5 = require("path");
2996
+ var import_hono5 = require("hono");
2997
+
2998
+ // src/lifecycle/constants.ts
2999
+ var import_node_os = require("os");
3000
+ var import_node_path4 = require("path");
3001
+ var TASK_NAME = process.env.THREADBASE_TASK_NAME ?? "Threadbase";
3002
+ function installDir() {
3003
+ return process.env.THREADBASE_INSTALL_DIR ?? (0, import_node_path4.join)((0, import_node_os.homedir)(), ".threadbase");
3004
+ }
3005
+
3006
+ // src/api/routes/logs.routes.ts
3007
+ var logger2 = getLogger("logs-api");
3008
+ function resolveLogPath(source) {
3009
+ return (0, import_node_path5.join)(installDir(), "logs", `${source}.log`);
3010
+ }
3011
+ function pickDefaultSource() {
3012
+ for (const source of ["stdout", "stderr", "dev"]) {
3013
+ const p = resolveLogPath(source);
3014
+ if ((0, import_node_fs3.existsSync)(p) && (0, import_node_fs3.statSync)(p).size > 0) return source;
3015
+ }
3016
+ return "stdout";
3017
+ }
3018
+ function readLogLines(filePath, sinceOffset, limit) {
3019
+ if (!(0, import_node_fs3.existsSync)(filePath)) {
3020
+ return { lines: [], offset: 0, total: 0 };
3021
+ }
3022
+ const fd = (0, import_node_fs3.openSync)(filePath, "r");
3023
+ try {
3024
+ const { size } = (0, import_node_fs3.fstatSync)(fd);
3025
+ if (size === 0) return { lines: [], offset: 0, total: 0 };
3026
+ const maxBytes = Math.min(size, 2 * 1024 * 1024);
3027
+ const start = size - maxBytes;
3028
+ const buf = Buffer.alloc(maxBytes);
3029
+ (0, import_node_fs3.readSync)(fd, buf, 0, maxBytes, start);
3030
+ let text = buf.toString("utf8");
3031
+ if (start > 0) {
3032
+ const firstNl = text.indexOf("\n");
3033
+ if (firstNl >= 0) text = text.slice(firstNl + 1);
3034
+ }
3035
+ const allLines = text.split("\n").filter((line) => line.trim() && !line.startsWith("==="));
3036
+ let lines;
3037
+ let newOffset;
3038
+ if (sinceOffset > 0 && sinceOffset < allLines.length) {
3039
+ lines = allLines.slice(sinceOffset, sinceOffset + limit);
3040
+ newOffset = sinceOffset + lines.length;
3041
+ } else if (sinceOffset >= allLines.length && sinceOffset > 0) {
3042
+ lines = [];
3043
+ newOffset = allLines.length;
3044
+ } else {
3045
+ lines = allLines.slice(-limit);
3046
+ newOffset = allLines.length;
3047
+ }
3048
+ return { lines, offset: newOffset, total: allLines.length };
3049
+ } finally {
3050
+ (0, import_node_fs3.closeSync)(fd);
3051
+ }
3052
+ }
3053
+ function createLogsRoutes() {
3054
+ const app = new import_hono5.Hono();
3055
+ app.get("/", (c) => {
3056
+ try {
3057
+ const sourceParam = (c.req.query("source") || "").toLowerCase();
3058
+ const source = sourceParam === "stdout" || sourceParam === "stderr" || sourceParam === "dev" ? sourceParam : pickDefaultSource();
3059
+ const logPath = resolveLogPath(source);
3060
+ const sinceOffset = parseInt(c.req.query("since") || "0", 10);
3061
+ const limit = Math.min(parseInt(c.req.query("limit") || "100", 10) || 100, 1e3);
3062
+ if (!(0, import_node_fs3.existsSync)(logPath)) {
3063
+ return c.json({
3064
+ logs: [],
3065
+ message: `No log file found for source=${source}`,
3066
+ offset: 0,
3067
+ total: 0,
3068
+ source
3069
+ });
3070
+ }
3071
+ const { lines, offset, total } = readLogLines(logPath, sinceOffset, limit);
3072
+ const stats = (0, import_node_fs3.statSync)(logPath);
3073
+ return c.json({
3074
+ logs: lines,
3075
+ offset,
3076
+ total,
3077
+ hasMore: offset < total,
3078
+ source,
3079
+ fileSize: stats.size,
3080
+ fileModified: stats.mtime.toISOString()
3081
+ });
3082
+ } catch (error) {
3083
+ logger2.error("Failed to read logs", { error: String(error) });
3084
+ return c.json(
3085
+ {
3086
+ error: "Failed to read logs",
3087
+ logs: [],
3088
+ offset: 0,
3089
+ total: 0
3090
+ },
3091
+ 500
3092
+ );
3093
+ }
3094
+ });
3095
+ app.get("/meta", (c) => {
3096
+ try {
3097
+ const sources = ["stdout", "stderr", "dev"].map((source) => {
3098
+ const logPath = resolveLogPath(source);
3099
+ if (!(0, import_node_fs3.existsSync)(logPath)) {
3100
+ return { source, exists: false, total: 0, fileSize: 0 };
3101
+ }
3102
+ const stats = (0, import_node_fs3.statSync)(logPath);
3103
+ return {
3104
+ source,
3105
+ exists: true,
3106
+ fileSize: stats.size,
3107
+ fileModified: stats.mtime.toISOString()
3108
+ };
3109
+ });
3110
+ return c.json({
3111
+ defaultSource: pickDefaultSource(),
3112
+ sources
3113
+ });
3114
+ } catch (error) {
3115
+ logger2.error("Failed to read log metadata", { error: String(error) });
3116
+ return c.json({ error: "Failed to read log metadata", exists: false }, 500);
3117
+ }
3118
+ });
3119
+ return app;
3120
+ }
3121
+
2776
3122
  // src/api/routes/misc.routes.ts
2777
3123
  var import_node_child_process = require("child_process");
2778
3124
  var import_node_crypto2 = require("crypto");
2779
- var import_hono5 = require("hono");
2780
- var import_os4 = require("os");
3125
+ var import_hono6 = require("hono");
3126
+ var import_os5 = require("os");
2781
3127
 
2782
3128
  // 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");
3129
+ var import_node_fs4 = require("fs");
3130
+ var import_node_os2 = require("os");
3131
+ var import_node_path6 = require("path");
2786
3132
  var import_yaml = require("yaml");
2787
3133
 
2788
3134
  // src/schemas/updateConfig.schema.ts
@@ -2798,12 +3144,12 @@ var UpdateConfigSchema = import_zod.z.object({
2798
3144
  }).strict();
2799
3145
 
2800
3146
  // src/config/update-config.ts
2801
- var DEFAULT_CONFIG_PATH = (0, import_node_path4.join)((0, import_node_os.homedir)(), ".threadbase", "update.yaml");
3147
+ var DEFAULT_CONFIG_PATH = (0, import_node_path6.join)((0, import_node_os2.homedir)(), ".threadbase", "update.yaml");
2802
3148
  function loadUpdateConfig(opts = {}) {
2803
3149
  const path = opts.path ?? DEFAULT_CONFIG_PATH;
2804
3150
  let raw;
2805
3151
  try {
2806
- raw = (0, import_node_fs3.readFileSync)(path, "utf-8");
3152
+ raw = (0, import_node_fs4.readFileSync)(path, "utf-8");
2807
3153
  } catch (err) {
2808
3154
  if (err.code === "ENOENT") return null;
2809
3155
  throw err;
@@ -2850,12 +3196,12 @@ function verifyWebhookSignature(body, header, secret) {
2850
3196
  }
2851
3197
  var clientLog = getLogger("client");
2852
3198
  var createMiscRoutes = (deps) => {
2853
- const app = new import_hono5.Hono();
3199
+ const app = new import_hono6.Hono();
2854
3200
  app.get("/api/info", (c) => {
2855
3201
  const ptyIds = deps.ptyAttachedIds();
2856
3202
  return c.json({
2857
3203
  version: getVersion(),
2858
- machineName: (0, import_os4.hostname)(),
3204
+ machineName: (0, import_os5.hostname)(),
2859
3205
  platform: process.platform,
2860
3206
  activeSessions: deps.sessionStore.list(ptyIds).filter((s) => s.status === "running").length,
2861
3207
  publicUrl: deps.publicUrl
@@ -2926,11 +3272,11 @@ var createMiscRoutes = (deps) => {
2926
3272
  };
2927
3273
 
2928
3274
  // src/api/routes/pair.routes.ts
2929
- var import_hono6 = require("hono");
3275
+ var import_hono7 = require("hono");
2930
3276
  var ALREADY_HANDLED3 = 597;
2931
3277
  var alreadyHandled3 = () => new Response(null, { status: ALREADY_HANDLED3 });
2932
3278
  var createPairRoutes = (deps) => {
2933
- const app = new import_hono6.Hono();
3279
+ const app = new import_hono7.Hono();
2934
3280
  app.post("/start", (c) => {
2935
3281
  deps.handlePairStart(c.env.outgoing);
2936
3282
  return alreadyHandled3();
@@ -2943,11 +3289,11 @@ var createPairRoutes = (deps) => {
2943
3289
  };
2944
3290
 
2945
3291
  // src/api/routes/projects.routes.ts
2946
- var import_hono7 = require("hono");
3292
+ var import_hono8 = require("hono");
2947
3293
  var ALREADY_HANDLED4 = 597;
2948
3294
  var alreadyHandled4 = () => new Response(null, { status: ALREADY_HANDLED4 });
2949
3295
  var createProjectRoutes = (deps) => {
2950
- const app = new import_hono7.Hono();
3296
+ const app = new import_hono8.Hono();
2951
3297
  app.get("/", (c) => {
2952
3298
  const url = new URL(c.req.url);
2953
3299
  deps.handleListProjects(url, c.env.outgoing);
@@ -2962,11 +3308,11 @@ var createProjectRoutes = (deps) => {
2962
3308
  };
2963
3309
 
2964
3310
  // src/api/routes/scanner.routes.ts
2965
- var import_hono8 = require("hono");
3311
+ var import_hono9 = require("hono");
2966
3312
  var ALREADY_HANDLED5 = 597;
2967
3313
  var alreadyHandled5 = () => new Response(null, { status: ALREADY_HANDLED5 });
2968
3314
  var createScannerRoutes = (deps) => {
2969
- const app = new import_hono8.Hono();
3315
+ const app = new import_hono9.Hono();
2970
3316
  app.get("/api/search", async (c) => {
2971
3317
  const url = new URL(c.req.url);
2972
3318
  await deps.handleSearch(url, c.env.outgoing);
@@ -2976,11 +3322,11 @@ var createScannerRoutes = (deps) => {
2976
3322
  };
2977
3323
 
2978
3324
  // src/api/routes/sessions.routes.ts
2979
- var import_hono9 = require("hono");
3325
+ var import_hono10 = require("hono");
2980
3326
  var ALREADY_HANDLED6 = 597;
2981
3327
  var alreadyHandled6 = () => new Response(null, { status: ALREADY_HANDLED6 });
2982
3328
  var createSessionRoutes = (deps) => {
2983
- const app = new import_hono9.Hono();
3329
+ const app = new import_hono10.Hono();
2984
3330
  app.get("/count", (c) => {
2985
3331
  deps.handleSessionsCount(c.env.outgoing);
2986
3332
  return alreadyHandled6();
@@ -3047,9 +3393,9 @@ var createSessionRoutes = (deps) => {
3047
3393
  };
3048
3394
 
3049
3395
  // src/api/routes/ws.routes.ts
3050
- var import_hono10 = require("hono");
3396
+ var import_hono11 = require("hono");
3051
3397
  var createWsRoutes = (deps, upgradeWebSocket) => {
3052
- const app = new import_hono10.Hono();
3398
+ const app = new import_hono11.Hono();
3053
3399
  app.get(
3054
3400
  "/ws",
3055
3401
  upgradeWebSocket((c) => {
@@ -3077,7 +3423,7 @@ var createWsRoutes = (deps, upgradeWebSocket) => {
3077
3423
 
3078
3424
  // src/api/app.ts
3079
3425
  var createHonoApp = (deps, upgradeWebSocket) => {
3080
- const app = new import_hono11.Hono();
3426
+ const app = new import_hono12.Hono();
3081
3427
  const httpLog = getLogger("http");
3082
3428
  app.use("*", async (c, next) => {
3083
3429
  const start = Date.now();
@@ -3106,6 +3452,7 @@ var createHonoApp = (deps, upgradeWebSocket) => {
3106
3452
  app.route("/api", createBrowseRoutes(deps));
3107
3453
  app.route("/", createScannerRoutes(deps));
3108
3454
  app.route("/internal", createProgressRoutes(deps));
3455
+ app.route("/api/logs", createLogsRoutes());
3109
3456
  if (upgradeWebSocket) {
3110
3457
  app.route("/", createWsRoutes(deps, upgradeWebSocket));
3111
3458
  }
@@ -3114,7 +3461,7 @@ var createHonoApp = (deps, upgradeWebSocket) => {
3114
3461
 
3115
3462
  // src/browse.ts
3116
3463
  var import_promises2 = require("fs/promises");
3117
- var import_path8 = require("path");
3464
+ var import_path9 = require("path");
3118
3465
  var BrowsePathNotFoundError = class extends Error {
3119
3466
  constructor(message) {
3120
3467
  super(message);
@@ -3122,15 +3469,15 @@ var BrowsePathNotFoundError = class extends Error {
3122
3469
  }
3123
3470
  };
3124
3471
  async function resolveBrowsePath(browseRoot, relativePath) {
3125
- const normalizedRoot = (0, import_path8.resolve)(browseRoot);
3472
+ const normalizedRoot = (0, import_path9.resolve)(browseRoot);
3126
3473
  let sanitized;
3127
3474
  if (process.platform !== "win32" && relativePath.startsWith("/") && relativePath.length > 1 && relativePath.includes("/", 1)) {
3128
3475
  sanitized = relativePath;
3129
3476
  } else {
3130
3477
  sanitized = relativePath.replace(/^[/\\]+/, "");
3131
3478
  }
3132
- const target = sanitized ? (0, import_path8.resolve)(normalizedRoot, sanitized) : normalizedRoot;
3133
- const rootPrefix = normalizedRoot.endsWith(import_path8.sep) ? normalizedRoot : `${normalizedRoot}${import_path8.sep}`;
3479
+ const target = sanitized ? (0, import_path9.resolve)(normalizedRoot, sanitized) : normalizedRoot;
3480
+ const rootPrefix = normalizedRoot.endsWith(import_path9.sep) ? normalizedRoot : `${normalizedRoot}${import_path9.sep}`;
3134
3481
  if (!target.startsWith(rootPrefix) && target !== normalizedRoot) {
3135
3482
  throw new Error("Path outside browse root");
3136
3483
  }
@@ -3152,7 +3499,7 @@ async function createDirectory(parentAbsolutePath, name) {
3152
3499
  if (name.includes("/") || name.includes("\\") || name === ".." || name === ".") {
3153
3500
  throw new Error("Invalid directory name");
3154
3501
  }
3155
- const target = (0, import_path8.join)(parentAbsolutePath, name);
3502
+ const target = (0, import_path9.join)(parentAbsolutePath, name);
3156
3503
  try {
3157
3504
  const s = await (0, import_promises2.stat)(target);
3158
3505
  if (s.isDirectory()) throw new Error("Directory already exists");
@@ -3166,19 +3513,19 @@ async function createDirectory(parentAbsolutePath, name) {
3166
3513
  // src/conversation-cache.ts
3167
3514
  var import_scanner2 = require("@threadbase-sh/scanner");
3168
3515
  var import_better_sqlite3 = __toESM(require("better-sqlite3"), 1);
3169
- var import_fs8 = require("fs");
3516
+ var import_fs9 = require("fs");
3170
3517
  var import_promises3 = require("fs/promises");
3171
- var import_path10 = require("path");
3518
+ var import_path11 = require("path");
3172
3519
  var import_promises4 = require("timers/promises");
3173
3520
 
3174
3521
  // src/db/sqlite-migrate.ts
3175
- var import_fs6 = require("fs");
3176
- var import_path9 = require("path");
3522
+ var import_fs7 = require("fs");
3523
+ var import_path10 = require("path");
3177
3524
  var import_url2 = require("url");
3178
3525
  var import_meta2 = {};
3179
3526
  function getMigrationsDir2() {
3180
3527
  if (typeof import_meta2 !== "undefined" && import_meta2.url) {
3181
- return (0, import_path9.dirname)((0, import_url2.fileURLToPath)(import_meta2.url));
3528
+ return (0, import_path10.dirname)((0, import_url2.fileURLToPath)(import_meta2.url));
3182
3529
  }
3183
3530
  return __dirname;
3184
3531
  }
@@ -3190,8 +3537,8 @@ CREATE TABLE IF NOT EXISTS schema_migrations (
3190
3537
  `;
3191
3538
  function runSqliteMigrations(db, migrationsDir) {
3192
3539
  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();
3540
+ const dir = migrationsDir ?? (0, import_path10.join)(getMigrationsDir2(), "migrations");
3541
+ const files = (0, import_fs7.readdirSync)(dir).filter((f) => f.endsWith(".sql")).sort();
3195
3542
  const appliedRows = db.prepare("SELECT id FROM schema_migrations").all();
3196
3543
  const appliedSet = new Set(appliedRows.map((r) => r.id));
3197
3544
  const recordApplied = db.prepare("INSERT INTO schema_migrations (id, applied_at) VALUES (?, ?)");
@@ -3202,7 +3549,7 @@ function runSqliteMigrations(db, migrationsDir) {
3202
3549
  skipped.push(file);
3203
3550
  continue;
3204
3551
  }
3205
- const sql = (0, import_fs6.readFileSync)((0, import_path9.join)(dir, file), "utf-8");
3552
+ const sql = (0, import_fs7.readFileSync)((0, import_path10.join)(dir, file), "utf-8");
3206
3553
  const tx = db.transaction(() => {
3207
3554
  db.exec(sql);
3208
3555
  recordApplied.run(file, (/* @__PURE__ */ new Date()).toISOString());
@@ -3214,7 +3561,7 @@ function runSqliteMigrations(db, migrationsDir) {
3214
3561
  }
3215
3562
 
3216
3563
  // src/services/conversations/isAgentConversation.ts
3217
- var import_fs7 = require("fs");
3564
+ var import_fs8 = require("fs");
3218
3565
  var DEFAULT_AGENT_ENTRYPOINTS = /* @__PURE__ */ new Set(["sdk-cli", "claude-vscode"]);
3219
3566
  var CHUNK_BYTES = 64 * 1024;
3220
3567
  var ENTRYPOINT_PROBE = `"entrypoint":`;
@@ -3236,12 +3583,12 @@ function isAgentFile(filePath, entrypoints = DEFAULT_AGENT_ENTRYPOINTS) {
3236
3583
  if (cached2 !== void 0) return cached2;
3237
3584
  let fd;
3238
3585
  try {
3239
- fd = (0, import_fs7.openSync)(filePath, "r");
3586
+ fd = (0, import_fs8.openSync)(filePath, "r");
3240
3587
  } catch {
3241
3588
  return false;
3242
3589
  }
3243
3590
  try {
3244
- const fileSize = (0, import_fs7.statSync)(filePath).size;
3591
+ const fileSize = (0, import_fs8.statSync)(filePath).size;
3245
3592
  if (fileSize === 0) {
3246
3593
  fileDecisionCache.set(key, false);
3247
3594
  return false;
@@ -3252,7 +3599,7 @@ function isAgentFile(filePath, entrypoints = DEFAULT_AGENT_ENTRYPOINTS) {
3252
3599
  let carry = "";
3253
3600
  while (offset < fileSize) {
3254
3601
  const toRead = Math.min(CHUNK_BYTES, fileSize - offset);
3255
- const got = (0, import_fs7.readSync)(fd, buf, 0, toRead, offset);
3602
+ const got = (0, import_fs8.readSync)(fd, buf, 0, toRead, offset);
3256
3603
  if (got <= 0) break;
3257
3604
  const chunk = carry + buf.toString("utf8", 0, got);
3258
3605
  for (const marker of markers) {
@@ -3273,7 +3620,7 @@ function isAgentFile(filePath, entrypoints = DEFAULT_AGENT_ENTRYPOINTS) {
3273
3620
  } catch {
3274
3621
  return false;
3275
3622
  } finally {
3276
- (0, import_fs7.closeSync)(fd);
3623
+ (0, import_fs8.closeSync)(fd);
3277
3624
  }
3278
3625
  }
3279
3626
  function parseAgentEntrypointsEnv(raw) {
@@ -3810,7 +4157,7 @@ var ConversationCache = class _ConversationCache {
3810
4157
  if (!fileState) return null;
3811
4158
  let stat3;
3812
4159
  try {
3813
- stat3 = (0, import_fs8.statSync)(filePath);
4160
+ stat3 = (0, import_fs9.statSync)(filePath);
3814
4161
  } catch {
3815
4162
  return null;
3816
4163
  }
@@ -3831,17 +4178,17 @@ var ConversationCache = class _ConversationCache {
3831
4178
  );
3832
4179
  if (rows.length === 0) return { messages: [], total, fromIndex: from };
3833
4180
  const messages = [];
3834
- const fd = (0, import_fs8.openSync)(filePath, "r");
4181
+ const fd = (0, import_fs9.openSync)(filePath, "r");
3835
4182
  try {
3836
4183
  const state = (0, import_scanner2.createJsonlParseState)();
3837
4184
  for (const row of rows) {
3838
4185
  const buf = Buffer.alloc(row.byte_length);
3839
- (0, import_fs8.readSync)(fd, buf, 0, row.byte_length, row.byte_offset);
4186
+ (0, import_fs9.readSync)(fd, buf, 0, row.byte_length, row.byte_offset);
3840
4187
  const msg = (0, import_scanner2.parseJsonlLine)(buf.toString("utf-8"), state);
3841
4188
  if (msg) messages.push(msg);
3842
4189
  }
3843
4190
  } finally {
3844
- (0, import_fs8.closeSync)(fd);
4191
+ (0, import_fs9.closeSync)(fd);
3845
4192
  }
3846
4193
  return { messages, total, fromIndex: from };
3847
4194
  }
@@ -3869,14 +4216,14 @@ var ConversationCache = class _ConversationCache {
3869
4216
  isAgentFileCached(filePath) {
3870
4217
  let s;
3871
4218
  try {
3872
- s = (0, import_fs8.statSync)(filePath);
4219
+ s = (0, import_fs9.statSync)(filePath);
3873
4220
  } catch {
3874
4221
  return false;
3875
4222
  }
3876
4223
  return this.classifyAgentFile(filePath, s.mtimeMs, s.size);
3877
4224
  }
3878
4225
  static open(dbPath, tailSize = 10, migrationsDir, options) {
3879
- (0, import_fs8.mkdirSync)((0, import_path10.dirname)(dbPath), { recursive: true });
4226
+ (0, import_fs9.mkdirSync)((0, import_path11.dirname)(dbPath), { recursive: true });
3880
4227
  const db = new import_better_sqlite3.default(dbPath);
3881
4228
  db.pragma("journal_mode = WAL");
3882
4229
  db.pragma("foreign_keys = ON");
@@ -4057,7 +4404,7 @@ var ConversationCache = class _ConversationCache {
4057
4404
  let mtimeMs = null;
4058
4405
  let fileSize = null;
4059
4406
  try {
4060
- const s = (0, import_fs8.statSync)(m.filePath);
4407
+ const s = (0, import_fs9.statSync)(m.filePath);
4061
4408
  mtimeMs = s.mtimeMs;
4062
4409
  fileSize = s.size;
4063
4410
  } catch {
@@ -4116,8 +4463,8 @@ var ConversationCache = class _ConversationCache {
4116
4463
  let fileSize;
4117
4464
  let fd;
4118
4465
  try {
4119
- fileSize = (0, import_fs8.statSync)(filePath).size;
4120
- fd = (0, import_fs8.openSync)(filePath, "r");
4466
+ fileSize = (0, import_fs9.statSync)(filePath).size;
4467
+ fd = (0, import_fs9.openSync)(filePath, "r");
4121
4468
  } catch {
4122
4469
  return false;
4123
4470
  }
@@ -4130,7 +4477,7 @@ var ConversationCache = class _ConversationCache {
4130
4477
  while (pos > 0 && lines.length < this.tailSize * 4) {
4131
4478
  const toRead = Math.min(CHUNK, pos);
4132
4479
  pos -= toRead;
4133
- (0, import_fs8.readSync)(fd, buf, 0, toRead, pos);
4480
+ (0, import_fs9.readSync)(fd, buf, 0, toRead, pos);
4134
4481
  const chunk = buf.subarray(0, toRead).toString("utf8");
4135
4482
  const combined = chunk + partial;
4136
4483
  const parts = combined.split("\n");
@@ -4141,7 +4488,7 @@ var ConversationCache = class _ConversationCache {
4141
4488
  }
4142
4489
  if (partial) lines.push(partial);
4143
4490
  } finally {
4144
- (0, import_fs8.closeSync)(fd);
4491
+ (0, import_fs9.closeSync)(fd);
4145
4492
  }
4146
4493
  const msgs = [];
4147
4494
  for (let i = 0; i < lines.length && msgs.length < this.tailSize; i++) {
@@ -4343,7 +4690,7 @@ var ConversationCache = class _ConversationCache {
4343
4690
  * `handleGetConversation` can still serve the cached tail even when the
4344
4691
  * JSONL has been deleted.
4345
4692
  */
4346
- pruneGhostFiles(exists = import_fs8.existsSync) {
4693
+ pruneGhostFiles(exists = import_fs9.existsSync) {
4347
4694
  const rows = this.stmts.allFilePaths.all();
4348
4695
  const ghosts = [];
4349
4696
  const prune = this.db.transaction((ids) => {
@@ -4398,7 +4745,7 @@ var ConversationCache = class _ConversationCache {
4398
4745
  * Returns the removed IDs.
4399
4746
  */
4400
4747
  reconcileDeletions(livePaths, opts) {
4401
- const exists = opts?.exists ?? import_fs8.existsSync;
4748
+ const exists = opts?.exists ?? import_fs9.existsSync;
4402
4749
  const rows = this.stmts.allFilePaths.all();
4403
4750
  const removed = [];
4404
4751
  const drop = this.db.transaction((ids) => {
@@ -4627,23 +4974,23 @@ async function recordUpload(pool2, instanceId, row) {
4627
4974
  }
4628
4975
 
4629
4976
  // src/handlers/handleListProjects.ts
4630
- var import_fs9 = require("fs");
4631
- var import_os5 = require("os");
4632
- var import_path11 = require("path");
4977
+ var import_fs10 = require("fs");
4978
+ var import_os6 = require("os");
4979
+ var import_path12 = require("path");
4633
4980
  function decodeProjectPath(dirName) {
4634
4981
  return dirName.replace(/-/g, "/");
4635
4982
  }
4636
4983
  function handleListProjects(url, res) {
4637
4984
  const limit = Math.max(1, parseInt(url.searchParams.get("limit") ?? "50", 10) || 50);
4638
4985
  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");
4986
+ const projectsDir = (0, import_path12.join)((0, import_os6.homedir)(), ".claude", "projects");
4640
4987
  let entries;
4641
4988
  try {
4642
- entries = (0, import_fs9.readdirSync)(projectsDir).map((dirName) => {
4643
- const fullPath = (0, import_path11.join)(projectsDir, dirName);
4989
+ entries = (0, import_fs10.readdirSync)(projectsDir).map((dirName) => {
4990
+ const fullPath = (0, import_path12.join)(projectsDir, dirName);
4644
4991
  let mtime = 0;
4645
4992
  try {
4646
- mtime = (0, import_fs9.statSync)(fullPath).mtimeMs;
4993
+ mtime = (0, import_fs10.statSync)(fullPath).mtimeMs;
4647
4994
  } catch {
4648
4995
  }
4649
4996
  const path = decodeProjectPath(dirName);
@@ -4738,7 +5085,7 @@ function seal(plaintext, recipientPublicKeyBase64) {
4738
5085
 
4739
5086
  // src/services/conversations/conversationWatcher.ts
4740
5087
  var import_chokidar = __toESM(require("chokidar"), 1);
4741
- var import_fs10 = require("fs");
5088
+ var import_fs11 = require("fs");
4742
5089
  var import_promises5 = require("fs/promises");
4743
5090
  var ConversationWatcher = class {
4744
5091
  files = /* @__PURE__ */ new Map();
@@ -4761,7 +5108,7 @@ var ConversationWatcher = class {
4761
5108
  if (this.files.has(filePath)) return;
4762
5109
  let offset;
4763
5110
  try {
4764
- offset = (0, import_fs10.statSync)(filePath).size;
5111
+ offset = (0, import_fs11.statSync)(filePath).size;
4765
5112
  } catch {
4766
5113
  offset = 0;
4767
5114
  }
@@ -4936,14 +5283,14 @@ function findSearchTarget(messages, query) {
4936
5283
  }
4937
5284
 
4938
5285
  // src/services/conversations/pruneAgentConversations.ts
4939
- var import_fs11 = require("fs");
5286
+ var import_fs12 = require("fs");
4940
5287
  function pruneAgentConversations(cache) {
4941
5288
  const db = cache.getDatabase();
4942
5289
  const rows = db.prepare("SELECT id, file_path FROM conversation_meta").all();
4943
5290
  let pruned = 0;
4944
5291
  let missing = 0;
4945
5292
  for (const row of rows) {
4946
- if (!(0, import_fs11.existsSync)(row.file_path)) {
5293
+ if (!(0, import_fs12.existsSync)(row.file_path)) {
4947
5294
  missing += 1;
4948
5295
  continue;
4949
5296
  }
@@ -5286,7 +5633,7 @@ function discoveredToResponse(d, conversationId) {
5286
5633
  var import_crypto8 = require("crypto");
5287
5634
  var import_promises6 = require("fs/promises");
5288
5635
  var import_heic_convert = __toESM(require("heic-convert"), 1);
5289
- var import_path12 = require("path");
5636
+ var import_path13 = require("path");
5290
5637
  var UPLOAD_DIR_NAME = ".threadbase-uploads";
5291
5638
  var MAX_BYTES = 25 * 1024 * 1024;
5292
5639
  var HEIC_MIMES = /* @__PURE__ */ new Set(["image/heic", "image/heif"]);
@@ -5319,9 +5666,9 @@ async function saveUploadFile(input) {
5319
5666
  }
5320
5667
  const id = `up_${(0, import_crypto8.randomBytes)(8).toString("hex")}`;
5321
5668
  const safeName = sanitizeFilename(originalName) || `file${MIME_TO_EXT[mimeType] ?? ""}`;
5322
- const dir = (0, import_path12.join)(input.projectPath, UPLOAD_DIR_NAME, input.sessionId);
5669
+ const dir = (0, import_path13.join)(input.projectPath, UPLOAD_DIR_NAME, input.sessionId);
5323
5670
  await (0, import_promises6.mkdir)(dir, { recursive: true });
5324
- const filePath = (0, import_path12.join)(dir, `${Date.now()}-${id}-${safeName}`);
5671
+ const filePath = (0, import_path13.join)(dir, `${Date.now()}-${id}-${safeName}`);
5325
5672
  await (0, import_promises6.writeFile)(filePath, buffer);
5326
5673
  return {
5327
5674
  id,
@@ -5580,7 +5927,7 @@ var DEFAULT_PTY_GRACE_PERIOD_MS = 27e4;
5580
5927
  var DEFAULT_WS_AUTH_TIMEOUT_MS = 5e3;
5581
5928
  var WS_CLOSE_UNAUTHORIZED = 4401;
5582
5929
  var REFRESH_TTL_MS = 2e3;
5583
- var START_READY_TIMEOUT_MS = 15e3;
5930
+ var START_READY_TIMEOUT_MS = 1e4;
5584
5931
  function parseIncludeAgentsEnv(raw) {
5585
5932
  if (raw === void 0) return false;
5586
5933
  const v = raw.trim().toLowerCase();
@@ -5714,12 +6061,12 @@ var StreamerServer = class {
5714
6061
  this.disableDb = config.disableDb ?? false;
5715
6062
  this.scannerPersistenceDisabled = config.scannerPersistent === false;
5716
6063
  this.scanProfiles = config.scanProfiles;
5717
- this.codexRoots = config.codexRoots ?? [(0, import_path13.join)((0, import_os6.homedir)(), ".codex", "sessions")];
6064
+ this.codexRoots = config.codexRoots ?? [(0, import_path14.join)((0, import_os7.homedir)(), ".codex", "sessions")];
5718
6065
  this.ptyGracePeriodMs = config.ptyGracePeriodMs ?? DEFAULT_PTY_GRACE_PERIOD_MS;
5719
6066
  this.wsAuthTimeoutMs = config.wsAuthTimeoutMs ?? DEFAULT_WS_AUTH_TIMEOUT_MS;
5720
6067
  this.defaultSystemPrompt = config.defaultSystemPrompt ?? DEFAULT_SYSTEM_PROMPT;
5721
6068
  this.defaultPermissionMode = config.defaultPermissionMode ?? loadDefaultPermissionMode() ?? "acceptEdits";
5722
- this.cacheDir = config.cacheDir ?? loadCacheDir() ?? (0, import_path13.join)((0, import_os6.homedir)(), ".threadbase", "cache");
6069
+ this.cacheDir = config.cacheDir ?? loadCacheDir() ?? (0, import_path14.join)((0, import_os7.homedir)(), ".threadbase", "cache");
5723
6070
  this.tailSize = config.tailSize ?? loadTailSize() ?? 10;
5724
6071
  this.directoryDebounceMs = parseDirScanDebounceEnv(process.env.THREADBASE_DIR_SCAN_DEBOUNCE_MS) ?? config.directoryScanDebounceMs ?? 1e3;
5725
6072
  this.markScannerStaleDebounced = debounce(() => {
@@ -5760,7 +6107,7 @@ var StreamerServer = class {
5760
6107
  const seqs = cache.extendMessageIndex(
5761
6108
  filePath,
5762
6109
  spans,
5763
- (0, import_fs12.statSync)(filePath),
6110
+ (0, import_fs13.statSync)(filePath),
5764
6111
  readFrom,
5765
6112
  endOffset
5766
6113
  );
@@ -5921,7 +6268,7 @@ var StreamerServer = class {
5921
6268
  temporalClient,
5922
6269
  taskQueue: agentConfig.temporal.taskQueue
5923
6270
  });
5924
- const conversationsBaseDir = agentConfig.conversationsDir || (0, import_path13.join)((0, import_path13.dirname)(this.cacheDir), "conversations");
6271
+ const conversationsBaseDir = agentConfig.conversationsDir || (0, import_path14.join)((0, import_path14.dirname)(this.cacheDir), "conversations");
5925
6272
  conversationWriter = createConversationWriter({
5926
6273
  baseDir: conversationsBaseDir
5927
6274
  });
@@ -6019,6 +6366,38 @@ var StreamerServer = class {
6019
6366
  const lines = await this.ptyManager.getOutputLines(msg.sessionId, 200);
6020
6367
  ws.send(JSON.stringify({ type: "terminal_replay", sessionId: msg.sessionId, lines }));
6021
6368
  }
6369
+ const pendingGate = this.pendingPermission.get(msg.sessionId);
6370
+ if (pendingGate) {
6371
+ this.log.info(`[ws.replay_permission] ${msg.sessionId.slice(0, 8)}`, {
6372
+ event: "ws.replay_permission",
6373
+ sessionId: msg.sessionId
6374
+ });
6375
+ ws.send(
6376
+ JSON.stringify({
6377
+ type: "permission",
6378
+ sessionId: msg.sessionId,
6379
+ ...pendingGate.prompt ? { prompt: pendingGate.prompt } : {},
6380
+ ...pendingGate.detail ? { detail: pendingGate.detail } : {},
6381
+ options: pendingGate.options,
6382
+ ...pendingGate.cursor !== void 0 ? { cursor: pendingGate.cursor } : {}
6383
+ })
6384
+ );
6385
+ }
6386
+ const pendingQuestion = this.pendingQuestions.get(msg.sessionId);
6387
+ if (pendingQuestion) {
6388
+ this.log.info(`[ws.replay_question] ${msg.sessionId.slice(0, 8)}`, {
6389
+ event: "ws.replay_question",
6390
+ sessionId: msg.sessionId
6391
+ });
6392
+ ws.send(
6393
+ JSON.stringify({
6394
+ type: "question",
6395
+ sessionId: msg.sessionId,
6396
+ toolUseId: pendingQuestion.toolUseId,
6397
+ questions: pendingQuestion.questions
6398
+ })
6399
+ );
6400
+ }
6022
6401
  }
6023
6402
  if (msg.type === "hold_session" && typeof msg.sessionId === "string") {
6024
6403
  if (this.sessionSubscribers.get(msg.sessionId)?.has(ws)) {
@@ -6189,7 +6568,7 @@ var StreamerServer = class {
6189
6568
  });
6190
6569
  try {
6191
6570
  this.cache = ConversationCache.open(
6192
- (0, import_path13.join)(this.cacheDir, "cache.db"),
6571
+ (0, import_path14.join)(this.cacheDir, "cache.db"),
6193
6572
  this.tailSize,
6194
6573
  void 0,
6195
6574
  {
@@ -6215,11 +6594,11 @@ var StreamerServer = class {
6215
6594
  if (this.scanProfiles && this.scanProfiles.length > 0) {
6216
6595
  for (const profile of this.scanProfiles) {
6217
6596
  if (profile.enabled) {
6218
- this.fileWatcher.watchDirectory((0, import_path13.join)(profile.configDir, "projects"));
6597
+ this.fileWatcher.watchDirectory((0, import_path14.join)(profile.configDir, "projects"));
6219
6598
  }
6220
6599
  }
6221
6600
  } else {
6222
- this.fileWatcher.watchDirectory((0, import_path13.join)((0, import_os6.homedir)(), ".claude", "projects"));
6601
+ this.fileWatcher.watchDirectory((0, import_path14.join)((0, import_os7.homedir)(), ".claude", "projects"));
6223
6602
  }
6224
6603
  } catch (err) {
6225
6604
  const message = err instanceof Error ? err.message : String(err);
@@ -6804,17 +7183,17 @@ var StreamerServer = class {
6804
7183
  return scanner;
6805
7184
  }
6806
7185
  findJsonlPath(uuid) {
6807
- const projectsDir = (0, import_path13.join)((0, import_os6.homedir)(), ".claude", "projects");
6808
- if (!(0, import_fs12.existsSync)(projectsDir)) return null;
7186
+ const projectsDir = (0, import_path14.join)((0, import_os7.homedir)(), ".claude", "projects");
7187
+ if (!(0, import_fs13.existsSync)(projectsDir)) return null;
6809
7188
  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);
7189
+ for (const dir of (0, import_fs13.readdirSync)(projectsDir)) {
7190
+ const fp = (0, import_path14.join)(projectsDir, dir, filename);
7191
+ if ((0, import_fs13.existsSync)(fp)) return fp;
7192
+ const projectDir = (0, import_path14.join)(projectsDir, dir);
6814
7193
  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;
7194
+ for (const sub of (0, import_fs13.readdirSync)(projectDir)) {
7195
+ const subagentPath = (0, import_path14.join)(projectDir, sub, "subagents", filename);
7196
+ if ((0, import_fs13.existsSync)(subagentPath)) return subagentPath;
6818
7197
  }
6819
7198
  } catch {
6820
7199
  }
@@ -6823,7 +7202,7 @@ var StreamerServer = class {
6823
7202
  }
6824
7203
  async readCwdFromJsonl(filePath) {
6825
7204
  return new Promise((resolve2) => {
6826
- const rl = (0, import_readline.createInterface)({ input: (0, import_fs12.createReadStream)(filePath), crlfDelay: Infinity });
7205
+ const rl = (0, import_readline.createInterface)({ input: (0, import_fs13.createReadStream)(filePath), crlfDelay: Infinity });
6827
7206
  let found = false;
6828
7207
  rl.on("line", (line) => {
6829
7208
  if (found) return;
@@ -6961,7 +7340,7 @@ var StreamerServer = class {
6961
7340
  if (!conv.filePath) return false;
6962
7341
  let mtimeMs = null;
6963
7342
  try {
6964
- mtimeMs = (0, import_fs12.statSync)(conv.filePath).mtimeMs;
7343
+ mtimeMs = (0, import_fs13.statSync)(conv.filePath).mtimeMs;
6965
7344
  } catch {
6966
7345
  return false;
6967
7346
  }
@@ -7329,7 +7708,7 @@ var StreamerServer = class {
7329
7708
  handleGetSession(sessionId, res) {
7330
7709
  const session = this.sessionStore.get(sessionId, this.ptyAttachedIds());
7331
7710
  if (session) {
7332
- if (!(0, import_fs12.existsSync)(session.projectPath)) {
7711
+ if (!(0, import_fs13.existsSync)(session.projectPath)) {
7333
7712
  session.failureReason = `Project directory not found: ${session.projectPath}`;
7334
7713
  }
7335
7714
  json(res, 200, session);
@@ -7537,6 +7916,11 @@ var StreamerServer = class {
7537
7916
  const answerKeys = sanitizeAnswerKeys(o.answerKeys);
7538
7917
  return answerKeys === void 0 ? { index: o.index, label: o.label } : { ...o, answerKeys };
7539
7918
  });
7919
+ const subscriberCount = this.sessionSubscribers.get(sessionId)?.size ?? 0;
7920
+ this.log.info(
7921
+ `[ws.broadcast_permission] ${sessionId.slice(0, 8)} subscribers=${subscriberCount}`,
7922
+ { event: "ws.broadcast_permission", sessionId, subscriberCount }
7923
+ );
7540
7924
  this.wsHub.broadcast({
7541
7925
  type: "permission",
7542
7926
  sessionId,
@@ -7729,7 +8113,7 @@ var StreamerServer = class {
7729
8113
  sessionStore: this.sessionStore,
7730
8114
  // biome-ignore lint/style/noNonNullAssertion: agentClient is set when agentConfig.enabled is true
7731
8115
  agentClient: this.agentClient,
7732
- conversationsDir: this.cacheDir ? (0, import_path13.join)((0, import_path13.dirname)(this.cacheDir), "conversations") : "",
8116
+ conversationsDir: this.cacheDir ? (0, import_path14.join)((0, import_path14.dirname)(this.cacheDir), "conversations") : "",
7733
8117
  agentConfig: this.agentConfig
7734
8118
  });
7735
8119
  json(res, result.status, result.body);
@@ -7870,9 +8254,9 @@ var StreamerServer = class {
7870
8254
  // was passed to Claude via --session-id so the filename matches from the start.
7871
8255
  watchForJsonl(sessionId, projectPath) {
7872
8256
  const encoded = projectPath.replace(/[/\\:.]/g, "-");
7873
- const projectsDir = (0, import_path13.join)((0, import_os6.homedir)(), ".claude", "projects", encoded);
8257
+ const projectsDir = (0, import_path14.join)((0, import_os7.homedir)(), ".claude", "projects", encoded);
7874
8258
  const expectedFile = `${sessionId}.jsonl`;
7875
- const filePath = (0, import_path13.join)(projectsDir, expectedFile);
8259
+ const filePath = (0, import_path14.join)(projectsDir, expectedFile);
7876
8260
  const deadline = Date.now() + 12e4;
7877
8261
  let watcher = null;
7878
8262
  const cleanup = () => {
@@ -7890,12 +8274,12 @@ var StreamerServer = class {
7890
8274
  cleanup();
7891
8275
  return;
7892
8276
  }
7893
- let resolvedFilePath = (0, import_fs12.existsSync)(filePath) ? filePath : null;
7894
- if (!resolvedFilePath && (0, import_fs12.existsSync)(projectsDir)) {
8277
+ let resolvedFilePath = (0, import_fs13.existsSync)(filePath) ? filePath : null;
8278
+ if (!resolvedFilePath && (0, import_fs13.existsSync)(projectsDir)) {
7895
8279
  try {
7896
8280
  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);
8281
+ 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];
8282
+ if (recent) resolvedFilePath = (0, import_path14.join)(projectsDir, recent.f);
7899
8283
  } catch {
7900
8284
  }
7901
8285
  }
@@ -7904,7 +8288,7 @@ var StreamerServer = class {
7904
8288
  this.sessionFileMap.set(sessionId, resolvedFilePath);
7905
8289
  this.fileWatcher.watch(resolvedFilePath);
7906
8290
  try {
7907
- const existing = (0, import_fs12.readFileSync)(resolvedFilePath, "utf8").split("\n").filter(Boolean);
8291
+ const existing = (0, import_fs13.readFileSync)(resolvedFilePath, "utf8").split("\n").filter(Boolean);
7908
8292
  if (existing.length > 0) {
7909
8293
  this.broadcastConversationLines(sessionId, existing);
7910
8294
  }
@@ -7927,7 +8311,7 @@ var StreamerServer = class {
7927
8311
  if (this.sessionFileMap.has(sessionId)) return;
7928
8312
  try {
7929
8313
  require("fs").mkdirSync(projectsDir, { recursive: true });
7930
- watcher = (0, import_fs12.watch)(projectsDir, tryWire);
8314
+ watcher = (0, import_fs13.watch)(projectsDir, tryWire);
7931
8315
  watcher.on("error", cleanup);
7932
8316
  } catch {
7933
8317
  }
@@ -7943,7 +8327,7 @@ var StreamerServer = class {
7943
8327
  watchForCodexRollout(sessionId, projectPath) {
7944
8328
  const deadline = Date.now() + 12e4;
7945
8329
  const now = /* @__PURE__ */ new Date();
7946
- const dateDir = (0, import_path13.join)(
8330
+ const dateDir = (0, import_path14.join)(
7947
8331
  String(now.getFullYear()),
7948
8332
  String(now.getMonth() + 1).padStart(2, "0"),
7949
8333
  String(now.getDate()).padStart(2, "0")
@@ -7956,7 +8340,7 @@ var StreamerServer = class {
7956
8340
  };
7957
8341
  const matchesProjectPath = (candidatePath) => {
7958
8342
  try {
7959
- const firstLine = (0, import_fs12.readFileSync)(candidatePath, "utf8").split("\n", 1)[0];
8343
+ const firstLine = (0, import_fs13.readFileSync)(candidatePath, "utf8").split("\n", 1)[0];
7960
8344
  if (!firstLine) return null;
7961
8345
  const parsed = JSON.parse(firstLine);
7962
8346
  if (parsed?.type !== "session_meta") return null;
@@ -7984,18 +8368,18 @@ var StreamerServer = class {
7984
8368
  this.sessionStore.listManaged().filter((s) => s.id !== sessionId && s.boundConversationId != null).map((s) => s.boundConversationId)
7985
8369
  );
7986
8370
  for (const root of this.codexRoots) {
7987
- const sessionsDir = (0, import_path13.join)(root, dateDir);
7988
- if (!(0, import_fs12.existsSync)(sessionsDir)) continue;
8371
+ const sessionsDir = (0, import_path14.join)(root, dateDir);
8372
+ if (!(0, import_fs13.existsSync)(sessionsDir)) continue;
7989
8373
  let candidateFiles;
7990
8374
  try {
7991
- candidateFiles = (0, import_fs12.readdirSync)(sessionsDir).filter((f) => f.endsWith(".jsonl"));
8375
+ candidateFiles = (0, import_fs13.readdirSync)(sessionsDir).filter((f) => f.endsWith(".jsonl"));
7992
8376
  } catch {
7993
8377
  continue;
7994
8378
  }
7995
8379
  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);
8380
+ 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
8381
  for (const { f } of recentCandidates) {
7998
- const candidatePath = (0, import_path13.join)(sessionsDir, f);
8382
+ const candidatePath = (0, import_path14.join)(sessionsDir, f);
7999
8383
  const match = matchesProjectPath(candidatePath);
8000
8384
  if (!match) continue;
8001
8385
  if (boundElsewhere.has(match.id)) continue;
@@ -8005,7 +8389,7 @@ var StreamerServer = class {
8005
8389
  this.sessionFileMap.set(sessionId, candidatePath);
8006
8390
  this.fileWatcher.watch(candidatePath);
8007
8391
  try {
8008
- const existing = (0, import_fs12.readFileSync)(candidatePath, "utf8").split("\n").filter(Boolean);
8392
+ const existing = (0, import_fs13.readFileSync)(candidatePath, "utf8").split("\n").filter(Boolean);
8009
8393
  if (existing.length > 0) {
8010
8394
  this.broadcastConversationLines(sessionId, existing);
8011
8395
  }
@@ -8127,7 +8511,7 @@ var StreamerServer = class {
8127
8511
  };
8128
8512
  function classifyResumability(cwd) {
8129
8513
  if (!cwd) return { resumable: true };
8130
- if ((0, import_fs12.existsSync)(cwd)) return { resumable: true };
8514
+ if ((0, import_fs13.existsSync)(cwd)) return { resumable: true };
8131
8515
  const ranInWorktree = /\/\.worktrees\//.test(cwd) || /\/\.claude\/worktrees\//.test(cwd);
8132
8516
  return {
8133
8517
  resumable: false,