@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.js
CHANGED
|
@@ -164,7 +164,7 @@ function verifySignature(rawBody, signature, secret) {
|
|
|
164
164
|
}
|
|
165
165
|
}
|
|
166
166
|
function isWithinSkew(timestampHeader, skewSeconds) {
|
|
167
|
-
if (!timestampHeader) return
|
|
167
|
+
if (!timestampHeader) return true;
|
|
168
168
|
const t = Number(timestampHeader);
|
|
169
169
|
if (!Number.isFinite(t)) return false;
|
|
170
170
|
const now = Math.floor(Date.now() / 1e3);
|
|
@@ -680,13 +680,80 @@ function isProviderResumable(_provider, availabilityResumable) {
|
|
|
680
680
|
return availabilityResumable;
|
|
681
681
|
}
|
|
682
682
|
|
|
683
|
+
// src/services/questions/codexGateAnswers.ts
|
|
684
|
+
import { mkdirSync as mkdirSync2, readFileSync as readFileSync3, writeFileSync as writeFileSync2 } from "fs";
|
|
685
|
+
import { homedir as homedir3 } from "os";
|
|
686
|
+
import { dirname as dirname3, join as join5 } from "path";
|
|
687
|
+
function gateAnswersPath() {
|
|
688
|
+
const dir = process.env.THREADBASE_CONFIG_DIR ?? join5(homedir3(), ".threadbase");
|
|
689
|
+
return join5(dir, "gate-answers.json");
|
|
690
|
+
}
|
|
691
|
+
function loadGateAnswers() {
|
|
692
|
+
try {
|
|
693
|
+
const parsed = JSON.parse(readFileSync3(gateAnswersPath(), "utf-8"));
|
|
694
|
+
return parsed && typeof parsed === "object" ? parsed : {};
|
|
695
|
+
} catch {
|
|
696
|
+
return {};
|
|
697
|
+
}
|
|
698
|
+
}
|
|
699
|
+
function saveGateAnswer(key, value) {
|
|
700
|
+
const path = gateAnswersPath();
|
|
701
|
+
mkdirSync2(dirname3(path), { recursive: true });
|
|
702
|
+
writeFileSync2(path, `${JSON.stringify({ ...loadGateAnswers(), [key]: value }, null, 2)}
|
|
703
|
+
`);
|
|
704
|
+
}
|
|
705
|
+
function rememberedGateDigit(gate) {
|
|
706
|
+
const answers = loadGateAnswers();
|
|
707
|
+
if (gate === "hooks") {
|
|
708
|
+
if (answers.codexHooksGate === "trust_all") return "2";
|
|
709
|
+
if (answers.codexHooksGate === "continue_untrusted") return "3";
|
|
710
|
+
return null;
|
|
711
|
+
}
|
|
712
|
+
return answers.codexTrustGate === "yes" ? "1" : null;
|
|
713
|
+
}
|
|
714
|
+
|
|
715
|
+
// src/utils/debounce.ts
|
|
716
|
+
function debounce(fn, waitMs) {
|
|
717
|
+
let timer = null;
|
|
718
|
+
let lastArgs = null;
|
|
719
|
+
const run2 = () => {
|
|
720
|
+
timer = null;
|
|
721
|
+
if (lastArgs) {
|
|
722
|
+
const args = lastArgs;
|
|
723
|
+
lastArgs = null;
|
|
724
|
+
fn(...args);
|
|
725
|
+
}
|
|
726
|
+
};
|
|
727
|
+
const debounced = (...args) => {
|
|
728
|
+
lastArgs = args;
|
|
729
|
+
if (timer) clearTimeout(timer);
|
|
730
|
+
timer = setTimeout(run2, waitMs);
|
|
731
|
+
};
|
|
732
|
+
debounced.cancel = () => {
|
|
733
|
+
if (timer) clearTimeout(timer);
|
|
734
|
+
timer = null;
|
|
735
|
+
lastArgs = null;
|
|
736
|
+
};
|
|
737
|
+
debounced.flush = () => {
|
|
738
|
+
if (timer) {
|
|
739
|
+
clearTimeout(timer);
|
|
740
|
+
run2();
|
|
741
|
+
}
|
|
742
|
+
};
|
|
743
|
+
return debounced;
|
|
744
|
+
}
|
|
745
|
+
|
|
683
746
|
// src/codex-pty-runner.ts
|
|
684
747
|
var OUTPUT_BUFFER_MAX = 65536;
|
|
748
|
+
var INPUT_HISTORY_MAX = 50;
|
|
685
749
|
var PTY_COLS = 120;
|
|
686
750
|
var PTY_ROWS = 40;
|
|
687
751
|
var SCREEN_SCROLLBACK = 1e3;
|
|
688
752
|
var CODEX_PROMPT_READY_TEXT = "Ready";
|
|
689
753
|
var CODEX_TRUST_GATE_REGEX = /trust the contents/i;
|
|
754
|
+
var CODEX_HOOKS_GATE_REGEX = /hooks need review/i;
|
|
755
|
+
var QUIET_DETECT_MS = 500;
|
|
756
|
+
var CODEX_READY_FALLBACK_MS = 8e3;
|
|
690
757
|
var SUBMIT_BYTES = "\r";
|
|
691
758
|
var CODEX_SUBMIT_DELAY_MS = 16;
|
|
692
759
|
function digestBytes(s) {
|
|
@@ -694,6 +761,40 @@ function digestBytes(s) {
|
|
|
694
761
|
if (escaped.length <= 200) return escaped;
|
|
695
762
|
return `${escaped.slice(0, 100)}\u2026[${escaped.length - 200}B omitted]\u2026${escaped.slice(-100)}`;
|
|
696
763
|
}
|
|
764
|
+
function gateCard(gate, lines) {
|
|
765
|
+
if (gate === "hooks") {
|
|
766
|
+
const countLine = lines.find((l) => /new or changed/i.test(l))?.trim();
|
|
767
|
+
return {
|
|
768
|
+
prompt: [
|
|
769
|
+
"Hooks need review",
|
|
770
|
+
countLine,
|
|
771
|
+
"Hooks can run outside the sandbox after you trust them."
|
|
772
|
+
].filter(Boolean).join(" \u2014 "),
|
|
773
|
+
options: [
|
|
774
|
+
{ index: 2, label: "Trust all and continue", answerKeys: "2\r" },
|
|
775
|
+
{ index: 3, label: "Continue without trusting (hooks won't run)", answerKeys: "3\r" },
|
|
776
|
+
{
|
|
777
|
+
index: 4,
|
|
778
|
+
label: "Trust all and continue (remember for all projects)",
|
|
779
|
+
answerKeys: "4\r"
|
|
780
|
+
},
|
|
781
|
+
{
|
|
782
|
+
index: 5,
|
|
783
|
+
label: "Continue without trusting (remember for all projects)",
|
|
784
|
+
answerKeys: "5\r"
|
|
785
|
+
}
|
|
786
|
+
]
|
|
787
|
+
};
|
|
788
|
+
}
|
|
789
|
+
return {
|
|
790
|
+
prompt: lines.find((l) => CODEX_TRUST_GATE_REGEX.test(l))?.trim() ?? "Do you trust the contents of this directory?",
|
|
791
|
+
options: [
|
|
792
|
+
{ index: 1, label: "Yes, continue", answerKeys: "1\r" },
|
|
793
|
+
{ index: 2, label: "No, quit", answerKeys: "2\r" },
|
|
794
|
+
{ index: 3, label: "Yes, continue (remember for all projects)", answerKeys: "3\r" }
|
|
795
|
+
]
|
|
796
|
+
};
|
|
797
|
+
}
|
|
697
798
|
var pty = null;
|
|
698
799
|
async function loadPty() {
|
|
699
800
|
if (pty) return pty;
|
|
@@ -720,11 +821,12 @@ var CodexPtyRunner = class {
|
|
|
720
821
|
onOutput;
|
|
721
822
|
onStatusChange;
|
|
722
823
|
onReady;
|
|
723
|
-
//
|
|
724
|
-
//
|
|
824
|
+
// Broadcasts Codex's blocking startup gates (directory trust, hooks review)
|
|
825
|
+
// as question cards; null dismisses the card once the gate leaves the screen.
|
|
725
826
|
onPermissionChange;
|
|
726
827
|
onLiveQuestion;
|
|
727
828
|
onLiveQuestionGone;
|
|
829
|
+
onUserMessage;
|
|
728
830
|
log;
|
|
729
831
|
// Tracks sessions whose PTY has spawned but Codex hasn't yet reached its
|
|
730
832
|
// "Ready" status bar — i.e. onReady hasn't fired.
|
|
@@ -732,8 +834,18 @@ var CodexPtyRunner = class {
|
|
|
732
834
|
// Inputs received via sendInput() while the session was still pendingReady.
|
|
733
835
|
// Flushed in arrival order once Codex reaches Ready.
|
|
734
836
|
queuedInputs = /* @__PURE__ */ new Map();
|
|
735
|
-
//
|
|
736
|
-
|
|
837
|
+
// Gate currently on a session's screen (card broadcast, unanswered). While
|
|
838
|
+
// set, queued-input flushes are held — a flushed digit would CONFIRM a
|
|
839
|
+
// dialog option — and sendKeys() intercepts remember-variant digits.
|
|
840
|
+
openGate = /* @__PURE__ */ new Map();
|
|
841
|
+
// `${sessionId}:${gate}` once a gate has been actioned (auto-answered or
|
|
842
|
+
// card broadcast) — dedupes repaints of the same dialog.
|
|
843
|
+
gateActioned = /* @__PURE__ */ new Set();
|
|
844
|
+
// Per-session trailing debounce re-armed on every chunk; on quiet, re-runs
|
|
845
|
+
// screen detection so a blocked/truncated boot still reaches ready.
|
|
846
|
+
quietCheckers = /* @__PURE__ */ new Map();
|
|
847
|
+
// Per-session flat backstop from spawn (CODEX_READY_FALLBACK_MS).
|
|
848
|
+
readyFallbackTimers = /* @__PURE__ */ new Map();
|
|
737
849
|
// In-flight start()/startFresh() calls keyed by sessionId. A second
|
|
738
850
|
// concurrent resume for the same session (double-tap, client retry) awaits
|
|
739
851
|
// the first call's promise instead of spawning a duplicate PTY (CRITICAL #3).
|
|
@@ -745,6 +857,7 @@ var CodexPtyRunner = class {
|
|
|
745
857
|
this.onPermissionChange = options.onPermissionChange;
|
|
746
858
|
this.onLiveQuestion = options.onLiveQuestion;
|
|
747
859
|
this.onLiveQuestionGone = options.onLiveQuestionGone;
|
|
860
|
+
this.onUserMessage = options.onUserMessage;
|
|
748
861
|
this.log = options.logger ?? getLogger("codex-pty");
|
|
749
862
|
}
|
|
750
863
|
// Resume an existing Codex session. sessionId is the Codex-persisted
|
|
@@ -788,10 +901,12 @@ var CodexPtyRunner = class {
|
|
|
788
901
|
lastOutput: "",
|
|
789
902
|
process: proc,
|
|
790
903
|
outputBuffer: Buffer.alloc(0),
|
|
791
|
-
screen: createScreen()
|
|
904
|
+
screen: createScreen(),
|
|
905
|
+
inputHistory: []
|
|
792
906
|
};
|
|
793
907
|
this.sessions.set(sessionId, session);
|
|
794
908
|
this.pendingReady.add(sessionId);
|
|
909
|
+
this.armReadyFallback(sessionId);
|
|
795
910
|
proc.onData((data) => {
|
|
796
911
|
this.handleOutput(sessionId, data);
|
|
797
912
|
});
|
|
@@ -833,10 +948,12 @@ var CodexPtyRunner = class {
|
|
|
833
948
|
lastOutput: "",
|
|
834
949
|
process: proc,
|
|
835
950
|
outputBuffer: Buffer.alloc(0),
|
|
836
|
-
screen: createScreen()
|
|
951
|
+
screen: createScreen(),
|
|
952
|
+
inputHistory: []
|
|
837
953
|
};
|
|
838
954
|
this.sessions.set(sessionId, session);
|
|
839
955
|
this.pendingReady.add(sessionId);
|
|
956
|
+
this.armReadyFallback(sessionId);
|
|
840
957
|
proc.onData((data) => {
|
|
841
958
|
this.handleOutput(sessionId, data);
|
|
842
959
|
});
|
|
@@ -846,6 +963,21 @@ var CodexPtyRunner = class {
|
|
|
846
963
|
});
|
|
847
964
|
return toPublicSession(session);
|
|
848
965
|
}
|
|
966
|
+
// Flat backstop: if neither the "Ready" marker nor the quiet-checker settled
|
|
967
|
+
// the session within CODEX_READY_FALLBACK_MS of spawn, mark it ready anyway
|
|
968
|
+
// so start requests resolve and mobile can watch the boot live. unref() so a
|
|
969
|
+
// pending timer never holds the process open.
|
|
970
|
+
armReadyFallback(sessionId) {
|
|
971
|
+
const timer = setTimeout(() => {
|
|
972
|
+
this.readyFallbackTimers.delete(sessionId);
|
|
973
|
+
const session = this.sessions.get(sessionId);
|
|
974
|
+
if (session?.status === "running" && this.pendingReady.has(sessionId)) {
|
|
975
|
+
this.markReady(sessionId, session, "fallback:timeout");
|
|
976
|
+
}
|
|
977
|
+
}, CODEX_READY_FALLBACK_MS);
|
|
978
|
+
timer.unref?.();
|
|
979
|
+
this.readyFallbackTimers.set(sessionId, timer);
|
|
980
|
+
}
|
|
849
981
|
// Write raw key bytes directly to the PTY, same as PTYManager.sendKeys.
|
|
850
982
|
sendKeys(sessionId, keys) {
|
|
851
983
|
const session = this.sessions.get(sessionId);
|
|
@@ -857,13 +989,47 @@ var CodexPtyRunner = class {
|
|
|
857
989
|
session.status = "running";
|
|
858
990
|
this.onStatusChange?.(toPublicSession(session));
|
|
859
991
|
}
|
|
992
|
+
const gate = this.openGate.get(sessionId);
|
|
993
|
+
const digit = gate ? /^([0-9])\r?$/.exec(keys)?.[1] : void 0;
|
|
994
|
+
const out = gate && digit ? this.resolveGateAnswer(sessionId, gate, digit) : keys;
|
|
860
995
|
this.log.info(
|
|
861
|
-
`[codex.keys.write] ${sessionId.slice(0, 8)} bytes=${
|
|
862
|
-
{ event: "codex.keys_write", sessionId, byteLen:
|
|
996
|
+
`[codex.keys.write] ${sessionId.slice(0, 8)} bytes=${out.length} digest=${digestBytes(out)}`,
|
|
997
|
+
{ event: "codex.keys_write", sessionId, byteLen: out.length }
|
|
863
998
|
);
|
|
864
|
-
session.process.write(
|
|
999
|
+
session.process.write(out);
|
|
865
1000
|
session.lastActivityAt = /* @__PURE__ */ new Date();
|
|
866
1001
|
}
|
|
1002
|
+
// Map a gate-card digit to the PTY bytes that answer the real dialog,
|
|
1003
|
+
// persisting the choice when the digit was a synthetic "remember for all
|
|
1004
|
+
// projects" option (those numbers don't exist on the actual dialog and must
|
|
1005
|
+
// never reach codex). The trailing \r mobile sends is dropped: a digit alone
|
|
1006
|
+
// selects AND confirms (live-probe verified), and a stray Enter would land
|
|
1007
|
+
// on whatever screen follows.
|
|
1008
|
+
resolveGateAnswer(sessionId, gate, digit) {
|
|
1009
|
+
let real = digit;
|
|
1010
|
+
let remembered = false;
|
|
1011
|
+
if (gate === "hooks" && digit === "4") {
|
|
1012
|
+
saveGateAnswer("codexHooksGate", "trust_all");
|
|
1013
|
+
real = "2";
|
|
1014
|
+
remembered = true;
|
|
1015
|
+
} else if (gate === "hooks" && digit === "5") {
|
|
1016
|
+
saveGateAnswer("codexHooksGate", "continue_untrusted");
|
|
1017
|
+
real = "3";
|
|
1018
|
+
remembered = true;
|
|
1019
|
+
} else if (gate === "trust" && digit === "3") {
|
|
1020
|
+
saveGateAnswer("codexTrustGate", "yes");
|
|
1021
|
+
real = "1";
|
|
1022
|
+
remembered = true;
|
|
1023
|
+
}
|
|
1024
|
+
this.log.info(`[codex.gate_answer] ${sessionId.slice(0, 8)} ${gate} digit=${real}`, {
|
|
1025
|
+
event: "codex.gate_answer",
|
|
1026
|
+
sessionId,
|
|
1027
|
+
gate,
|
|
1028
|
+
digit: real,
|
|
1029
|
+
remembered
|
|
1030
|
+
});
|
|
1031
|
+
return real;
|
|
1032
|
+
}
|
|
867
1033
|
sendInput(sessionId, input) {
|
|
868
1034
|
const session = this.sessions.get(sessionId);
|
|
869
1035
|
if (!session) throw new Error(`Session not found: ${sessionId}`);
|
|
@@ -901,6 +1067,7 @@ var CodexPtyRunner = class {
|
|
|
901
1067
|
// confirmed Codex accepts plain keystrokes), then submit \r after a short
|
|
902
1068
|
// delay so Codex's TUI gets an event-loop tick to process the input first.
|
|
903
1069
|
writeSubmit(sessionId, session, input, path, promptCount) {
|
|
1070
|
+
this.recordUserMessage(session, input);
|
|
904
1071
|
this.log.info(
|
|
905
1072
|
`[codex.input.write] ${sessionId.slice(0, 8)} promptCount=${promptCount} bytes=${input.length} digest=${digestBytes(input)}`,
|
|
906
1073
|
{
|
|
@@ -933,8 +1100,12 @@ var CodexPtyRunner = class {
|
|
|
933
1100
|
}, CODEX_SUBMIT_DELAY_MS);
|
|
934
1101
|
}
|
|
935
1102
|
// Drain any inputs sent while the session was still pendingReady, writing
|
|
936
|
-
// them in arrival order now that Codex is Ready.
|
|
1103
|
+
// them in arrival order now that Codex is Ready. No-op while a gate dialog
|
|
1104
|
+
// is open (a flushed digit would confirm a dialog option) or while still
|
|
1105
|
+
// pendingReady (markReady drains it) — the gate-close path re-drives it for
|
|
1106
|
+
// the ready-with-gate-open case.
|
|
937
1107
|
flushQueuedInputs(sessionId) {
|
|
1108
|
+
if (this.openGate.has(sessionId) || this.pendingReady.has(sessionId)) return;
|
|
938
1109
|
const queue = this.queuedInputs.get(sessionId);
|
|
939
1110
|
if (!queue || queue.length === 0) return;
|
|
940
1111
|
this.queuedInputs.delete(sessionId);
|
|
@@ -979,7 +1150,7 @@ var CodexPtyRunner = class {
|
|
|
979
1150
|
if (!session) return;
|
|
980
1151
|
this.pendingReady.delete(sessionId);
|
|
981
1152
|
this.queuedInputs.delete(sessionId);
|
|
982
|
-
this.
|
|
1153
|
+
this.clearSessionDetectors(sessionId);
|
|
983
1154
|
try {
|
|
984
1155
|
session.process.kill("SIGINT");
|
|
985
1156
|
} catch {
|
|
@@ -990,6 +1161,21 @@ var CodexPtyRunner = class {
|
|
|
990
1161
|
this.sessions.delete(sessionId);
|
|
991
1162
|
this.onStatusChange?.(toPublicSession(session));
|
|
992
1163
|
}
|
|
1164
|
+
// Drop a session's detection state: quiet-checker, ready-fallback timer,
|
|
1165
|
+
// gate bookkeeping — and dismiss a still-open gate card so mobile doesn't
|
|
1166
|
+
// keep rendering a question for a dead PTY.
|
|
1167
|
+
clearSessionDetectors(sessionId) {
|
|
1168
|
+
this.quietCheckers.get(sessionId)?.cancel();
|
|
1169
|
+
this.quietCheckers.delete(sessionId);
|
|
1170
|
+
const timer = this.readyFallbackTimers.get(sessionId);
|
|
1171
|
+
if (timer) clearTimeout(timer);
|
|
1172
|
+
this.readyFallbackTimers.delete(sessionId);
|
|
1173
|
+
if (this.openGate.delete(sessionId)) {
|
|
1174
|
+
this.onPermissionChange?.(sessionId, null);
|
|
1175
|
+
}
|
|
1176
|
+
this.gateActioned.delete(`${sessionId}:hooks`);
|
|
1177
|
+
this.gateActioned.delete(`${sessionId}:trust`);
|
|
1178
|
+
}
|
|
993
1179
|
getOutput(sessionId) {
|
|
994
1180
|
const session = this.sessions.get(sessionId);
|
|
995
1181
|
if (!session) throw new Error(`Session not found: ${sessionId}`);
|
|
@@ -1011,6 +1197,19 @@ var CodexPtyRunner = class {
|
|
|
1011
1197
|
}
|
|
1012
1198
|
return lines.slice(-maxLines);
|
|
1013
1199
|
}
|
|
1200
|
+
getInputHistory(sessionId) {
|
|
1201
|
+
return this.sessions.get(sessionId)?.inputHistory ?? [];
|
|
1202
|
+
}
|
|
1203
|
+
// Record a submitted user message as ground truth and fire onUserMessage.
|
|
1204
|
+
// Called from writeSubmit (direct and flush paths) — never from sendKeys.
|
|
1205
|
+
recordUserMessage(session, text) {
|
|
1206
|
+
const ts = Date.now();
|
|
1207
|
+
session.inputHistory.push({ text, ts });
|
|
1208
|
+
if (session.inputHistory.length > INPUT_HISTORY_MAX) {
|
|
1209
|
+
session.inputHistory.shift();
|
|
1210
|
+
}
|
|
1211
|
+
this.onUserMessage?.(session.id, text, ts);
|
|
1212
|
+
}
|
|
1014
1213
|
getSession(sessionId) {
|
|
1015
1214
|
const session = this.sessions.get(sessionId);
|
|
1016
1215
|
return session ? toPublicSession(session) : null;
|
|
@@ -1029,10 +1228,19 @@ var CodexPtyRunner = class {
|
|
|
1029
1228
|
}
|
|
1030
1229
|
session.screen.dispose();
|
|
1031
1230
|
}
|
|
1231
|
+
for (const sessionId of Array.from(this.quietCheckers.keys())) {
|
|
1232
|
+
this.clearSessionDetectors(sessionId);
|
|
1233
|
+
}
|
|
1234
|
+
for (const timer of this.readyFallbackTimers.values()) {
|
|
1235
|
+
clearTimeout(timer);
|
|
1236
|
+
}
|
|
1032
1237
|
this.sessions.clear();
|
|
1033
1238
|
this.pendingReady.clear();
|
|
1034
1239
|
this.queuedInputs.clear();
|
|
1035
|
-
this.
|
|
1240
|
+
this.openGate.clear();
|
|
1241
|
+
this.gateActioned.clear();
|
|
1242
|
+
this.quietCheckers.clear();
|
|
1243
|
+
this.readyFallbackTimers.clear();
|
|
1036
1244
|
}
|
|
1037
1245
|
handleOutput(sessionId, data) {
|
|
1038
1246
|
const session = this.sessions.get(sessionId);
|
|
@@ -1047,44 +1255,92 @@ var CodexPtyRunner = class {
|
|
|
1047
1255
|
session.screen.write(data);
|
|
1048
1256
|
session.lastOutput = stripAnsi(data);
|
|
1049
1257
|
this.onOutput?.(sessionId, data);
|
|
1050
|
-
this.
|
|
1258
|
+
this.detectScreenState(sessionId, "chunk").catch((err) => {
|
|
1051
1259
|
this.log.warn("[codex.ready_detect] failed", {
|
|
1052
1260
|
event: "codex.ready_detect_failed",
|
|
1053
1261
|
sessionId,
|
|
1054
1262
|
err
|
|
1055
1263
|
});
|
|
1056
1264
|
});
|
|
1265
|
+
let quiet = this.quietCheckers.get(sessionId);
|
|
1266
|
+
if (!quiet) {
|
|
1267
|
+
quiet = debounce(() => {
|
|
1268
|
+
this.detectScreenState(sessionId, "quiet").catch((err) => {
|
|
1269
|
+
this.log.warn("[codex.ready_detect] failed", {
|
|
1270
|
+
event: "codex.ready_detect_failed",
|
|
1271
|
+
sessionId,
|
|
1272
|
+
err
|
|
1273
|
+
});
|
|
1274
|
+
});
|
|
1275
|
+
}, QUIET_DETECT_MS);
|
|
1276
|
+
this.quietCheckers.set(sessionId, quiet);
|
|
1277
|
+
}
|
|
1278
|
+
quiet();
|
|
1057
1279
|
}
|
|
1058
|
-
// Renders the session's headless screen and
|
|
1059
|
-
//
|
|
1060
|
-
//
|
|
1061
|
-
//
|
|
1062
|
-
//
|
|
1063
|
-
|
|
1064
|
-
|
|
1280
|
+
// Renders the session's headless screen and drives both detections:
|
|
1281
|
+
// - Gates (directory trust, hooks review) — checked on EVERY pass,
|
|
1282
|
+
// independent of pendingReady, so a gate appearing after ready is still
|
|
1283
|
+
// surfaced and a gate leaving the screen closes its card.
|
|
1284
|
+
// - Readiness — the "Ready" status-bar marker while pendingReady, plus the
|
|
1285
|
+
// quiet path: after QUIET_DETECT_MS of PTY silence a still-pending
|
|
1286
|
+
// session is marked ready anyway (`›` alone is NOT a marker — Phase 0 —
|
|
1287
|
+
// but a quiet boot screen is more useful to the user live than a
|
|
1288
|
+
// spinner, and "Ready" may be truncated off the 120-col status bar).
|
|
1289
|
+
async detectScreenState(sessionId, trigger) {
|
|
1290
|
+
const session = this.sessions.get(sessionId);
|
|
1291
|
+
if (!session || session.status === "idle") return;
|
|
1065
1292
|
const lines = await this.getOutputLines(sessionId, PTY_ROWS);
|
|
1066
1293
|
const screenText = lines.join("\n");
|
|
1067
|
-
|
|
1068
|
-
|
|
1069
|
-
|
|
1070
|
-
|
|
1071
|
-
|
|
1072
|
-
|
|
1073
|
-
});
|
|
1074
|
-
session.process.write("\r");
|
|
1075
|
-
}
|
|
1076
|
-
return;
|
|
1294
|
+
const gate = CODEX_HOOKS_GATE_REGEX.test(screenText) ? "hooks" : CODEX_TRUST_GATE_REGEX.test(screenText) ? "trust" : null;
|
|
1295
|
+
if (gate) {
|
|
1296
|
+
this.handleGate(sessionId, session, gate, lines);
|
|
1297
|
+
} else if (this.openGate.delete(sessionId)) {
|
|
1298
|
+
this.onPermissionChange?.(sessionId, null);
|
|
1299
|
+
this.flushQueuedInputs(sessionId);
|
|
1077
1300
|
}
|
|
1301
|
+
if (session.status !== "running" || !this.pendingReady.has(sessionId)) return;
|
|
1078
1302
|
const lastNonBlank2 = [...lines].reverse().find((l) => l.trim() !== "") ?? "";
|
|
1079
|
-
if (
|
|
1080
|
-
|
|
1303
|
+
if (lastNonBlank2.includes(CODEX_PROMPT_READY_TEXT)) {
|
|
1304
|
+
this.markReady(sessionId, session, `marker:${CODEX_PROMPT_READY_TEXT}`);
|
|
1305
|
+
} else if (trigger === "quiet") {
|
|
1306
|
+
this.markReady(sessionId, session, "quiet:timeout");
|
|
1307
|
+
}
|
|
1081
1308
|
}
|
|
1082
|
-
|
|
1309
|
+
// Answer a gate from the persisted remember-store, or surface it as a
|
|
1310
|
+
// question card over the permission transport. Actioned once per session and
|
|
1311
|
+
// gate type — repaints of the same dialog neither re-write nor re-broadcast.
|
|
1312
|
+
handleGate(sessionId, session, gate, lines) {
|
|
1313
|
+
const key = `${sessionId}:${gate}`;
|
|
1314
|
+
if (this.gateActioned.has(key)) return;
|
|
1315
|
+
this.gateActioned.add(key);
|
|
1316
|
+
const remembered = rememberedGateDigit(gate);
|
|
1317
|
+
if (remembered) {
|
|
1318
|
+
this.log.info(`[codex.gate_auto_answer] ${sessionId.slice(0, 8)} ${gate} \u2192 ${remembered}`, {
|
|
1319
|
+
event: "codex.gate_auto_answer",
|
|
1320
|
+
sessionId,
|
|
1321
|
+
gate,
|
|
1322
|
+
digit: remembered
|
|
1323
|
+
});
|
|
1324
|
+
session.process.write(remembered);
|
|
1325
|
+
return;
|
|
1326
|
+
}
|
|
1327
|
+
this.openGate.set(sessionId, gate);
|
|
1328
|
+
const card = gateCard(gate, lines);
|
|
1329
|
+
this.log.info(`[codex.gate_prompt] ${sessionId.slice(0, 8)} ${gate}`, {
|
|
1330
|
+
event: "codex.gate_prompt",
|
|
1331
|
+
sessionId,
|
|
1332
|
+
gate,
|
|
1333
|
+
prompt: card.prompt
|
|
1334
|
+
});
|
|
1335
|
+
this.onPermissionChange?.(sessionId, card);
|
|
1336
|
+
}
|
|
1337
|
+
markReady(sessionId, session, reason) {
|
|
1083
1338
|
session.lastActivityAt = /* @__PURE__ */ new Date();
|
|
1084
1339
|
session.status = "waiting_input";
|
|
1085
|
-
this.log.info(`[codex.ready] ${sessionId.slice(0, 8)}`, {
|
|
1340
|
+
this.log.info(`[codex.ready] ${sessionId.slice(0, 8)} ${reason}`, {
|
|
1086
1341
|
event: "codex.ready",
|
|
1087
|
-
sessionId
|
|
1342
|
+
sessionId,
|
|
1343
|
+
reason
|
|
1088
1344
|
});
|
|
1089
1345
|
this.onStatusChange?.(toPublicSession(session));
|
|
1090
1346
|
if (this.pendingReady.has(sessionId)) {
|
|
@@ -1110,7 +1366,7 @@ var CodexPtyRunner = class {
|
|
|
1110
1366
|
session.screen.dispose();
|
|
1111
1367
|
this.sessions.delete(sessionId);
|
|
1112
1368
|
this.queuedInputs.delete(sessionId);
|
|
1113
|
-
this.
|
|
1369
|
+
this.clearSessionDetectors(sessionId);
|
|
1114
1370
|
}
|
|
1115
1371
|
};
|
|
1116
1372
|
function toPublicSession(s) {
|
|
@@ -1293,9 +1549,15 @@ function detectShellPrompt(lines) {
|
|
|
1293
1549
|
]
|
|
1294
1550
|
};
|
|
1295
1551
|
}
|
|
1296
|
-
|
|
1552
|
+
const lastNumberedIdx = (() => {
|
|
1553
|
+
for (let i = last.idx; i >= 0; i--) {
|
|
1554
|
+
if (NUMBERED_RE.test(lines[i])) return i;
|
|
1555
|
+
}
|
|
1556
|
+
return -1;
|
|
1557
|
+
})();
|
|
1558
|
+
if (lastNumberedIdx >= 0) {
|
|
1297
1559
|
const options = [];
|
|
1298
|
-
for (let i = 0; i <=
|
|
1560
|
+
for (let i = 0; i <= lastNumberedIdx; i++) {
|
|
1299
1561
|
const m = NUMBERED_RE.exec(lines[i]);
|
|
1300
1562
|
if (!m) continue;
|
|
1301
1563
|
const num = Number.parseInt(m[1], 10);
|
|
@@ -1323,45 +1585,15 @@ function detectShellPrompt(lines) {
|
|
|
1323
1585
|
return null;
|
|
1324
1586
|
}
|
|
1325
1587
|
|
|
1326
|
-
// src/utils/debounce.ts
|
|
1327
|
-
function debounce(fn, waitMs) {
|
|
1328
|
-
let timer = null;
|
|
1329
|
-
let lastArgs = null;
|
|
1330
|
-
const run2 = () => {
|
|
1331
|
-
timer = null;
|
|
1332
|
-
if (lastArgs) {
|
|
1333
|
-
const args = lastArgs;
|
|
1334
|
-
lastArgs = null;
|
|
1335
|
-
fn(...args);
|
|
1336
|
-
}
|
|
1337
|
-
};
|
|
1338
|
-
const debounced = (...args) => {
|
|
1339
|
-
lastArgs = args;
|
|
1340
|
-
if (timer) clearTimeout(timer);
|
|
1341
|
-
timer = setTimeout(run2, waitMs);
|
|
1342
|
-
};
|
|
1343
|
-
debounced.cancel = () => {
|
|
1344
|
-
if (timer) clearTimeout(timer);
|
|
1345
|
-
timer = null;
|
|
1346
|
-
lastArgs = null;
|
|
1347
|
-
};
|
|
1348
|
-
debounced.flush = () => {
|
|
1349
|
-
if (timer) {
|
|
1350
|
-
clearTimeout(timer);
|
|
1351
|
-
run2();
|
|
1352
|
-
}
|
|
1353
|
-
};
|
|
1354
|
-
return debounced;
|
|
1355
|
-
}
|
|
1356
|
-
|
|
1357
1588
|
// src/pty-manager.ts
|
|
1358
1589
|
var OUTPUT_BUFFER_MAX2 = 65536;
|
|
1590
|
+
var INPUT_HISTORY_MAX2 = 50;
|
|
1359
1591
|
var PTY_COLS2 = 120;
|
|
1360
1592
|
var PTY_ROWS2 = 40;
|
|
1361
1593
|
var SCREEN_SCROLLBACK2 = 1e3;
|
|
1362
1594
|
var CLAUDE_PROMPT_MARKERS = ["\u256D", "\u276F"];
|
|
1363
1595
|
var PROMPT_MARKER_FALLBACK_MS = 1e4;
|
|
1364
|
-
var
|
|
1596
|
+
var QUIET_DETECT_MS2 = 500;
|
|
1365
1597
|
function buildPasteBytes(input) {
|
|
1366
1598
|
return `\x1B[200~${input}\x1B[201~`;
|
|
1367
1599
|
}
|
|
@@ -1414,6 +1646,7 @@ var PTYManager = class {
|
|
|
1414
1646
|
onPermissionChange;
|
|
1415
1647
|
onLiveQuestion;
|
|
1416
1648
|
onLiveQuestionGone;
|
|
1649
|
+
onUserMessage;
|
|
1417
1650
|
// Per-session permission-gate state. True between an OSC 777 (gate open) and
|
|
1418
1651
|
// the next prompt-ready without a fresh 777 (gate closed). Prevents
|
|
1419
1652
|
// re-broadcasting open/close on every chunk.
|
|
@@ -1459,6 +1692,7 @@ var PTYManager = class {
|
|
|
1459
1692
|
this.onPermissionChange = options.onPermissionChange;
|
|
1460
1693
|
this.onLiveQuestion = options.onLiveQuestion;
|
|
1461
1694
|
this.onLiveQuestionGone = options.onLiveQuestionGone;
|
|
1695
|
+
this.onUserMessage = options.onUserMessage;
|
|
1462
1696
|
this.log = options.logger ?? getLogger("pty");
|
|
1463
1697
|
}
|
|
1464
1698
|
// Resume an existing Claude conversation. sessionId is the JSONL UUID.
|
|
@@ -1521,7 +1755,8 @@ var PTYManager = class {
|
|
|
1521
1755
|
lastOutput: "",
|
|
1522
1756
|
process: proc,
|
|
1523
1757
|
outputBuffer: Buffer.alloc(0),
|
|
1524
|
-
screen: createScreen2()
|
|
1758
|
+
screen: createScreen2(),
|
|
1759
|
+
inputHistory: []
|
|
1525
1760
|
};
|
|
1526
1761
|
this.sessions.set(sessionId, session);
|
|
1527
1762
|
this.pendingReady.add(sessionId);
|
|
@@ -1572,7 +1807,8 @@ var PTYManager = class {
|
|
|
1572
1807
|
lastOutput: "",
|
|
1573
1808
|
process: proc,
|
|
1574
1809
|
outputBuffer: Buffer.alloc(0),
|
|
1575
|
-
screen: createScreen2()
|
|
1810
|
+
screen: createScreen2(),
|
|
1811
|
+
inputHistory: []
|
|
1576
1812
|
};
|
|
1577
1813
|
this.sessions.set(sessionId, session);
|
|
1578
1814
|
this.pendingReady.add(sessionId);
|
|
@@ -1652,6 +1888,7 @@ var PTYManager = class {
|
|
|
1652
1888
|
// step gives the TUI as many extra ticks as it needs, capped at
|
|
1653
1889
|
// SUBMIT_MAX_WAIT_MS so a silent/wedged PTY still gets its \r eventually.
|
|
1654
1890
|
writeSubmit(sessionId, session, input, path, promptCount) {
|
|
1891
|
+
this.recordUserMessage(session, input);
|
|
1655
1892
|
const pasteBytes = buildPasteBytes(input);
|
|
1656
1893
|
this.log.info(
|
|
1657
1894
|
`[pty.input.write] ${sessionId.slice(0, 8)} promptCount=${promptCount} bytes=${pasteBytes.length} digest=${digestBytes2(pasteBytes)}`,
|
|
@@ -1782,6 +2019,20 @@ var PTYManager = class {
|
|
|
1782
2019
|
}
|
|
1783
2020
|
return lines.slice(-maxLines);
|
|
1784
2021
|
}
|
|
2022
|
+
getInputHistory(sessionId) {
|
|
2023
|
+
return this.sessions.get(sessionId)?.inputHistory ?? [];
|
|
2024
|
+
}
|
|
2025
|
+
// Record a submitted user message as ground truth and fire onUserMessage.
|
|
2026
|
+
// Called from writeSubmit (both direct and flush paths) — never from
|
|
2027
|
+
// sendKeys, so raw keystrokes aren't logged as messages.
|
|
2028
|
+
recordUserMessage(session, text) {
|
|
2029
|
+
const ts = Date.now();
|
|
2030
|
+
session.inputHistory.push({ text, ts });
|
|
2031
|
+
if (session.inputHistory.length > INPUT_HISTORY_MAX2) {
|
|
2032
|
+
session.inputHistory.shift();
|
|
2033
|
+
}
|
|
2034
|
+
this.onUserMessage?.(session.id, text, ts);
|
|
2035
|
+
}
|
|
1785
2036
|
getSession(sessionId) {
|
|
1786
2037
|
const session = this.sessions.get(sessionId);
|
|
1787
2038
|
return session ? toPublicSession2(session) : null;
|
|
@@ -1861,7 +2112,7 @@ var PTYManager = class {
|
|
|
1861
2112
|
});
|
|
1862
2113
|
let quiet = this.quietCheckers.get(sessionId);
|
|
1863
2114
|
if (!quiet) {
|
|
1864
|
-
quiet = debounce(() => this.handleQuiet(sessionId),
|
|
2115
|
+
quiet = debounce(() => this.handleQuiet(sessionId), QUIET_DETECT_MS2);
|
|
1865
2116
|
this.quietCheckers.set(sessionId, quiet);
|
|
1866
2117
|
}
|
|
1867
2118
|
quiet();
|
|
@@ -2078,6 +2329,9 @@ var LiveSessionManager = class {
|
|
|
2078
2329
|
getOutputLines(sessionId, maxLines) {
|
|
2079
2330
|
return this.runnerFor(sessionId).getOutputLines(sessionId, maxLines);
|
|
2080
2331
|
}
|
|
2332
|
+
getInputHistory(sessionId) {
|
|
2333
|
+
return this.runnerFor(sessionId).getInputHistory(sessionId);
|
|
2334
|
+
}
|
|
2081
2335
|
getSession(sessionId) {
|
|
2082
2336
|
for (const runner of this.runners.values()) {
|
|
2083
2337
|
const session = runner.getSession(sessionId);
|
|
@@ -2122,7 +2376,7 @@ var LiveSessionManager = class {
|
|
|
2122
2376
|
// src/process-discovery.ts
|
|
2123
2377
|
import { execFile } from "child_process";
|
|
2124
2378
|
import { platform as platform2 } from "os";
|
|
2125
|
-
import { basename as basename4, dirname as
|
|
2379
|
+
import { basename as basename4, dirname as dirname4 } from "path";
|
|
2126
2380
|
async function discoverClaudeProcesses() {
|
|
2127
2381
|
if (platform2() === "win32") return discoverWindows();
|
|
2128
2382
|
return discoverUnix();
|
|
@@ -2244,7 +2498,7 @@ async function getProcessInfoWindows(pid) {
|
|
|
2244
2498
|
const startedAt = /* @__PURE__ */ new Date(`${year}-${month}-${day}T${hour}:${min}:${sec}`);
|
|
2245
2499
|
if (Number.isNaN(startedAt.getTime())) return null;
|
|
2246
2500
|
const exePath = parts[3] ?? "";
|
|
2247
|
-
const cwd = exePath ?
|
|
2501
|
+
const cwd = exePath ? dirname4(exePath) : "";
|
|
2248
2502
|
return { cwd, args, startedAt };
|
|
2249
2503
|
} catch {
|
|
2250
2504
|
return null;
|
|
@@ -2276,16 +2530,16 @@ import {
|
|
|
2276
2530
|
import { EventEmitter } from "events";
|
|
2277
2531
|
import {
|
|
2278
2532
|
createReadStream,
|
|
2279
|
-
existsSync as
|
|
2533
|
+
existsSync as existsSync8,
|
|
2280
2534
|
watch as fsWatch,
|
|
2281
2535
|
readdirSync as readdirSync4,
|
|
2282
|
-
readFileSync as
|
|
2283
|
-
statSync as
|
|
2536
|
+
readFileSync as readFileSync7,
|
|
2537
|
+
statSync as statSync6
|
|
2284
2538
|
} from "fs";
|
|
2285
2539
|
import { realpath as realpath2 } from "fs/promises";
|
|
2286
2540
|
import { createServer } from "http";
|
|
2287
|
-
import { homedir as
|
|
2288
|
-
import { dirname as
|
|
2541
|
+
import { homedir as homedir7 } from "os";
|
|
2542
|
+
import { dirname as dirname8, join as join15 } from "path";
|
|
2289
2543
|
import { createInterface } from "readline";
|
|
2290
2544
|
|
|
2291
2545
|
// node_modules/nanoid/index.js
|
|
@@ -2469,7 +2723,7 @@ async function handleSendAgentInput(sessionId, body, deps) {
|
|
|
2469
2723
|
|
|
2470
2724
|
// src/agent/handle-start-agent-session.ts
|
|
2471
2725
|
import { existsSync as existsSync4 } from "fs";
|
|
2472
|
-
import { join as
|
|
2726
|
+
import { join as join6 } from "path";
|
|
2473
2727
|
function validateBody(body) {
|
|
2474
2728
|
if (body === null || body === void 0 || typeof body !== "object") {
|
|
2475
2729
|
return { ok: false };
|
|
@@ -2499,7 +2753,7 @@ async function handleStartAgentSession(body, deps) {
|
|
|
2499
2753
|
}
|
|
2500
2754
|
let conversationId = parsed.conversationId;
|
|
2501
2755
|
if (conversationId) {
|
|
2502
|
-
const jsonlPath =
|
|
2756
|
+
const jsonlPath = join6(deps.conversationsDir, `${conversationId}.jsonl`);
|
|
2503
2757
|
if (!existsSync4(jsonlPath)) {
|
|
2504
2758
|
return {
|
|
2505
2759
|
status: 404,
|
|
@@ -2544,14 +2798,15 @@ async function handleStartAgentSession(body, deps) {
|
|
|
2544
2798
|
}
|
|
2545
2799
|
|
|
2546
2800
|
// src/api/app.ts
|
|
2547
|
-
import { Hono as
|
|
2801
|
+
import { Hono as Hono12 } from "hono";
|
|
2548
2802
|
|
|
2549
2803
|
// src/api/middleware/auth.middleware.ts
|
|
2550
2804
|
function isLocalRequest(remoteAddr) {
|
|
2551
2805
|
const addr = remoteAddr ?? "";
|
|
2552
2806
|
return addr === "127.0.0.1" || addr === "::1" || addr === "::ffff:127.0.0.1";
|
|
2553
2807
|
}
|
|
2554
|
-
var PUBLIC_PATHS = /* @__PURE__ */ new Set(["/healthz"
|
|
2808
|
+
var PUBLIC_PATHS = /* @__PURE__ */ new Set(["/healthz"]);
|
|
2809
|
+
var LOCAL_ONLY_PATHS = /* @__PURE__ */ new Set(["/api/logs", "/api/logs/meta"]);
|
|
2555
2810
|
var PUBLIC_POST_PATHS = /* @__PURE__ */ new Set(["/api/pair/exchange", "/api/__update"]);
|
|
2556
2811
|
var PUBLIC_POST_PREFIXES = ["/internal/sessions/"];
|
|
2557
2812
|
var authMiddleware = (deps) => async (c, next) => {
|
|
@@ -2562,8 +2817,12 @@ var authMiddleware = (deps) => async (c, next) => {
|
|
|
2562
2817
|
await next();
|
|
2563
2818
|
return;
|
|
2564
2819
|
}
|
|
2820
|
+
const remoteAddr = c.env.incoming?.socket?.remoteAddress;
|
|
2821
|
+
if (LOCAL_ONLY_PATHS.has(path) && isLocalRequest(remoteAddr)) {
|
|
2822
|
+
await next();
|
|
2823
|
+
return;
|
|
2824
|
+
}
|
|
2565
2825
|
if (deps.localNoAuth) {
|
|
2566
|
-
const remoteAddr = c.env.incoming?.socket?.remoteAddress;
|
|
2567
2826
|
if (isLocalRequest(remoteAddr)) {
|
|
2568
2827
|
await next();
|
|
2569
2828
|
return;
|
|
@@ -2696,8 +2955,8 @@ var createConversationRoutes = (deps) => {
|
|
|
2696
2955
|
import { Hono as Hono4 } from "hono";
|
|
2697
2956
|
|
|
2698
2957
|
// src/version.ts
|
|
2699
|
-
import { readFileSync as
|
|
2700
|
-
import { dirname as
|
|
2958
|
+
import { readFileSync as readFileSync4, realpathSync } from "fs";
|
|
2959
|
+
import { dirname as dirname5, join as join7 } from "path";
|
|
2701
2960
|
var cached;
|
|
2702
2961
|
function getVersion() {
|
|
2703
2962
|
if (cached !== void 0) return cached;
|
|
@@ -2706,22 +2965,22 @@ function getVersion() {
|
|
|
2706
2965
|
}
|
|
2707
2966
|
function resolveVersion() {
|
|
2708
2967
|
const scriptPath = process.argv[1] ?? "";
|
|
2709
|
-
const here = scriptPath ?
|
|
2968
|
+
const here = scriptPath ? dirname5(scriptPath) : process.cwd();
|
|
2710
2969
|
let realHere = here;
|
|
2711
2970
|
try {
|
|
2712
|
-
realHere =
|
|
2971
|
+
realHere = dirname5(realpathSync(scriptPath));
|
|
2713
2972
|
} catch {
|
|
2714
2973
|
}
|
|
2715
|
-
const searchDirs = realHere === here ? [here,
|
|
2974
|
+
const searchDirs = realHere === here ? [here, join7(here, "..")] : [here, join7(here, ".."), realHere, join7(realHere, "..")];
|
|
2716
2975
|
for (const dir of searchDirs) {
|
|
2717
2976
|
try {
|
|
2718
|
-
const v =
|
|
2977
|
+
const v = readFileSync4(join7(dir, "version.txt"), "utf8").trim();
|
|
2719
2978
|
if (v) return v;
|
|
2720
2979
|
} catch {
|
|
2721
2980
|
}
|
|
2722
2981
|
}
|
|
2723
2982
|
try {
|
|
2724
|
-
const pkg = JSON.parse(
|
|
2983
|
+
const pkg = JSON.parse(readFileSync4(join7(here, "..", "package.json"), "utf8"));
|
|
2725
2984
|
if (pkg.version) return `${pkg.version}+source`;
|
|
2726
2985
|
} catch {
|
|
2727
2986
|
}
|
|
@@ -2735,16 +2994,145 @@ var createHealthRoutes = () => {
|
|
|
2735
2994
|
return app;
|
|
2736
2995
|
};
|
|
2737
2996
|
|
|
2997
|
+
// src/api/routes/logs.routes.ts
|
|
2998
|
+
import { closeSync, existsSync as existsSync5, fstatSync, openSync, readSync, statSync } from "fs";
|
|
2999
|
+
import { join as join9 } from "path";
|
|
3000
|
+
import { Hono as Hono5 } from "hono";
|
|
3001
|
+
|
|
3002
|
+
// src/lifecycle/constants.ts
|
|
3003
|
+
import { homedir as homedir4 } from "os";
|
|
3004
|
+
import { join as join8 } from "path";
|
|
3005
|
+
var TASK_NAME = process.env.THREADBASE_TASK_NAME ?? "Threadbase";
|
|
3006
|
+
function installDir() {
|
|
3007
|
+
return process.env.THREADBASE_INSTALL_DIR ?? join8(homedir4(), ".threadbase");
|
|
3008
|
+
}
|
|
3009
|
+
|
|
3010
|
+
// src/api/routes/logs.routes.ts
|
|
3011
|
+
var logger2 = getLogger("logs-api");
|
|
3012
|
+
function resolveLogPath(source) {
|
|
3013
|
+
return join9(installDir(), "logs", `${source}.log`);
|
|
3014
|
+
}
|
|
3015
|
+
function pickDefaultSource() {
|
|
3016
|
+
for (const source of ["stdout", "stderr", "dev"]) {
|
|
3017
|
+
const p = resolveLogPath(source);
|
|
3018
|
+
if (existsSync5(p) && statSync(p).size > 0) return source;
|
|
3019
|
+
}
|
|
3020
|
+
return "stdout";
|
|
3021
|
+
}
|
|
3022
|
+
function readLogLines(filePath, sinceOffset, limit) {
|
|
3023
|
+
if (!existsSync5(filePath)) {
|
|
3024
|
+
return { lines: [], offset: 0, total: 0 };
|
|
3025
|
+
}
|
|
3026
|
+
const fd = openSync(filePath, "r");
|
|
3027
|
+
try {
|
|
3028
|
+
const { size } = fstatSync(fd);
|
|
3029
|
+
if (size === 0) return { lines: [], offset: 0, total: 0 };
|
|
3030
|
+
const maxBytes = Math.min(size, 2 * 1024 * 1024);
|
|
3031
|
+
const start = size - maxBytes;
|
|
3032
|
+
const buf = Buffer.alloc(maxBytes);
|
|
3033
|
+
readSync(fd, buf, 0, maxBytes, start);
|
|
3034
|
+
let text = buf.toString("utf8");
|
|
3035
|
+
if (start > 0) {
|
|
3036
|
+
const firstNl = text.indexOf("\n");
|
|
3037
|
+
if (firstNl >= 0) text = text.slice(firstNl + 1);
|
|
3038
|
+
}
|
|
3039
|
+
const allLines = text.split("\n").filter((line) => line.trim() && !line.startsWith("==="));
|
|
3040
|
+
let lines;
|
|
3041
|
+
let newOffset;
|
|
3042
|
+
if (sinceOffset > 0 && sinceOffset < allLines.length) {
|
|
3043
|
+
lines = allLines.slice(sinceOffset, sinceOffset + limit);
|
|
3044
|
+
newOffset = sinceOffset + lines.length;
|
|
3045
|
+
} else if (sinceOffset >= allLines.length && sinceOffset > 0) {
|
|
3046
|
+
lines = [];
|
|
3047
|
+
newOffset = allLines.length;
|
|
3048
|
+
} else {
|
|
3049
|
+
lines = allLines.slice(-limit);
|
|
3050
|
+
newOffset = allLines.length;
|
|
3051
|
+
}
|
|
3052
|
+
return { lines, offset: newOffset, total: allLines.length };
|
|
3053
|
+
} finally {
|
|
3054
|
+
closeSync(fd);
|
|
3055
|
+
}
|
|
3056
|
+
}
|
|
3057
|
+
function createLogsRoutes() {
|
|
3058
|
+
const app = new Hono5();
|
|
3059
|
+
app.get("/", (c) => {
|
|
3060
|
+
try {
|
|
3061
|
+
const sourceParam = (c.req.query("source") || "").toLowerCase();
|
|
3062
|
+
const source = sourceParam === "stdout" || sourceParam === "stderr" || sourceParam === "dev" ? sourceParam : pickDefaultSource();
|
|
3063
|
+
const logPath = resolveLogPath(source);
|
|
3064
|
+
const sinceOffset = parseInt(c.req.query("since") || "0", 10);
|
|
3065
|
+
const limit = Math.min(parseInt(c.req.query("limit") || "100", 10) || 100, 1e3);
|
|
3066
|
+
if (!existsSync5(logPath)) {
|
|
3067
|
+
return c.json({
|
|
3068
|
+
logs: [],
|
|
3069
|
+
message: `No log file found for source=${source}`,
|
|
3070
|
+
offset: 0,
|
|
3071
|
+
total: 0,
|
|
3072
|
+
source
|
|
3073
|
+
});
|
|
3074
|
+
}
|
|
3075
|
+
const { lines, offset, total } = readLogLines(logPath, sinceOffset, limit);
|
|
3076
|
+
const stats = statSync(logPath);
|
|
3077
|
+
return c.json({
|
|
3078
|
+
logs: lines,
|
|
3079
|
+
offset,
|
|
3080
|
+
total,
|
|
3081
|
+
hasMore: offset < total,
|
|
3082
|
+
source,
|
|
3083
|
+
fileSize: stats.size,
|
|
3084
|
+
fileModified: stats.mtime.toISOString()
|
|
3085
|
+
});
|
|
3086
|
+
} catch (error) {
|
|
3087
|
+
logger2.error("Failed to read logs", { error: String(error) });
|
|
3088
|
+
return c.json(
|
|
3089
|
+
{
|
|
3090
|
+
error: "Failed to read logs",
|
|
3091
|
+
logs: [],
|
|
3092
|
+
offset: 0,
|
|
3093
|
+
total: 0
|
|
3094
|
+
},
|
|
3095
|
+
500
|
|
3096
|
+
);
|
|
3097
|
+
}
|
|
3098
|
+
});
|
|
3099
|
+
app.get("/meta", (c) => {
|
|
3100
|
+
try {
|
|
3101
|
+
const sources = ["stdout", "stderr", "dev"].map((source) => {
|
|
3102
|
+
const logPath = resolveLogPath(source);
|
|
3103
|
+
if (!existsSync5(logPath)) {
|
|
3104
|
+
return { source, exists: false, total: 0, fileSize: 0 };
|
|
3105
|
+
}
|
|
3106
|
+
const stats = statSync(logPath);
|
|
3107
|
+
return {
|
|
3108
|
+
source,
|
|
3109
|
+
exists: true,
|
|
3110
|
+
fileSize: stats.size,
|
|
3111
|
+
fileModified: stats.mtime.toISOString()
|
|
3112
|
+
};
|
|
3113
|
+
});
|
|
3114
|
+
return c.json({
|
|
3115
|
+
defaultSource: pickDefaultSource(),
|
|
3116
|
+
sources
|
|
3117
|
+
});
|
|
3118
|
+
} catch (error) {
|
|
3119
|
+
logger2.error("Failed to read log metadata", { error: String(error) });
|
|
3120
|
+
return c.json({ error: "Failed to read log metadata", exists: false }, 500);
|
|
3121
|
+
}
|
|
3122
|
+
});
|
|
3123
|
+
return app;
|
|
3124
|
+
}
|
|
3125
|
+
|
|
2738
3126
|
// src/api/routes/misc.routes.ts
|
|
2739
3127
|
import { spawn } from "child_process";
|
|
2740
3128
|
import { createHmac, timingSafeEqual as timingSafeEqual2 } from "crypto";
|
|
2741
|
-
import { Hono as
|
|
3129
|
+
import { Hono as Hono6 } from "hono";
|
|
2742
3130
|
import { hostname } from "os";
|
|
2743
3131
|
|
|
2744
3132
|
// src/config/update-config.ts
|
|
2745
|
-
import { readFileSync as
|
|
2746
|
-
import { homedir as
|
|
2747
|
-
import { join as
|
|
3133
|
+
import { readFileSync as readFileSync5 } from "fs";
|
|
3134
|
+
import { homedir as homedir5 } from "os";
|
|
3135
|
+
import { join as join10 } from "path";
|
|
2748
3136
|
import { parse as parseYaml } from "yaml";
|
|
2749
3137
|
|
|
2750
3138
|
// src/schemas/updateConfig.schema.ts
|
|
@@ -2760,12 +3148,12 @@ var UpdateConfigSchema = z.object({
|
|
|
2760
3148
|
}).strict();
|
|
2761
3149
|
|
|
2762
3150
|
// src/config/update-config.ts
|
|
2763
|
-
var DEFAULT_CONFIG_PATH =
|
|
3151
|
+
var DEFAULT_CONFIG_PATH = join10(homedir5(), ".threadbase", "update.yaml");
|
|
2764
3152
|
function loadUpdateConfig(opts = {}) {
|
|
2765
3153
|
const path = opts.path ?? DEFAULT_CONFIG_PATH;
|
|
2766
3154
|
let raw;
|
|
2767
3155
|
try {
|
|
2768
|
-
raw =
|
|
3156
|
+
raw = readFileSync5(path, "utf-8");
|
|
2769
3157
|
} catch (err) {
|
|
2770
3158
|
if (err.code === "ENOENT") return null;
|
|
2771
3159
|
throw err;
|
|
@@ -2812,7 +3200,7 @@ function verifyWebhookSignature(body, header, secret) {
|
|
|
2812
3200
|
}
|
|
2813
3201
|
var clientLog = getLogger("client");
|
|
2814
3202
|
var createMiscRoutes = (deps) => {
|
|
2815
|
-
const app = new
|
|
3203
|
+
const app = new Hono6();
|
|
2816
3204
|
app.get("/api/info", (c) => {
|
|
2817
3205
|
const ptyIds = deps.ptyAttachedIds();
|
|
2818
3206
|
return c.json({
|
|
@@ -2888,11 +3276,11 @@ var createMiscRoutes = (deps) => {
|
|
|
2888
3276
|
};
|
|
2889
3277
|
|
|
2890
3278
|
// src/api/routes/pair.routes.ts
|
|
2891
|
-
import { Hono as
|
|
3279
|
+
import { Hono as Hono7 } from "hono";
|
|
2892
3280
|
var ALREADY_HANDLED3 = 597;
|
|
2893
3281
|
var alreadyHandled3 = () => new Response(null, { status: ALREADY_HANDLED3 });
|
|
2894
3282
|
var createPairRoutes = (deps) => {
|
|
2895
|
-
const app = new
|
|
3283
|
+
const app = new Hono7();
|
|
2896
3284
|
app.post("/start", (c) => {
|
|
2897
3285
|
deps.handlePairStart(c.env.outgoing);
|
|
2898
3286
|
return alreadyHandled3();
|
|
@@ -2905,11 +3293,11 @@ var createPairRoutes = (deps) => {
|
|
|
2905
3293
|
};
|
|
2906
3294
|
|
|
2907
3295
|
// src/api/routes/projects.routes.ts
|
|
2908
|
-
import { Hono as
|
|
3296
|
+
import { Hono as Hono8 } from "hono";
|
|
2909
3297
|
var ALREADY_HANDLED4 = 597;
|
|
2910
3298
|
var alreadyHandled4 = () => new Response(null, { status: ALREADY_HANDLED4 });
|
|
2911
3299
|
var createProjectRoutes = (deps) => {
|
|
2912
|
-
const app = new
|
|
3300
|
+
const app = new Hono8();
|
|
2913
3301
|
app.get("/", (c) => {
|
|
2914
3302
|
const url = new URL(c.req.url);
|
|
2915
3303
|
deps.handleListProjects(url, c.env.outgoing);
|
|
@@ -2924,11 +3312,11 @@ var createProjectRoutes = (deps) => {
|
|
|
2924
3312
|
};
|
|
2925
3313
|
|
|
2926
3314
|
// src/api/routes/scanner.routes.ts
|
|
2927
|
-
import { Hono as
|
|
3315
|
+
import { Hono as Hono9 } from "hono";
|
|
2928
3316
|
var ALREADY_HANDLED5 = 597;
|
|
2929
3317
|
var alreadyHandled5 = () => new Response(null, { status: ALREADY_HANDLED5 });
|
|
2930
3318
|
var createScannerRoutes = (deps) => {
|
|
2931
|
-
const app = new
|
|
3319
|
+
const app = new Hono9();
|
|
2932
3320
|
app.get("/api/search", async (c) => {
|
|
2933
3321
|
const url = new URL(c.req.url);
|
|
2934
3322
|
await deps.handleSearch(url, c.env.outgoing);
|
|
@@ -2938,11 +3326,11 @@ var createScannerRoutes = (deps) => {
|
|
|
2938
3326
|
};
|
|
2939
3327
|
|
|
2940
3328
|
// src/api/routes/sessions.routes.ts
|
|
2941
|
-
import { Hono as
|
|
3329
|
+
import { Hono as Hono10 } from "hono";
|
|
2942
3330
|
var ALREADY_HANDLED6 = 597;
|
|
2943
3331
|
var alreadyHandled6 = () => new Response(null, { status: ALREADY_HANDLED6 });
|
|
2944
3332
|
var createSessionRoutes = (deps) => {
|
|
2945
|
-
const app = new
|
|
3333
|
+
const app = new Hono10();
|
|
2946
3334
|
app.get("/count", (c) => {
|
|
2947
3335
|
deps.handleSessionsCount(c.env.outgoing);
|
|
2948
3336
|
return alreadyHandled6();
|
|
@@ -3009,21 +3397,19 @@ var createSessionRoutes = (deps) => {
|
|
|
3009
3397
|
};
|
|
3010
3398
|
|
|
3011
3399
|
// src/api/routes/ws.routes.ts
|
|
3012
|
-
import { Hono as
|
|
3400
|
+
import { Hono as Hono11 } from "hono";
|
|
3013
3401
|
var createWsRoutes = (deps, upgradeWebSocket) => {
|
|
3014
|
-
const app = new
|
|
3402
|
+
const app = new Hono11();
|
|
3015
3403
|
app.get(
|
|
3016
3404
|
"/ws",
|
|
3017
|
-
upgradeWebSocket((
|
|
3018
|
-
const key = c.req.query("key");
|
|
3019
|
-
const preAuthed = typeof key === "string" && validateApiKey(key, deps.apiKey);
|
|
3405
|
+
upgradeWebSocket(() => {
|
|
3020
3406
|
let openWs = null;
|
|
3021
3407
|
return {
|
|
3022
3408
|
onOpen(_evt, ws) {
|
|
3023
3409
|
const raw = ws.raw;
|
|
3024
3410
|
if (!raw) return;
|
|
3025
3411
|
openWs = raw;
|
|
3026
|
-
deps.handleWsOpen(raw
|
|
3412
|
+
deps.handleWsOpen(raw);
|
|
3027
3413
|
},
|
|
3028
3414
|
onMessage(evt, _ws) {
|
|
3029
3415
|
if (openWs) deps.handleWsMessage(openWs, evt.data);
|
|
@@ -3039,7 +3425,7 @@ var createWsRoutes = (deps, upgradeWebSocket) => {
|
|
|
3039
3425
|
|
|
3040
3426
|
// src/api/app.ts
|
|
3041
3427
|
var createHonoApp = (deps, upgradeWebSocket) => {
|
|
3042
|
-
const app = new
|
|
3428
|
+
const app = new Hono12();
|
|
3043
3429
|
const httpLog = getLogger("http");
|
|
3044
3430
|
app.use("*", async (c, next) => {
|
|
3045
3431
|
const start = Date.now();
|
|
@@ -3068,6 +3454,7 @@ var createHonoApp = (deps, upgradeWebSocket) => {
|
|
|
3068
3454
|
app.route("/api", createBrowseRoutes(deps));
|
|
3069
3455
|
app.route("/", createScannerRoutes(deps));
|
|
3070
3456
|
app.route("/internal", createProgressRoutes(deps));
|
|
3457
|
+
app.route("/api/logs", createLogsRoutes());
|
|
3071
3458
|
if (upgradeWebSocket) {
|
|
3072
3459
|
app.route("/", createWsRoutes(deps, upgradeWebSocket));
|
|
3073
3460
|
}
|
|
@@ -3076,7 +3463,7 @@ var createHonoApp = (deps, upgradeWebSocket) => {
|
|
|
3076
3463
|
|
|
3077
3464
|
// src/browse.ts
|
|
3078
3465
|
import { mkdir as mkdir2, readdir, realpath, stat } from "fs/promises";
|
|
3079
|
-
import { join as
|
|
3466
|
+
import { join as join11, resolve, sep } from "path";
|
|
3080
3467
|
var BrowsePathNotFoundError = class extends Error {
|
|
3081
3468
|
constructor(message) {
|
|
3082
3469
|
super(message);
|
|
@@ -3114,7 +3501,7 @@ async function createDirectory(parentAbsolutePath, name) {
|
|
|
3114
3501
|
if (name.includes("/") || name.includes("\\") || name === ".." || name === ".") {
|
|
3115
3502
|
throw new Error("Invalid directory name");
|
|
3116
3503
|
}
|
|
3117
|
-
const target =
|
|
3504
|
+
const target = join11(parentAbsolutePath, name);
|
|
3118
3505
|
try {
|
|
3119
3506
|
const s = await stat(target);
|
|
3120
3507
|
if (s.isDirectory()) throw new Error("Directory already exists");
|
|
@@ -3131,18 +3518,18 @@ import {
|
|
|
3131
3518
|
parseJsonlLine
|
|
3132
3519
|
} from "@threadbase-sh/scanner";
|
|
3133
3520
|
import Database from "better-sqlite3";
|
|
3134
|
-
import { closeSync as
|
|
3521
|
+
import { closeSync as closeSync3, existsSync as existsSync6, mkdirSync as mkdirSync3, openSync as openSync3, readSync as readSync3, statSync as statSync3 } from "fs";
|
|
3135
3522
|
import { open as openAsync } from "fs/promises";
|
|
3136
|
-
import { dirname as
|
|
3523
|
+
import { dirname as dirname7 } from "path";
|
|
3137
3524
|
import { setImmediate as yieldToEventLoop } from "timers/promises";
|
|
3138
3525
|
|
|
3139
3526
|
// src/db/sqlite-migrate.ts
|
|
3140
|
-
import { readdirSync as readdirSync2, readFileSync as
|
|
3141
|
-
import { dirname as
|
|
3527
|
+
import { readdirSync as readdirSync2, readFileSync as readFileSync6 } from "fs";
|
|
3528
|
+
import { dirname as dirname6, join as join12 } from "path";
|
|
3142
3529
|
import { fileURLToPath as fileURLToPath2 } from "url";
|
|
3143
3530
|
function getMigrationsDir2() {
|
|
3144
3531
|
if (typeof import.meta !== "undefined" && import.meta.url) {
|
|
3145
|
-
return
|
|
3532
|
+
return dirname6(fileURLToPath2(import.meta.url));
|
|
3146
3533
|
}
|
|
3147
3534
|
return __dirname;
|
|
3148
3535
|
}
|
|
@@ -3154,7 +3541,7 @@ CREATE TABLE IF NOT EXISTS schema_migrations (
|
|
|
3154
3541
|
`;
|
|
3155
3542
|
function runSqliteMigrations(db, migrationsDir) {
|
|
3156
3543
|
db.exec(SCHEMA_MIGRATIONS_SQL);
|
|
3157
|
-
const dir = migrationsDir ??
|
|
3544
|
+
const dir = migrationsDir ?? join12(getMigrationsDir2(), "migrations");
|
|
3158
3545
|
const files = readdirSync2(dir).filter((f) => f.endsWith(".sql")).sort();
|
|
3159
3546
|
const appliedRows = db.prepare("SELECT id FROM schema_migrations").all();
|
|
3160
3547
|
const appliedSet = new Set(appliedRows.map((r) => r.id));
|
|
@@ -3166,7 +3553,7 @@ function runSqliteMigrations(db, migrationsDir) {
|
|
|
3166
3553
|
skipped.push(file);
|
|
3167
3554
|
continue;
|
|
3168
3555
|
}
|
|
3169
|
-
const sql =
|
|
3556
|
+
const sql = readFileSync6(join12(dir, file), "utf-8");
|
|
3170
3557
|
const tx = db.transaction(() => {
|
|
3171
3558
|
db.exec(sql);
|
|
3172
3559
|
recordApplied.run(file, (/* @__PURE__ */ new Date()).toISOString());
|
|
@@ -3178,7 +3565,7 @@ function runSqliteMigrations(db, migrationsDir) {
|
|
|
3178
3565
|
}
|
|
3179
3566
|
|
|
3180
3567
|
// src/services/conversations/isAgentConversation.ts
|
|
3181
|
-
import { closeSync, openSync, readSync, statSync } from "fs";
|
|
3568
|
+
import { closeSync as closeSync2, openSync as openSync2, readSync as readSync2, statSync as statSync2 } from "fs";
|
|
3182
3569
|
var DEFAULT_AGENT_ENTRYPOINTS = /* @__PURE__ */ new Set(["sdk-cli", "claude-vscode"]);
|
|
3183
3570
|
var CHUNK_BYTES = 64 * 1024;
|
|
3184
3571
|
var ENTRYPOINT_PROBE = `"entrypoint":`;
|
|
@@ -3200,12 +3587,12 @@ function isAgentFile(filePath, entrypoints = DEFAULT_AGENT_ENTRYPOINTS) {
|
|
|
3200
3587
|
if (cached2 !== void 0) return cached2;
|
|
3201
3588
|
let fd;
|
|
3202
3589
|
try {
|
|
3203
|
-
fd =
|
|
3590
|
+
fd = openSync2(filePath, "r");
|
|
3204
3591
|
} catch {
|
|
3205
3592
|
return false;
|
|
3206
3593
|
}
|
|
3207
3594
|
try {
|
|
3208
|
-
const fileSize =
|
|
3595
|
+
const fileSize = statSync2(filePath).size;
|
|
3209
3596
|
if (fileSize === 0) {
|
|
3210
3597
|
fileDecisionCache.set(key, false);
|
|
3211
3598
|
return false;
|
|
@@ -3216,7 +3603,7 @@ function isAgentFile(filePath, entrypoints = DEFAULT_AGENT_ENTRYPOINTS) {
|
|
|
3216
3603
|
let carry = "";
|
|
3217
3604
|
while (offset < fileSize) {
|
|
3218
3605
|
const toRead = Math.min(CHUNK_BYTES, fileSize - offset);
|
|
3219
|
-
const got =
|
|
3606
|
+
const got = readSync2(fd, buf, 0, toRead, offset);
|
|
3220
3607
|
if (got <= 0) break;
|
|
3221
3608
|
const chunk = carry + buf.toString("utf8", 0, got);
|
|
3222
3609
|
for (const marker of markers) {
|
|
@@ -3237,7 +3624,7 @@ function isAgentFile(filePath, entrypoints = DEFAULT_AGENT_ENTRYPOINTS) {
|
|
|
3237
3624
|
} catch {
|
|
3238
3625
|
return false;
|
|
3239
3626
|
} finally {
|
|
3240
|
-
|
|
3627
|
+
closeSync2(fd);
|
|
3241
3628
|
}
|
|
3242
3629
|
}
|
|
3243
3630
|
function parseAgentEntrypointsEnv(raw) {
|
|
@@ -3774,7 +4161,7 @@ var ConversationCache = class _ConversationCache {
|
|
|
3774
4161
|
if (!fileState) return null;
|
|
3775
4162
|
let stat3;
|
|
3776
4163
|
try {
|
|
3777
|
-
stat3 =
|
|
4164
|
+
stat3 = statSync3(filePath);
|
|
3778
4165
|
} catch {
|
|
3779
4166
|
return null;
|
|
3780
4167
|
}
|
|
@@ -3795,17 +4182,17 @@ var ConversationCache = class _ConversationCache {
|
|
|
3795
4182
|
);
|
|
3796
4183
|
if (rows.length === 0) return { messages: [], total, fromIndex: from };
|
|
3797
4184
|
const messages = [];
|
|
3798
|
-
const fd =
|
|
4185
|
+
const fd = openSync3(filePath, "r");
|
|
3799
4186
|
try {
|
|
3800
4187
|
const state = createJsonlParseState();
|
|
3801
4188
|
for (const row of rows) {
|
|
3802
4189
|
const buf = Buffer.alloc(row.byte_length);
|
|
3803
|
-
|
|
4190
|
+
readSync3(fd, buf, 0, row.byte_length, row.byte_offset);
|
|
3804
4191
|
const msg = parseJsonlLine(buf.toString("utf-8"), state);
|
|
3805
4192
|
if (msg) messages.push(msg);
|
|
3806
4193
|
}
|
|
3807
4194
|
} finally {
|
|
3808
|
-
|
|
4195
|
+
closeSync3(fd);
|
|
3809
4196
|
}
|
|
3810
4197
|
return { messages, total, fromIndex: from };
|
|
3811
4198
|
}
|
|
@@ -3833,14 +4220,14 @@ var ConversationCache = class _ConversationCache {
|
|
|
3833
4220
|
isAgentFileCached(filePath) {
|
|
3834
4221
|
let s;
|
|
3835
4222
|
try {
|
|
3836
|
-
s =
|
|
4223
|
+
s = statSync3(filePath);
|
|
3837
4224
|
} catch {
|
|
3838
4225
|
return false;
|
|
3839
4226
|
}
|
|
3840
4227
|
return this.classifyAgentFile(filePath, s.mtimeMs, s.size);
|
|
3841
4228
|
}
|
|
3842
4229
|
static open(dbPath, tailSize = 10, migrationsDir, options) {
|
|
3843
|
-
|
|
4230
|
+
mkdirSync3(dirname7(dbPath), { recursive: true });
|
|
3844
4231
|
const db = new Database(dbPath);
|
|
3845
4232
|
db.pragma("journal_mode = WAL");
|
|
3846
4233
|
db.pragma("foreign_keys = ON");
|
|
@@ -4021,7 +4408,7 @@ var ConversationCache = class _ConversationCache {
|
|
|
4021
4408
|
let mtimeMs = null;
|
|
4022
4409
|
let fileSize = null;
|
|
4023
4410
|
try {
|
|
4024
|
-
const s =
|
|
4411
|
+
const s = statSync3(m.filePath);
|
|
4025
4412
|
mtimeMs = s.mtimeMs;
|
|
4026
4413
|
fileSize = s.size;
|
|
4027
4414
|
} catch {
|
|
@@ -4080,8 +4467,8 @@ var ConversationCache = class _ConversationCache {
|
|
|
4080
4467
|
let fileSize;
|
|
4081
4468
|
let fd;
|
|
4082
4469
|
try {
|
|
4083
|
-
fileSize =
|
|
4084
|
-
fd =
|
|
4470
|
+
fileSize = statSync3(filePath).size;
|
|
4471
|
+
fd = openSync3(filePath, "r");
|
|
4085
4472
|
} catch {
|
|
4086
4473
|
return false;
|
|
4087
4474
|
}
|
|
@@ -4094,7 +4481,7 @@ var ConversationCache = class _ConversationCache {
|
|
|
4094
4481
|
while (pos > 0 && lines.length < this.tailSize * 4) {
|
|
4095
4482
|
const toRead = Math.min(CHUNK, pos);
|
|
4096
4483
|
pos -= toRead;
|
|
4097
|
-
|
|
4484
|
+
readSync3(fd, buf, 0, toRead, pos);
|
|
4098
4485
|
const chunk = buf.subarray(0, toRead).toString("utf8");
|
|
4099
4486
|
const combined = chunk + partial;
|
|
4100
4487
|
const parts = combined.split("\n");
|
|
@@ -4105,7 +4492,7 @@ var ConversationCache = class _ConversationCache {
|
|
|
4105
4492
|
}
|
|
4106
4493
|
if (partial) lines.push(partial);
|
|
4107
4494
|
} finally {
|
|
4108
|
-
|
|
4495
|
+
closeSync3(fd);
|
|
4109
4496
|
}
|
|
4110
4497
|
const msgs = [];
|
|
4111
4498
|
for (let i = 0; i < lines.length && msgs.length < this.tailSize; i++) {
|
|
@@ -4307,7 +4694,7 @@ var ConversationCache = class _ConversationCache {
|
|
|
4307
4694
|
* `handleGetConversation` can still serve the cached tail even when the
|
|
4308
4695
|
* JSONL has been deleted.
|
|
4309
4696
|
*/
|
|
4310
|
-
pruneGhostFiles(exists =
|
|
4697
|
+
pruneGhostFiles(exists = existsSync6) {
|
|
4311
4698
|
const rows = this.stmts.allFilePaths.all();
|
|
4312
4699
|
const ghosts = [];
|
|
4313
4700
|
const prune = this.db.transaction((ids) => {
|
|
@@ -4362,7 +4749,7 @@ var ConversationCache = class _ConversationCache {
|
|
|
4362
4749
|
* Returns the removed IDs.
|
|
4363
4750
|
*/
|
|
4364
4751
|
reconcileDeletions(livePaths, opts) {
|
|
4365
|
-
const exists = opts?.exists ??
|
|
4752
|
+
const exists = opts?.exists ?? existsSync6;
|
|
4366
4753
|
const rows = this.stmts.allFilePaths.all();
|
|
4367
4754
|
const removed = [];
|
|
4368
4755
|
const drop = this.db.transaction((ids) => {
|
|
@@ -4591,23 +4978,23 @@ async function recordUpload(pool2, instanceId, row) {
|
|
|
4591
4978
|
}
|
|
4592
4979
|
|
|
4593
4980
|
// src/handlers/handleListProjects.ts
|
|
4594
|
-
import { readdirSync as readdirSync3, statSync as
|
|
4595
|
-
import { homedir as
|
|
4596
|
-
import { join as
|
|
4981
|
+
import { readdirSync as readdirSync3, statSync as statSync4 } from "fs";
|
|
4982
|
+
import { homedir as homedir6 } from "os";
|
|
4983
|
+
import { join as join13 } from "path";
|
|
4597
4984
|
function decodeProjectPath(dirName) {
|
|
4598
4985
|
return dirName.replace(/-/g, "/");
|
|
4599
4986
|
}
|
|
4600
4987
|
function handleListProjects(url, res) {
|
|
4601
4988
|
const limit = Math.max(1, parseInt(url.searchParams.get("limit") ?? "50", 10) || 50);
|
|
4602
4989
|
const offset = Math.max(0, parseInt(url.searchParams.get("offset") ?? "0", 10) || 0);
|
|
4603
|
-
const projectsDir =
|
|
4990
|
+
const projectsDir = join13(homedir6(), ".claude", "projects");
|
|
4604
4991
|
let entries;
|
|
4605
4992
|
try {
|
|
4606
4993
|
entries = readdirSync3(projectsDir).map((dirName) => {
|
|
4607
|
-
const fullPath =
|
|
4994
|
+
const fullPath = join13(projectsDir, dirName);
|
|
4608
4995
|
let mtime = 0;
|
|
4609
4996
|
try {
|
|
4610
|
-
mtime =
|
|
4997
|
+
mtime = statSync4(fullPath).mtimeMs;
|
|
4611
4998
|
} catch {
|
|
4612
4999
|
}
|
|
4613
5000
|
const path = decodeProjectPath(dirName);
|
|
@@ -4702,7 +5089,7 @@ function seal(plaintext, recipientPublicKeyBase64) {
|
|
|
4702
5089
|
|
|
4703
5090
|
// src/services/conversations/conversationWatcher.ts
|
|
4704
5091
|
import chokidar from "chokidar";
|
|
4705
|
-
import { statSync as
|
|
5092
|
+
import { statSync as statSync5 } from "fs";
|
|
4706
5093
|
import { open, stat as stat2 } from "fs/promises";
|
|
4707
5094
|
var ConversationWatcher = class {
|
|
4708
5095
|
files = /* @__PURE__ */ new Map();
|
|
@@ -4725,7 +5112,7 @@ var ConversationWatcher = class {
|
|
|
4725
5112
|
if (this.files.has(filePath)) return;
|
|
4726
5113
|
let offset;
|
|
4727
5114
|
try {
|
|
4728
|
-
offset =
|
|
5115
|
+
offset = statSync5(filePath).size;
|
|
4729
5116
|
} catch {
|
|
4730
5117
|
offset = 0;
|
|
4731
5118
|
}
|
|
@@ -4900,14 +5287,14 @@ function findSearchTarget(messages, query) {
|
|
|
4900
5287
|
}
|
|
4901
5288
|
|
|
4902
5289
|
// src/services/conversations/pruneAgentConversations.ts
|
|
4903
|
-
import { existsSync as
|
|
5290
|
+
import { existsSync as existsSync7 } from "fs";
|
|
4904
5291
|
function pruneAgentConversations(cache) {
|
|
4905
5292
|
const db = cache.getDatabase();
|
|
4906
5293
|
const rows = db.prepare("SELECT id, file_path FROM conversation_meta").all();
|
|
4907
5294
|
let pruned = 0;
|
|
4908
5295
|
let missing = 0;
|
|
4909
5296
|
for (const row of rows) {
|
|
4910
|
-
if (!
|
|
5297
|
+
if (!existsSync7(row.file_path)) {
|
|
4911
5298
|
missing += 1;
|
|
4912
5299
|
continue;
|
|
4913
5300
|
}
|
|
@@ -4930,13 +5317,6 @@ function deriveProjectChatTitle(input) {
|
|
|
4930
5317
|
return `Untitled \xB7 ${input.id.slice(0, 8)}`;
|
|
4931
5318
|
}
|
|
4932
5319
|
|
|
4933
|
-
// src/services/questions/permissionAnswerKeys.ts
|
|
4934
|
-
var ANSWER_KEYS_ALLOWLIST = /^(?:\r|[yn]\r|\x03|\d+\r)$/;
|
|
4935
|
-
function sanitizeAnswerKeys(keys) {
|
|
4936
|
-
if (keys === void 0) return void 0;
|
|
4937
|
-
return ANSWER_KEYS_ALLOWLIST.test(keys) ? keys : void 0;
|
|
4938
|
-
}
|
|
4939
|
-
|
|
4940
5320
|
// src/services/questions/detectAskUserQuestion.ts
|
|
4941
5321
|
function normalizeContent2(raw) {
|
|
4942
5322
|
if (Array.isArray(raw)) return raw;
|
|
@@ -5250,7 +5630,7 @@ function discoveredToResponse(d, conversationId) {
|
|
|
5250
5630
|
import { randomBytes as randomBytes3 } from "crypto";
|
|
5251
5631
|
import { mkdir as mkdir3, writeFile } from "fs/promises";
|
|
5252
5632
|
import heicConvert from "heic-convert";
|
|
5253
|
-
import { join as
|
|
5633
|
+
import { join as join14 } from "path";
|
|
5254
5634
|
var UPLOAD_DIR_NAME = ".threadbase-uploads";
|
|
5255
5635
|
var MAX_BYTES = 25 * 1024 * 1024;
|
|
5256
5636
|
var HEIC_MIMES = /* @__PURE__ */ new Set(["image/heic", "image/heif"]);
|
|
@@ -5283,9 +5663,9 @@ async function saveUploadFile(input) {
|
|
|
5283
5663
|
}
|
|
5284
5664
|
const id = `up_${randomBytes3(8).toString("hex")}`;
|
|
5285
5665
|
const safeName = sanitizeFilename(originalName) || `file${MIME_TO_EXT[mimeType] ?? ""}`;
|
|
5286
|
-
const dir =
|
|
5666
|
+
const dir = join14(input.projectPath, UPLOAD_DIR_NAME, input.sessionId);
|
|
5287
5667
|
await mkdir3(dir, { recursive: true });
|
|
5288
|
-
const filePath =
|
|
5668
|
+
const filePath = join14(dir, `${Date.now()}-${id}-${safeName}`);
|
|
5289
5669
|
await writeFile(filePath, buffer);
|
|
5290
5670
|
return {
|
|
5291
5671
|
id,
|
|
@@ -5541,10 +5921,8 @@ var WSHub = class {
|
|
|
5541
5921
|
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.`;
|
|
5542
5922
|
var DEFAULT_SYSTEM_PROMPT = "When presenting options or choices to the user, limit the options to at most 3.";
|
|
5543
5923
|
var DEFAULT_PTY_GRACE_PERIOD_MS = 27e4;
|
|
5544
|
-
var DEFAULT_WS_AUTH_TIMEOUT_MS = 5e3;
|
|
5545
|
-
var WS_CLOSE_UNAUTHORIZED = 4401;
|
|
5546
5924
|
var REFRESH_TTL_MS = 2e3;
|
|
5547
|
-
var START_READY_TIMEOUT_MS =
|
|
5925
|
+
var START_READY_TIMEOUT_MS = 1e4;
|
|
5548
5926
|
function parseIncludeAgentsEnv(raw) {
|
|
5549
5927
|
if (raw === void 0) return false;
|
|
5550
5928
|
const v = raw.trim().toLowerCase();
|
|
@@ -5633,14 +6011,6 @@ var StreamerServer = class {
|
|
|
5633
6011
|
clientIdToWs = /* @__PURE__ */ new Map();
|
|
5634
6012
|
// Reverse map for cleanup on close
|
|
5635
6013
|
wsToClientId = /* @__PURE__ */ new Map();
|
|
5636
|
-
// M1 — WS auth. Sockets that have authenticated (via ?key= at upgrade OR a
|
|
5637
|
-
// { type: "auth", token } first message). Only authed sockets are added to
|
|
5638
|
-
// the hub and receive broadcasts.
|
|
5639
|
-
// Plan: https://github.com/RonenMars/threadbase-streamer/blob/a251353bfa417bd48ce3f15086bc336a2c622629/docs/plans/2026-06-24-security-hardening.md#L40
|
|
5640
|
-
wsAuthed = /* @__PURE__ */ new Set();
|
|
5641
|
-
// Keyless sockets awaiting their first-message auth handshake → close timer.
|
|
5642
|
-
wsAuthPending = /* @__PURE__ */ new Map();
|
|
5643
|
-
wsAuthTimeoutMs;
|
|
5644
6014
|
cache = null;
|
|
5645
6015
|
projectsRepo = null;
|
|
5646
6016
|
conversationsRepo = null;
|
|
@@ -5678,12 +6048,11 @@ var StreamerServer = class {
|
|
|
5678
6048
|
this.disableDb = config.disableDb ?? false;
|
|
5679
6049
|
this.scannerPersistenceDisabled = config.scannerPersistent === false;
|
|
5680
6050
|
this.scanProfiles = config.scanProfiles;
|
|
5681
|
-
this.codexRoots = config.codexRoots ?? [
|
|
6051
|
+
this.codexRoots = config.codexRoots ?? [join15(homedir7(), ".codex", "sessions")];
|
|
5682
6052
|
this.ptyGracePeriodMs = config.ptyGracePeriodMs ?? DEFAULT_PTY_GRACE_PERIOD_MS;
|
|
5683
|
-
this.wsAuthTimeoutMs = config.wsAuthTimeoutMs ?? DEFAULT_WS_AUTH_TIMEOUT_MS;
|
|
5684
6053
|
this.defaultSystemPrompt = config.defaultSystemPrompt ?? DEFAULT_SYSTEM_PROMPT;
|
|
5685
6054
|
this.defaultPermissionMode = config.defaultPermissionMode ?? loadDefaultPermissionMode() ?? "acceptEdits";
|
|
5686
|
-
this.cacheDir = config.cacheDir ?? loadCacheDir() ??
|
|
6055
|
+
this.cacheDir = config.cacheDir ?? loadCacheDir() ?? join15(homedir7(), ".threadbase", "cache");
|
|
5687
6056
|
this.tailSize = config.tailSize ?? loadTailSize() ?? 10;
|
|
5688
6057
|
this.directoryDebounceMs = parseDirScanDebounceEnv(process.env.THREADBASE_DIR_SCAN_DEBOUNCE_MS) ?? config.directoryScanDebounceMs ?? 1e3;
|
|
5689
6058
|
this.markScannerStaleDebounced = debounce(() => {
|
|
@@ -5724,7 +6093,7 @@ var StreamerServer = class {
|
|
|
5724
6093
|
const seqs = cache.extendMessageIndex(
|
|
5725
6094
|
filePath,
|
|
5726
6095
|
spans,
|
|
5727
|
-
|
|
6096
|
+
statSync6(filePath),
|
|
5728
6097
|
readFrom,
|
|
5729
6098
|
endOffset
|
|
5730
6099
|
);
|
|
@@ -5809,6 +6178,9 @@ var StreamerServer = class {
|
|
|
5809
6178
|
onOutput: (sessionId, data) => {
|
|
5810
6179
|
this.wsHub.broadcast({ type: "terminal_output", sessionId, data });
|
|
5811
6180
|
},
|
|
6181
|
+
onUserMessage: (sessionId, text, ts) => {
|
|
6182
|
+
this.wsHub.broadcast({ type: "user_message", sessionId, text, ts });
|
|
6183
|
+
},
|
|
5812
6184
|
onPermissionChange: (sessionId, gate) => {
|
|
5813
6185
|
this.handlePermissionChange(sessionId, gate);
|
|
5814
6186
|
},
|
|
@@ -5885,7 +6257,7 @@ var StreamerServer = class {
|
|
|
5885
6257
|
temporalClient,
|
|
5886
6258
|
taskQueue: agentConfig.temporal.taskQueue
|
|
5887
6259
|
});
|
|
5888
|
-
const conversationsBaseDir = agentConfig.conversationsDir ||
|
|
6260
|
+
const conversationsBaseDir = agentConfig.conversationsDir || join15(dirname8(this.cacheDir), "conversations");
|
|
5889
6261
|
conversationWriter = createConversationWriter({
|
|
5890
6262
|
baseDir: conversationsBaseDir
|
|
5891
6263
|
});
|
|
@@ -5938,39 +6310,17 @@ var StreamerServer = class {
|
|
|
5938
6310
|
handlePairExchange: (req, res) => this.handlePairExchange(req, res),
|
|
5939
6311
|
handleBrowse: (url, res) => this.handleBrowse(url, res),
|
|
5940
6312
|
handleMkdir: (req, res) => this.handleMkdir(req, res),
|
|
5941
|
-
handleWsOpen: (ws
|
|
5942
|
-
|
|
5943
|
-
|
|
5944
|
-
|
|
6313
|
+
handleWsOpen: (ws) => {
|
|
6314
|
+
this.wsHub.addClient(ws);
|
|
6315
|
+
const sessions = this.sessionStore.list(this.ptyAttachedIds());
|
|
6316
|
+
ws.send(JSON.stringify({ type: "session_list", sessions }));
|
|
6317
|
+
if (this.cacheReady) {
|
|
6318
|
+
ws.send(JSON.stringify({ type: "cache_ready" }));
|
|
5945
6319
|
}
|
|
5946
|
-
const timer = setTimeout(() => {
|
|
5947
|
-
this.wsAuthPending.delete(ws);
|
|
5948
|
-
try {
|
|
5949
|
-
ws.close(WS_CLOSE_UNAUTHORIZED, "auth timeout");
|
|
5950
|
-
} catch {
|
|
5951
|
-
}
|
|
5952
|
-
}, this.wsAuthTimeoutMs);
|
|
5953
|
-
this.wsAuthPending.set(ws, timer);
|
|
5954
6320
|
},
|
|
5955
6321
|
handleWsMessage: async (ws, raw) => {
|
|
5956
6322
|
try {
|
|
5957
6323
|
const msg = JSON.parse(String(raw));
|
|
5958
|
-
if (!this.wsAuthed.has(ws)) {
|
|
5959
|
-
if (msg.type === "auth" && typeof msg.token === "string") {
|
|
5960
|
-
const t = this.wsAuthPending.get(ws);
|
|
5961
|
-
if (t) clearTimeout(t);
|
|
5962
|
-
this.wsAuthPending.delete(ws);
|
|
5963
|
-
if (validateApiKey(msg.token, this.apiKey)) {
|
|
5964
|
-
this.completeWsAuth(ws);
|
|
5965
|
-
} else {
|
|
5966
|
-
try {
|
|
5967
|
-
ws.close(WS_CLOSE_UNAUTHORIZED, "unauthorized");
|
|
5968
|
-
} catch {
|
|
5969
|
-
}
|
|
5970
|
-
}
|
|
5971
|
-
}
|
|
5972
|
-
return;
|
|
5973
|
-
}
|
|
5974
6324
|
if (msg.type === "register" && typeof msg.clientId === "string") {
|
|
5975
6325
|
const oldClientId = this.wsToClientId.get(ws);
|
|
5976
6326
|
if (oldClientId) this.clientIdToWs.delete(oldClientId);
|
|
@@ -5981,24 +6331,56 @@ var StreamerServer = class {
|
|
|
5981
6331
|
this.addSessionSubscriber(msg.sessionId, ws);
|
|
5982
6332
|
if (this.ptyManager.hasSession(msg.sessionId)) {
|
|
5983
6333
|
const lines = await this.ptyManager.getOutputLines(msg.sessionId, 200);
|
|
5984
|
-
|
|
6334
|
+
const userMessages = this.ptyManager.getInputHistory(msg.sessionId);
|
|
6335
|
+
ws.send(
|
|
6336
|
+
JSON.stringify({
|
|
6337
|
+
type: "terminal_replay",
|
|
6338
|
+
sessionId: msg.sessionId,
|
|
6339
|
+
lines,
|
|
6340
|
+
userMessages
|
|
6341
|
+
})
|
|
6342
|
+
);
|
|
6343
|
+
}
|
|
6344
|
+
const pendingGate = this.pendingPermission.get(msg.sessionId);
|
|
6345
|
+
if (pendingGate) {
|
|
6346
|
+
this.log.info(`[ws.replay_permission] ${msg.sessionId.slice(0, 8)}`, {
|
|
6347
|
+
event: "ws.replay_permission",
|
|
6348
|
+
sessionId: msg.sessionId
|
|
6349
|
+
});
|
|
6350
|
+
ws.send(
|
|
6351
|
+
JSON.stringify({
|
|
6352
|
+
type: "permission",
|
|
6353
|
+
sessionId: msg.sessionId,
|
|
6354
|
+
...pendingGate.prompt ? { prompt: pendingGate.prompt } : {},
|
|
6355
|
+
...pendingGate.detail ? { detail: pendingGate.detail } : {},
|
|
6356
|
+
options: pendingGate.options,
|
|
6357
|
+
...pendingGate.cursor !== void 0 ? { cursor: pendingGate.cursor } : {}
|
|
6358
|
+
})
|
|
6359
|
+
);
|
|
6360
|
+
}
|
|
6361
|
+
const pendingQuestion = this.pendingQuestions.get(msg.sessionId);
|
|
6362
|
+
if (pendingQuestion) {
|
|
6363
|
+
this.log.info(`[ws.replay_question] ${msg.sessionId.slice(0, 8)}`, {
|
|
6364
|
+
event: "ws.replay_question",
|
|
6365
|
+
sessionId: msg.sessionId
|
|
6366
|
+
});
|
|
6367
|
+
ws.send(
|
|
6368
|
+
JSON.stringify({
|
|
6369
|
+
type: "question",
|
|
6370
|
+
sessionId: msg.sessionId,
|
|
6371
|
+
toolUseId: pendingQuestion.toolUseId,
|
|
6372
|
+
questions: pendingQuestion.questions
|
|
6373
|
+
})
|
|
6374
|
+
);
|
|
5985
6375
|
}
|
|
5986
6376
|
}
|
|
5987
6377
|
if (msg.type === "hold_session" && typeof msg.sessionId === "string") {
|
|
5988
|
-
|
|
5989
|
-
this.startGraceTimer(msg.sessionId, 0);
|
|
5990
|
-
}
|
|
6378
|
+
this.startGraceTimer(msg.sessionId, 0);
|
|
5991
6379
|
}
|
|
5992
6380
|
} catch {
|
|
5993
6381
|
}
|
|
5994
6382
|
},
|
|
5995
6383
|
handleWsClose: (ws) => {
|
|
5996
|
-
const pendingTimer = this.wsAuthPending.get(ws);
|
|
5997
|
-
if (pendingTimer) {
|
|
5998
|
-
clearTimeout(pendingTimer);
|
|
5999
|
-
this.wsAuthPending.delete(ws);
|
|
6000
|
-
}
|
|
6001
|
-
this.wsAuthed.delete(ws);
|
|
6002
6384
|
const clientId = this.wsToClientId.get(ws);
|
|
6003
6385
|
if (clientId) {
|
|
6004
6386
|
this.clientIdToWs.delete(clientId);
|
|
@@ -6068,20 +6450,6 @@ var StreamerServer = class {
|
|
|
6068
6450
|
this.wsHub.broadcast(payload);
|
|
6069
6451
|
}
|
|
6070
6452
|
}
|
|
6071
|
-
// M1: finalize a WebSocket auth (via ?key= at upgrade or a first-message
|
|
6072
|
-
// handshake) — register it with the hub and send the initial snapshot. Only
|
|
6073
|
-
// authed sockets reach this, so no unauthenticated client ever receives a
|
|
6074
|
-
// broadcast.
|
|
6075
|
-
// Plan: https://github.com/RonenMars/threadbase-streamer/blob/a251353bfa417bd48ce3f15086bc336a2c622629/docs/plans/2026-06-24-security-hardening.md#L40
|
|
6076
|
-
completeWsAuth(ws) {
|
|
6077
|
-
this.wsAuthed.add(ws);
|
|
6078
|
-
this.wsHub.addClient(ws);
|
|
6079
|
-
const sessions = this.sessionStore.list(this.ptyAttachedIds());
|
|
6080
|
-
ws.send(JSON.stringify({ type: "session_list", sessions }));
|
|
6081
|
-
if (this.cacheReady) {
|
|
6082
|
-
ws.send(JSON.stringify({ type: "cache_ready" }));
|
|
6083
|
-
}
|
|
6084
|
-
}
|
|
6085
6453
|
addSessionSubscriber(sessionId, ws) {
|
|
6086
6454
|
let subs = this.sessionSubscribers.get(sessionId);
|
|
6087
6455
|
if (!subs) {
|
|
@@ -6153,7 +6521,7 @@ var StreamerServer = class {
|
|
|
6153
6521
|
});
|
|
6154
6522
|
try {
|
|
6155
6523
|
this.cache = ConversationCache.open(
|
|
6156
|
-
|
|
6524
|
+
join15(this.cacheDir, "cache.db"),
|
|
6157
6525
|
this.tailSize,
|
|
6158
6526
|
void 0,
|
|
6159
6527
|
{
|
|
@@ -6179,11 +6547,11 @@ var StreamerServer = class {
|
|
|
6179
6547
|
if (this.scanProfiles && this.scanProfiles.length > 0) {
|
|
6180
6548
|
for (const profile of this.scanProfiles) {
|
|
6181
6549
|
if (profile.enabled) {
|
|
6182
|
-
this.fileWatcher.watchDirectory(
|
|
6550
|
+
this.fileWatcher.watchDirectory(join15(profile.configDir, "projects"));
|
|
6183
6551
|
}
|
|
6184
6552
|
}
|
|
6185
6553
|
} else {
|
|
6186
|
-
this.fileWatcher.watchDirectory(
|
|
6554
|
+
this.fileWatcher.watchDirectory(join15(homedir7(), ".claude", "projects"));
|
|
6187
6555
|
}
|
|
6188
6556
|
} catch (err) {
|
|
6189
6557
|
const message = err instanceof Error ? err.message : String(err);
|
|
@@ -6368,9 +6736,6 @@ var StreamerServer = class {
|
|
|
6368
6736
|
this.ptyManager.dispose();
|
|
6369
6737
|
this.fileWatcher.dispose();
|
|
6370
6738
|
this.wsHub.dispose();
|
|
6371
|
-
for (const timer of this.wsAuthPending.values()) clearTimeout(timer);
|
|
6372
|
-
this.wsAuthPending.clear();
|
|
6373
|
-
this.wsAuthed.clear();
|
|
6374
6739
|
this.pairTokens.dispose();
|
|
6375
6740
|
if (this.dbPool) {
|
|
6376
6741
|
await this.dbPool.end();
|
|
@@ -6768,17 +7133,17 @@ var StreamerServer = class {
|
|
|
6768
7133
|
return scanner;
|
|
6769
7134
|
}
|
|
6770
7135
|
findJsonlPath(uuid) {
|
|
6771
|
-
const projectsDir =
|
|
6772
|
-
if (!
|
|
7136
|
+
const projectsDir = join15(homedir7(), ".claude", "projects");
|
|
7137
|
+
if (!existsSync8(projectsDir)) return null;
|
|
6773
7138
|
const filename = `${uuid}.jsonl`;
|
|
6774
7139
|
for (const dir of readdirSync4(projectsDir)) {
|
|
6775
|
-
const fp =
|
|
6776
|
-
if (
|
|
6777
|
-
const projectDir =
|
|
7140
|
+
const fp = join15(projectsDir, dir, filename);
|
|
7141
|
+
if (existsSync8(fp)) return fp;
|
|
7142
|
+
const projectDir = join15(projectsDir, dir);
|
|
6778
7143
|
try {
|
|
6779
7144
|
for (const sub of readdirSync4(projectDir)) {
|
|
6780
|
-
const subagentPath =
|
|
6781
|
-
if (
|
|
7145
|
+
const subagentPath = join15(projectDir, sub, "subagents", filename);
|
|
7146
|
+
if (existsSync8(subagentPath)) return subagentPath;
|
|
6782
7147
|
}
|
|
6783
7148
|
} catch {
|
|
6784
7149
|
}
|
|
@@ -6925,7 +7290,7 @@ var StreamerServer = class {
|
|
|
6925
7290
|
if (!conv.filePath) return false;
|
|
6926
7291
|
let mtimeMs = null;
|
|
6927
7292
|
try {
|
|
6928
|
-
mtimeMs =
|
|
7293
|
+
mtimeMs = statSync6(conv.filePath).mtimeMs;
|
|
6929
7294
|
} catch {
|
|
6930
7295
|
return false;
|
|
6931
7296
|
}
|
|
@@ -7293,7 +7658,7 @@ var StreamerServer = class {
|
|
|
7293
7658
|
handleGetSession(sessionId, res) {
|
|
7294
7659
|
const session = this.sessionStore.get(sessionId, this.ptyAttachedIds());
|
|
7295
7660
|
if (session) {
|
|
7296
|
-
if (!
|
|
7661
|
+
if (!existsSync8(session.projectPath)) {
|
|
7297
7662
|
session.failureReason = `Project directory not found: ${session.projectPath}`;
|
|
7298
7663
|
}
|
|
7299
7664
|
json(res, 200, session);
|
|
@@ -7497,16 +7862,17 @@ var StreamerServer = class {
|
|
|
7497
7862
|
return;
|
|
7498
7863
|
}
|
|
7499
7864
|
this.pendingPermission.set(sessionId, gate);
|
|
7500
|
-
const
|
|
7501
|
-
|
|
7502
|
-
|
|
7503
|
-
|
|
7865
|
+
const subscriberCount = this.sessionSubscribers.get(sessionId)?.size ?? 0;
|
|
7866
|
+
this.log.info(
|
|
7867
|
+
`[ws.broadcast_permission] ${sessionId.slice(0, 8)} subscribers=${subscriberCount}`,
|
|
7868
|
+
{ event: "ws.broadcast_permission", sessionId, subscriberCount }
|
|
7869
|
+
);
|
|
7504
7870
|
this.wsHub.broadcast({
|
|
7505
7871
|
type: "permission",
|
|
7506
7872
|
sessionId,
|
|
7507
7873
|
...gate.prompt ? { prompt: gate.prompt } : {},
|
|
7508
7874
|
...gate.detail ? { detail: gate.detail } : {},
|
|
7509
|
-
options:
|
|
7875
|
+
options: gate.options,
|
|
7510
7876
|
...gate.cursor !== void 0 ? { cursor: gate.cursor } : {}
|
|
7511
7877
|
});
|
|
7512
7878
|
}
|
|
@@ -7693,7 +8059,7 @@ var StreamerServer = class {
|
|
|
7693
8059
|
sessionStore: this.sessionStore,
|
|
7694
8060
|
// biome-ignore lint/style/noNonNullAssertion: agentClient is set when agentConfig.enabled is true
|
|
7695
8061
|
agentClient: this.agentClient,
|
|
7696
|
-
conversationsDir: this.cacheDir ?
|
|
8062
|
+
conversationsDir: this.cacheDir ? join15(dirname8(this.cacheDir), "conversations") : "",
|
|
7697
8063
|
agentConfig: this.agentConfig
|
|
7698
8064
|
});
|
|
7699
8065
|
json(res, result.status, result.body);
|
|
@@ -7834,9 +8200,9 @@ var StreamerServer = class {
|
|
|
7834
8200
|
// was passed to Claude via --session-id so the filename matches from the start.
|
|
7835
8201
|
watchForJsonl(sessionId, projectPath) {
|
|
7836
8202
|
const encoded = projectPath.replace(/[/\\:.]/g, "-");
|
|
7837
|
-
const projectsDir =
|
|
8203
|
+
const projectsDir = join15(homedir7(), ".claude", "projects", encoded);
|
|
7838
8204
|
const expectedFile = `${sessionId}.jsonl`;
|
|
7839
|
-
const filePath =
|
|
8205
|
+
const filePath = join15(projectsDir, expectedFile);
|
|
7840
8206
|
const deadline = Date.now() + 12e4;
|
|
7841
8207
|
let watcher = null;
|
|
7842
8208
|
const cleanup = () => {
|
|
@@ -7854,12 +8220,12 @@ var StreamerServer = class {
|
|
|
7854
8220
|
cleanup();
|
|
7855
8221
|
return;
|
|
7856
8222
|
}
|
|
7857
|
-
let resolvedFilePath =
|
|
7858
|
-
if (!resolvedFilePath &&
|
|
8223
|
+
let resolvedFilePath = existsSync8(filePath) ? filePath : null;
|
|
8224
|
+
if (!resolvedFilePath && existsSync8(projectsDir)) {
|
|
7859
8225
|
try {
|
|
7860
8226
|
const now = Date.now();
|
|
7861
|
-
const recent = readdirSync4(projectsDir).filter((f) => f.endsWith(".jsonl")).map((f) => ({ f, mtime:
|
|
7862
|
-
if (recent) resolvedFilePath =
|
|
8227
|
+
const recent = readdirSync4(projectsDir).filter((f) => f.endsWith(".jsonl")).map((f) => ({ f, mtime: statSync6(join15(projectsDir, f)).mtimeMs })).filter(({ mtime }) => now - mtime < 5e3).sort((a, b) => b.mtime - a.mtime)[0];
|
|
8228
|
+
if (recent) resolvedFilePath = join15(projectsDir, recent.f);
|
|
7863
8229
|
} catch {
|
|
7864
8230
|
}
|
|
7865
8231
|
}
|
|
@@ -7868,7 +8234,7 @@ var StreamerServer = class {
|
|
|
7868
8234
|
this.sessionFileMap.set(sessionId, resolvedFilePath);
|
|
7869
8235
|
this.fileWatcher.watch(resolvedFilePath);
|
|
7870
8236
|
try {
|
|
7871
|
-
const existing =
|
|
8237
|
+
const existing = readFileSync7(resolvedFilePath, "utf8").split("\n").filter(Boolean);
|
|
7872
8238
|
if (existing.length > 0) {
|
|
7873
8239
|
this.broadcastConversationLines(sessionId, existing);
|
|
7874
8240
|
}
|
|
@@ -7907,7 +8273,7 @@ var StreamerServer = class {
|
|
|
7907
8273
|
watchForCodexRollout(sessionId, projectPath) {
|
|
7908
8274
|
const deadline = Date.now() + 12e4;
|
|
7909
8275
|
const now = /* @__PURE__ */ new Date();
|
|
7910
|
-
const dateDir =
|
|
8276
|
+
const dateDir = join15(
|
|
7911
8277
|
String(now.getFullYear()),
|
|
7912
8278
|
String(now.getMonth() + 1).padStart(2, "0"),
|
|
7913
8279
|
String(now.getDate()).padStart(2, "0")
|
|
@@ -7920,7 +8286,7 @@ var StreamerServer = class {
|
|
|
7920
8286
|
};
|
|
7921
8287
|
const matchesProjectPath = (candidatePath) => {
|
|
7922
8288
|
try {
|
|
7923
|
-
const firstLine =
|
|
8289
|
+
const firstLine = readFileSync7(candidatePath, "utf8").split("\n", 1)[0];
|
|
7924
8290
|
if (!firstLine) return null;
|
|
7925
8291
|
const parsed = JSON.parse(firstLine);
|
|
7926
8292
|
if (parsed?.type !== "session_meta") return null;
|
|
@@ -7948,8 +8314,8 @@ var StreamerServer = class {
|
|
|
7948
8314
|
this.sessionStore.listManaged().filter((s) => s.id !== sessionId && s.boundConversationId != null).map((s) => s.boundConversationId)
|
|
7949
8315
|
);
|
|
7950
8316
|
for (const root of this.codexRoots) {
|
|
7951
|
-
const sessionsDir =
|
|
7952
|
-
if (!
|
|
8317
|
+
const sessionsDir = join15(root, dateDir);
|
|
8318
|
+
if (!existsSync8(sessionsDir)) continue;
|
|
7953
8319
|
let candidateFiles;
|
|
7954
8320
|
try {
|
|
7955
8321
|
candidateFiles = readdirSync4(sessionsDir).filter((f) => f.endsWith(".jsonl"));
|
|
@@ -7957,9 +8323,9 @@ var StreamerServer = class {
|
|
|
7957
8323
|
continue;
|
|
7958
8324
|
}
|
|
7959
8325
|
const nowMs = Date.now();
|
|
7960
|
-
const recentCandidates = candidateFiles.map((f) => ({ f, mtime:
|
|
8326
|
+
const recentCandidates = candidateFiles.map((f) => ({ f, mtime: statSync6(join15(sessionsDir, f)).mtimeMs })).filter(({ mtime }) => nowMs - mtime < 1e4).sort((a, b) => b.mtime - a.mtime);
|
|
7961
8327
|
for (const { f } of recentCandidates) {
|
|
7962
|
-
const candidatePath =
|
|
8328
|
+
const candidatePath = join15(sessionsDir, f);
|
|
7963
8329
|
const match = matchesProjectPath(candidatePath);
|
|
7964
8330
|
if (!match) continue;
|
|
7965
8331
|
if (boundElsewhere.has(match.id)) continue;
|
|
@@ -7969,7 +8335,7 @@ var StreamerServer = class {
|
|
|
7969
8335
|
this.sessionFileMap.set(sessionId, candidatePath);
|
|
7970
8336
|
this.fileWatcher.watch(candidatePath);
|
|
7971
8337
|
try {
|
|
7972
|
-
const existing =
|
|
8338
|
+
const existing = readFileSync7(candidatePath, "utf8").split("\n").filter(Boolean);
|
|
7973
8339
|
if (existing.length > 0) {
|
|
7974
8340
|
this.broadcastConversationLines(sessionId, existing);
|
|
7975
8341
|
}
|
|
@@ -8091,7 +8457,7 @@ var StreamerServer = class {
|
|
|
8091
8457
|
};
|
|
8092
8458
|
function classifyResumability(cwd) {
|
|
8093
8459
|
if (!cwd) return { resumable: true };
|
|
8094
|
-
if (
|
|
8460
|
+
if (existsSync8(cwd)) return { resumable: true };
|
|
8095
8461
|
const ranInWorktree = /\/\.worktrees\//.test(cwd) || /\/\.claude\/worktrees\//.test(cwd);
|
|
8096
8462
|
return {
|
|
8097
8463
|
resumable: false,
|