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