@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.cjs
CHANGED
|
@@ -215,7 +215,7 @@ function verifySignature(rawBody, signature, secret) {
|
|
|
215
215
|
}
|
|
216
216
|
}
|
|
217
217
|
function isWithinSkew(timestampHeader, skewSeconds) {
|
|
218
|
-
if (!timestampHeader) return
|
|
218
|
+
if (!timestampHeader) return false;
|
|
219
219
|
const t = Number(timestampHeader);
|
|
220
220
|
if (!Number.isFinite(t)) return false;
|
|
221
221
|
const now = Math.floor(Date.now() / 1e3);
|
|
@@ -552,13 +552,13 @@ async function createPool(config) {
|
|
|
552
552
|
}
|
|
553
553
|
|
|
554
554
|
// src/live-session-manager.ts
|
|
555
|
-
var
|
|
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,14 +2794,15 @@ 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) {
|
|
2589
2801
|
const addr = remoteAddr ?? "";
|
|
2590
2802
|
return addr === "127.0.0.1" || addr === "::1" || addr === "::ffff:127.0.0.1";
|
|
2591
2803
|
}
|
|
2592
|
-
var PUBLIC_PATHS = /* @__PURE__ */ new Set(["/healthz"]);
|
|
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,19 +3393,21 @@ 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
|
-
upgradeWebSocket(() => {
|
|
3401
|
+
upgradeWebSocket((c) => {
|
|
3402
|
+
const key = c.req.query("key");
|
|
3403
|
+
const preAuthed = typeof key === "string" && validateApiKey(key, deps.apiKey);
|
|
3056
3404
|
let openWs = null;
|
|
3057
3405
|
return {
|
|
3058
3406
|
onOpen(_evt, ws) {
|
|
3059
3407
|
const raw = ws.raw;
|
|
3060
3408
|
if (!raw) return;
|
|
3061
3409
|
openWs = raw;
|
|
3062
|
-
deps.handleWsOpen(raw);
|
|
3410
|
+
deps.handleWsOpen(raw, preAuthed);
|
|
3063
3411
|
},
|
|
3064
3412
|
onMessage(evt, _ws) {
|
|
3065
3413
|
if (openWs) deps.handleWsMessage(openWs, evt.data);
|
|
@@ -3075,7 +3423,7 @@ var createWsRoutes = (deps, upgradeWebSocket) => {
|
|
|
3075
3423
|
|
|
3076
3424
|
// src/api/app.ts
|
|
3077
3425
|
var createHonoApp = (deps, upgradeWebSocket) => {
|
|
3078
|
-
const app = new
|
|
3426
|
+
const app = new import_hono12.Hono();
|
|
3079
3427
|
const httpLog = getLogger("http");
|
|
3080
3428
|
app.use("*", async (c, next) => {
|
|
3081
3429
|
const start = Date.now();
|
|
@@ -3104,6 +3452,7 @@ var createHonoApp = (deps, upgradeWebSocket) => {
|
|
|
3104
3452
|
app.route("/api", createBrowseRoutes(deps));
|
|
3105
3453
|
app.route("/", createScannerRoutes(deps));
|
|
3106
3454
|
app.route("/internal", createProgressRoutes(deps));
|
|
3455
|
+
app.route("/api/logs", createLogsRoutes());
|
|
3107
3456
|
if (upgradeWebSocket) {
|
|
3108
3457
|
app.route("/", createWsRoutes(deps, upgradeWebSocket));
|
|
3109
3458
|
}
|
|
@@ -3112,7 +3461,7 @@ var createHonoApp = (deps, upgradeWebSocket) => {
|
|
|
3112
3461
|
|
|
3113
3462
|
// src/browse.ts
|
|
3114
3463
|
var import_promises2 = require("fs/promises");
|
|
3115
|
-
var
|
|
3464
|
+
var import_path9 = require("path");
|
|
3116
3465
|
var BrowsePathNotFoundError = class extends Error {
|
|
3117
3466
|
constructor(message) {
|
|
3118
3467
|
super(message);
|
|
@@ -3120,15 +3469,15 @@ var BrowsePathNotFoundError = class extends Error {
|
|
|
3120
3469
|
}
|
|
3121
3470
|
};
|
|
3122
3471
|
async function resolveBrowsePath(browseRoot, relativePath) {
|
|
3123
|
-
const normalizedRoot = (0,
|
|
3472
|
+
const normalizedRoot = (0, import_path9.resolve)(browseRoot);
|
|
3124
3473
|
let sanitized;
|
|
3125
3474
|
if (process.platform !== "win32" && relativePath.startsWith("/") && relativePath.length > 1 && relativePath.includes("/", 1)) {
|
|
3126
3475
|
sanitized = relativePath;
|
|
3127
3476
|
} else {
|
|
3128
3477
|
sanitized = relativePath.replace(/^[/\\]+/, "");
|
|
3129
3478
|
}
|
|
3130
|
-
const target = sanitized ? (0,
|
|
3131
|
-
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}`;
|
|
3132
3481
|
if (!target.startsWith(rootPrefix) && target !== normalizedRoot) {
|
|
3133
3482
|
throw new Error("Path outside browse root");
|
|
3134
3483
|
}
|
|
@@ -3150,7 +3499,7 @@ async function createDirectory(parentAbsolutePath, name) {
|
|
|
3150
3499
|
if (name.includes("/") || name.includes("\\") || name === ".." || name === ".") {
|
|
3151
3500
|
throw new Error("Invalid directory name");
|
|
3152
3501
|
}
|
|
3153
|
-
const target = (0,
|
|
3502
|
+
const target = (0, import_path9.join)(parentAbsolutePath, name);
|
|
3154
3503
|
try {
|
|
3155
3504
|
const s = await (0, import_promises2.stat)(target);
|
|
3156
3505
|
if (s.isDirectory()) throw new Error("Directory already exists");
|
|
@@ -3164,19 +3513,19 @@ async function createDirectory(parentAbsolutePath, name) {
|
|
|
3164
3513
|
// src/conversation-cache.ts
|
|
3165
3514
|
var import_scanner2 = require("@threadbase-sh/scanner");
|
|
3166
3515
|
var import_better_sqlite3 = __toESM(require("better-sqlite3"), 1);
|
|
3167
|
-
var
|
|
3516
|
+
var import_fs9 = require("fs");
|
|
3168
3517
|
var import_promises3 = require("fs/promises");
|
|
3169
|
-
var
|
|
3518
|
+
var import_path11 = require("path");
|
|
3170
3519
|
var import_promises4 = require("timers/promises");
|
|
3171
3520
|
|
|
3172
3521
|
// src/db/sqlite-migrate.ts
|
|
3173
|
-
var
|
|
3174
|
-
var
|
|
3522
|
+
var import_fs7 = require("fs");
|
|
3523
|
+
var import_path10 = require("path");
|
|
3175
3524
|
var import_url2 = require("url");
|
|
3176
3525
|
var import_meta2 = {};
|
|
3177
3526
|
function getMigrationsDir2() {
|
|
3178
3527
|
if (typeof import_meta2 !== "undefined" && import_meta2.url) {
|
|
3179
|
-
return (0,
|
|
3528
|
+
return (0, import_path10.dirname)((0, import_url2.fileURLToPath)(import_meta2.url));
|
|
3180
3529
|
}
|
|
3181
3530
|
return __dirname;
|
|
3182
3531
|
}
|
|
@@ -3188,8 +3537,8 @@ CREATE TABLE IF NOT EXISTS schema_migrations (
|
|
|
3188
3537
|
`;
|
|
3189
3538
|
function runSqliteMigrations(db, migrationsDir) {
|
|
3190
3539
|
db.exec(SCHEMA_MIGRATIONS_SQL);
|
|
3191
|
-
const dir = migrationsDir ?? (0,
|
|
3192
|
-
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();
|
|
3193
3542
|
const appliedRows = db.prepare("SELECT id FROM schema_migrations").all();
|
|
3194
3543
|
const appliedSet = new Set(appliedRows.map((r) => r.id));
|
|
3195
3544
|
const recordApplied = db.prepare("INSERT INTO schema_migrations (id, applied_at) VALUES (?, ?)");
|
|
@@ -3200,7 +3549,7 @@ function runSqliteMigrations(db, migrationsDir) {
|
|
|
3200
3549
|
skipped.push(file);
|
|
3201
3550
|
continue;
|
|
3202
3551
|
}
|
|
3203
|
-
const sql = (0,
|
|
3552
|
+
const sql = (0, import_fs7.readFileSync)((0, import_path10.join)(dir, file), "utf-8");
|
|
3204
3553
|
const tx = db.transaction(() => {
|
|
3205
3554
|
db.exec(sql);
|
|
3206
3555
|
recordApplied.run(file, (/* @__PURE__ */ new Date()).toISOString());
|
|
@@ -3212,7 +3561,7 @@ function runSqliteMigrations(db, migrationsDir) {
|
|
|
3212
3561
|
}
|
|
3213
3562
|
|
|
3214
3563
|
// src/services/conversations/isAgentConversation.ts
|
|
3215
|
-
var
|
|
3564
|
+
var import_fs8 = require("fs");
|
|
3216
3565
|
var DEFAULT_AGENT_ENTRYPOINTS = /* @__PURE__ */ new Set(["sdk-cli", "claude-vscode"]);
|
|
3217
3566
|
var CHUNK_BYTES = 64 * 1024;
|
|
3218
3567
|
var ENTRYPOINT_PROBE = `"entrypoint":`;
|
|
@@ -3234,12 +3583,12 @@ function isAgentFile(filePath, entrypoints = DEFAULT_AGENT_ENTRYPOINTS) {
|
|
|
3234
3583
|
if (cached2 !== void 0) return cached2;
|
|
3235
3584
|
let fd;
|
|
3236
3585
|
try {
|
|
3237
|
-
fd = (0,
|
|
3586
|
+
fd = (0, import_fs8.openSync)(filePath, "r");
|
|
3238
3587
|
} catch {
|
|
3239
3588
|
return false;
|
|
3240
3589
|
}
|
|
3241
3590
|
try {
|
|
3242
|
-
const fileSize = (0,
|
|
3591
|
+
const fileSize = (0, import_fs8.statSync)(filePath).size;
|
|
3243
3592
|
if (fileSize === 0) {
|
|
3244
3593
|
fileDecisionCache.set(key, false);
|
|
3245
3594
|
return false;
|
|
@@ -3250,7 +3599,7 @@ function isAgentFile(filePath, entrypoints = DEFAULT_AGENT_ENTRYPOINTS) {
|
|
|
3250
3599
|
let carry = "";
|
|
3251
3600
|
while (offset < fileSize) {
|
|
3252
3601
|
const toRead = Math.min(CHUNK_BYTES, fileSize - offset);
|
|
3253
|
-
const got = (0,
|
|
3602
|
+
const got = (0, import_fs8.readSync)(fd, buf, 0, toRead, offset);
|
|
3254
3603
|
if (got <= 0) break;
|
|
3255
3604
|
const chunk = carry + buf.toString("utf8", 0, got);
|
|
3256
3605
|
for (const marker of markers) {
|
|
@@ -3271,7 +3620,7 @@ function isAgentFile(filePath, entrypoints = DEFAULT_AGENT_ENTRYPOINTS) {
|
|
|
3271
3620
|
} catch {
|
|
3272
3621
|
return false;
|
|
3273
3622
|
} finally {
|
|
3274
|
-
(0,
|
|
3623
|
+
(0, import_fs8.closeSync)(fd);
|
|
3275
3624
|
}
|
|
3276
3625
|
}
|
|
3277
3626
|
function parseAgentEntrypointsEnv(raw) {
|
|
@@ -3808,7 +4157,7 @@ var ConversationCache = class _ConversationCache {
|
|
|
3808
4157
|
if (!fileState) return null;
|
|
3809
4158
|
let stat3;
|
|
3810
4159
|
try {
|
|
3811
|
-
stat3 = (0,
|
|
4160
|
+
stat3 = (0, import_fs9.statSync)(filePath);
|
|
3812
4161
|
} catch {
|
|
3813
4162
|
return null;
|
|
3814
4163
|
}
|
|
@@ -3829,17 +4178,17 @@ var ConversationCache = class _ConversationCache {
|
|
|
3829
4178
|
);
|
|
3830
4179
|
if (rows.length === 0) return { messages: [], total, fromIndex: from };
|
|
3831
4180
|
const messages = [];
|
|
3832
|
-
const fd = (0,
|
|
4181
|
+
const fd = (0, import_fs9.openSync)(filePath, "r");
|
|
3833
4182
|
try {
|
|
3834
4183
|
const state = (0, import_scanner2.createJsonlParseState)();
|
|
3835
4184
|
for (const row of rows) {
|
|
3836
4185
|
const buf = Buffer.alloc(row.byte_length);
|
|
3837
|
-
(0,
|
|
4186
|
+
(0, import_fs9.readSync)(fd, buf, 0, row.byte_length, row.byte_offset);
|
|
3838
4187
|
const msg = (0, import_scanner2.parseJsonlLine)(buf.toString("utf-8"), state);
|
|
3839
4188
|
if (msg) messages.push(msg);
|
|
3840
4189
|
}
|
|
3841
4190
|
} finally {
|
|
3842
|
-
(0,
|
|
4191
|
+
(0, import_fs9.closeSync)(fd);
|
|
3843
4192
|
}
|
|
3844
4193
|
return { messages, total, fromIndex: from };
|
|
3845
4194
|
}
|
|
@@ -3867,14 +4216,14 @@ var ConversationCache = class _ConversationCache {
|
|
|
3867
4216
|
isAgentFileCached(filePath) {
|
|
3868
4217
|
let s;
|
|
3869
4218
|
try {
|
|
3870
|
-
s = (0,
|
|
4219
|
+
s = (0, import_fs9.statSync)(filePath);
|
|
3871
4220
|
} catch {
|
|
3872
4221
|
return false;
|
|
3873
4222
|
}
|
|
3874
4223
|
return this.classifyAgentFile(filePath, s.mtimeMs, s.size);
|
|
3875
4224
|
}
|
|
3876
4225
|
static open(dbPath, tailSize = 10, migrationsDir, options) {
|
|
3877
|
-
(0,
|
|
4226
|
+
(0, import_fs9.mkdirSync)((0, import_path11.dirname)(dbPath), { recursive: true });
|
|
3878
4227
|
const db = new import_better_sqlite3.default(dbPath);
|
|
3879
4228
|
db.pragma("journal_mode = WAL");
|
|
3880
4229
|
db.pragma("foreign_keys = ON");
|
|
@@ -4055,7 +4404,7 @@ var ConversationCache = class _ConversationCache {
|
|
|
4055
4404
|
let mtimeMs = null;
|
|
4056
4405
|
let fileSize = null;
|
|
4057
4406
|
try {
|
|
4058
|
-
const s = (0,
|
|
4407
|
+
const s = (0, import_fs9.statSync)(m.filePath);
|
|
4059
4408
|
mtimeMs = s.mtimeMs;
|
|
4060
4409
|
fileSize = s.size;
|
|
4061
4410
|
} catch {
|
|
@@ -4114,8 +4463,8 @@ var ConversationCache = class _ConversationCache {
|
|
|
4114
4463
|
let fileSize;
|
|
4115
4464
|
let fd;
|
|
4116
4465
|
try {
|
|
4117
|
-
fileSize = (0,
|
|
4118
|
-
fd = (0,
|
|
4466
|
+
fileSize = (0, import_fs9.statSync)(filePath).size;
|
|
4467
|
+
fd = (0, import_fs9.openSync)(filePath, "r");
|
|
4119
4468
|
} catch {
|
|
4120
4469
|
return false;
|
|
4121
4470
|
}
|
|
@@ -4128,7 +4477,7 @@ var ConversationCache = class _ConversationCache {
|
|
|
4128
4477
|
while (pos > 0 && lines.length < this.tailSize * 4) {
|
|
4129
4478
|
const toRead = Math.min(CHUNK, pos);
|
|
4130
4479
|
pos -= toRead;
|
|
4131
|
-
(0,
|
|
4480
|
+
(0, import_fs9.readSync)(fd, buf, 0, toRead, pos);
|
|
4132
4481
|
const chunk = buf.subarray(0, toRead).toString("utf8");
|
|
4133
4482
|
const combined = chunk + partial;
|
|
4134
4483
|
const parts = combined.split("\n");
|
|
@@ -4139,7 +4488,7 @@ var ConversationCache = class _ConversationCache {
|
|
|
4139
4488
|
}
|
|
4140
4489
|
if (partial) lines.push(partial);
|
|
4141
4490
|
} finally {
|
|
4142
|
-
(0,
|
|
4491
|
+
(0, import_fs9.closeSync)(fd);
|
|
4143
4492
|
}
|
|
4144
4493
|
const msgs = [];
|
|
4145
4494
|
for (let i = 0; i < lines.length && msgs.length < this.tailSize; i++) {
|
|
@@ -4341,7 +4690,7 @@ var ConversationCache = class _ConversationCache {
|
|
|
4341
4690
|
* `handleGetConversation` can still serve the cached tail even when the
|
|
4342
4691
|
* JSONL has been deleted.
|
|
4343
4692
|
*/
|
|
4344
|
-
pruneGhostFiles(exists =
|
|
4693
|
+
pruneGhostFiles(exists = import_fs9.existsSync) {
|
|
4345
4694
|
const rows = this.stmts.allFilePaths.all();
|
|
4346
4695
|
const ghosts = [];
|
|
4347
4696
|
const prune = this.db.transaction((ids) => {
|
|
@@ -4396,7 +4745,7 @@ var ConversationCache = class _ConversationCache {
|
|
|
4396
4745
|
* Returns the removed IDs.
|
|
4397
4746
|
*/
|
|
4398
4747
|
reconcileDeletions(livePaths, opts) {
|
|
4399
|
-
const exists = opts?.exists ??
|
|
4748
|
+
const exists = opts?.exists ?? import_fs9.existsSync;
|
|
4400
4749
|
const rows = this.stmts.allFilePaths.all();
|
|
4401
4750
|
const removed = [];
|
|
4402
4751
|
const drop = this.db.transaction((ids) => {
|
|
@@ -4625,23 +4974,23 @@ async function recordUpload(pool2, instanceId, row) {
|
|
|
4625
4974
|
}
|
|
4626
4975
|
|
|
4627
4976
|
// src/handlers/handleListProjects.ts
|
|
4628
|
-
var
|
|
4629
|
-
var
|
|
4630
|
-
var
|
|
4977
|
+
var import_fs10 = require("fs");
|
|
4978
|
+
var import_os6 = require("os");
|
|
4979
|
+
var import_path12 = require("path");
|
|
4631
4980
|
function decodeProjectPath(dirName) {
|
|
4632
4981
|
return dirName.replace(/-/g, "/");
|
|
4633
4982
|
}
|
|
4634
4983
|
function handleListProjects(url, res) {
|
|
4635
4984
|
const limit = Math.max(1, parseInt(url.searchParams.get("limit") ?? "50", 10) || 50);
|
|
4636
4985
|
const offset = Math.max(0, parseInt(url.searchParams.get("offset") ?? "0", 10) || 0);
|
|
4637
|
-
const projectsDir = (0,
|
|
4986
|
+
const projectsDir = (0, import_path12.join)((0, import_os6.homedir)(), ".claude", "projects");
|
|
4638
4987
|
let entries;
|
|
4639
4988
|
try {
|
|
4640
|
-
entries = (0,
|
|
4641
|
-
const fullPath = (0,
|
|
4989
|
+
entries = (0, import_fs10.readdirSync)(projectsDir).map((dirName) => {
|
|
4990
|
+
const fullPath = (0, import_path12.join)(projectsDir, dirName);
|
|
4642
4991
|
let mtime = 0;
|
|
4643
4992
|
try {
|
|
4644
|
-
mtime = (0,
|
|
4993
|
+
mtime = (0, import_fs10.statSync)(fullPath).mtimeMs;
|
|
4645
4994
|
} catch {
|
|
4646
4995
|
}
|
|
4647
4996
|
const path = decodeProjectPath(dirName);
|
|
@@ -4736,7 +5085,7 @@ function seal(plaintext, recipientPublicKeyBase64) {
|
|
|
4736
5085
|
|
|
4737
5086
|
// src/services/conversations/conversationWatcher.ts
|
|
4738
5087
|
var import_chokidar = __toESM(require("chokidar"), 1);
|
|
4739
|
-
var
|
|
5088
|
+
var import_fs11 = require("fs");
|
|
4740
5089
|
var import_promises5 = require("fs/promises");
|
|
4741
5090
|
var ConversationWatcher = class {
|
|
4742
5091
|
files = /* @__PURE__ */ new Map();
|
|
@@ -4759,7 +5108,7 @@ var ConversationWatcher = class {
|
|
|
4759
5108
|
if (this.files.has(filePath)) return;
|
|
4760
5109
|
let offset;
|
|
4761
5110
|
try {
|
|
4762
|
-
offset = (0,
|
|
5111
|
+
offset = (0, import_fs11.statSync)(filePath).size;
|
|
4763
5112
|
} catch {
|
|
4764
5113
|
offset = 0;
|
|
4765
5114
|
}
|
|
@@ -4934,14 +5283,14 @@ function findSearchTarget(messages, query) {
|
|
|
4934
5283
|
}
|
|
4935
5284
|
|
|
4936
5285
|
// src/services/conversations/pruneAgentConversations.ts
|
|
4937
|
-
var
|
|
5286
|
+
var import_fs12 = require("fs");
|
|
4938
5287
|
function pruneAgentConversations(cache) {
|
|
4939
5288
|
const db = cache.getDatabase();
|
|
4940
5289
|
const rows = db.prepare("SELECT id, file_path FROM conversation_meta").all();
|
|
4941
5290
|
let pruned = 0;
|
|
4942
5291
|
let missing = 0;
|
|
4943
5292
|
for (const row of rows) {
|
|
4944
|
-
if (!(0,
|
|
5293
|
+
if (!(0, import_fs12.existsSync)(row.file_path)) {
|
|
4945
5294
|
missing += 1;
|
|
4946
5295
|
continue;
|
|
4947
5296
|
}
|
|
@@ -4964,6 +5313,13 @@ function deriveProjectChatTitle(input) {
|
|
|
4964
5313
|
return `Untitled \xB7 ${input.id.slice(0, 8)}`;
|
|
4965
5314
|
}
|
|
4966
5315
|
|
|
5316
|
+
// src/services/questions/permissionAnswerKeys.ts
|
|
5317
|
+
var ANSWER_KEYS_ALLOWLIST = /^(?:\r|[yn]\r|\x03|\d+\r)$/;
|
|
5318
|
+
function sanitizeAnswerKeys(keys) {
|
|
5319
|
+
if (keys === void 0) return void 0;
|
|
5320
|
+
return ANSWER_KEYS_ALLOWLIST.test(keys) ? keys : void 0;
|
|
5321
|
+
}
|
|
5322
|
+
|
|
4967
5323
|
// src/services/questions/detectAskUserQuestion.ts
|
|
4968
5324
|
function normalizeContent2(raw) {
|
|
4969
5325
|
if (Array.isArray(raw)) return raw;
|
|
@@ -5277,7 +5633,7 @@ function discoveredToResponse(d, conversationId) {
|
|
|
5277
5633
|
var import_crypto8 = require("crypto");
|
|
5278
5634
|
var import_promises6 = require("fs/promises");
|
|
5279
5635
|
var import_heic_convert = __toESM(require("heic-convert"), 1);
|
|
5280
|
-
var
|
|
5636
|
+
var import_path13 = require("path");
|
|
5281
5637
|
var UPLOAD_DIR_NAME = ".threadbase-uploads";
|
|
5282
5638
|
var MAX_BYTES = 25 * 1024 * 1024;
|
|
5283
5639
|
var HEIC_MIMES = /* @__PURE__ */ new Set(["image/heic", "image/heif"]);
|
|
@@ -5310,9 +5666,9 @@ async function saveUploadFile(input) {
|
|
|
5310
5666
|
}
|
|
5311
5667
|
const id = `up_${(0, import_crypto8.randomBytes)(8).toString("hex")}`;
|
|
5312
5668
|
const safeName = sanitizeFilename(originalName) || `file${MIME_TO_EXT[mimeType] ?? ""}`;
|
|
5313
|
-
const dir = (0,
|
|
5669
|
+
const dir = (0, import_path13.join)(input.projectPath, UPLOAD_DIR_NAME, input.sessionId);
|
|
5314
5670
|
await (0, import_promises6.mkdir)(dir, { recursive: true });
|
|
5315
|
-
const filePath = (0,
|
|
5671
|
+
const filePath = (0, import_path13.join)(dir, `${Date.now()}-${id}-${safeName}`);
|
|
5316
5672
|
await (0, import_promises6.writeFile)(filePath, buffer);
|
|
5317
5673
|
return {
|
|
5318
5674
|
id,
|
|
@@ -5568,8 +5924,10 @@ var WSHub = class {
|
|
|
5568
5924
|
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.`;
|
|
5569
5925
|
var DEFAULT_SYSTEM_PROMPT = "When presenting options or choices to the user, limit the options to at most 3.";
|
|
5570
5926
|
var DEFAULT_PTY_GRACE_PERIOD_MS = 27e4;
|
|
5927
|
+
var DEFAULT_WS_AUTH_TIMEOUT_MS = 5e3;
|
|
5928
|
+
var WS_CLOSE_UNAUTHORIZED = 4401;
|
|
5571
5929
|
var REFRESH_TTL_MS = 2e3;
|
|
5572
|
-
var START_READY_TIMEOUT_MS =
|
|
5930
|
+
var START_READY_TIMEOUT_MS = 1e4;
|
|
5573
5931
|
function parseIncludeAgentsEnv(raw) {
|
|
5574
5932
|
if (raw === void 0) return false;
|
|
5575
5933
|
const v = raw.trim().toLowerCase();
|
|
@@ -5658,6 +6016,14 @@ var StreamerServer = class {
|
|
|
5658
6016
|
clientIdToWs = /* @__PURE__ */ new Map();
|
|
5659
6017
|
// Reverse map for cleanup on close
|
|
5660
6018
|
wsToClientId = /* @__PURE__ */ new Map();
|
|
6019
|
+
// M1 — WS auth. Sockets that have authenticated (via ?key= at upgrade OR a
|
|
6020
|
+
// { type: "auth", token } first message). Only authed sockets are added to
|
|
6021
|
+
// the hub and receive broadcasts.
|
|
6022
|
+
// Plan: https://github.com/RonenMars/threadbase-streamer/blob/a251353bfa417bd48ce3f15086bc336a2c622629/docs/plans/2026-06-24-security-hardening.md#L40
|
|
6023
|
+
wsAuthed = /* @__PURE__ */ new Set();
|
|
6024
|
+
// Keyless sockets awaiting their first-message auth handshake → close timer.
|
|
6025
|
+
wsAuthPending = /* @__PURE__ */ new Map();
|
|
6026
|
+
wsAuthTimeoutMs;
|
|
5661
6027
|
cache = null;
|
|
5662
6028
|
projectsRepo = null;
|
|
5663
6029
|
conversationsRepo = null;
|
|
@@ -5695,11 +6061,12 @@ var StreamerServer = class {
|
|
|
5695
6061
|
this.disableDb = config.disableDb ?? false;
|
|
5696
6062
|
this.scannerPersistenceDisabled = config.scannerPersistent === false;
|
|
5697
6063
|
this.scanProfiles = config.scanProfiles;
|
|
5698
|
-
this.codexRoots = config.codexRoots ?? [(0,
|
|
6064
|
+
this.codexRoots = config.codexRoots ?? [(0, import_path14.join)((0, import_os7.homedir)(), ".codex", "sessions")];
|
|
5699
6065
|
this.ptyGracePeriodMs = config.ptyGracePeriodMs ?? DEFAULT_PTY_GRACE_PERIOD_MS;
|
|
6066
|
+
this.wsAuthTimeoutMs = config.wsAuthTimeoutMs ?? DEFAULT_WS_AUTH_TIMEOUT_MS;
|
|
5700
6067
|
this.defaultSystemPrompt = config.defaultSystemPrompt ?? DEFAULT_SYSTEM_PROMPT;
|
|
5701
6068
|
this.defaultPermissionMode = config.defaultPermissionMode ?? loadDefaultPermissionMode() ?? "acceptEdits";
|
|
5702
|
-
this.cacheDir = config.cacheDir ?? loadCacheDir() ?? (0,
|
|
6069
|
+
this.cacheDir = config.cacheDir ?? loadCacheDir() ?? (0, import_path14.join)((0, import_os7.homedir)(), ".threadbase", "cache");
|
|
5703
6070
|
this.tailSize = config.tailSize ?? loadTailSize() ?? 10;
|
|
5704
6071
|
this.directoryDebounceMs = parseDirScanDebounceEnv(process.env.THREADBASE_DIR_SCAN_DEBOUNCE_MS) ?? config.directoryScanDebounceMs ?? 1e3;
|
|
5705
6072
|
this.markScannerStaleDebounced = debounce(() => {
|
|
@@ -5740,7 +6107,7 @@ var StreamerServer = class {
|
|
|
5740
6107
|
const seqs = cache.extendMessageIndex(
|
|
5741
6108
|
filePath,
|
|
5742
6109
|
spans,
|
|
5743
|
-
(0,
|
|
6110
|
+
(0, import_fs13.statSync)(filePath),
|
|
5744
6111
|
readFrom,
|
|
5745
6112
|
endOffset
|
|
5746
6113
|
);
|
|
@@ -5901,7 +6268,7 @@ var StreamerServer = class {
|
|
|
5901
6268
|
temporalClient,
|
|
5902
6269
|
taskQueue: agentConfig.temporal.taskQueue
|
|
5903
6270
|
});
|
|
5904
|
-
const conversationsBaseDir = agentConfig.conversationsDir || (0,
|
|
6271
|
+
const conversationsBaseDir = agentConfig.conversationsDir || (0, import_path14.join)((0, import_path14.dirname)(this.cacheDir), "conversations");
|
|
5905
6272
|
conversationWriter = createConversationWriter({
|
|
5906
6273
|
baseDir: conversationsBaseDir
|
|
5907
6274
|
});
|
|
@@ -5954,17 +6321,39 @@ var StreamerServer = class {
|
|
|
5954
6321
|
handlePairExchange: (req, res) => this.handlePairExchange(req, res),
|
|
5955
6322
|
handleBrowse: (url, res) => this.handleBrowse(url, res),
|
|
5956
6323
|
handleMkdir: (req, res) => this.handleMkdir(req, res),
|
|
5957
|
-
handleWsOpen: (ws) => {
|
|
5958
|
-
|
|
5959
|
-
|
|
5960
|
-
|
|
5961
|
-
if (this.cacheReady) {
|
|
5962
|
-
ws.send(JSON.stringify({ type: "cache_ready" }));
|
|
6324
|
+
handleWsOpen: (ws, preAuthed) => {
|
|
6325
|
+
if (preAuthed) {
|
|
6326
|
+
this.completeWsAuth(ws);
|
|
6327
|
+
return;
|
|
5963
6328
|
}
|
|
6329
|
+
const timer = setTimeout(() => {
|
|
6330
|
+
this.wsAuthPending.delete(ws);
|
|
6331
|
+
try {
|
|
6332
|
+
ws.close(WS_CLOSE_UNAUTHORIZED, "auth timeout");
|
|
6333
|
+
} catch {
|
|
6334
|
+
}
|
|
6335
|
+
}, this.wsAuthTimeoutMs);
|
|
6336
|
+
this.wsAuthPending.set(ws, timer);
|
|
5964
6337
|
},
|
|
5965
6338
|
handleWsMessage: async (ws, raw) => {
|
|
5966
6339
|
try {
|
|
5967
6340
|
const msg = JSON.parse(String(raw));
|
|
6341
|
+
if (!this.wsAuthed.has(ws)) {
|
|
6342
|
+
if (msg.type === "auth" && typeof msg.token === "string") {
|
|
6343
|
+
const t = this.wsAuthPending.get(ws);
|
|
6344
|
+
if (t) clearTimeout(t);
|
|
6345
|
+
this.wsAuthPending.delete(ws);
|
|
6346
|
+
if (validateApiKey(msg.token, this.apiKey)) {
|
|
6347
|
+
this.completeWsAuth(ws);
|
|
6348
|
+
} else {
|
|
6349
|
+
try {
|
|
6350
|
+
ws.close(WS_CLOSE_UNAUTHORIZED, "unauthorized");
|
|
6351
|
+
} catch {
|
|
6352
|
+
}
|
|
6353
|
+
}
|
|
6354
|
+
}
|
|
6355
|
+
return;
|
|
6356
|
+
}
|
|
5968
6357
|
if (msg.type === "register" && typeof msg.clientId === "string") {
|
|
5969
6358
|
const oldClientId = this.wsToClientId.get(ws);
|
|
5970
6359
|
if (oldClientId) this.clientIdToWs.delete(oldClientId);
|
|
@@ -5977,14 +6366,54 @@ var StreamerServer = class {
|
|
|
5977
6366
|
const lines = await this.ptyManager.getOutputLines(msg.sessionId, 200);
|
|
5978
6367
|
ws.send(JSON.stringify({ type: "terminal_replay", sessionId: msg.sessionId, lines }));
|
|
5979
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
|
+
}
|
|
5980
6401
|
}
|
|
5981
6402
|
if (msg.type === "hold_session" && typeof msg.sessionId === "string") {
|
|
5982
|
-
this.
|
|
6403
|
+
if (this.sessionSubscribers.get(msg.sessionId)?.has(ws)) {
|
|
6404
|
+
this.startGraceTimer(msg.sessionId, 0);
|
|
6405
|
+
}
|
|
5983
6406
|
}
|
|
5984
6407
|
} catch {
|
|
5985
6408
|
}
|
|
5986
6409
|
},
|
|
5987
6410
|
handleWsClose: (ws) => {
|
|
6411
|
+
const pendingTimer = this.wsAuthPending.get(ws);
|
|
6412
|
+
if (pendingTimer) {
|
|
6413
|
+
clearTimeout(pendingTimer);
|
|
6414
|
+
this.wsAuthPending.delete(ws);
|
|
6415
|
+
}
|
|
6416
|
+
this.wsAuthed.delete(ws);
|
|
5988
6417
|
const clientId = this.wsToClientId.get(ws);
|
|
5989
6418
|
if (clientId) {
|
|
5990
6419
|
this.clientIdToWs.delete(clientId);
|
|
@@ -6054,6 +6483,20 @@ var StreamerServer = class {
|
|
|
6054
6483
|
this.wsHub.broadcast(payload);
|
|
6055
6484
|
}
|
|
6056
6485
|
}
|
|
6486
|
+
// M1: finalize a WebSocket auth (via ?key= at upgrade or a first-message
|
|
6487
|
+
// handshake) — register it with the hub and send the initial snapshot. Only
|
|
6488
|
+
// authed sockets reach this, so no unauthenticated client ever receives a
|
|
6489
|
+
// broadcast.
|
|
6490
|
+
// Plan: https://github.com/RonenMars/threadbase-streamer/blob/a251353bfa417bd48ce3f15086bc336a2c622629/docs/plans/2026-06-24-security-hardening.md#L40
|
|
6491
|
+
completeWsAuth(ws) {
|
|
6492
|
+
this.wsAuthed.add(ws);
|
|
6493
|
+
this.wsHub.addClient(ws);
|
|
6494
|
+
const sessions = this.sessionStore.list(this.ptyAttachedIds());
|
|
6495
|
+
ws.send(JSON.stringify({ type: "session_list", sessions }));
|
|
6496
|
+
if (this.cacheReady) {
|
|
6497
|
+
ws.send(JSON.stringify({ type: "cache_ready" }));
|
|
6498
|
+
}
|
|
6499
|
+
}
|
|
6057
6500
|
addSessionSubscriber(sessionId, ws) {
|
|
6058
6501
|
let subs = this.sessionSubscribers.get(sessionId);
|
|
6059
6502
|
if (!subs) {
|
|
@@ -6125,7 +6568,7 @@ var StreamerServer = class {
|
|
|
6125
6568
|
});
|
|
6126
6569
|
try {
|
|
6127
6570
|
this.cache = ConversationCache.open(
|
|
6128
|
-
(0,
|
|
6571
|
+
(0, import_path14.join)(this.cacheDir, "cache.db"),
|
|
6129
6572
|
this.tailSize,
|
|
6130
6573
|
void 0,
|
|
6131
6574
|
{
|
|
@@ -6151,11 +6594,11 @@ var StreamerServer = class {
|
|
|
6151
6594
|
if (this.scanProfiles && this.scanProfiles.length > 0) {
|
|
6152
6595
|
for (const profile of this.scanProfiles) {
|
|
6153
6596
|
if (profile.enabled) {
|
|
6154
|
-
this.fileWatcher.watchDirectory((0,
|
|
6597
|
+
this.fileWatcher.watchDirectory((0, import_path14.join)(profile.configDir, "projects"));
|
|
6155
6598
|
}
|
|
6156
6599
|
}
|
|
6157
6600
|
} else {
|
|
6158
|
-
this.fileWatcher.watchDirectory((0,
|
|
6601
|
+
this.fileWatcher.watchDirectory((0, import_path14.join)((0, import_os7.homedir)(), ".claude", "projects"));
|
|
6159
6602
|
}
|
|
6160
6603
|
} catch (err) {
|
|
6161
6604
|
const message = err instanceof Error ? err.message : String(err);
|
|
@@ -6340,6 +6783,9 @@ var StreamerServer = class {
|
|
|
6340
6783
|
this.ptyManager.dispose();
|
|
6341
6784
|
this.fileWatcher.dispose();
|
|
6342
6785
|
this.wsHub.dispose();
|
|
6786
|
+
for (const timer of this.wsAuthPending.values()) clearTimeout(timer);
|
|
6787
|
+
this.wsAuthPending.clear();
|
|
6788
|
+
this.wsAuthed.clear();
|
|
6343
6789
|
this.pairTokens.dispose();
|
|
6344
6790
|
if (this.dbPool) {
|
|
6345
6791
|
await this.dbPool.end();
|
|
@@ -6737,17 +7183,17 @@ var StreamerServer = class {
|
|
|
6737
7183
|
return scanner;
|
|
6738
7184
|
}
|
|
6739
7185
|
findJsonlPath(uuid) {
|
|
6740
|
-
const projectsDir = (0,
|
|
6741
|
-
if (!(0,
|
|
7186
|
+
const projectsDir = (0, import_path14.join)((0, import_os7.homedir)(), ".claude", "projects");
|
|
7187
|
+
if (!(0, import_fs13.existsSync)(projectsDir)) return null;
|
|
6742
7188
|
const filename = `${uuid}.jsonl`;
|
|
6743
|
-
for (const dir of (0,
|
|
6744
|
-
const fp = (0,
|
|
6745
|
-
if ((0,
|
|
6746
|
-
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);
|
|
6747
7193
|
try {
|
|
6748
|
-
for (const sub of (0,
|
|
6749
|
-
const subagentPath = (0,
|
|
6750
|
-
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;
|
|
6751
7197
|
}
|
|
6752
7198
|
} catch {
|
|
6753
7199
|
}
|
|
@@ -6756,7 +7202,7 @@ var StreamerServer = class {
|
|
|
6756
7202
|
}
|
|
6757
7203
|
async readCwdFromJsonl(filePath) {
|
|
6758
7204
|
return new Promise((resolve2) => {
|
|
6759
|
-
const rl = (0, import_readline.createInterface)({ input: (0,
|
|
7205
|
+
const rl = (0, import_readline.createInterface)({ input: (0, import_fs13.createReadStream)(filePath), crlfDelay: Infinity });
|
|
6760
7206
|
let found = false;
|
|
6761
7207
|
rl.on("line", (line) => {
|
|
6762
7208
|
if (found) return;
|
|
@@ -6894,7 +7340,7 @@ var StreamerServer = class {
|
|
|
6894
7340
|
if (!conv.filePath) return false;
|
|
6895
7341
|
let mtimeMs = null;
|
|
6896
7342
|
try {
|
|
6897
|
-
mtimeMs = (0,
|
|
7343
|
+
mtimeMs = (0, import_fs13.statSync)(conv.filePath).mtimeMs;
|
|
6898
7344
|
} catch {
|
|
6899
7345
|
return false;
|
|
6900
7346
|
}
|
|
@@ -7262,7 +7708,7 @@ var StreamerServer = class {
|
|
|
7262
7708
|
handleGetSession(sessionId, res) {
|
|
7263
7709
|
const session = this.sessionStore.get(sessionId, this.ptyAttachedIds());
|
|
7264
7710
|
if (session) {
|
|
7265
|
-
if (!(0,
|
|
7711
|
+
if (!(0, import_fs13.existsSync)(session.projectPath)) {
|
|
7266
7712
|
session.failureReason = `Project directory not found: ${session.projectPath}`;
|
|
7267
7713
|
}
|
|
7268
7714
|
json(res, 200, session);
|
|
@@ -7466,12 +7912,21 @@ var StreamerServer = class {
|
|
|
7466
7912
|
return;
|
|
7467
7913
|
}
|
|
7468
7914
|
this.pendingPermission.set(sessionId, gate);
|
|
7915
|
+
const safeOptions = gate.options.map((o) => {
|
|
7916
|
+
const answerKeys = sanitizeAnswerKeys(o.answerKeys);
|
|
7917
|
+
return answerKeys === void 0 ? { index: o.index, label: o.label } : { ...o, answerKeys };
|
|
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
|
+
);
|
|
7469
7924
|
this.wsHub.broadcast({
|
|
7470
7925
|
type: "permission",
|
|
7471
7926
|
sessionId,
|
|
7472
7927
|
...gate.prompt ? { prompt: gate.prompt } : {},
|
|
7473
7928
|
...gate.detail ? { detail: gate.detail } : {},
|
|
7474
|
-
options:
|
|
7929
|
+
options: safeOptions,
|
|
7475
7930
|
...gate.cursor !== void 0 ? { cursor: gate.cursor } : {}
|
|
7476
7931
|
});
|
|
7477
7932
|
}
|
|
@@ -7658,7 +8113,7 @@ var StreamerServer = class {
|
|
|
7658
8113
|
sessionStore: this.sessionStore,
|
|
7659
8114
|
// biome-ignore lint/style/noNonNullAssertion: agentClient is set when agentConfig.enabled is true
|
|
7660
8115
|
agentClient: this.agentClient,
|
|
7661
|
-
conversationsDir: this.cacheDir ? (0,
|
|
8116
|
+
conversationsDir: this.cacheDir ? (0, import_path14.join)((0, import_path14.dirname)(this.cacheDir), "conversations") : "",
|
|
7662
8117
|
agentConfig: this.agentConfig
|
|
7663
8118
|
});
|
|
7664
8119
|
json(res, result.status, result.body);
|
|
@@ -7799,9 +8254,9 @@ var StreamerServer = class {
|
|
|
7799
8254
|
// was passed to Claude via --session-id so the filename matches from the start.
|
|
7800
8255
|
watchForJsonl(sessionId, projectPath) {
|
|
7801
8256
|
const encoded = projectPath.replace(/[/\\:.]/g, "-");
|
|
7802
|
-
const projectsDir = (0,
|
|
8257
|
+
const projectsDir = (0, import_path14.join)((0, import_os7.homedir)(), ".claude", "projects", encoded);
|
|
7803
8258
|
const expectedFile = `${sessionId}.jsonl`;
|
|
7804
|
-
const filePath = (0,
|
|
8259
|
+
const filePath = (0, import_path14.join)(projectsDir, expectedFile);
|
|
7805
8260
|
const deadline = Date.now() + 12e4;
|
|
7806
8261
|
let watcher = null;
|
|
7807
8262
|
const cleanup = () => {
|
|
@@ -7819,12 +8274,12 @@ var StreamerServer = class {
|
|
|
7819
8274
|
cleanup();
|
|
7820
8275
|
return;
|
|
7821
8276
|
}
|
|
7822
|
-
let resolvedFilePath = (0,
|
|
7823
|
-
if (!resolvedFilePath && (0,
|
|
8277
|
+
let resolvedFilePath = (0, import_fs13.existsSync)(filePath) ? filePath : null;
|
|
8278
|
+
if (!resolvedFilePath && (0, import_fs13.existsSync)(projectsDir)) {
|
|
7824
8279
|
try {
|
|
7825
8280
|
const now = Date.now();
|
|
7826
|
-
const recent = (0,
|
|
7827
|
-
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);
|
|
7828
8283
|
} catch {
|
|
7829
8284
|
}
|
|
7830
8285
|
}
|
|
@@ -7833,7 +8288,7 @@ var StreamerServer = class {
|
|
|
7833
8288
|
this.sessionFileMap.set(sessionId, resolvedFilePath);
|
|
7834
8289
|
this.fileWatcher.watch(resolvedFilePath);
|
|
7835
8290
|
try {
|
|
7836
|
-
const existing = (0,
|
|
8291
|
+
const existing = (0, import_fs13.readFileSync)(resolvedFilePath, "utf8").split("\n").filter(Boolean);
|
|
7837
8292
|
if (existing.length > 0) {
|
|
7838
8293
|
this.broadcastConversationLines(sessionId, existing);
|
|
7839
8294
|
}
|
|
@@ -7856,7 +8311,7 @@ var StreamerServer = class {
|
|
|
7856
8311
|
if (this.sessionFileMap.has(sessionId)) return;
|
|
7857
8312
|
try {
|
|
7858
8313
|
require("fs").mkdirSync(projectsDir, { recursive: true });
|
|
7859
|
-
watcher = (0,
|
|
8314
|
+
watcher = (0, import_fs13.watch)(projectsDir, tryWire);
|
|
7860
8315
|
watcher.on("error", cleanup);
|
|
7861
8316
|
} catch {
|
|
7862
8317
|
}
|
|
@@ -7872,7 +8327,7 @@ var StreamerServer = class {
|
|
|
7872
8327
|
watchForCodexRollout(sessionId, projectPath) {
|
|
7873
8328
|
const deadline = Date.now() + 12e4;
|
|
7874
8329
|
const now = /* @__PURE__ */ new Date();
|
|
7875
|
-
const dateDir = (0,
|
|
8330
|
+
const dateDir = (0, import_path14.join)(
|
|
7876
8331
|
String(now.getFullYear()),
|
|
7877
8332
|
String(now.getMonth() + 1).padStart(2, "0"),
|
|
7878
8333
|
String(now.getDate()).padStart(2, "0")
|
|
@@ -7885,7 +8340,7 @@ var StreamerServer = class {
|
|
|
7885
8340
|
};
|
|
7886
8341
|
const matchesProjectPath = (candidatePath) => {
|
|
7887
8342
|
try {
|
|
7888
|
-
const firstLine = (0,
|
|
8343
|
+
const firstLine = (0, import_fs13.readFileSync)(candidatePath, "utf8").split("\n", 1)[0];
|
|
7889
8344
|
if (!firstLine) return null;
|
|
7890
8345
|
const parsed = JSON.parse(firstLine);
|
|
7891
8346
|
if (parsed?.type !== "session_meta") return null;
|
|
@@ -7913,18 +8368,18 @@ var StreamerServer = class {
|
|
|
7913
8368
|
this.sessionStore.listManaged().filter((s) => s.id !== sessionId && s.boundConversationId != null).map((s) => s.boundConversationId)
|
|
7914
8369
|
);
|
|
7915
8370
|
for (const root of this.codexRoots) {
|
|
7916
|
-
const sessionsDir = (0,
|
|
7917
|
-
if (!(0,
|
|
8371
|
+
const sessionsDir = (0, import_path14.join)(root, dateDir);
|
|
8372
|
+
if (!(0, import_fs13.existsSync)(sessionsDir)) continue;
|
|
7918
8373
|
let candidateFiles;
|
|
7919
8374
|
try {
|
|
7920
|
-
candidateFiles = (0,
|
|
8375
|
+
candidateFiles = (0, import_fs13.readdirSync)(sessionsDir).filter((f) => f.endsWith(".jsonl"));
|
|
7921
8376
|
} catch {
|
|
7922
8377
|
continue;
|
|
7923
8378
|
}
|
|
7924
8379
|
const nowMs = Date.now();
|
|
7925
|
-
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);
|
|
7926
8381
|
for (const { f } of recentCandidates) {
|
|
7927
|
-
const candidatePath = (0,
|
|
8382
|
+
const candidatePath = (0, import_path14.join)(sessionsDir, f);
|
|
7928
8383
|
const match = matchesProjectPath(candidatePath);
|
|
7929
8384
|
if (!match) continue;
|
|
7930
8385
|
if (boundElsewhere.has(match.id)) continue;
|
|
@@ -7934,7 +8389,7 @@ var StreamerServer = class {
|
|
|
7934
8389
|
this.sessionFileMap.set(sessionId, candidatePath);
|
|
7935
8390
|
this.fileWatcher.watch(candidatePath);
|
|
7936
8391
|
try {
|
|
7937
|
-
const existing = (0,
|
|
8392
|
+
const existing = (0, import_fs13.readFileSync)(candidatePath, "utf8").split("\n").filter(Boolean);
|
|
7938
8393
|
if (existing.length > 0) {
|
|
7939
8394
|
this.broadcastConversationLines(sessionId, existing);
|
|
7940
8395
|
}
|
|
@@ -8056,7 +8511,7 @@ var StreamerServer = class {
|
|
|
8056
8511
|
};
|
|
8057
8512
|
function classifyResumability(cwd) {
|
|
8058
8513
|
if (!cwd) return { resumable: true };
|
|
8059
|
-
if ((0,
|
|
8514
|
+
if ((0, import_fs13.existsSync)(cwd)) return { resumable: true };
|
|
8060
8515
|
const ranInWorktree = /\/\.worktrees\//.test(cwd) || /\/\.claude\/worktrees\//.test(cwd);
|
|
8061
8516
|
return {
|
|
8062
8517
|
resumable: false,
|