@threadbase-sh/streamer 1.29.1 → 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/cli.cjs +885 -438
- package/dist/cli.cjs.map +1 -1
- package/dist/index.cjs +662 -207
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +6 -1
- package/dist/index.d.ts +6 -1
- package/dist/index.js +645 -190
- package/dist/index.js.map +1 -1
- package/package.json +1 -1
package/dist/index.js
CHANGED
|
@@ -164,7 +164,7 @@ function verifySignature(rawBody, signature, secret) {
|
|
|
164
164
|
}
|
|
165
165
|
}
|
|
166
166
|
function isWithinSkew(timestampHeader, skewSeconds) {
|
|
167
|
-
if (!timestampHeader) return
|
|
167
|
+
if (!timestampHeader) return false;
|
|
168
168
|
const t = Number(timestampHeader);
|
|
169
169
|
if (!Number.isFinite(t)) return false;
|
|
170
170
|
const now = Math.floor(Date.now() / 1e3);
|
|
@@ -680,6 +680,69 @@ function isProviderResumable(_provider, availabilityResumable) {
|
|
|
680
680
|
return availabilityResumable;
|
|
681
681
|
}
|
|
682
682
|
|
|
683
|
+
// src/services/questions/codexGateAnswers.ts
|
|
684
|
+
import { mkdirSync as mkdirSync2, readFileSync as readFileSync3, writeFileSync as writeFileSync2 } from "fs";
|
|
685
|
+
import { homedir as homedir3 } from "os";
|
|
686
|
+
import { dirname as dirname3, join as join5 } from "path";
|
|
687
|
+
function gateAnswersPath() {
|
|
688
|
+
const dir = process.env.THREADBASE_CONFIG_DIR ?? join5(homedir3(), ".threadbase");
|
|
689
|
+
return join5(dir, "gate-answers.json");
|
|
690
|
+
}
|
|
691
|
+
function loadGateAnswers() {
|
|
692
|
+
try {
|
|
693
|
+
const parsed = JSON.parse(readFileSync3(gateAnswersPath(), "utf-8"));
|
|
694
|
+
return parsed && typeof parsed === "object" ? parsed : {};
|
|
695
|
+
} catch {
|
|
696
|
+
return {};
|
|
697
|
+
}
|
|
698
|
+
}
|
|
699
|
+
function saveGateAnswer(key, value) {
|
|
700
|
+
const path = gateAnswersPath();
|
|
701
|
+
mkdirSync2(dirname3(path), { recursive: true });
|
|
702
|
+
writeFileSync2(path, `${JSON.stringify({ ...loadGateAnswers(), [key]: value }, null, 2)}
|
|
703
|
+
`);
|
|
704
|
+
}
|
|
705
|
+
function rememberedGateDigit(gate) {
|
|
706
|
+
const answers = loadGateAnswers();
|
|
707
|
+
if (gate === "hooks") {
|
|
708
|
+
if (answers.codexHooksGate === "trust_all") return "2";
|
|
709
|
+
if (answers.codexHooksGate === "continue_untrusted") return "3";
|
|
710
|
+
return null;
|
|
711
|
+
}
|
|
712
|
+
return answers.codexTrustGate === "yes" ? "1" : null;
|
|
713
|
+
}
|
|
714
|
+
|
|
715
|
+
// src/utils/debounce.ts
|
|
716
|
+
function debounce(fn, waitMs) {
|
|
717
|
+
let timer = null;
|
|
718
|
+
let lastArgs = null;
|
|
719
|
+
const run2 = () => {
|
|
720
|
+
timer = null;
|
|
721
|
+
if (lastArgs) {
|
|
722
|
+
const args = lastArgs;
|
|
723
|
+
lastArgs = null;
|
|
724
|
+
fn(...args);
|
|
725
|
+
}
|
|
726
|
+
};
|
|
727
|
+
const debounced = (...args) => {
|
|
728
|
+
lastArgs = args;
|
|
729
|
+
if (timer) clearTimeout(timer);
|
|
730
|
+
timer = setTimeout(run2, waitMs);
|
|
731
|
+
};
|
|
732
|
+
debounced.cancel = () => {
|
|
733
|
+
if (timer) clearTimeout(timer);
|
|
734
|
+
timer = null;
|
|
735
|
+
lastArgs = null;
|
|
736
|
+
};
|
|
737
|
+
debounced.flush = () => {
|
|
738
|
+
if (timer) {
|
|
739
|
+
clearTimeout(timer);
|
|
740
|
+
run2();
|
|
741
|
+
}
|
|
742
|
+
};
|
|
743
|
+
return debounced;
|
|
744
|
+
}
|
|
745
|
+
|
|
683
746
|
// src/codex-pty-runner.ts
|
|
684
747
|
var OUTPUT_BUFFER_MAX = 65536;
|
|
685
748
|
var PTY_COLS = 120;
|
|
@@ -687,6 +750,9 @@ var PTY_ROWS = 40;
|
|
|
687
750
|
var SCREEN_SCROLLBACK = 1e3;
|
|
688
751
|
var CODEX_PROMPT_READY_TEXT = "Ready";
|
|
689
752
|
var CODEX_TRUST_GATE_REGEX = /trust the contents/i;
|
|
753
|
+
var CODEX_HOOKS_GATE_REGEX = /hooks need review/i;
|
|
754
|
+
var QUIET_DETECT_MS = 500;
|
|
755
|
+
var CODEX_READY_FALLBACK_MS = 8e3;
|
|
690
756
|
var SUBMIT_BYTES = "\r";
|
|
691
757
|
var CODEX_SUBMIT_DELAY_MS = 16;
|
|
692
758
|
function digestBytes(s) {
|
|
@@ -694,6 +760,40 @@ function digestBytes(s) {
|
|
|
694
760
|
if (escaped.length <= 200) return escaped;
|
|
695
761
|
return `${escaped.slice(0, 100)}\u2026[${escaped.length - 200}B omitted]\u2026${escaped.slice(-100)}`;
|
|
696
762
|
}
|
|
763
|
+
function gateCard(gate, lines) {
|
|
764
|
+
if (gate === "hooks") {
|
|
765
|
+
const countLine = lines.find((l) => /new or changed/i.test(l))?.trim();
|
|
766
|
+
return {
|
|
767
|
+
prompt: [
|
|
768
|
+
"Hooks need review",
|
|
769
|
+
countLine,
|
|
770
|
+
"Hooks can run outside the sandbox after you trust them."
|
|
771
|
+
].filter(Boolean).join(" \u2014 "),
|
|
772
|
+
options: [
|
|
773
|
+
{ index: 2, label: "Trust all and continue", answerKeys: "2\r" },
|
|
774
|
+
{ index: 3, label: "Continue without trusting (hooks won't run)", answerKeys: "3\r" },
|
|
775
|
+
{
|
|
776
|
+
index: 4,
|
|
777
|
+
label: "Trust all and continue (remember for all projects)",
|
|
778
|
+
answerKeys: "4\r"
|
|
779
|
+
},
|
|
780
|
+
{
|
|
781
|
+
index: 5,
|
|
782
|
+
label: "Continue without trusting (remember for all projects)",
|
|
783
|
+
answerKeys: "5\r"
|
|
784
|
+
}
|
|
785
|
+
]
|
|
786
|
+
};
|
|
787
|
+
}
|
|
788
|
+
return {
|
|
789
|
+
prompt: lines.find((l) => CODEX_TRUST_GATE_REGEX.test(l))?.trim() ?? "Do you trust the contents of this directory?",
|
|
790
|
+
options: [
|
|
791
|
+
{ index: 1, label: "Yes, continue", answerKeys: "1\r" },
|
|
792
|
+
{ index: 2, label: "No, quit", answerKeys: "2\r" },
|
|
793
|
+
{ index: 3, label: "Yes, continue (remember for all projects)", answerKeys: "3\r" }
|
|
794
|
+
]
|
|
795
|
+
};
|
|
796
|
+
}
|
|
697
797
|
var pty = null;
|
|
698
798
|
async function loadPty() {
|
|
699
799
|
if (pty) return pty;
|
|
@@ -720,8 +820,8 @@ var CodexPtyRunner = class {
|
|
|
720
820
|
onOutput;
|
|
721
821
|
onStatusChange;
|
|
722
822
|
onReady;
|
|
723
|
-
//
|
|
724
|
-
//
|
|
823
|
+
// Broadcasts Codex's blocking startup gates (directory trust, hooks review)
|
|
824
|
+
// as question cards; null dismisses the card once the gate leaves the screen.
|
|
725
825
|
onPermissionChange;
|
|
726
826
|
onLiveQuestion;
|
|
727
827
|
onLiveQuestionGone;
|
|
@@ -732,8 +832,18 @@ var CodexPtyRunner = class {
|
|
|
732
832
|
// Inputs received via sendInput() while the session was still pendingReady.
|
|
733
833
|
// Flushed in arrival order once Codex reaches Ready.
|
|
734
834
|
queuedInputs = /* @__PURE__ */ new Map();
|
|
735
|
-
//
|
|
736
|
-
|
|
835
|
+
// Gate currently on a session's screen (card broadcast, unanswered). While
|
|
836
|
+
// set, queued-input flushes are held — a flushed digit would CONFIRM a
|
|
837
|
+
// dialog option — and sendKeys() intercepts remember-variant digits.
|
|
838
|
+
openGate = /* @__PURE__ */ new Map();
|
|
839
|
+
// `${sessionId}:${gate}` once a gate has been actioned (auto-answered or
|
|
840
|
+
// card broadcast) — dedupes repaints of the same dialog.
|
|
841
|
+
gateActioned = /* @__PURE__ */ new Set();
|
|
842
|
+
// Per-session trailing debounce re-armed on every chunk; on quiet, re-runs
|
|
843
|
+
// screen detection so a blocked/truncated boot still reaches ready.
|
|
844
|
+
quietCheckers = /* @__PURE__ */ new Map();
|
|
845
|
+
// Per-session flat backstop from spawn (CODEX_READY_FALLBACK_MS).
|
|
846
|
+
readyFallbackTimers = /* @__PURE__ */ new Map();
|
|
737
847
|
// In-flight start()/startFresh() calls keyed by sessionId. A second
|
|
738
848
|
// concurrent resume for the same session (double-tap, client retry) awaits
|
|
739
849
|
// the first call's promise instead of spawning a duplicate PTY (CRITICAL #3).
|
|
@@ -792,6 +902,7 @@ var CodexPtyRunner = class {
|
|
|
792
902
|
};
|
|
793
903
|
this.sessions.set(sessionId, session);
|
|
794
904
|
this.pendingReady.add(sessionId);
|
|
905
|
+
this.armReadyFallback(sessionId);
|
|
795
906
|
proc.onData((data) => {
|
|
796
907
|
this.handleOutput(sessionId, data);
|
|
797
908
|
});
|
|
@@ -837,6 +948,7 @@ var CodexPtyRunner = class {
|
|
|
837
948
|
};
|
|
838
949
|
this.sessions.set(sessionId, session);
|
|
839
950
|
this.pendingReady.add(sessionId);
|
|
951
|
+
this.armReadyFallback(sessionId);
|
|
840
952
|
proc.onData((data) => {
|
|
841
953
|
this.handleOutput(sessionId, data);
|
|
842
954
|
});
|
|
@@ -846,6 +958,21 @@ var CodexPtyRunner = class {
|
|
|
846
958
|
});
|
|
847
959
|
return toPublicSession(session);
|
|
848
960
|
}
|
|
961
|
+
// Flat backstop: if neither the "Ready" marker nor the quiet-checker settled
|
|
962
|
+
// the session within CODEX_READY_FALLBACK_MS of spawn, mark it ready anyway
|
|
963
|
+
// so start requests resolve and mobile can watch the boot live. unref() so a
|
|
964
|
+
// pending timer never holds the process open.
|
|
965
|
+
armReadyFallback(sessionId) {
|
|
966
|
+
const timer = setTimeout(() => {
|
|
967
|
+
this.readyFallbackTimers.delete(sessionId);
|
|
968
|
+
const session = this.sessions.get(sessionId);
|
|
969
|
+
if (session?.status === "running" && this.pendingReady.has(sessionId)) {
|
|
970
|
+
this.markReady(sessionId, session, "fallback:timeout");
|
|
971
|
+
}
|
|
972
|
+
}, CODEX_READY_FALLBACK_MS);
|
|
973
|
+
timer.unref?.();
|
|
974
|
+
this.readyFallbackTimers.set(sessionId, timer);
|
|
975
|
+
}
|
|
849
976
|
// Write raw key bytes directly to the PTY, same as PTYManager.sendKeys.
|
|
850
977
|
sendKeys(sessionId, keys) {
|
|
851
978
|
const session = this.sessions.get(sessionId);
|
|
@@ -857,13 +984,47 @@ var CodexPtyRunner = class {
|
|
|
857
984
|
session.status = "running";
|
|
858
985
|
this.onStatusChange?.(toPublicSession(session));
|
|
859
986
|
}
|
|
987
|
+
const gate = this.openGate.get(sessionId);
|
|
988
|
+
const digit = gate ? /^([0-9])\r?$/.exec(keys)?.[1] : void 0;
|
|
989
|
+
const out = gate && digit ? this.resolveGateAnswer(sessionId, gate, digit) : keys;
|
|
860
990
|
this.log.info(
|
|
861
|
-
`[codex.keys.write] ${sessionId.slice(0, 8)} bytes=${
|
|
862
|
-
{ event: "codex.keys_write", sessionId, byteLen:
|
|
991
|
+
`[codex.keys.write] ${sessionId.slice(0, 8)} bytes=${out.length} digest=${digestBytes(out)}`,
|
|
992
|
+
{ event: "codex.keys_write", sessionId, byteLen: out.length }
|
|
863
993
|
);
|
|
864
|
-
session.process.write(
|
|
994
|
+
session.process.write(out);
|
|
865
995
|
session.lastActivityAt = /* @__PURE__ */ new Date();
|
|
866
996
|
}
|
|
997
|
+
// Map a gate-card digit to the PTY bytes that answer the real dialog,
|
|
998
|
+
// persisting the choice when the digit was a synthetic "remember for all
|
|
999
|
+
// projects" option (those numbers don't exist on the actual dialog and must
|
|
1000
|
+
// never reach codex). The trailing \r mobile sends is dropped: a digit alone
|
|
1001
|
+
// selects AND confirms (live-probe verified), and a stray Enter would land
|
|
1002
|
+
// on whatever screen follows.
|
|
1003
|
+
resolveGateAnswer(sessionId, gate, digit) {
|
|
1004
|
+
let real = digit;
|
|
1005
|
+
let remembered = false;
|
|
1006
|
+
if (gate === "hooks" && digit === "4") {
|
|
1007
|
+
saveGateAnswer("codexHooksGate", "trust_all");
|
|
1008
|
+
real = "2";
|
|
1009
|
+
remembered = true;
|
|
1010
|
+
} else if (gate === "hooks" && digit === "5") {
|
|
1011
|
+
saveGateAnswer("codexHooksGate", "continue_untrusted");
|
|
1012
|
+
real = "3";
|
|
1013
|
+
remembered = true;
|
|
1014
|
+
} else if (gate === "trust" && digit === "3") {
|
|
1015
|
+
saveGateAnswer("codexTrustGate", "yes");
|
|
1016
|
+
real = "1";
|
|
1017
|
+
remembered = true;
|
|
1018
|
+
}
|
|
1019
|
+
this.log.info(`[codex.gate_answer] ${sessionId.slice(0, 8)} ${gate} digit=${real}`, {
|
|
1020
|
+
event: "codex.gate_answer",
|
|
1021
|
+
sessionId,
|
|
1022
|
+
gate,
|
|
1023
|
+
digit: real,
|
|
1024
|
+
remembered
|
|
1025
|
+
});
|
|
1026
|
+
return real;
|
|
1027
|
+
}
|
|
867
1028
|
sendInput(sessionId, input) {
|
|
868
1029
|
const session = this.sessions.get(sessionId);
|
|
869
1030
|
if (!session) throw new Error(`Session not found: ${sessionId}`);
|
|
@@ -933,8 +1094,12 @@ var CodexPtyRunner = class {
|
|
|
933
1094
|
}, CODEX_SUBMIT_DELAY_MS);
|
|
934
1095
|
}
|
|
935
1096
|
// Drain any inputs sent while the session was still pendingReady, writing
|
|
936
|
-
// them in arrival order now that Codex is Ready.
|
|
1097
|
+
// them in arrival order now that Codex is Ready. No-op while a gate dialog
|
|
1098
|
+
// is open (a flushed digit would confirm a dialog option) or while still
|
|
1099
|
+
// pendingReady (markReady drains it) — the gate-close path re-drives it for
|
|
1100
|
+
// the ready-with-gate-open case.
|
|
937
1101
|
flushQueuedInputs(sessionId) {
|
|
1102
|
+
if (this.openGate.has(sessionId) || this.pendingReady.has(sessionId)) return;
|
|
938
1103
|
const queue = this.queuedInputs.get(sessionId);
|
|
939
1104
|
if (!queue || queue.length === 0) return;
|
|
940
1105
|
this.queuedInputs.delete(sessionId);
|
|
@@ -979,7 +1144,7 @@ var CodexPtyRunner = class {
|
|
|
979
1144
|
if (!session) return;
|
|
980
1145
|
this.pendingReady.delete(sessionId);
|
|
981
1146
|
this.queuedInputs.delete(sessionId);
|
|
982
|
-
this.
|
|
1147
|
+
this.clearSessionDetectors(sessionId);
|
|
983
1148
|
try {
|
|
984
1149
|
session.process.kill("SIGINT");
|
|
985
1150
|
} catch {
|
|
@@ -990,6 +1155,21 @@ var CodexPtyRunner = class {
|
|
|
990
1155
|
this.sessions.delete(sessionId);
|
|
991
1156
|
this.onStatusChange?.(toPublicSession(session));
|
|
992
1157
|
}
|
|
1158
|
+
// Drop a session's detection state: quiet-checker, ready-fallback timer,
|
|
1159
|
+
// gate bookkeeping — and dismiss a still-open gate card so mobile doesn't
|
|
1160
|
+
// keep rendering a question for a dead PTY.
|
|
1161
|
+
clearSessionDetectors(sessionId) {
|
|
1162
|
+
this.quietCheckers.get(sessionId)?.cancel();
|
|
1163
|
+
this.quietCheckers.delete(sessionId);
|
|
1164
|
+
const timer = this.readyFallbackTimers.get(sessionId);
|
|
1165
|
+
if (timer) clearTimeout(timer);
|
|
1166
|
+
this.readyFallbackTimers.delete(sessionId);
|
|
1167
|
+
if (this.openGate.delete(sessionId)) {
|
|
1168
|
+
this.onPermissionChange?.(sessionId, null);
|
|
1169
|
+
}
|
|
1170
|
+
this.gateActioned.delete(`${sessionId}:hooks`);
|
|
1171
|
+
this.gateActioned.delete(`${sessionId}:trust`);
|
|
1172
|
+
}
|
|
993
1173
|
getOutput(sessionId) {
|
|
994
1174
|
const session = this.sessions.get(sessionId);
|
|
995
1175
|
if (!session) throw new Error(`Session not found: ${sessionId}`);
|
|
@@ -1029,10 +1209,19 @@ var CodexPtyRunner = class {
|
|
|
1029
1209
|
}
|
|
1030
1210
|
session.screen.dispose();
|
|
1031
1211
|
}
|
|
1212
|
+
for (const sessionId of Array.from(this.quietCheckers.keys())) {
|
|
1213
|
+
this.clearSessionDetectors(sessionId);
|
|
1214
|
+
}
|
|
1215
|
+
for (const timer of this.readyFallbackTimers.values()) {
|
|
1216
|
+
clearTimeout(timer);
|
|
1217
|
+
}
|
|
1032
1218
|
this.sessions.clear();
|
|
1033
1219
|
this.pendingReady.clear();
|
|
1034
1220
|
this.queuedInputs.clear();
|
|
1035
|
-
this.
|
|
1221
|
+
this.openGate.clear();
|
|
1222
|
+
this.gateActioned.clear();
|
|
1223
|
+
this.quietCheckers.clear();
|
|
1224
|
+
this.readyFallbackTimers.clear();
|
|
1036
1225
|
}
|
|
1037
1226
|
handleOutput(sessionId, data) {
|
|
1038
1227
|
const session = this.sessions.get(sessionId);
|
|
@@ -1047,44 +1236,92 @@ var CodexPtyRunner = class {
|
|
|
1047
1236
|
session.screen.write(data);
|
|
1048
1237
|
session.lastOutput = stripAnsi(data);
|
|
1049
1238
|
this.onOutput?.(sessionId, data);
|
|
1050
|
-
this.
|
|
1239
|
+
this.detectScreenState(sessionId, "chunk").catch((err) => {
|
|
1051
1240
|
this.log.warn("[codex.ready_detect] failed", {
|
|
1052
1241
|
event: "codex.ready_detect_failed",
|
|
1053
1242
|
sessionId,
|
|
1054
1243
|
err
|
|
1055
1244
|
});
|
|
1056
1245
|
});
|
|
1246
|
+
let quiet = this.quietCheckers.get(sessionId);
|
|
1247
|
+
if (!quiet) {
|
|
1248
|
+
quiet = debounce(() => {
|
|
1249
|
+
this.detectScreenState(sessionId, "quiet").catch((err) => {
|
|
1250
|
+
this.log.warn("[codex.ready_detect] failed", {
|
|
1251
|
+
event: "codex.ready_detect_failed",
|
|
1252
|
+
sessionId,
|
|
1253
|
+
err
|
|
1254
|
+
});
|
|
1255
|
+
});
|
|
1256
|
+
}, QUIET_DETECT_MS);
|
|
1257
|
+
this.quietCheckers.set(sessionId, quiet);
|
|
1258
|
+
}
|
|
1259
|
+
quiet();
|
|
1057
1260
|
}
|
|
1058
|
-
// Renders the session's headless screen and
|
|
1059
|
-
//
|
|
1060
|
-
//
|
|
1061
|
-
//
|
|
1062
|
-
//
|
|
1063
|
-
|
|
1064
|
-
|
|
1261
|
+
// Renders the session's headless screen and drives both detections:
|
|
1262
|
+
// - Gates (directory trust, hooks review) — checked on EVERY pass,
|
|
1263
|
+
// independent of pendingReady, so a gate appearing after ready is still
|
|
1264
|
+
// surfaced and a gate leaving the screen closes its card.
|
|
1265
|
+
// - Readiness — the "Ready" status-bar marker while pendingReady, plus the
|
|
1266
|
+
// quiet path: after QUIET_DETECT_MS of PTY silence a still-pending
|
|
1267
|
+
// session is marked ready anyway (`›` alone is NOT a marker — Phase 0 —
|
|
1268
|
+
// but a quiet boot screen is more useful to the user live than a
|
|
1269
|
+
// spinner, and "Ready" may be truncated off the 120-col status bar).
|
|
1270
|
+
async detectScreenState(sessionId, trigger) {
|
|
1271
|
+
const session = this.sessions.get(sessionId);
|
|
1272
|
+
if (!session || session.status === "idle") return;
|
|
1065
1273
|
const lines = await this.getOutputLines(sessionId, PTY_ROWS);
|
|
1066
1274
|
const screenText = lines.join("\n");
|
|
1067
|
-
|
|
1068
|
-
|
|
1069
|
-
|
|
1070
|
-
|
|
1071
|
-
|
|
1072
|
-
|
|
1073
|
-
});
|
|
1074
|
-
session.process.write("\r");
|
|
1075
|
-
}
|
|
1076
|
-
return;
|
|
1275
|
+
const gate = CODEX_HOOKS_GATE_REGEX.test(screenText) ? "hooks" : CODEX_TRUST_GATE_REGEX.test(screenText) ? "trust" : null;
|
|
1276
|
+
if (gate) {
|
|
1277
|
+
this.handleGate(sessionId, session, gate, lines);
|
|
1278
|
+
} else if (this.openGate.delete(sessionId)) {
|
|
1279
|
+
this.onPermissionChange?.(sessionId, null);
|
|
1280
|
+
this.flushQueuedInputs(sessionId);
|
|
1077
1281
|
}
|
|
1282
|
+
if (session.status !== "running" || !this.pendingReady.has(sessionId)) return;
|
|
1078
1283
|
const lastNonBlank2 = [...lines].reverse().find((l) => l.trim() !== "") ?? "";
|
|
1079
|
-
if (
|
|
1080
|
-
|
|
1284
|
+
if (lastNonBlank2.includes(CODEX_PROMPT_READY_TEXT)) {
|
|
1285
|
+
this.markReady(sessionId, session, `marker:${CODEX_PROMPT_READY_TEXT}`);
|
|
1286
|
+
} else if (trigger === "quiet") {
|
|
1287
|
+
this.markReady(sessionId, session, "quiet:timeout");
|
|
1288
|
+
}
|
|
1081
1289
|
}
|
|
1082
|
-
|
|
1290
|
+
// Answer a gate from the persisted remember-store, or surface it as a
|
|
1291
|
+
// question card over the permission transport. Actioned once per session and
|
|
1292
|
+
// gate type — repaints of the same dialog neither re-write nor re-broadcast.
|
|
1293
|
+
handleGate(sessionId, session, gate, lines) {
|
|
1294
|
+
const key = `${sessionId}:${gate}`;
|
|
1295
|
+
if (this.gateActioned.has(key)) return;
|
|
1296
|
+
this.gateActioned.add(key);
|
|
1297
|
+
const remembered = rememberedGateDigit(gate);
|
|
1298
|
+
if (remembered) {
|
|
1299
|
+
this.log.info(`[codex.gate_auto_answer] ${sessionId.slice(0, 8)} ${gate} \u2192 ${remembered}`, {
|
|
1300
|
+
event: "codex.gate_auto_answer",
|
|
1301
|
+
sessionId,
|
|
1302
|
+
gate,
|
|
1303
|
+
digit: remembered
|
|
1304
|
+
});
|
|
1305
|
+
session.process.write(remembered);
|
|
1306
|
+
return;
|
|
1307
|
+
}
|
|
1308
|
+
this.openGate.set(sessionId, gate);
|
|
1309
|
+
const card = gateCard(gate, lines);
|
|
1310
|
+
this.log.info(`[codex.gate_prompt] ${sessionId.slice(0, 8)} ${gate}`, {
|
|
1311
|
+
event: "codex.gate_prompt",
|
|
1312
|
+
sessionId,
|
|
1313
|
+
gate,
|
|
1314
|
+
prompt: card.prompt
|
|
1315
|
+
});
|
|
1316
|
+
this.onPermissionChange?.(sessionId, card);
|
|
1317
|
+
}
|
|
1318
|
+
markReady(sessionId, session, reason) {
|
|
1083
1319
|
session.lastActivityAt = /* @__PURE__ */ new Date();
|
|
1084
1320
|
session.status = "waiting_input";
|
|
1085
|
-
this.log.info(`[codex.ready] ${sessionId.slice(0, 8)}`, {
|
|
1321
|
+
this.log.info(`[codex.ready] ${sessionId.slice(0, 8)} ${reason}`, {
|
|
1086
1322
|
event: "codex.ready",
|
|
1087
|
-
sessionId
|
|
1323
|
+
sessionId,
|
|
1324
|
+
reason
|
|
1088
1325
|
});
|
|
1089
1326
|
this.onStatusChange?.(toPublicSession(session));
|
|
1090
1327
|
if (this.pendingReady.has(sessionId)) {
|
|
@@ -1110,7 +1347,7 @@ var CodexPtyRunner = class {
|
|
|
1110
1347
|
session.screen.dispose();
|
|
1111
1348
|
this.sessions.delete(sessionId);
|
|
1112
1349
|
this.queuedInputs.delete(sessionId);
|
|
1113
|
-
this.
|
|
1350
|
+
this.clearSessionDetectors(sessionId);
|
|
1114
1351
|
}
|
|
1115
1352
|
};
|
|
1116
1353
|
function toPublicSession(s) {
|
|
@@ -1293,9 +1530,15 @@ function detectShellPrompt(lines) {
|
|
|
1293
1530
|
]
|
|
1294
1531
|
};
|
|
1295
1532
|
}
|
|
1296
|
-
|
|
1533
|
+
const lastNumberedIdx = (() => {
|
|
1534
|
+
for (let i = last.idx; i >= 0; i--) {
|
|
1535
|
+
if (NUMBERED_RE.test(lines[i])) return i;
|
|
1536
|
+
}
|
|
1537
|
+
return -1;
|
|
1538
|
+
})();
|
|
1539
|
+
if (lastNumberedIdx >= 0) {
|
|
1297
1540
|
const options = [];
|
|
1298
|
-
for (let i = 0; i <=
|
|
1541
|
+
for (let i = 0; i <= lastNumberedIdx; i++) {
|
|
1299
1542
|
const m = NUMBERED_RE.exec(lines[i]);
|
|
1300
1543
|
if (!m) continue;
|
|
1301
1544
|
const num = Number.parseInt(m[1], 10);
|
|
@@ -1323,37 +1566,6 @@ function detectShellPrompt(lines) {
|
|
|
1323
1566
|
return null;
|
|
1324
1567
|
}
|
|
1325
1568
|
|
|
1326
|
-
// src/utils/debounce.ts
|
|
1327
|
-
function debounce(fn, waitMs) {
|
|
1328
|
-
let timer = null;
|
|
1329
|
-
let lastArgs = null;
|
|
1330
|
-
const run2 = () => {
|
|
1331
|
-
timer = null;
|
|
1332
|
-
if (lastArgs) {
|
|
1333
|
-
const args = lastArgs;
|
|
1334
|
-
lastArgs = null;
|
|
1335
|
-
fn(...args);
|
|
1336
|
-
}
|
|
1337
|
-
};
|
|
1338
|
-
const debounced = (...args) => {
|
|
1339
|
-
lastArgs = args;
|
|
1340
|
-
if (timer) clearTimeout(timer);
|
|
1341
|
-
timer = setTimeout(run2, waitMs);
|
|
1342
|
-
};
|
|
1343
|
-
debounced.cancel = () => {
|
|
1344
|
-
if (timer) clearTimeout(timer);
|
|
1345
|
-
timer = null;
|
|
1346
|
-
lastArgs = null;
|
|
1347
|
-
};
|
|
1348
|
-
debounced.flush = () => {
|
|
1349
|
-
if (timer) {
|
|
1350
|
-
clearTimeout(timer);
|
|
1351
|
-
run2();
|
|
1352
|
-
}
|
|
1353
|
-
};
|
|
1354
|
-
return debounced;
|
|
1355
|
-
}
|
|
1356
|
-
|
|
1357
1569
|
// src/pty-manager.ts
|
|
1358
1570
|
var OUTPUT_BUFFER_MAX2 = 65536;
|
|
1359
1571
|
var PTY_COLS2 = 120;
|
|
@@ -1361,7 +1573,7 @@ var PTY_ROWS2 = 40;
|
|
|
1361
1573
|
var SCREEN_SCROLLBACK2 = 1e3;
|
|
1362
1574
|
var CLAUDE_PROMPT_MARKERS = ["\u256D", "\u276F"];
|
|
1363
1575
|
var PROMPT_MARKER_FALLBACK_MS = 1e4;
|
|
1364
|
-
var
|
|
1576
|
+
var QUIET_DETECT_MS2 = 500;
|
|
1365
1577
|
function buildPasteBytes(input) {
|
|
1366
1578
|
return `\x1B[200~${input}\x1B[201~`;
|
|
1367
1579
|
}
|
|
@@ -1861,7 +2073,7 @@ var PTYManager = class {
|
|
|
1861
2073
|
});
|
|
1862
2074
|
let quiet = this.quietCheckers.get(sessionId);
|
|
1863
2075
|
if (!quiet) {
|
|
1864
|
-
quiet = debounce(() => this.handleQuiet(sessionId),
|
|
2076
|
+
quiet = debounce(() => this.handleQuiet(sessionId), QUIET_DETECT_MS2);
|
|
1865
2077
|
this.quietCheckers.set(sessionId, quiet);
|
|
1866
2078
|
}
|
|
1867
2079
|
quiet();
|
|
@@ -2122,7 +2334,7 @@ var LiveSessionManager = class {
|
|
|
2122
2334
|
// src/process-discovery.ts
|
|
2123
2335
|
import { execFile } from "child_process";
|
|
2124
2336
|
import { platform as platform2 } from "os";
|
|
2125
|
-
import { basename as basename4, dirname as
|
|
2337
|
+
import { basename as basename4, dirname as dirname4 } from "path";
|
|
2126
2338
|
async function discoverClaudeProcesses() {
|
|
2127
2339
|
if (platform2() === "win32") return discoverWindows();
|
|
2128
2340
|
return discoverUnix();
|
|
@@ -2244,7 +2456,7 @@ async function getProcessInfoWindows(pid) {
|
|
|
2244
2456
|
const startedAt = /* @__PURE__ */ new Date(`${year}-${month}-${day}T${hour}:${min}:${sec}`);
|
|
2245
2457
|
if (Number.isNaN(startedAt.getTime())) return null;
|
|
2246
2458
|
const exePath = parts[3] ?? "";
|
|
2247
|
-
const cwd = exePath ?
|
|
2459
|
+
const cwd = exePath ? dirname4(exePath) : "";
|
|
2248
2460
|
return { cwd, args, startedAt };
|
|
2249
2461
|
} catch {
|
|
2250
2462
|
return null;
|
|
@@ -2276,16 +2488,16 @@ import {
|
|
|
2276
2488
|
import { EventEmitter } from "events";
|
|
2277
2489
|
import {
|
|
2278
2490
|
createReadStream,
|
|
2279
|
-
existsSync as
|
|
2491
|
+
existsSync as existsSync8,
|
|
2280
2492
|
watch as fsWatch,
|
|
2281
2493
|
readdirSync as readdirSync4,
|
|
2282
|
-
readFileSync as
|
|
2283
|
-
statSync as
|
|
2494
|
+
readFileSync as readFileSync7,
|
|
2495
|
+
statSync as statSync6
|
|
2284
2496
|
} from "fs";
|
|
2285
2497
|
import { realpath as realpath2 } from "fs/promises";
|
|
2286
2498
|
import { createServer } from "http";
|
|
2287
|
-
import { homedir as
|
|
2288
|
-
import { dirname as
|
|
2499
|
+
import { homedir as homedir7 } from "os";
|
|
2500
|
+
import { dirname as dirname8, join as join15 } from "path";
|
|
2289
2501
|
import { createInterface } from "readline";
|
|
2290
2502
|
|
|
2291
2503
|
// node_modules/nanoid/index.js
|
|
@@ -2469,7 +2681,7 @@ async function handleSendAgentInput(sessionId, body, deps) {
|
|
|
2469
2681
|
|
|
2470
2682
|
// src/agent/handle-start-agent-session.ts
|
|
2471
2683
|
import { existsSync as existsSync4 } from "fs";
|
|
2472
|
-
import { join as
|
|
2684
|
+
import { join as join6 } from "path";
|
|
2473
2685
|
function validateBody(body) {
|
|
2474
2686
|
if (body === null || body === void 0 || typeof body !== "object") {
|
|
2475
2687
|
return { ok: false };
|
|
@@ -2499,7 +2711,7 @@ async function handleStartAgentSession(body, deps) {
|
|
|
2499
2711
|
}
|
|
2500
2712
|
let conversationId = parsed.conversationId;
|
|
2501
2713
|
if (conversationId) {
|
|
2502
|
-
const jsonlPath =
|
|
2714
|
+
const jsonlPath = join6(deps.conversationsDir, `${conversationId}.jsonl`);
|
|
2503
2715
|
if (!existsSync4(jsonlPath)) {
|
|
2504
2716
|
return {
|
|
2505
2717
|
status: 404,
|
|
@@ -2544,14 +2756,15 @@ async function handleStartAgentSession(body, deps) {
|
|
|
2544
2756
|
}
|
|
2545
2757
|
|
|
2546
2758
|
// src/api/app.ts
|
|
2547
|
-
import { Hono as
|
|
2759
|
+
import { Hono as Hono12 } from "hono";
|
|
2548
2760
|
|
|
2549
2761
|
// src/api/middleware/auth.middleware.ts
|
|
2550
2762
|
function isLocalRequest(remoteAddr) {
|
|
2551
2763
|
const addr = remoteAddr ?? "";
|
|
2552
2764
|
return addr === "127.0.0.1" || addr === "::1" || addr === "::ffff:127.0.0.1";
|
|
2553
2765
|
}
|
|
2554
|
-
var PUBLIC_PATHS = /* @__PURE__ */ new Set(["/healthz"]);
|
|
2766
|
+
var PUBLIC_PATHS = /* @__PURE__ */ new Set(["/healthz", "/ws"]);
|
|
2767
|
+
var LOCAL_ONLY_PATHS = /* @__PURE__ */ new Set(["/api/logs", "/api/logs/meta"]);
|
|
2555
2768
|
var PUBLIC_POST_PATHS = /* @__PURE__ */ new Set(["/api/pair/exchange", "/api/__update"]);
|
|
2556
2769
|
var PUBLIC_POST_PREFIXES = ["/internal/sessions/"];
|
|
2557
2770
|
var authMiddleware = (deps) => async (c, next) => {
|
|
@@ -2562,8 +2775,12 @@ var authMiddleware = (deps) => async (c, next) => {
|
|
|
2562
2775
|
await next();
|
|
2563
2776
|
return;
|
|
2564
2777
|
}
|
|
2778
|
+
const remoteAddr = c.env.incoming?.socket?.remoteAddress;
|
|
2779
|
+
if (LOCAL_ONLY_PATHS.has(path) && isLocalRequest(remoteAddr)) {
|
|
2780
|
+
await next();
|
|
2781
|
+
return;
|
|
2782
|
+
}
|
|
2565
2783
|
if (deps.localNoAuth) {
|
|
2566
|
-
const remoteAddr = c.env.incoming?.socket?.remoteAddress;
|
|
2567
2784
|
if (isLocalRequest(remoteAddr)) {
|
|
2568
2785
|
await next();
|
|
2569
2786
|
return;
|
|
@@ -2696,8 +2913,8 @@ var createConversationRoutes = (deps) => {
|
|
|
2696
2913
|
import { Hono as Hono4 } from "hono";
|
|
2697
2914
|
|
|
2698
2915
|
// src/version.ts
|
|
2699
|
-
import { readFileSync as
|
|
2700
|
-
import { dirname as
|
|
2916
|
+
import { readFileSync as readFileSync4, realpathSync } from "fs";
|
|
2917
|
+
import { dirname as dirname5, join as join7 } from "path";
|
|
2701
2918
|
var cached;
|
|
2702
2919
|
function getVersion() {
|
|
2703
2920
|
if (cached !== void 0) return cached;
|
|
@@ -2706,22 +2923,22 @@ function getVersion() {
|
|
|
2706
2923
|
}
|
|
2707
2924
|
function resolveVersion() {
|
|
2708
2925
|
const scriptPath = process.argv[1] ?? "";
|
|
2709
|
-
const here = scriptPath ?
|
|
2926
|
+
const here = scriptPath ? dirname5(scriptPath) : process.cwd();
|
|
2710
2927
|
let realHere = here;
|
|
2711
2928
|
try {
|
|
2712
|
-
realHere =
|
|
2929
|
+
realHere = dirname5(realpathSync(scriptPath));
|
|
2713
2930
|
} catch {
|
|
2714
2931
|
}
|
|
2715
|
-
const searchDirs = realHere === here ? [here,
|
|
2932
|
+
const searchDirs = realHere === here ? [here, join7(here, "..")] : [here, join7(here, ".."), realHere, join7(realHere, "..")];
|
|
2716
2933
|
for (const dir of searchDirs) {
|
|
2717
2934
|
try {
|
|
2718
|
-
const v =
|
|
2935
|
+
const v = readFileSync4(join7(dir, "version.txt"), "utf8").trim();
|
|
2719
2936
|
if (v) return v;
|
|
2720
2937
|
} catch {
|
|
2721
2938
|
}
|
|
2722
2939
|
}
|
|
2723
2940
|
try {
|
|
2724
|
-
const pkg = JSON.parse(
|
|
2941
|
+
const pkg = JSON.parse(readFileSync4(join7(here, "..", "package.json"), "utf8"));
|
|
2725
2942
|
if (pkg.version) return `${pkg.version}+source`;
|
|
2726
2943
|
} catch {
|
|
2727
2944
|
}
|
|
@@ -2735,16 +2952,145 @@ var createHealthRoutes = () => {
|
|
|
2735
2952
|
return app;
|
|
2736
2953
|
};
|
|
2737
2954
|
|
|
2955
|
+
// src/api/routes/logs.routes.ts
|
|
2956
|
+
import { closeSync, existsSync as existsSync5, fstatSync, openSync, readSync, statSync } from "fs";
|
|
2957
|
+
import { join as join9 } from "path";
|
|
2958
|
+
import { Hono as Hono5 } from "hono";
|
|
2959
|
+
|
|
2960
|
+
// src/lifecycle/constants.ts
|
|
2961
|
+
import { homedir as homedir4 } from "os";
|
|
2962
|
+
import { join as join8 } from "path";
|
|
2963
|
+
var TASK_NAME = process.env.THREADBASE_TASK_NAME ?? "Threadbase";
|
|
2964
|
+
function installDir() {
|
|
2965
|
+
return process.env.THREADBASE_INSTALL_DIR ?? join8(homedir4(), ".threadbase");
|
|
2966
|
+
}
|
|
2967
|
+
|
|
2968
|
+
// src/api/routes/logs.routes.ts
|
|
2969
|
+
var logger2 = getLogger("logs-api");
|
|
2970
|
+
function resolveLogPath(source) {
|
|
2971
|
+
return join9(installDir(), "logs", `${source}.log`);
|
|
2972
|
+
}
|
|
2973
|
+
function pickDefaultSource() {
|
|
2974
|
+
for (const source of ["stdout", "stderr", "dev"]) {
|
|
2975
|
+
const p = resolveLogPath(source);
|
|
2976
|
+
if (existsSync5(p) && statSync(p).size > 0) return source;
|
|
2977
|
+
}
|
|
2978
|
+
return "stdout";
|
|
2979
|
+
}
|
|
2980
|
+
function readLogLines(filePath, sinceOffset, limit) {
|
|
2981
|
+
if (!existsSync5(filePath)) {
|
|
2982
|
+
return { lines: [], offset: 0, total: 0 };
|
|
2983
|
+
}
|
|
2984
|
+
const fd = openSync(filePath, "r");
|
|
2985
|
+
try {
|
|
2986
|
+
const { size } = fstatSync(fd);
|
|
2987
|
+
if (size === 0) return { lines: [], offset: 0, total: 0 };
|
|
2988
|
+
const maxBytes = Math.min(size, 2 * 1024 * 1024);
|
|
2989
|
+
const start = size - maxBytes;
|
|
2990
|
+
const buf = Buffer.alloc(maxBytes);
|
|
2991
|
+
readSync(fd, buf, 0, maxBytes, start);
|
|
2992
|
+
let text = buf.toString("utf8");
|
|
2993
|
+
if (start > 0) {
|
|
2994
|
+
const firstNl = text.indexOf("\n");
|
|
2995
|
+
if (firstNl >= 0) text = text.slice(firstNl + 1);
|
|
2996
|
+
}
|
|
2997
|
+
const allLines = text.split("\n").filter((line) => line.trim() && !line.startsWith("==="));
|
|
2998
|
+
let lines;
|
|
2999
|
+
let newOffset;
|
|
3000
|
+
if (sinceOffset > 0 && sinceOffset < allLines.length) {
|
|
3001
|
+
lines = allLines.slice(sinceOffset, sinceOffset + limit);
|
|
3002
|
+
newOffset = sinceOffset + lines.length;
|
|
3003
|
+
} else if (sinceOffset >= allLines.length && sinceOffset > 0) {
|
|
3004
|
+
lines = [];
|
|
3005
|
+
newOffset = allLines.length;
|
|
3006
|
+
} else {
|
|
3007
|
+
lines = allLines.slice(-limit);
|
|
3008
|
+
newOffset = allLines.length;
|
|
3009
|
+
}
|
|
3010
|
+
return { lines, offset: newOffset, total: allLines.length };
|
|
3011
|
+
} finally {
|
|
3012
|
+
closeSync(fd);
|
|
3013
|
+
}
|
|
3014
|
+
}
|
|
3015
|
+
function createLogsRoutes() {
|
|
3016
|
+
const app = new Hono5();
|
|
3017
|
+
app.get("/", (c) => {
|
|
3018
|
+
try {
|
|
3019
|
+
const sourceParam = (c.req.query("source") || "").toLowerCase();
|
|
3020
|
+
const source = sourceParam === "stdout" || sourceParam === "stderr" || sourceParam === "dev" ? sourceParam : pickDefaultSource();
|
|
3021
|
+
const logPath = resolveLogPath(source);
|
|
3022
|
+
const sinceOffset = parseInt(c.req.query("since") || "0", 10);
|
|
3023
|
+
const limit = Math.min(parseInt(c.req.query("limit") || "100", 10) || 100, 1e3);
|
|
3024
|
+
if (!existsSync5(logPath)) {
|
|
3025
|
+
return c.json({
|
|
3026
|
+
logs: [],
|
|
3027
|
+
message: `No log file found for source=${source}`,
|
|
3028
|
+
offset: 0,
|
|
3029
|
+
total: 0,
|
|
3030
|
+
source
|
|
3031
|
+
});
|
|
3032
|
+
}
|
|
3033
|
+
const { lines, offset, total } = readLogLines(logPath, sinceOffset, limit);
|
|
3034
|
+
const stats = statSync(logPath);
|
|
3035
|
+
return c.json({
|
|
3036
|
+
logs: lines,
|
|
3037
|
+
offset,
|
|
3038
|
+
total,
|
|
3039
|
+
hasMore: offset < total,
|
|
3040
|
+
source,
|
|
3041
|
+
fileSize: stats.size,
|
|
3042
|
+
fileModified: stats.mtime.toISOString()
|
|
3043
|
+
});
|
|
3044
|
+
} catch (error) {
|
|
3045
|
+
logger2.error("Failed to read logs", { error: String(error) });
|
|
3046
|
+
return c.json(
|
|
3047
|
+
{
|
|
3048
|
+
error: "Failed to read logs",
|
|
3049
|
+
logs: [],
|
|
3050
|
+
offset: 0,
|
|
3051
|
+
total: 0
|
|
3052
|
+
},
|
|
3053
|
+
500
|
|
3054
|
+
);
|
|
3055
|
+
}
|
|
3056
|
+
});
|
|
3057
|
+
app.get("/meta", (c) => {
|
|
3058
|
+
try {
|
|
3059
|
+
const sources = ["stdout", "stderr", "dev"].map((source) => {
|
|
3060
|
+
const logPath = resolveLogPath(source);
|
|
3061
|
+
if (!existsSync5(logPath)) {
|
|
3062
|
+
return { source, exists: false, total: 0, fileSize: 0 };
|
|
3063
|
+
}
|
|
3064
|
+
const stats = statSync(logPath);
|
|
3065
|
+
return {
|
|
3066
|
+
source,
|
|
3067
|
+
exists: true,
|
|
3068
|
+
fileSize: stats.size,
|
|
3069
|
+
fileModified: stats.mtime.toISOString()
|
|
3070
|
+
};
|
|
3071
|
+
});
|
|
3072
|
+
return c.json({
|
|
3073
|
+
defaultSource: pickDefaultSource(),
|
|
3074
|
+
sources
|
|
3075
|
+
});
|
|
3076
|
+
} catch (error) {
|
|
3077
|
+
logger2.error("Failed to read log metadata", { error: String(error) });
|
|
3078
|
+
return c.json({ error: "Failed to read log metadata", exists: false }, 500);
|
|
3079
|
+
}
|
|
3080
|
+
});
|
|
3081
|
+
return app;
|
|
3082
|
+
}
|
|
3083
|
+
|
|
2738
3084
|
// src/api/routes/misc.routes.ts
|
|
2739
3085
|
import { spawn } from "child_process";
|
|
2740
3086
|
import { createHmac, timingSafeEqual as timingSafeEqual2 } from "crypto";
|
|
2741
|
-
import { Hono as
|
|
3087
|
+
import { Hono as Hono6 } from "hono";
|
|
2742
3088
|
import { hostname } from "os";
|
|
2743
3089
|
|
|
2744
3090
|
// src/config/update-config.ts
|
|
2745
|
-
import { readFileSync as
|
|
2746
|
-
import { homedir as
|
|
2747
|
-
import { join as
|
|
3091
|
+
import { readFileSync as readFileSync5 } from "fs";
|
|
3092
|
+
import { homedir as homedir5 } from "os";
|
|
3093
|
+
import { join as join10 } from "path";
|
|
2748
3094
|
import { parse as parseYaml } from "yaml";
|
|
2749
3095
|
|
|
2750
3096
|
// src/schemas/updateConfig.schema.ts
|
|
@@ -2760,12 +3106,12 @@ var UpdateConfigSchema = z.object({
|
|
|
2760
3106
|
}).strict();
|
|
2761
3107
|
|
|
2762
3108
|
// src/config/update-config.ts
|
|
2763
|
-
var DEFAULT_CONFIG_PATH =
|
|
3109
|
+
var DEFAULT_CONFIG_PATH = join10(homedir5(), ".threadbase", "update.yaml");
|
|
2764
3110
|
function loadUpdateConfig(opts = {}) {
|
|
2765
3111
|
const path = opts.path ?? DEFAULT_CONFIG_PATH;
|
|
2766
3112
|
let raw;
|
|
2767
3113
|
try {
|
|
2768
|
-
raw =
|
|
3114
|
+
raw = readFileSync5(path, "utf-8");
|
|
2769
3115
|
} catch (err) {
|
|
2770
3116
|
if (err.code === "ENOENT") return null;
|
|
2771
3117
|
throw err;
|
|
@@ -2812,7 +3158,7 @@ function verifyWebhookSignature(body, header, secret) {
|
|
|
2812
3158
|
}
|
|
2813
3159
|
var clientLog = getLogger("client");
|
|
2814
3160
|
var createMiscRoutes = (deps) => {
|
|
2815
|
-
const app = new
|
|
3161
|
+
const app = new Hono6();
|
|
2816
3162
|
app.get("/api/info", (c) => {
|
|
2817
3163
|
const ptyIds = deps.ptyAttachedIds();
|
|
2818
3164
|
return c.json({
|
|
@@ -2888,11 +3234,11 @@ var createMiscRoutes = (deps) => {
|
|
|
2888
3234
|
};
|
|
2889
3235
|
|
|
2890
3236
|
// src/api/routes/pair.routes.ts
|
|
2891
|
-
import { Hono as
|
|
3237
|
+
import { Hono as Hono7 } from "hono";
|
|
2892
3238
|
var ALREADY_HANDLED3 = 597;
|
|
2893
3239
|
var alreadyHandled3 = () => new Response(null, { status: ALREADY_HANDLED3 });
|
|
2894
3240
|
var createPairRoutes = (deps) => {
|
|
2895
|
-
const app = new
|
|
3241
|
+
const app = new Hono7();
|
|
2896
3242
|
app.post("/start", (c) => {
|
|
2897
3243
|
deps.handlePairStart(c.env.outgoing);
|
|
2898
3244
|
return alreadyHandled3();
|
|
@@ -2905,11 +3251,11 @@ var createPairRoutes = (deps) => {
|
|
|
2905
3251
|
};
|
|
2906
3252
|
|
|
2907
3253
|
// src/api/routes/projects.routes.ts
|
|
2908
|
-
import { Hono as
|
|
3254
|
+
import { Hono as Hono8 } from "hono";
|
|
2909
3255
|
var ALREADY_HANDLED4 = 597;
|
|
2910
3256
|
var alreadyHandled4 = () => new Response(null, { status: ALREADY_HANDLED4 });
|
|
2911
3257
|
var createProjectRoutes = (deps) => {
|
|
2912
|
-
const app = new
|
|
3258
|
+
const app = new Hono8();
|
|
2913
3259
|
app.get("/", (c) => {
|
|
2914
3260
|
const url = new URL(c.req.url);
|
|
2915
3261
|
deps.handleListProjects(url, c.env.outgoing);
|
|
@@ -2924,11 +3270,11 @@ var createProjectRoutes = (deps) => {
|
|
|
2924
3270
|
};
|
|
2925
3271
|
|
|
2926
3272
|
// src/api/routes/scanner.routes.ts
|
|
2927
|
-
import { Hono as
|
|
3273
|
+
import { Hono as Hono9 } from "hono";
|
|
2928
3274
|
var ALREADY_HANDLED5 = 597;
|
|
2929
3275
|
var alreadyHandled5 = () => new Response(null, { status: ALREADY_HANDLED5 });
|
|
2930
3276
|
var createScannerRoutes = (deps) => {
|
|
2931
|
-
const app = new
|
|
3277
|
+
const app = new Hono9();
|
|
2932
3278
|
app.get("/api/search", async (c) => {
|
|
2933
3279
|
const url = new URL(c.req.url);
|
|
2934
3280
|
await deps.handleSearch(url, c.env.outgoing);
|
|
@@ -2938,11 +3284,11 @@ var createScannerRoutes = (deps) => {
|
|
|
2938
3284
|
};
|
|
2939
3285
|
|
|
2940
3286
|
// src/api/routes/sessions.routes.ts
|
|
2941
|
-
import { Hono as
|
|
3287
|
+
import { Hono as Hono10 } from "hono";
|
|
2942
3288
|
var ALREADY_HANDLED6 = 597;
|
|
2943
3289
|
var alreadyHandled6 = () => new Response(null, { status: ALREADY_HANDLED6 });
|
|
2944
3290
|
var createSessionRoutes = (deps) => {
|
|
2945
|
-
const app = new
|
|
3291
|
+
const app = new Hono10();
|
|
2946
3292
|
app.get("/count", (c) => {
|
|
2947
3293
|
deps.handleSessionsCount(c.env.outgoing);
|
|
2948
3294
|
return alreadyHandled6();
|
|
@@ -3009,19 +3355,21 @@ var createSessionRoutes = (deps) => {
|
|
|
3009
3355
|
};
|
|
3010
3356
|
|
|
3011
3357
|
// src/api/routes/ws.routes.ts
|
|
3012
|
-
import { Hono as
|
|
3358
|
+
import { Hono as Hono11 } from "hono";
|
|
3013
3359
|
var createWsRoutes = (deps, upgradeWebSocket) => {
|
|
3014
|
-
const app = new
|
|
3360
|
+
const app = new Hono11();
|
|
3015
3361
|
app.get(
|
|
3016
3362
|
"/ws",
|
|
3017
|
-
upgradeWebSocket(() => {
|
|
3363
|
+
upgradeWebSocket((c) => {
|
|
3364
|
+
const key = c.req.query("key");
|
|
3365
|
+
const preAuthed = typeof key === "string" && validateApiKey(key, deps.apiKey);
|
|
3018
3366
|
let openWs = null;
|
|
3019
3367
|
return {
|
|
3020
3368
|
onOpen(_evt, ws) {
|
|
3021
3369
|
const raw = ws.raw;
|
|
3022
3370
|
if (!raw) return;
|
|
3023
3371
|
openWs = raw;
|
|
3024
|
-
deps.handleWsOpen(raw);
|
|
3372
|
+
deps.handleWsOpen(raw, preAuthed);
|
|
3025
3373
|
},
|
|
3026
3374
|
onMessage(evt, _ws) {
|
|
3027
3375
|
if (openWs) deps.handleWsMessage(openWs, evt.data);
|
|
@@ -3037,7 +3385,7 @@ var createWsRoutes = (deps, upgradeWebSocket) => {
|
|
|
3037
3385
|
|
|
3038
3386
|
// src/api/app.ts
|
|
3039
3387
|
var createHonoApp = (deps, upgradeWebSocket) => {
|
|
3040
|
-
const app = new
|
|
3388
|
+
const app = new Hono12();
|
|
3041
3389
|
const httpLog = getLogger("http");
|
|
3042
3390
|
app.use("*", async (c, next) => {
|
|
3043
3391
|
const start = Date.now();
|
|
@@ -3066,6 +3414,7 @@ var createHonoApp = (deps, upgradeWebSocket) => {
|
|
|
3066
3414
|
app.route("/api", createBrowseRoutes(deps));
|
|
3067
3415
|
app.route("/", createScannerRoutes(deps));
|
|
3068
3416
|
app.route("/internal", createProgressRoutes(deps));
|
|
3417
|
+
app.route("/api/logs", createLogsRoutes());
|
|
3069
3418
|
if (upgradeWebSocket) {
|
|
3070
3419
|
app.route("/", createWsRoutes(deps, upgradeWebSocket));
|
|
3071
3420
|
}
|
|
@@ -3074,7 +3423,7 @@ var createHonoApp = (deps, upgradeWebSocket) => {
|
|
|
3074
3423
|
|
|
3075
3424
|
// src/browse.ts
|
|
3076
3425
|
import { mkdir as mkdir2, readdir, realpath, stat } from "fs/promises";
|
|
3077
|
-
import { join as
|
|
3426
|
+
import { join as join11, resolve, sep } from "path";
|
|
3078
3427
|
var BrowsePathNotFoundError = class extends Error {
|
|
3079
3428
|
constructor(message) {
|
|
3080
3429
|
super(message);
|
|
@@ -3112,7 +3461,7 @@ async function createDirectory(parentAbsolutePath, name) {
|
|
|
3112
3461
|
if (name.includes("/") || name.includes("\\") || name === ".." || name === ".") {
|
|
3113
3462
|
throw new Error("Invalid directory name");
|
|
3114
3463
|
}
|
|
3115
|
-
const target =
|
|
3464
|
+
const target = join11(parentAbsolutePath, name);
|
|
3116
3465
|
try {
|
|
3117
3466
|
const s = await stat(target);
|
|
3118
3467
|
if (s.isDirectory()) throw new Error("Directory already exists");
|
|
@@ -3129,18 +3478,18 @@ import {
|
|
|
3129
3478
|
parseJsonlLine
|
|
3130
3479
|
} from "@threadbase-sh/scanner";
|
|
3131
3480
|
import Database from "better-sqlite3";
|
|
3132
|
-
import { closeSync as
|
|
3481
|
+
import { closeSync as closeSync3, existsSync as existsSync6, mkdirSync as mkdirSync3, openSync as openSync3, readSync as readSync3, statSync as statSync3 } from "fs";
|
|
3133
3482
|
import { open as openAsync } from "fs/promises";
|
|
3134
|
-
import { dirname as
|
|
3483
|
+
import { dirname as dirname7 } from "path";
|
|
3135
3484
|
import { setImmediate as yieldToEventLoop } from "timers/promises";
|
|
3136
3485
|
|
|
3137
3486
|
// src/db/sqlite-migrate.ts
|
|
3138
|
-
import { readdirSync as readdirSync2, readFileSync as
|
|
3139
|
-
import { dirname as
|
|
3487
|
+
import { readdirSync as readdirSync2, readFileSync as readFileSync6 } from "fs";
|
|
3488
|
+
import { dirname as dirname6, join as join12 } from "path";
|
|
3140
3489
|
import { fileURLToPath as fileURLToPath2 } from "url";
|
|
3141
3490
|
function getMigrationsDir2() {
|
|
3142
3491
|
if (typeof import.meta !== "undefined" && import.meta.url) {
|
|
3143
|
-
return
|
|
3492
|
+
return dirname6(fileURLToPath2(import.meta.url));
|
|
3144
3493
|
}
|
|
3145
3494
|
return __dirname;
|
|
3146
3495
|
}
|
|
@@ -3152,7 +3501,7 @@ CREATE TABLE IF NOT EXISTS schema_migrations (
|
|
|
3152
3501
|
`;
|
|
3153
3502
|
function runSqliteMigrations(db, migrationsDir) {
|
|
3154
3503
|
db.exec(SCHEMA_MIGRATIONS_SQL);
|
|
3155
|
-
const dir = migrationsDir ??
|
|
3504
|
+
const dir = migrationsDir ?? join12(getMigrationsDir2(), "migrations");
|
|
3156
3505
|
const files = readdirSync2(dir).filter((f) => f.endsWith(".sql")).sort();
|
|
3157
3506
|
const appliedRows = db.prepare("SELECT id FROM schema_migrations").all();
|
|
3158
3507
|
const appliedSet = new Set(appliedRows.map((r) => r.id));
|
|
@@ -3164,7 +3513,7 @@ function runSqliteMigrations(db, migrationsDir) {
|
|
|
3164
3513
|
skipped.push(file);
|
|
3165
3514
|
continue;
|
|
3166
3515
|
}
|
|
3167
|
-
const sql =
|
|
3516
|
+
const sql = readFileSync6(join12(dir, file), "utf-8");
|
|
3168
3517
|
const tx = db.transaction(() => {
|
|
3169
3518
|
db.exec(sql);
|
|
3170
3519
|
recordApplied.run(file, (/* @__PURE__ */ new Date()).toISOString());
|
|
@@ -3176,7 +3525,7 @@ function runSqliteMigrations(db, migrationsDir) {
|
|
|
3176
3525
|
}
|
|
3177
3526
|
|
|
3178
3527
|
// src/services/conversations/isAgentConversation.ts
|
|
3179
|
-
import { closeSync, openSync, readSync, statSync } from "fs";
|
|
3528
|
+
import { closeSync as closeSync2, openSync as openSync2, readSync as readSync2, statSync as statSync2 } from "fs";
|
|
3180
3529
|
var DEFAULT_AGENT_ENTRYPOINTS = /* @__PURE__ */ new Set(["sdk-cli", "claude-vscode"]);
|
|
3181
3530
|
var CHUNK_BYTES = 64 * 1024;
|
|
3182
3531
|
var ENTRYPOINT_PROBE = `"entrypoint":`;
|
|
@@ -3198,12 +3547,12 @@ function isAgentFile(filePath, entrypoints = DEFAULT_AGENT_ENTRYPOINTS) {
|
|
|
3198
3547
|
if (cached2 !== void 0) return cached2;
|
|
3199
3548
|
let fd;
|
|
3200
3549
|
try {
|
|
3201
|
-
fd =
|
|
3550
|
+
fd = openSync2(filePath, "r");
|
|
3202
3551
|
} catch {
|
|
3203
3552
|
return false;
|
|
3204
3553
|
}
|
|
3205
3554
|
try {
|
|
3206
|
-
const fileSize =
|
|
3555
|
+
const fileSize = statSync2(filePath).size;
|
|
3207
3556
|
if (fileSize === 0) {
|
|
3208
3557
|
fileDecisionCache.set(key, false);
|
|
3209
3558
|
return false;
|
|
@@ -3214,7 +3563,7 @@ function isAgentFile(filePath, entrypoints = DEFAULT_AGENT_ENTRYPOINTS) {
|
|
|
3214
3563
|
let carry = "";
|
|
3215
3564
|
while (offset < fileSize) {
|
|
3216
3565
|
const toRead = Math.min(CHUNK_BYTES, fileSize - offset);
|
|
3217
|
-
const got =
|
|
3566
|
+
const got = readSync2(fd, buf, 0, toRead, offset);
|
|
3218
3567
|
if (got <= 0) break;
|
|
3219
3568
|
const chunk = carry + buf.toString("utf8", 0, got);
|
|
3220
3569
|
for (const marker of markers) {
|
|
@@ -3235,7 +3584,7 @@ function isAgentFile(filePath, entrypoints = DEFAULT_AGENT_ENTRYPOINTS) {
|
|
|
3235
3584
|
} catch {
|
|
3236
3585
|
return false;
|
|
3237
3586
|
} finally {
|
|
3238
|
-
|
|
3587
|
+
closeSync2(fd);
|
|
3239
3588
|
}
|
|
3240
3589
|
}
|
|
3241
3590
|
function parseAgentEntrypointsEnv(raw) {
|
|
@@ -3772,7 +4121,7 @@ var ConversationCache = class _ConversationCache {
|
|
|
3772
4121
|
if (!fileState) return null;
|
|
3773
4122
|
let stat3;
|
|
3774
4123
|
try {
|
|
3775
|
-
stat3 =
|
|
4124
|
+
stat3 = statSync3(filePath);
|
|
3776
4125
|
} catch {
|
|
3777
4126
|
return null;
|
|
3778
4127
|
}
|
|
@@ -3793,17 +4142,17 @@ var ConversationCache = class _ConversationCache {
|
|
|
3793
4142
|
);
|
|
3794
4143
|
if (rows.length === 0) return { messages: [], total, fromIndex: from };
|
|
3795
4144
|
const messages = [];
|
|
3796
|
-
const fd =
|
|
4145
|
+
const fd = openSync3(filePath, "r");
|
|
3797
4146
|
try {
|
|
3798
4147
|
const state = createJsonlParseState();
|
|
3799
4148
|
for (const row of rows) {
|
|
3800
4149
|
const buf = Buffer.alloc(row.byte_length);
|
|
3801
|
-
|
|
4150
|
+
readSync3(fd, buf, 0, row.byte_length, row.byte_offset);
|
|
3802
4151
|
const msg = parseJsonlLine(buf.toString("utf-8"), state);
|
|
3803
4152
|
if (msg) messages.push(msg);
|
|
3804
4153
|
}
|
|
3805
4154
|
} finally {
|
|
3806
|
-
|
|
4155
|
+
closeSync3(fd);
|
|
3807
4156
|
}
|
|
3808
4157
|
return { messages, total, fromIndex: from };
|
|
3809
4158
|
}
|
|
@@ -3831,14 +4180,14 @@ var ConversationCache = class _ConversationCache {
|
|
|
3831
4180
|
isAgentFileCached(filePath) {
|
|
3832
4181
|
let s;
|
|
3833
4182
|
try {
|
|
3834
|
-
s =
|
|
4183
|
+
s = statSync3(filePath);
|
|
3835
4184
|
} catch {
|
|
3836
4185
|
return false;
|
|
3837
4186
|
}
|
|
3838
4187
|
return this.classifyAgentFile(filePath, s.mtimeMs, s.size);
|
|
3839
4188
|
}
|
|
3840
4189
|
static open(dbPath, tailSize = 10, migrationsDir, options) {
|
|
3841
|
-
|
|
4190
|
+
mkdirSync3(dirname7(dbPath), { recursive: true });
|
|
3842
4191
|
const db = new Database(dbPath);
|
|
3843
4192
|
db.pragma("journal_mode = WAL");
|
|
3844
4193
|
db.pragma("foreign_keys = ON");
|
|
@@ -4019,7 +4368,7 @@ var ConversationCache = class _ConversationCache {
|
|
|
4019
4368
|
let mtimeMs = null;
|
|
4020
4369
|
let fileSize = null;
|
|
4021
4370
|
try {
|
|
4022
|
-
const s =
|
|
4371
|
+
const s = statSync3(m.filePath);
|
|
4023
4372
|
mtimeMs = s.mtimeMs;
|
|
4024
4373
|
fileSize = s.size;
|
|
4025
4374
|
} catch {
|
|
@@ -4078,8 +4427,8 @@ var ConversationCache = class _ConversationCache {
|
|
|
4078
4427
|
let fileSize;
|
|
4079
4428
|
let fd;
|
|
4080
4429
|
try {
|
|
4081
|
-
fileSize =
|
|
4082
|
-
fd =
|
|
4430
|
+
fileSize = statSync3(filePath).size;
|
|
4431
|
+
fd = openSync3(filePath, "r");
|
|
4083
4432
|
} catch {
|
|
4084
4433
|
return false;
|
|
4085
4434
|
}
|
|
@@ -4092,7 +4441,7 @@ var ConversationCache = class _ConversationCache {
|
|
|
4092
4441
|
while (pos > 0 && lines.length < this.tailSize * 4) {
|
|
4093
4442
|
const toRead = Math.min(CHUNK, pos);
|
|
4094
4443
|
pos -= toRead;
|
|
4095
|
-
|
|
4444
|
+
readSync3(fd, buf, 0, toRead, pos);
|
|
4096
4445
|
const chunk = buf.subarray(0, toRead).toString("utf8");
|
|
4097
4446
|
const combined = chunk + partial;
|
|
4098
4447
|
const parts = combined.split("\n");
|
|
@@ -4103,7 +4452,7 @@ var ConversationCache = class _ConversationCache {
|
|
|
4103
4452
|
}
|
|
4104
4453
|
if (partial) lines.push(partial);
|
|
4105
4454
|
} finally {
|
|
4106
|
-
|
|
4455
|
+
closeSync3(fd);
|
|
4107
4456
|
}
|
|
4108
4457
|
const msgs = [];
|
|
4109
4458
|
for (let i = 0; i < lines.length && msgs.length < this.tailSize; i++) {
|
|
@@ -4305,7 +4654,7 @@ var ConversationCache = class _ConversationCache {
|
|
|
4305
4654
|
* `handleGetConversation` can still serve the cached tail even when the
|
|
4306
4655
|
* JSONL has been deleted.
|
|
4307
4656
|
*/
|
|
4308
|
-
pruneGhostFiles(exists =
|
|
4657
|
+
pruneGhostFiles(exists = existsSync6) {
|
|
4309
4658
|
const rows = this.stmts.allFilePaths.all();
|
|
4310
4659
|
const ghosts = [];
|
|
4311
4660
|
const prune = this.db.transaction((ids) => {
|
|
@@ -4360,7 +4709,7 @@ var ConversationCache = class _ConversationCache {
|
|
|
4360
4709
|
* Returns the removed IDs.
|
|
4361
4710
|
*/
|
|
4362
4711
|
reconcileDeletions(livePaths, opts) {
|
|
4363
|
-
const exists = opts?.exists ??
|
|
4712
|
+
const exists = opts?.exists ?? existsSync6;
|
|
4364
4713
|
const rows = this.stmts.allFilePaths.all();
|
|
4365
4714
|
const removed = [];
|
|
4366
4715
|
const drop = this.db.transaction((ids) => {
|
|
@@ -4589,23 +4938,23 @@ async function recordUpload(pool2, instanceId, row) {
|
|
|
4589
4938
|
}
|
|
4590
4939
|
|
|
4591
4940
|
// src/handlers/handleListProjects.ts
|
|
4592
|
-
import { readdirSync as readdirSync3, statSync as
|
|
4593
|
-
import { homedir as
|
|
4594
|
-
import { join as
|
|
4941
|
+
import { readdirSync as readdirSync3, statSync as statSync4 } from "fs";
|
|
4942
|
+
import { homedir as homedir6 } from "os";
|
|
4943
|
+
import { join as join13 } from "path";
|
|
4595
4944
|
function decodeProjectPath(dirName) {
|
|
4596
4945
|
return dirName.replace(/-/g, "/");
|
|
4597
4946
|
}
|
|
4598
4947
|
function handleListProjects(url, res) {
|
|
4599
4948
|
const limit = Math.max(1, parseInt(url.searchParams.get("limit") ?? "50", 10) || 50);
|
|
4600
4949
|
const offset = Math.max(0, parseInt(url.searchParams.get("offset") ?? "0", 10) || 0);
|
|
4601
|
-
const projectsDir =
|
|
4950
|
+
const projectsDir = join13(homedir6(), ".claude", "projects");
|
|
4602
4951
|
let entries;
|
|
4603
4952
|
try {
|
|
4604
4953
|
entries = readdirSync3(projectsDir).map((dirName) => {
|
|
4605
|
-
const fullPath =
|
|
4954
|
+
const fullPath = join13(projectsDir, dirName);
|
|
4606
4955
|
let mtime = 0;
|
|
4607
4956
|
try {
|
|
4608
|
-
mtime =
|
|
4957
|
+
mtime = statSync4(fullPath).mtimeMs;
|
|
4609
4958
|
} catch {
|
|
4610
4959
|
}
|
|
4611
4960
|
const path = decodeProjectPath(dirName);
|
|
@@ -4700,7 +5049,7 @@ function seal(plaintext, recipientPublicKeyBase64) {
|
|
|
4700
5049
|
|
|
4701
5050
|
// src/services/conversations/conversationWatcher.ts
|
|
4702
5051
|
import chokidar from "chokidar";
|
|
4703
|
-
import { statSync as
|
|
5052
|
+
import { statSync as statSync5 } from "fs";
|
|
4704
5053
|
import { open, stat as stat2 } from "fs/promises";
|
|
4705
5054
|
var ConversationWatcher = class {
|
|
4706
5055
|
files = /* @__PURE__ */ new Map();
|
|
@@ -4723,7 +5072,7 @@ var ConversationWatcher = class {
|
|
|
4723
5072
|
if (this.files.has(filePath)) return;
|
|
4724
5073
|
let offset;
|
|
4725
5074
|
try {
|
|
4726
|
-
offset =
|
|
5075
|
+
offset = statSync5(filePath).size;
|
|
4727
5076
|
} catch {
|
|
4728
5077
|
offset = 0;
|
|
4729
5078
|
}
|
|
@@ -4898,14 +5247,14 @@ function findSearchTarget(messages, query) {
|
|
|
4898
5247
|
}
|
|
4899
5248
|
|
|
4900
5249
|
// src/services/conversations/pruneAgentConversations.ts
|
|
4901
|
-
import { existsSync as
|
|
5250
|
+
import { existsSync as existsSync7 } from "fs";
|
|
4902
5251
|
function pruneAgentConversations(cache) {
|
|
4903
5252
|
const db = cache.getDatabase();
|
|
4904
5253
|
const rows = db.prepare("SELECT id, file_path FROM conversation_meta").all();
|
|
4905
5254
|
let pruned = 0;
|
|
4906
5255
|
let missing = 0;
|
|
4907
5256
|
for (const row of rows) {
|
|
4908
|
-
if (!
|
|
5257
|
+
if (!existsSync7(row.file_path)) {
|
|
4909
5258
|
missing += 1;
|
|
4910
5259
|
continue;
|
|
4911
5260
|
}
|
|
@@ -4928,6 +5277,13 @@ function deriveProjectChatTitle(input) {
|
|
|
4928
5277
|
return `Untitled \xB7 ${input.id.slice(0, 8)}`;
|
|
4929
5278
|
}
|
|
4930
5279
|
|
|
5280
|
+
// src/services/questions/permissionAnswerKeys.ts
|
|
5281
|
+
var ANSWER_KEYS_ALLOWLIST = /^(?:\r|[yn]\r|\x03|\d+\r)$/;
|
|
5282
|
+
function sanitizeAnswerKeys(keys) {
|
|
5283
|
+
if (keys === void 0) return void 0;
|
|
5284
|
+
return ANSWER_KEYS_ALLOWLIST.test(keys) ? keys : void 0;
|
|
5285
|
+
}
|
|
5286
|
+
|
|
4931
5287
|
// src/services/questions/detectAskUserQuestion.ts
|
|
4932
5288
|
function normalizeContent2(raw) {
|
|
4933
5289
|
if (Array.isArray(raw)) return raw;
|
|
@@ -5241,7 +5597,7 @@ function discoveredToResponse(d, conversationId) {
|
|
|
5241
5597
|
import { randomBytes as randomBytes3 } from "crypto";
|
|
5242
5598
|
import { mkdir as mkdir3, writeFile } from "fs/promises";
|
|
5243
5599
|
import heicConvert from "heic-convert";
|
|
5244
|
-
import { join as
|
|
5600
|
+
import { join as join14 } from "path";
|
|
5245
5601
|
var UPLOAD_DIR_NAME = ".threadbase-uploads";
|
|
5246
5602
|
var MAX_BYTES = 25 * 1024 * 1024;
|
|
5247
5603
|
var HEIC_MIMES = /* @__PURE__ */ new Set(["image/heic", "image/heif"]);
|
|
@@ -5274,9 +5630,9 @@ async function saveUploadFile(input) {
|
|
|
5274
5630
|
}
|
|
5275
5631
|
const id = `up_${randomBytes3(8).toString("hex")}`;
|
|
5276
5632
|
const safeName = sanitizeFilename(originalName) || `file${MIME_TO_EXT[mimeType] ?? ""}`;
|
|
5277
|
-
const dir =
|
|
5633
|
+
const dir = join14(input.projectPath, UPLOAD_DIR_NAME, input.sessionId);
|
|
5278
5634
|
await mkdir3(dir, { recursive: true });
|
|
5279
|
-
const filePath =
|
|
5635
|
+
const filePath = join14(dir, `${Date.now()}-${id}-${safeName}`);
|
|
5280
5636
|
await writeFile(filePath, buffer);
|
|
5281
5637
|
return {
|
|
5282
5638
|
id,
|
|
@@ -5532,8 +5888,10 @@ var WSHub = class {
|
|
|
5532
5888
|
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.`;
|
|
5533
5889
|
var DEFAULT_SYSTEM_PROMPT = "When presenting options or choices to the user, limit the options to at most 3.";
|
|
5534
5890
|
var DEFAULT_PTY_GRACE_PERIOD_MS = 27e4;
|
|
5891
|
+
var DEFAULT_WS_AUTH_TIMEOUT_MS = 5e3;
|
|
5892
|
+
var WS_CLOSE_UNAUTHORIZED = 4401;
|
|
5535
5893
|
var REFRESH_TTL_MS = 2e3;
|
|
5536
|
-
var START_READY_TIMEOUT_MS =
|
|
5894
|
+
var START_READY_TIMEOUT_MS = 1e4;
|
|
5537
5895
|
function parseIncludeAgentsEnv(raw) {
|
|
5538
5896
|
if (raw === void 0) return false;
|
|
5539
5897
|
const v = raw.trim().toLowerCase();
|
|
@@ -5622,6 +5980,14 @@ var StreamerServer = class {
|
|
|
5622
5980
|
clientIdToWs = /* @__PURE__ */ new Map();
|
|
5623
5981
|
// Reverse map for cleanup on close
|
|
5624
5982
|
wsToClientId = /* @__PURE__ */ new Map();
|
|
5983
|
+
// M1 — WS auth. Sockets that have authenticated (via ?key= at upgrade OR a
|
|
5984
|
+
// { type: "auth", token } first message). Only authed sockets are added to
|
|
5985
|
+
// the hub and receive broadcasts.
|
|
5986
|
+
// Plan: https://github.com/RonenMars/threadbase-streamer/blob/a251353bfa417bd48ce3f15086bc336a2c622629/docs/plans/2026-06-24-security-hardening.md#L40
|
|
5987
|
+
wsAuthed = /* @__PURE__ */ new Set();
|
|
5988
|
+
// Keyless sockets awaiting their first-message auth handshake → close timer.
|
|
5989
|
+
wsAuthPending = /* @__PURE__ */ new Map();
|
|
5990
|
+
wsAuthTimeoutMs;
|
|
5625
5991
|
cache = null;
|
|
5626
5992
|
projectsRepo = null;
|
|
5627
5993
|
conversationsRepo = null;
|
|
@@ -5659,11 +6025,12 @@ var StreamerServer = class {
|
|
|
5659
6025
|
this.disableDb = config.disableDb ?? false;
|
|
5660
6026
|
this.scannerPersistenceDisabled = config.scannerPersistent === false;
|
|
5661
6027
|
this.scanProfiles = config.scanProfiles;
|
|
5662
|
-
this.codexRoots = config.codexRoots ?? [
|
|
6028
|
+
this.codexRoots = config.codexRoots ?? [join15(homedir7(), ".codex", "sessions")];
|
|
5663
6029
|
this.ptyGracePeriodMs = config.ptyGracePeriodMs ?? DEFAULT_PTY_GRACE_PERIOD_MS;
|
|
6030
|
+
this.wsAuthTimeoutMs = config.wsAuthTimeoutMs ?? DEFAULT_WS_AUTH_TIMEOUT_MS;
|
|
5664
6031
|
this.defaultSystemPrompt = config.defaultSystemPrompt ?? DEFAULT_SYSTEM_PROMPT;
|
|
5665
6032
|
this.defaultPermissionMode = config.defaultPermissionMode ?? loadDefaultPermissionMode() ?? "acceptEdits";
|
|
5666
|
-
this.cacheDir = config.cacheDir ?? loadCacheDir() ??
|
|
6033
|
+
this.cacheDir = config.cacheDir ?? loadCacheDir() ?? join15(homedir7(), ".threadbase", "cache");
|
|
5667
6034
|
this.tailSize = config.tailSize ?? loadTailSize() ?? 10;
|
|
5668
6035
|
this.directoryDebounceMs = parseDirScanDebounceEnv(process.env.THREADBASE_DIR_SCAN_DEBOUNCE_MS) ?? config.directoryScanDebounceMs ?? 1e3;
|
|
5669
6036
|
this.markScannerStaleDebounced = debounce(() => {
|
|
@@ -5704,7 +6071,7 @@ var StreamerServer = class {
|
|
|
5704
6071
|
const seqs = cache.extendMessageIndex(
|
|
5705
6072
|
filePath,
|
|
5706
6073
|
spans,
|
|
5707
|
-
|
|
6074
|
+
statSync6(filePath),
|
|
5708
6075
|
readFrom,
|
|
5709
6076
|
endOffset
|
|
5710
6077
|
);
|
|
@@ -5865,7 +6232,7 @@ var StreamerServer = class {
|
|
|
5865
6232
|
temporalClient,
|
|
5866
6233
|
taskQueue: agentConfig.temporal.taskQueue
|
|
5867
6234
|
});
|
|
5868
|
-
const conversationsBaseDir = agentConfig.conversationsDir ||
|
|
6235
|
+
const conversationsBaseDir = agentConfig.conversationsDir || join15(dirname8(this.cacheDir), "conversations");
|
|
5869
6236
|
conversationWriter = createConversationWriter({
|
|
5870
6237
|
baseDir: conversationsBaseDir
|
|
5871
6238
|
});
|
|
@@ -5918,17 +6285,39 @@ var StreamerServer = class {
|
|
|
5918
6285
|
handlePairExchange: (req, res) => this.handlePairExchange(req, res),
|
|
5919
6286
|
handleBrowse: (url, res) => this.handleBrowse(url, res),
|
|
5920
6287
|
handleMkdir: (req, res) => this.handleMkdir(req, res),
|
|
5921
|
-
handleWsOpen: (ws) => {
|
|
5922
|
-
|
|
5923
|
-
|
|
5924
|
-
|
|
5925
|
-
if (this.cacheReady) {
|
|
5926
|
-
ws.send(JSON.stringify({ type: "cache_ready" }));
|
|
6288
|
+
handleWsOpen: (ws, preAuthed) => {
|
|
6289
|
+
if (preAuthed) {
|
|
6290
|
+
this.completeWsAuth(ws);
|
|
6291
|
+
return;
|
|
5927
6292
|
}
|
|
6293
|
+
const timer = setTimeout(() => {
|
|
6294
|
+
this.wsAuthPending.delete(ws);
|
|
6295
|
+
try {
|
|
6296
|
+
ws.close(WS_CLOSE_UNAUTHORIZED, "auth timeout");
|
|
6297
|
+
} catch {
|
|
6298
|
+
}
|
|
6299
|
+
}, this.wsAuthTimeoutMs);
|
|
6300
|
+
this.wsAuthPending.set(ws, timer);
|
|
5928
6301
|
},
|
|
5929
6302
|
handleWsMessage: async (ws, raw) => {
|
|
5930
6303
|
try {
|
|
5931
6304
|
const msg = JSON.parse(String(raw));
|
|
6305
|
+
if (!this.wsAuthed.has(ws)) {
|
|
6306
|
+
if (msg.type === "auth" && typeof msg.token === "string") {
|
|
6307
|
+
const t = this.wsAuthPending.get(ws);
|
|
6308
|
+
if (t) clearTimeout(t);
|
|
6309
|
+
this.wsAuthPending.delete(ws);
|
|
6310
|
+
if (validateApiKey(msg.token, this.apiKey)) {
|
|
6311
|
+
this.completeWsAuth(ws);
|
|
6312
|
+
} else {
|
|
6313
|
+
try {
|
|
6314
|
+
ws.close(WS_CLOSE_UNAUTHORIZED, "unauthorized");
|
|
6315
|
+
} catch {
|
|
6316
|
+
}
|
|
6317
|
+
}
|
|
6318
|
+
}
|
|
6319
|
+
return;
|
|
6320
|
+
}
|
|
5932
6321
|
if (msg.type === "register" && typeof msg.clientId === "string") {
|
|
5933
6322
|
const oldClientId = this.wsToClientId.get(ws);
|
|
5934
6323
|
if (oldClientId) this.clientIdToWs.delete(oldClientId);
|
|
@@ -5941,14 +6330,54 @@ var StreamerServer = class {
|
|
|
5941
6330
|
const lines = await this.ptyManager.getOutputLines(msg.sessionId, 200);
|
|
5942
6331
|
ws.send(JSON.stringify({ type: "terminal_replay", sessionId: msg.sessionId, lines }));
|
|
5943
6332
|
}
|
|
6333
|
+
const pendingGate = this.pendingPermission.get(msg.sessionId);
|
|
6334
|
+
if (pendingGate) {
|
|
6335
|
+
this.log.info(`[ws.replay_permission] ${msg.sessionId.slice(0, 8)}`, {
|
|
6336
|
+
event: "ws.replay_permission",
|
|
6337
|
+
sessionId: msg.sessionId
|
|
6338
|
+
});
|
|
6339
|
+
ws.send(
|
|
6340
|
+
JSON.stringify({
|
|
6341
|
+
type: "permission",
|
|
6342
|
+
sessionId: msg.sessionId,
|
|
6343
|
+
...pendingGate.prompt ? { prompt: pendingGate.prompt } : {},
|
|
6344
|
+
...pendingGate.detail ? { detail: pendingGate.detail } : {},
|
|
6345
|
+
options: pendingGate.options,
|
|
6346
|
+
...pendingGate.cursor !== void 0 ? { cursor: pendingGate.cursor } : {}
|
|
6347
|
+
})
|
|
6348
|
+
);
|
|
6349
|
+
}
|
|
6350
|
+
const pendingQuestion = this.pendingQuestions.get(msg.sessionId);
|
|
6351
|
+
if (pendingQuestion) {
|
|
6352
|
+
this.log.info(`[ws.replay_question] ${msg.sessionId.slice(0, 8)}`, {
|
|
6353
|
+
event: "ws.replay_question",
|
|
6354
|
+
sessionId: msg.sessionId
|
|
6355
|
+
});
|
|
6356
|
+
ws.send(
|
|
6357
|
+
JSON.stringify({
|
|
6358
|
+
type: "question",
|
|
6359
|
+
sessionId: msg.sessionId,
|
|
6360
|
+
toolUseId: pendingQuestion.toolUseId,
|
|
6361
|
+
questions: pendingQuestion.questions
|
|
6362
|
+
})
|
|
6363
|
+
);
|
|
6364
|
+
}
|
|
5944
6365
|
}
|
|
5945
6366
|
if (msg.type === "hold_session" && typeof msg.sessionId === "string") {
|
|
5946
|
-
this.
|
|
6367
|
+
if (this.sessionSubscribers.get(msg.sessionId)?.has(ws)) {
|
|
6368
|
+
this.startGraceTimer(msg.sessionId, 0);
|
|
6369
|
+
}
|
|
5947
6370
|
}
|
|
5948
6371
|
} catch {
|
|
5949
6372
|
}
|
|
5950
6373
|
},
|
|
5951
6374
|
handleWsClose: (ws) => {
|
|
6375
|
+
const pendingTimer = this.wsAuthPending.get(ws);
|
|
6376
|
+
if (pendingTimer) {
|
|
6377
|
+
clearTimeout(pendingTimer);
|
|
6378
|
+
this.wsAuthPending.delete(ws);
|
|
6379
|
+
}
|
|
6380
|
+
this.wsAuthed.delete(ws);
|
|
5952
6381
|
const clientId = this.wsToClientId.get(ws);
|
|
5953
6382
|
if (clientId) {
|
|
5954
6383
|
this.clientIdToWs.delete(clientId);
|
|
@@ -6018,6 +6447,20 @@ var StreamerServer = class {
|
|
|
6018
6447
|
this.wsHub.broadcast(payload);
|
|
6019
6448
|
}
|
|
6020
6449
|
}
|
|
6450
|
+
// M1: finalize a WebSocket auth (via ?key= at upgrade or a first-message
|
|
6451
|
+
// handshake) — register it with the hub and send the initial snapshot. Only
|
|
6452
|
+
// authed sockets reach this, so no unauthenticated client ever receives a
|
|
6453
|
+
// broadcast.
|
|
6454
|
+
// Plan: https://github.com/RonenMars/threadbase-streamer/blob/a251353bfa417bd48ce3f15086bc336a2c622629/docs/plans/2026-06-24-security-hardening.md#L40
|
|
6455
|
+
completeWsAuth(ws) {
|
|
6456
|
+
this.wsAuthed.add(ws);
|
|
6457
|
+
this.wsHub.addClient(ws);
|
|
6458
|
+
const sessions = this.sessionStore.list(this.ptyAttachedIds());
|
|
6459
|
+
ws.send(JSON.stringify({ type: "session_list", sessions }));
|
|
6460
|
+
if (this.cacheReady) {
|
|
6461
|
+
ws.send(JSON.stringify({ type: "cache_ready" }));
|
|
6462
|
+
}
|
|
6463
|
+
}
|
|
6021
6464
|
addSessionSubscriber(sessionId, ws) {
|
|
6022
6465
|
let subs = this.sessionSubscribers.get(sessionId);
|
|
6023
6466
|
if (!subs) {
|
|
@@ -6089,7 +6532,7 @@ var StreamerServer = class {
|
|
|
6089
6532
|
});
|
|
6090
6533
|
try {
|
|
6091
6534
|
this.cache = ConversationCache.open(
|
|
6092
|
-
|
|
6535
|
+
join15(this.cacheDir, "cache.db"),
|
|
6093
6536
|
this.tailSize,
|
|
6094
6537
|
void 0,
|
|
6095
6538
|
{
|
|
@@ -6115,11 +6558,11 @@ var StreamerServer = class {
|
|
|
6115
6558
|
if (this.scanProfiles && this.scanProfiles.length > 0) {
|
|
6116
6559
|
for (const profile of this.scanProfiles) {
|
|
6117
6560
|
if (profile.enabled) {
|
|
6118
|
-
this.fileWatcher.watchDirectory(
|
|
6561
|
+
this.fileWatcher.watchDirectory(join15(profile.configDir, "projects"));
|
|
6119
6562
|
}
|
|
6120
6563
|
}
|
|
6121
6564
|
} else {
|
|
6122
|
-
this.fileWatcher.watchDirectory(
|
|
6565
|
+
this.fileWatcher.watchDirectory(join15(homedir7(), ".claude", "projects"));
|
|
6123
6566
|
}
|
|
6124
6567
|
} catch (err) {
|
|
6125
6568
|
const message = err instanceof Error ? err.message : String(err);
|
|
@@ -6304,6 +6747,9 @@ var StreamerServer = class {
|
|
|
6304
6747
|
this.ptyManager.dispose();
|
|
6305
6748
|
this.fileWatcher.dispose();
|
|
6306
6749
|
this.wsHub.dispose();
|
|
6750
|
+
for (const timer of this.wsAuthPending.values()) clearTimeout(timer);
|
|
6751
|
+
this.wsAuthPending.clear();
|
|
6752
|
+
this.wsAuthed.clear();
|
|
6307
6753
|
this.pairTokens.dispose();
|
|
6308
6754
|
if (this.dbPool) {
|
|
6309
6755
|
await this.dbPool.end();
|
|
@@ -6701,17 +7147,17 @@ var StreamerServer = class {
|
|
|
6701
7147
|
return scanner;
|
|
6702
7148
|
}
|
|
6703
7149
|
findJsonlPath(uuid) {
|
|
6704
|
-
const projectsDir =
|
|
6705
|
-
if (!
|
|
7150
|
+
const projectsDir = join15(homedir7(), ".claude", "projects");
|
|
7151
|
+
if (!existsSync8(projectsDir)) return null;
|
|
6706
7152
|
const filename = `${uuid}.jsonl`;
|
|
6707
7153
|
for (const dir of readdirSync4(projectsDir)) {
|
|
6708
|
-
const fp =
|
|
6709
|
-
if (
|
|
6710
|
-
const projectDir =
|
|
7154
|
+
const fp = join15(projectsDir, dir, filename);
|
|
7155
|
+
if (existsSync8(fp)) return fp;
|
|
7156
|
+
const projectDir = join15(projectsDir, dir);
|
|
6711
7157
|
try {
|
|
6712
7158
|
for (const sub of readdirSync4(projectDir)) {
|
|
6713
|
-
const subagentPath =
|
|
6714
|
-
if (
|
|
7159
|
+
const subagentPath = join15(projectDir, sub, "subagents", filename);
|
|
7160
|
+
if (existsSync8(subagentPath)) return subagentPath;
|
|
6715
7161
|
}
|
|
6716
7162
|
} catch {
|
|
6717
7163
|
}
|
|
@@ -6858,7 +7304,7 @@ var StreamerServer = class {
|
|
|
6858
7304
|
if (!conv.filePath) return false;
|
|
6859
7305
|
let mtimeMs = null;
|
|
6860
7306
|
try {
|
|
6861
|
-
mtimeMs =
|
|
7307
|
+
mtimeMs = statSync6(conv.filePath).mtimeMs;
|
|
6862
7308
|
} catch {
|
|
6863
7309
|
return false;
|
|
6864
7310
|
}
|
|
@@ -7226,7 +7672,7 @@ var StreamerServer = class {
|
|
|
7226
7672
|
handleGetSession(sessionId, res) {
|
|
7227
7673
|
const session = this.sessionStore.get(sessionId, this.ptyAttachedIds());
|
|
7228
7674
|
if (session) {
|
|
7229
|
-
if (!
|
|
7675
|
+
if (!existsSync8(session.projectPath)) {
|
|
7230
7676
|
session.failureReason = `Project directory not found: ${session.projectPath}`;
|
|
7231
7677
|
}
|
|
7232
7678
|
json(res, 200, session);
|
|
@@ -7430,12 +7876,21 @@ var StreamerServer = class {
|
|
|
7430
7876
|
return;
|
|
7431
7877
|
}
|
|
7432
7878
|
this.pendingPermission.set(sessionId, gate);
|
|
7879
|
+
const safeOptions = gate.options.map((o) => {
|
|
7880
|
+
const answerKeys = sanitizeAnswerKeys(o.answerKeys);
|
|
7881
|
+
return answerKeys === void 0 ? { index: o.index, label: o.label } : { ...o, answerKeys };
|
|
7882
|
+
});
|
|
7883
|
+
const subscriberCount = this.sessionSubscribers.get(sessionId)?.size ?? 0;
|
|
7884
|
+
this.log.info(
|
|
7885
|
+
`[ws.broadcast_permission] ${sessionId.slice(0, 8)} subscribers=${subscriberCount}`,
|
|
7886
|
+
{ event: "ws.broadcast_permission", sessionId, subscriberCount }
|
|
7887
|
+
);
|
|
7433
7888
|
this.wsHub.broadcast({
|
|
7434
7889
|
type: "permission",
|
|
7435
7890
|
sessionId,
|
|
7436
7891
|
...gate.prompt ? { prompt: gate.prompt } : {},
|
|
7437
7892
|
...gate.detail ? { detail: gate.detail } : {},
|
|
7438
|
-
options:
|
|
7893
|
+
options: safeOptions,
|
|
7439
7894
|
...gate.cursor !== void 0 ? { cursor: gate.cursor } : {}
|
|
7440
7895
|
});
|
|
7441
7896
|
}
|
|
@@ -7622,7 +8077,7 @@ var StreamerServer = class {
|
|
|
7622
8077
|
sessionStore: this.sessionStore,
|
|
7623
8078
|
// biome-ignore lint/style/noNonNullAssertion: agentClient is set when agentConfig.enabled is true
|
|
7624
8079
|
agentClient: this.agentClient,
|
|
7625
|
-
conversationsDir: this.cacheDir ?
|
|
8080
|
+
conversationsDir: this.cacheDir ? join15(dirname8(this.cacheDir), "conversations") : "",
|
|
7626
8081
|
agentConfig: this.agentConfig
|
|
7627
8082
|
});
|
|
7628
8083
|
json(res, result.status, result.body);
|
|
@@ -7763,9 +8218,9 @@ var StreamerServer = class {
|
|
|
7763
8218
|
// was passed to Claude via --session-id so the filename matches from the start.
|
|
7764
8219
|
watchForJsonl(sessionId, projectPath) {
|
|
7765
8220
|
const encoded = projectPath.replace(/[/\\:.]/g, "-");
|
|
7766
|
-
const projectsDir =
|
|
8221
|
+
const projectsDir = join15(homedir7(), ".claude", "projects", encoded);
|
|
7767
8222
|
const expectedFile = `${sessionId}.jsonl`;
|
|
7768
|
-
const filePath =
|
|
8223
|
+
const filePath = join15(projectsDir, expectedFile);
|
|
7769
8224
|
const deadline = Date.now() + 12e4;
|
|
7770
8225
|
let watcher = null;
|
|
7771
8226
|
const cleanup = () => {
|
|
@@ -7783,12 +8238,12 @@ var StreamerServer = class {
|
|
|
7783
8238
|
cleanup();
|
|
7784
8239
|
return;
|
|
7785
8240
|
}
|
|
7786
|
-
let resolvedFilePath =
|
|
7787
|
-
if (!resolvedFilePath &&
|
|
8241
|
+
let resolvedFilePath = existsSync8(filePath) ? filePath : null;
|
|
8242
|
+
if (!resolvedFilePath && existsSync8(projectsDir)) {
|
|
7788
8243
|
try {
|
|
7789
8244
|
const now = Date.now();
|
|
7790
|
-
const recent = readdirSync4(projectsDir).filter((f) => f.endsWith(".jsonl")).map((f) => ({ f, mtime:
|
|
7791
|
-
if (recent) resolvedFilePath =
|
|
8245
|
+
const recent = readdirSync4(projectsDir).filter((f) => f.endsWith(".jsonl")).map((f) => ({ f, mtime: statSync6(join15(projectsDir, f)).mtimeMs })).filter(({ mtime }) => now - mtime < 5e3).sort((a, b) => b.mtime - a.mtime)[0];
|
|
8246
|
+
if (recent) resolvedFilePath = join15(projectsDir, recent.f);
|
|
7792
8247
|
} catch {
|
|
7793
8248
|
}
|
|
7794
8249
|
}
|
|
@@ -7797,7 +8252,7 @@ var StreamerServer = class {
|
|
|
7797
8252
|
this.sessionFileMap.set(sessionId, resolvedFilePath);
|
|
7798
8253
|
this.fileWatcher.watch(resolvedFilePath);
|
|
7799
8254
|
try {
|
|
7800
|
-
const existing =
|
|
8255
|
+
const existing = readFileSync7(resolvedFilePath, "utf8").split("\n").filter(Boolean);
|
|
7801
8256
|
if (existing.length > 0) {
|
|
7802
8257
|
this.broadcastConversationLines(sessionId, existing);
|
|
7803
8258
|
}
|
|
@@ -7836,7 +8291,7 @@ var StreamerServer = class {
|
|
|
7836
8291
|
watchForCodexRollout(sessionId, projectPath) {
|
|
7837
8292
|
const deadline = Date.now() + 12e4;
|
|
7838
8293
|
const now = /* @__PURE__ */ new Date();
|
|
7839
|
-
const dateDir =
|
|
8294
|
+
const dateDir = join15(
|
|
7840
8295
|
String(now.getFullYear()),
|
|
7841
8296
|
String(now.getMonth() + 1).padStart(2, "0"),
|
|
7842
8297
|
String(now.getDate()).padStart(2, "0")
|
|
@@ -7849,7 +8304,7 @@ var StreamerServer = class {
|
|
|
7849
8304
|
};
|
|
7850
8305
|
const matchesProjectPath = (candidatePath) => {
|
|
7851
8306
|
try {
|
|
7852
|
-
const firstLine =
|
|
8307
|
+
const firstLine = readFileSync7(candidatePath, "utf8").split("\n", 1)[0];
|
|
7853
8308
|
if (!firstLine) return null;
|
|
7854
8309
|
const parsed = JSON.parse(firstLine);
|
|
7855
8310
|
if (parsed?.type !== "session_meta") return null;
|
|
@@ -7877,8 +8332,8 @@ var StreamerServer = class {
|
|
|
7877
8332
|
this.sessionStore.listManaged().filter((s) => s.id !== sessionId && s.boundConversationId != null).map((s) => s.boundConversationId)
|
|
7878
8333
|
);
|
|
7879
8334
|
for (const root of this.codexRoots) {
|
|
7880
|
-
const sessionsDir =
|
|
7881
|
-
if (!
|
|
8335
|
+
const sessionsDir = join15(root, dateDir);
|
|
8336
|
+
if (!existsSync8(sessionsDir)) continue;
|
|
7882
8337
|
let candidateFiles;
|
|
7883
8338
|
try {
|
|
7884
8339
|
candidateFiles = readdirSync4(sessionsDir).filter((f) => f.endsWith(".jsonl"));
|
|
@@ -7886,9 +8341,9 @@ var StreamerServer = class {
|
|
|
7886
8341
|
continue;
|
|
7887
8342
|
}
|
|
7888
8343
|
const nowMs = Date.now();
|
|
7889
|
-
const recentCandidates = candidateFiles.map((f) => ({ f, mtime:
|
|
8344
|
+
const recentCandidates = candidateFiles.map((f) => ({ f, mtime: statSync6(join15(sessionsDir, f)).mtimeMs })).filter(({ mtime }) => nowMs - mtime < 1e4).sort((a, b) => b.mtime - a.mtime);
|
|
7890
8345
|
for (const { f } of recentCandidates) {
|
|
7891
|
-
const candidatePath =
|
|
8346
|
+
const candidatePath = join15(sessionsDir, f);
|
|
7892
8347
|
const match = matchesProjectPath(candidatePath);
|
|
7893
8348
|
if (!match) continue;
|
|
7894
8349
|
if (boundElsewhere.has(match.id)) continue;
|
|
@@ -7898,7 +8353,7 @@ var StreamerServer = class {
|
|
|
7898
8353
|
this.sessionFileMap.set(sessionId, candidatePath);
|
|
7899
8354
|
this.fileWatcher.watch(candidatePath);
|
|
7900
8355
|
try {
|
|
7901
|
-
const existing =
|
|
8356
|
+
const existing = readFileSync7(candidatePath, "utf8").split("\n").filter(Boolean);
|
|
7902
8357
|
if (existing.length > 0) {
|
|
7903
8358
|
this.broadcastConversationLines(sessionId, existing);
|
|
7904
8359
|
}
|
|
@@ -8020,7 +8475,7 @@ var StreamerServer = class {
|
|
|
8020
8475
|
};
|
|
8021
8476
|
function classifyResumability(cwd) {
|
|
8022
8477
|
if (!cwd) return { resumable: true };
|
|
8023
|
-
if (
|
|
8478
|
+
if (existsSync8(cwd)) return { resumable: true };
|
|
8024
8479
|
const ranInWorktree = /\/\.worktrees\//.test(cwd) || /\/\.claude\/worktrees\//.test(cwd);
|
|
8025
8480
|
return {
|
|
8026
8481
|
resumable: false,
|