@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/cli.cjs +801 -426
- package/dist/cli.cjs.map +1 -1
- package/dist/index.cjs +579 -195
- package/dist/index.cjs.map +1 -1
- package/dist/index.js +562 -178
- package/dist/index.js.map +1 -1
- package/package.json +1 -1
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
|
|
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
|
|
561
|
-
var
|
|
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
|
-
//
|
|
776
|
-
//
|
|
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
|
-
//
|
|
788
|
-
|
|
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,
|
|
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,
|
|
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=${
|
|
914
|
-
{ event: "codex.keys_write", sessionId, byteLen:
|
|
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(
|
|
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.
|
|
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.
|
|
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.
|
|
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
|
|
1111
|
-
//
|
|
1112
|
-
//
|
|
1113
|
-
//
|
|
1114
|
-
//
|
|
1115
|
-
|
|
1116
|
-
|
|
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
|
-
|
|
1120
|
-
|
|
1121
|
-
|
|
1122
|
-
|
|
1123
|
-
|
|
1124
|
-
|
|
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 (
|
|
1132
|
-
|
|
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,
|
|
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.
|
|
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
|
|
1193
|
-
var
|
|
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
|
-
|
|
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 <=
|
|
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
|
|
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,
|
|
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,
|
|
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),
|
|
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,
|
|
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,
|
|
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
|
|
2177
|
-
var
|
|
2388
|
+
var import_os4 = require("os");
|
|
2389
|
+
var import_path8 = require("path");
|
|
2178
2390
|
async function discoverClaudeProcesses() {
|
|
2179
|
-
if ((0,
|
|
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,
|
|
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,
|
|
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,
|
|
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
|
|
2534
|
+
var import_fs13 = require("fs");
|
|
2323
2535
|
var import_promises7 = require("fs/promises");
|
|
2324
2536
|
var import_http = require("http");
|
|
2325
|
-
var
|
|
2326
|
-
var
|
|
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
|
|
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
|
|
2780
|
-
var
|
|
3125
|
+
var import_hono6 = require("hono");
|
|
3126
|
+
var import_os5 = require("os");
|
|
2781
3127
|
|
|
2782
3128
|
// src/config/update-config.ts
|
|
2783
|
-
var
|
|
2784
|
-
var
|
|
2785
|
-
var
|
|
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,
|
|
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,
|
|
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
|
|
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,
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
3396
|
+
var import_hono11 = require("hono");
|
|
3051
3397
|
var createWsRoutes = (deps, upgradeWebSocket) => {
|
|
3052
|
-
const app = new
|
|
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
|
|
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
|
|
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,
|
|
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,
|
|
3133
|
-
const rootPrefix = normalizedRoot.endsWith(
|
|
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,
|
|
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
|
|
3516
|
+
var import_fs9 = require("fs");
|
|
3170
3517
|
var import_promises3 = require("fs/promises");
|
|
3171
|
-
var
|
|
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
|
|
3176
|
-
var
|
|
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,
|
|
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,
|
|
3194
|
-
const files = (0,
|
|
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,
|
|
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
|
|
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,
|
|
3586
|
+
fd = (0, import_fs8.openSync)(filePath, "r");
|
|
3240
3587
|
} catch {
|
|
3241
3588
|
return false;
|
|
3242
3589
|
}
|
|
3243
3590
|
try {
|
|
3244
|
-
const fileSize = (0,
|
|
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,
|
|
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,
|
|
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,
|
|
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,
|
|
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,
|
|
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,
|
|
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,
|
|
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,
|
|
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,
|
|
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,
|
|
4120
|
-
fd = (0,
|
|
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,
|
|
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,
|
|
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 =
|
|
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 ??
|
|
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
|
|
4631
|
-
var
|
|
4632
|
-
var
|
|
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,
|
|
4986
|
+
const projectsDir = (0, import_path12.join)((0, import_os6.homedir)(), ".claude", "projects");
|
|
4640
4987
|
let entries;
|
|
4641
4988
|
try {
|
|
4642
|
-
entries = (0,
|
|
4643
|
-
const fullPath = (0,
|
|
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,
|
|
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
|
|
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,
|
|
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
|
|
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,
|
|
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
|
|
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,
|
|
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,
|
|
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 =
|
|
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,
|
|
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,
|
|
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,
|
|
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,
|
|
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,
|
|
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,
|
|
6597
|
+
this.fileWatcher.watchDirectory((0, import_path14.join)(profile.configDir, "projects"));
|
|
6219
6598
|
}
|
|
6220
6599
|
}
|
|
6221
6600
|
} else {
|
|
6222
|
-
this.fileWatcher.watchDirectory((0,
|
|
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,
|
|
6808
|
-
if (!(0,
|
|
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,
|
|
6811
|
-
const fp = (0,
|
|
6812
|
-
if ((0,
|
|
6813
|
-
const projectDir = (0,
|
|
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,
|
|
6816
|
-
const subagentPath = (0,
|
|
6817
|
-
if ((0,
|
|
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,
|
|
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,
|
|
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,
|
|
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,
|
|
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,
|
|
8257
|
+
const projectsDir = (0, import_path14.join)((0, import_os7.homedir)(), ".claude", "projects", encoded);
|
|
7874
8258
|
const expectedFile = `${sessionId}.jsonl`;
|
|
7875
|
-
const filePath = (0,
|
|
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,
|
|
7894
|
-
if (!resolvedFilePath && (0,
|
|
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,
|
|
7898
|
-
if (recent) resolvedFilePath = (0,
|
|
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,
|
|
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,
|
|
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,
|
|
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,
|
|
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,
|
|
7988
|
-
if (!(0,
|
|
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,
|
|
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,
|
|
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,
|
|
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,
|
|
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,
|
|
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,
|