@threadbase-sh/streamer 1.29.2 → 1.30.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/cli.cjs +801 -426
- package/dist/cli.cjs.map +1 -1
- package/dist/index.cjs +579 -195
- package/dist/index.cjs.map +1 -1
- package/dist/index.js +562 -178
- package/dist/index.js.map +1 -1
- package/package.json +1 -1
package/dist/index.js
CHANGED
|
@@ -680,6 +680,69 @@ function isProviderResumable(_provider, availabilityResumable) {
|
|
|
680
680
|
return availabilityResumable;
|
|
681
681
|
}
|
|
682
682
|
|
|
683
|
+
// src/services/questions/codexGateAnswers.ts
|
|
684
|
+
import { mkdirSync as mkdirSync2, readFileSync as readFileSync3, writeFileSync as writeFileSync2 } from "fs";
|
|
685
|
+
import { homedir as homedir3 } from "os";
|
|
686
|
+
import { dirname as dirname3, join as join5 } from "path";
|
|
687
|
+
function gateAnswersPath() {
|
|
688
|
+
const dir = process.env.THREADBASE_CONFIG_DIR ?? join5(homedir3(), ".threadbase");
|
|
689
|
+
return join5(dir, "gate-answers.json");
|
|
690
|
+
}
|
|
691
|
+
function loadGateAnswers() {
|
|
692
|
+
try {
|
|
693
|
+
const parsed = JSON.parse(readFileSync3(gateAnswersPath(), "utf-8"));
|
|
694
|
+
return parsed && typeof parsed === "object" ? parsed : {};
|
|
695
|
+
} catch {
|
|
696
|
+
return {};
|
|
697
|
+
}
|
|
698
|
+
}
|
|
699
|
+
function saveGateAnswer(key, value) {
|
|
700
|
+
const path = gateAnswersPath();
|
|
701
|
+
mkdirSync2(dirname3(path), { recursive: true });
|
|
702
|
+
writeFileSync2(path, `${JSON.stringify({ ...loadGateAnswers(), [key]: value }, null, 2)}
|
|
703
|
+
`);
|
|
704
|
+
}
|
|
705
|
+
function rememberedGateDigit(gate) {
|
|
706
|
+
const answers = loadGateAnswers();
|
|
707
|
+
if (gate === "hooks") {
|
|
708
|
+
if (answers.codexHooksGate === "trust_all") return "2";
|
|
709
|
+
if (answers.codexHooksGate === "continue_untrusted") return "3";
|
|
710
|
+
return null;
|
|
711
|
+
}
|
|
712
|
+
return answers.codexTrustGate === "yes" ? "1" : null;
|
|
713
|
+
}
|
|
714
|
+
|
|
715
|
+
// src/utils/debounce.ts
|
|
716
|
+
function debounce(fn, waitMs) {
|
|
717
|
+
let timer = null;
|
|
718
|
+
let lastArgs = null;
|
|
719
|
+
const run2 = () => {
|
|
720
|
+
timer = null;
|
|
721
|
+
if (lastArgs) {
|
|
722
|
+
const args = lastArgs;
|
|
723
|
+
lastArgs = null;
|
|
724
|
+
fn(...args);
|
|
725
|
+
}
|
|
726
|
+
};
|
|
727
|
+
const debounced = (...args) => {
|
|
728
|
+
lastArgs = args;
|
|
729
|
+
if (timer) clearTimeout(timer);
|
|
730
|
+
timer = setTimeout(run2, waitMs);
|
|
731
|
+
};
|
|
732
|
+
debounced.cancel = () => {
|
|
733
|
+
if (timer) clearTimeout(timer);
|
|
734
|
+
timer = null;
|
|
735
|
+
lastArgs = null;
|
|
736
|
+
};
|
|
737
|
+
debounced.flush = () => {
|
|
738
|
+
if (timer) {
|
|
739
|
+
clearTimeout(timer);
|
|
740
|
+
run2();
|
|
741
|
+
}
|
|
742
|
+
};
|
|
743
|
+
return debounced;
|
|
744
|
+
}
|
|
745
|
+
|
|
683
746
|
// src/codex-pty-runner.ts
|
|
684
747
|
var OUTPUT_BUFFER_MAX = 65536;
|
|
685
748
|
var PTY_COLS = 120;
|
|
@@ -687,6 +750,9 @@ var PTY_ROWS = 40;
|
|
|
687
750
|
var SCREEN_SCROLLBACK = 1e3;
|
|
688
751
|
var CODEX_PROMPT_READY_TEXT = "Ready";
|
|
689
752
|
var CODEX_TRUST_GATE_REGEX = /trust the contents/i;
|
|
753
|
+
var CODEX_HOOKS_GATE_REGEX = /hooks need review/i;
|
|
754
|
+
var QUIET_DETECT_MS = 500;
|
|
755
|
+
var CODEX_READY_FALLBACK_MS = 8e3;
|
|
690
756
|
var SUBMIT_BYTES = "\r";
|
|
691
757
|
var CODEX_SUBMIT_DELAY_MS = 16;
|
|
692
758
|
function digestBytes(s) {
|
|
@@ -694,6 +760,40 @@ function digestBytes(s) {
|
|
|
694
760
|
if (escaped.length <= 200) return escaped;
|
|
695
761
|
return `${escaped.slice(0, 100)}\u2026[${escaped.length - 200}B omitted]\u2026${escaped.slice(-100)}`;
|
|
696
762
|
}
|
|
763
|
+
function gateCard(gate, lines) {
|
|
764
|
+
if (gate === "hooks") {
|
|
765
|
+
const countLine = lines.find((l) => /new or changed/i.test(l))?.trim();
|
|
766
|
+
return {
|
|
767
|
+
prompt: [
|
|
768
|
+
"Hooks need review",
|
|
769
|
+
countLine,
|
|
770
|
+
"Hooks can run outside the sandbox after you trust them."
|
|
771
|
+
].filter(Boolean).join(" \u2014 "),
|
|
772
|
+
options: [
|
|
773
|
+
{ index: 2, label: "Trust all and continue", answerKeys: "2\r" },
|
|
774
|
+
{ index: 3, label: "Continue without trusting (hooks won't run)", answerKeys: "3\r" },
|
|
775
|
+
{
|
|
776
|
+
index: 4,
|
|
777
|
+
label: "Trust all and continue (remember for all projects)",
|
|
778
|
+
answerKeys: "4\r"
|
|
779
|
+
},
|
|
780
|
+
{
|
|
781
|
+
index: 5,
|
|
782
|
+
label: "Continue without trusting (remember for all projects)",
|
|
783
|
+
answerKeys: "5\r"
|
|
784
|
+
}
|
|
785
|
+
]
|
|
786
|
+
};
|
|
787
|
+
}
|
|
788
|
+
return {
|
|
789
|
+
prompt: lines.find((l) => CODEX_TRUST_GATE_REGEX.test(l))?.trim() ?? "Do you trust the contents of this directory?",
|
|
790
|
+
options: [
|
|
791
|
+
{ index: 1, label: "Yes, continue", answerKeys: "1\r" },
|
|
792
|
+
{ index: 2, label: "No, quit", answerKeys: "2\r" },
|
|
793
|
+
{ index: 3, label: "Yes, continue (remember for all projects)", answerKeys: "3\r" }
|
|
794
|
+
]
|
|
795
|
+
};
|
|
796
|
+
}
|
|
697
797
|
var pty = null;
|
|
698
798
|
async function loadPty() {
|
|
699
799
|
if (pty) return pty;
|
|
@@ -720,8 +820,8 @@ var CodexPtyRunner = class {
|
|
|
720
820
|
onOutput;
|
|
721
821
|
onStatusChange;
|
|
722
822
|
onReady;
|
|
723
|
-
//
|
|
724
|
-
//
|
|
823
|
+
// Broadcasts Codex's blocking startup gates (directory trust, hooks review)
|
|
824
|
+
// as question cards; null dismisses the card once the gate leaves the screen.
|
|
725
825
|
onPermissionChange;
|
|
726
826
|
onLiveQuestion;
|
|
727
827
|
onLiveQuestionGone;
|
|
@@ -732,8 +832,18 @@ var CodexPtyRunner = class {
|
|
|
732
832
|
// Inputs received via sendInput() while the session was still pendingReady.
|
|
733
833
|
// Flushed in arrival order once Codex reaches Ready.
|
|
734
834
|
queuedInputs = /* @__PURE__ */ new Map();
|
|
735
|
-
//
|
|
736
|
-
|
|
835
|
+
// Gate currently on a session's screen (card broadcast, unanswered). While
|
|
836
|
+
// set, queued-input flushes are held — a flushed digit would CONFIRM a
|
|
837
|
+
// dialog option — and sendKeys() intercepts remember-variant digits.
|
|
838
|
+
openGate = /* @__PURE__ */ new Map();
|
|
839
|
+
// `${sessionId}:${gate}` once a gate has been actioned (auto-answered or
|
|
840
|
+
// card broadcast) — dedupes repaints of the same dialog.
|
|
841
|
+
gateActioned = /* @__PURE__ */ new Set();
|
|
842
|
+
// Per-session trailing debounce re-armed on every chunk; on quiet, re-runs
|
|
843
|
+
// screen detection so a blocked/truncated boot still reaches ready.
|
|
844
|
+
quietCheckers = /* @__PURE__ */ new Map();
|
|
845
|
+
// Per-session flat backstop from spawn (CODEX_READY_FALLBACK_MS).
|
|
846
|
+
readyFallbackTimers = /* @__PURE__ */ new Map();
|
|
737
847
|
// In-flight start()/startFresh() calls keyed by sessionId. A second
|
|
738
848
|
// concurrent resume for the same session (double-tap, client retry) awaits
|
|
739
849
|
// the first call's promise instead of spawning a duplicate PTY (CRITICAL #3).
|
|
@@ -792,6 +902,7 @@ var CodexPtyRunner = class {
|
|
|
792
902
|
};
|
|
793
903
|
this.sessions.set(sessionId, session);
|
|
794
904
|
this.pendingReady.add(sessionId);
|
|
905
|
+
this.armReadyFallback(sessionId);
|
|
795
906
|
proc.onData((data) => {
|
|
796
907
|
this.handleOutput(sessionId, data);
|
|
797
908
|
});
|
|
@@ -837,6 +948,7 @@ var CodexPtyRunner = class {
|
|
|
837
948
|
};
|
|
838
949
|
this.sessions.set(sessionId, session);
|
|
839
950
|
this.pendingReady.add(sessionId);
|
|
951
|
+
this.armReadyFallback(sessionId);
|
|
840
952
|
proc.onData((data) => {
|
|
841
953
|
this.handleOutput(sessionId, data);
|
|
842
954
|
});
|
|
@@ -846,6 +958,21 @@ var CodexPtyRunner = class {
|
|
|
846
958
|
});
|
|
847
959
|
return toPublicSession(session);
|
|
848
960
|
}
|
|
961
|
+
// Flat backstop: if neither the "Ready" marker nor the quiet-checker settled
|
|
962
|
+
// the session within CODEX_READY_FALLBACK_MS of spawn, mark it ready anyway
|
|
963
|
+
// so start requests resolve and mobile can watch the boot live. unref() so a
|
|
964
|
+
// pending timer never holds the process open.
|
|
965
|
+
armReadyFallback(sessionId) {
|
|
966
|
+
const timer = setTimeout(() => {
|
|
967
|
+
this.readyFallbackTimers.delete(sessionId);
|
|
968
|
+
const session = this.sessions.get(sessionId);
|
|
969
|
+
if (session?.status === "running" && this.pendingReady.has(sessionId)) {
|
|
970
|
+
this.markReady(sessionId, session, "fallback:timeout");
|
|
971
|
+
}
|
|
972
|
+
}, CODEX_READY_FALLBACK_MS);
|
|
973
|
+
timer.unref?.();
|
|
974
|
+
this.readyFallbackTimers.set(sessionId, timer);
|
|
975
|
+
}
|
|
849
976
|
// Write raw key bytes directly to the PTY, same as PTYManager.sendKeys.
|
|
850
977
|
sendKeys(sessionId, keys) {
|
|
851
978
|
const session = this.sessions.get(sessionId);
|
|
@@ -857,13 +984,47 @@ var CodexPtyRunner = class {
|
|
|
857
984
|
session.status = "running";
|
|
858
985
|
this.onStatusChange?.(toPublicSession(session));
|
|
859
986
|
}
|
|
987
|
+
const gate = this.openGate.get(sessionId);
|
|
988
|
+
const digit = gate ? /^([0-9])\r?$/.exec(keys)?.[1] : void 0;
|
|
989
|
+
const out = gate && digit ? this.resolveGateAnswer(sessionId, gate, digit) : keys;
|
|
860
990
|
this.log.info(
|
|
861
|
-
`[codex.keys.write] ${sessionId.slice(0, 8)} bytes=${
|
|
862
|
-
{ event: "codex.keys_write", sessionId, byteLen:
|
|
991
|
+
`[codex.keys.write] ${sessionId.slice(0, 8)} bytes=${out.length} digest=${digestBytes(out)}`,
|
|
992
|
+
{ event: "codex.keys_write", sessionId, byteLen: out.length }
|
|
863
993
|
);
|
|
864
|
-
session.process.write(
|
|
994
|
+
session.process.write(out);
|
|
865
995
|
session.lastActivityAt = /* @__PURE__ */ new Date();
|
|
866
996
|
}
|
|
997
|
+
// Map a gate-card digit to the PTY bytes that answer the real dialog,
|
|
998
|
+
// persisting the choice when the digit was a synthetic "remember for all
|
|
999
|
+
// projects" option (those numbers don't exist on the actual dialog and must
|
|
1000
|
+
// never reach codex). The trailing \r mobile sends is dropped: a digit alone
|
|
1001
|
+
// selects AND confirms (live-probe verified), and a stray Enter would land
|
|
1002
|
+
// on whatever screen follows.
|
|
1003
|
+
resolveGateAnswer(sessionId, gate, digit) {
|
|
1004
|
+
let real = digit;
|
|
1005
|
+
let remembered = false;
|
|
1006
|
+
if (gate === "hooks" && digit === "4") {
|
|
1007
|
+
saveGateAnswer("codexHooksGate", "trust_all");
|
|
1008
|
+
real = "2";
|
|
1009
|
+
remembered = true;
|
|
1010
|
+
} else if (gate === "hooks" && digit === "5") {
|
|
1011
|
+
saveGateAnswer("codexHooksGate", "continue_untrusted");
|
|
1012
|
+
real = "3";
|
|
1013
|
+
remembered = true;
|
|
1014
|
+
} else if (gate === "trust" && digit === "3") {
|
|
1015
|
+
saveGateAnswer("codexTrustGate", "yes");
|
|
1016
|
+
real = "1";
|
|
1017
|
+
remembered = true;
|
|
1018
|
+
}
|
|
1019
|
+
this.log.info(`[codex.gate_answer] ${sessionId.slice(0, 8)} ${gate} digit=${real}`, {
|
|
1020
|
+
event: "codex.gate_answer",
|
|
1021
|
+
sessionId,
|
|
1022
|
+
gate,
|
|
1023
|
+
digit: real,
|
|
1024
|
+
remembered
|
|
1025
|
+
});
|
|
1026
|
+
return real;
|
|
1027
|
+
}
|
|
867
1028
|
sendInput(sessionId, input) {
|
|
868
1029
|
const session = this.sessions.get(sessionId);
|
|
869
1030
|
if (!session) throw new Error(`Session not found: ${sessionId}`);
|
|
@@ -933,8 +1094,12 @@ var CodexPtyRunner = class {
|
|
|
933
1094
|
}, CODEX_SUBMIT_DELAY_MS);
|
|
934
1095
|
}
|
|
935
1096
|
// Drain any inputs sent while the session was still pendingReady, writing
|
|
936
|
-
// them in arrival order now that Codex is Ready.
|
|
1097
|
+
// them in arrival order now that Codex is Ready. No-op while a gate dialog
|
|
1098
|
+
// is open (a flushed digit would confirm a dialog option) or while still
|
|
1099
|
+
// pendingReady (markReady drains it) — the gate-close path re-drives it for
|
|
1100
|
+
// the ready-with-gate-open case.
|
|
937
1101
|
flushQueuedInputs(sessionId) {
|
|
1102
|
+
if (this.openGate.has(sessionId) || this.pendingReady.has(sessionId)) return;
|
|
938
1103
|
const queue = this.queuedInputs.get(sessionId);
|
|
939
1104
|
if (!queue || queue.length === 0) return;
|
|
940
1105
|
this.queuedInputs.delete(sessionId);
|
|
@@ -979,7 +1144,7 @@ var CodexPtyRunner = class {
|
|
|
979
1144
|
if (!session) return;
|
|
980
1145
|
this.pendingReady.delete(sessionId);
|
|
981
1146
|
this.queuedInputs.delete(sessionId);
|
|
982
|
-
this.
|
|
1147
|
+
this.clearSessionDetectors(sessionId);
|
|
983
1148
|
try {
|
|
984
1149
|
session.process.kill("SIGINT");
|
|
985
1150
|
} catch {
|
|
@@ -990,6 +1155,21 @@ var CodexPtyRunner = class {
|
|
|
990
1155
|
this.sessions.delete(sessionId);
|
|
991
1156
|
this.onStatusChange?.(toPublicSession(session));
|
|
992
1157
|
}
|
|
1158
|
+
// Drop a session's detection state: quiet-checker, ready-fallback timer,
|
|
1159
|
+
// gate bookkeeping — and dismiss a still-open gate card so mobile doesn't
|
|
1160
|
+
// keep rendering a question for a dead PTY.
|
|
1161
|
+
clearSessionDetectors(sessionId) {
|
|
1162
|
+
this.quietCheckers.get(sessionId)?.cancel();
|
|
1163
|
+
this.quietCheckers.delete(sessionId);
|
|
1164
|
+
const timer = this.readyFallbackTimers.get(sessionId);
|
|
1165
|
+
if (timer) clearTimeout(timer);
|
|
1166
|
+
this.readyFallbackTimers.delete(sessionId);
|
|
1167
|
+
if (this.openGate.delete(sessionId)) {
|
|
1168
|
+
this.onPermissionChange?.(sessionId, null);
|
|
1169
|
+
}
|
|
1170
|
+
this.gateActioned.delete(`${sessionId}:hooks`);
|
|
1171
|
+
this.gateActioned.delete(`${sessionId}:trust`);
|
|
1172
|
+
}
|
|
993
1173
|
getOutput(sessionId) {
|
|
994
1174
|
const session = this.sessions.get(sessionId);
|
|
995
1175
|
if (!session) throw new Error(`Session not found: ${sessionId}`);
|
|
@@ -1029,10 +1209,19 @@ var CodexPtyRunner = class {
|
|
|
1029
1209
|
}
|
|
1030
1210
|
session.screen.dispose();
|
|
1031
1211
|
}
|
|
1212
|
+
for (const sessionId of Array.from(this.quietCheckers.keys())) {
|
|
1213
|
+
this.clearSessionDetectors(sessionId);
|
|
1214
|
+
}
|
|
1215
|
+
for (const timer of this.readyFallbackTimers.values()) {
|
|
1216
|
+
clearTimeout(timer);
|
|
1217
|
+
}
|
|
1032
1218
|
this.sessions.clear();
|
|
1033
1219
|
this.pendingReady.clear();
|
|
1034
1220
|
this.queuedInputs.clear();
|
|
1035
|
-
this.
|
|
1221
|
+
this.openGate.clear();
|
|
1222
|
+
this.gateActioned.clear();
|
|
1223
|
+
this.quietCheckers.clear();
|
|
1224
|
+
this.readyFallbackTimers.clear();
|
|
1036
1225
|
}
|
|
1037
1226
|
handleOutput(sessionId, data) {
|
|
1038
1227
|
const session = this.sessions.get(sessionId);
|
|
@@ -1047,44 +1236,92 @@ var CodexPtyRunner = class {
|
|
|
1047
1236
|
session.screen.write(data);
|
|
1048
1237
|
session.lastOutput = stripAnsi(data);
|
|
1049
1238
|
this.onOutput?.(sessionId, data);
|
|
1050
|
-
this.
|
|
1239
|
+
this.detectScreenState(sessionId, "chunk").catch((err) => {
|
|
1051
1240
|
this.log.warn("[codex.ready_detect] failed", {
|
|
1052
1241
|
event: "codex.ready_detect_failed",
|
|
1053
1242
|
sessionId,
|
|
1054
1243
|
err
|
|
1055
1244
|
});
|
|
1056
1245
|
});
|
|
1246
|
+
let quiet = this.quietCheckers.get(sessionId);
|
|
1247
|
+
if (!quiet) {
|
|
1248
|
+
quiet = debounce(() => {
|
|
1249
|
+
this.detectScreenState(sessionId, "quiet").catch((err) => {
|
|
1250
|
+
this.log.warn("[codex.ready_detect] failed", {
|
|
1251
|
+
event: "codex.ready_detect_failed",
|
|
1252
|
+
sessionId,
|
|
1253
|
+
err
|
|
1254
|
+
});
|
|
1255
|
+
});
|
|
1256
|
+
}, QUIET_DETECT_MS);
|
|
1257
|
+
this.quietCheckers.set(sessionId, quiet);
|
|
1258
|
+
}
|
|
1259
|
+
quiet();
|
|
1057
1260
|
}
|
|
1058
|
-
// Renders the session's headless screen and
|
|
1059
|
-
//
|
|
1060
|
-
//
|
|
1061
|
-
//
|
|
1062
|
-
//
|
|
1063
|
-
|
|
1064
|
-
|
|
1261
|
+
// Renders the session's headless screen and drives both detections:
|
|
1262
|
+
// - Gates (directory trust, hooks review) — checked on EVERY pass,
|
|
1263
|
+
// independent of pendingReady, so a gate appearing after ready is still
|
|
1264
|
+
// surfaced and a gate leaving the screen closes its card.
|
|
1265
|
+
// - Readiness — the "Ready" status-bar marker while pendingReady, plus the
|
|
1266
|
+
// quiet path: after QUIET_DETECT_MS of PTY silence a still-pending
|
|
1267
|
+
// session is marked ready anyway (`›` alone is NOT a marker — Phase 0 —
|
|
1268
|
+
// but a quiet boot screen is more useful to the user live than a
|
|
1269
|
+
// spinner, and "Ready" may be truncated off the 120-col status bar).
|
|
1270
|
+
async detectScreenState(sessionId, trigger) {
|
|
1271
|
+
const session = this.sessions.get(sessionId);
|
|
1272
|
+
if (!session || session.status === "idle") return;
|
|
1065
1273
|
const lines = await this.getOutputLines(sessionId, PTY_ROWS);
|
|
1066
1274
|
const screenText = lines.join("\n");
|
|
1067
|
-
|
|
1068
|
-
|
|
1069
|
-
|
|
1070
|
-
|
|
1071
|
-
|
|
1072
|
-
|
|
1073
|
-
});
|
|
1074
|
-
session.process.write("\r");
|
|
1075
|
-
}
|
|
1076
|
-
return;
|
|
1275
|
+
const gate = CODEX_HOOKS_GATE_REGEX.test(screenText) ? "hooks" : CODEX_TRUST_GATE_REGEX.test(screenText) ? "trust" : null;
|
|
1276
|
+
if (gate) {
|
|
1277
|
+
this.handleGate(sessionId, session, gate, lines);
|
|
1278
|
+
} else if (this.openGate.delete(sessionId)) {
|
|
1279
|
+
this.onPermissionChange?.(sessionId, null);
|
|
1280
|
+
this.flushQueuedInputs(sessionId);
|
|
1077
1281
|
}
|
|
1282
|
+
if (session.status !== "running" || !this.pendingReady.has(sessionId)) return;
|
|
1078
1283
|
const lastNonBlank2 = [...lines].reverse().find((l) => l.trim() !== "") ?? "";
|
|
1079
|
-
if (
|
|
1080
|
-
|
|
1284
|
+
if (lastNonBlank2.includes(CODEX_PROMPT_READY_TEXT)) {
|
|
1285
|
+
this.markReady(sessionId, session, `marker:${CODEX_PROMPT_READY_TEXT}`);
|
|
1286
|
+
} else if (trigger === "quiet") {
|
|
1287
|
+
this.markReady(sessionId, session, "quiet:timeout");
|
|
1288
|
+
}
|
|
1081
1289
|
}
|
|
1082
|
-
|
|
1290
|
+
// Answer a gate from the persisted remember-store, or surface it as a
|
|
1291
|
+
// question card over the permission transport. Actioned once per session and
|
|
1292
|
+
// gate type — repaints of the same dialog neither re-write nor re-broadcast.
|
|
1293
|
+
handleGate(sessionId, session, gate, lines) {
|
|
1294
|
+
const key = `${sessionId}:${gate}`;
|
|
1295
|
+
if (this.gateActioned.has(key)) return;
|
|
1296
|
+
this.gateActioned.add(key);
|
|
1297
|
+
const remembered = rememberedGateDigit(gate);
|
|
1298
|
+
if (remembered) {
|
|
1299
|
+
this.log.info(`[codex.gate_auto_answer] ${sessionId.slice(0, 8)} ${gate} \u2192 ${remembered}`, {
|
|
1300
|
+
event: "codex.gate_auto_answer",
|
|
1301
|
+
sessionId,
|
|
1302
|
+
gate,
|
|
1303
|
+
digit: remembered
|
|
1304
|
+
});
|
|
1305
|
+
session.process.write(remembered);
|
|
1306
|
+
return;
|
|
1307
|
+
}
|
|
1308
|
+
this.openGate.set(sessionId, gate);
|
|
1309
|
+
const card = gateCard(gate, lines);
|
|
1310
|
+
this.log.info(`[codex.gate_prompt] ${sessionId.slice(0, 8)} ${gate}`, {
|
|
1311
|
+
event: "codex.gate_prompt",
|
|
1312
|
+
sessionId,
|
|
1313
|
+
gate,
|
|
1314
|
+
prompt: card.prompt
|
|
1315
|
+
});
|
|
1316
|
+
this.onPermissionChange?.(sessionId, card);
|
|
1317
|
+
}
|
|
1318
|
+
markReady(sessionId, session, reason) {
|
|
1083
1319
|
session.lastActivityAt = /* @__PURE__ */ new Date();
|
|
1084
1320
|
session.status = "waiting_input";
|
|
1085
|
-
this.log.info(`[codex.ready] ${sessionId.slice(0, 8)}`, {
|
|
1321
|
+
this.log.info(`[codex.ready] ${sessionId.slice(0, 8)} ${reason}`, {
|
|
1086
1322
|
event: "codex.ready",
|
|
1087
|
-
sessionId
|
|
1323
|
+
sessionId,
|
|
1324
|
+
reason
|
|
1088
1325
|
});
|
|
1089
1326
|
this.onStatusChange?.(toPublicSession(session));
|
|
1090
1327
|
if (this.pendingReady.has(sessionId)) {
|
|
@@ -1110,7 +1347,7 @@ var CodexPtyRunner = class {
|
|
|
1110
1347
|
session.screen.dispose();
|
|
1111
1348
|
this.sessions.delete(sessionId);
|
|
1112
1349
|
this.queuedInputs.delete(sessionId);
|
|
1113
|
-
this.
|
|
1350
|
+
this.clearSessionDetectors(sessionId);
|
|
1114
1351
|
}
|
|
1115
1352
|
};
|
|
1116
1353
|
function toPublicSession(s) {
|
|
@@ -1293,9 +1530,15 @@ function detectShellPrompt(lines) {
|
|
|
1293
1530
|
]
|
|
1294
1531
|
};
|
|
1295
1532
|
}
|
|
1296
|
-
|
|
1533
|
+
const lastNumberedIdx = (() => {
|
|
1534
|
+
for (let i = last.idx; i >= 0; i--) {
|
|
1535
|
+
if (NUMBERED_RE.test(lines[i])) return i;
|
|
1536
|
+
}
|
|
1537
|
+
return -1;
|
|
1538
|
+
})();
|
|
1539
|
+
if (lastNumberedIdx >= 0) {
|
|
1297
1540
|
const options = [];
|
|
1298
|
-
for (let i = 0; i <=
|
|
1541
|
+
for (let i = 0; i <= lastNumberedIdx; i++) {
|
|
1299
1542
|
const m = NUMBERED_RE.exec(lines[i]);
|
|
1300
1543
|
if (!m) continue;
|
|
1301
1544
|
const num = Number.parseInt(m[1], 10);
|
|
@@ -1323,37 +1566,6 @@ function detectShellPrompt(lines) {
|
|
|
1323
1566
|
return null;
|
|
1324
1567
|
}
|
|
1325
1568
|
|
|
1326
|
-
// src/utils/debounce.ts
|
|
1327
|
-
function debounce(fn, waitMs) {
|
|
1328
|
-
let timer = null;
|
|
1329
|
-
let lastArgs = null;
|
|
1330
|
-
const run2 = () => {
|
|
1331
|
-
timer = null;
|
|
1332
|
-
if (lastArgs) {
|
|
1333
|
-
const args = lastArgs;
|
|
1334
|
-
lastArgs = null;
|
|
1335
|
-
fn(...args);
|
|
1336
|
-
}
|
|
1337
|
-
};
|
|
1338
|
-
const debounced = (...args) => {
|
|
1339
|
-
lastArgs = args;
|
|
1340
|
-
if (timer) clearTimeout(timer);
|
|
1341
|
-
timer = setTimeout(run2, waitMs);
|
|
1342
|
-
};
|
|
1343
|
-
debounced.cancel = () => {
|
|
1344
|
-
if (timer) clearTimeout(timer);
|
|
1345
|
-
timer = null;
|
|
1346
|
-
lastArgs = null;
|
|
1347
|
-
};
|
|
1348
|
-
debounced.flush = () => {
|
|
1349
|
-
if (timer) {
|
|
1350
|
-
clearTimeout(timer);
|
|
1351
|
-
run2();
|
|
1352
|
-
}
|
|
1353
|
-
};
|
|
1354
|
-
return debounced;
|
|
1355
|
-
}
|
|
1356
|
-
|
|
1357
1569
|
// src/pty-manager.ts
|
|
1358
1570
|
var OUTPUT_BUFFER_MAX2 = 65536;
|
|
1359
1571
|
var PTY_COLS2 = 120;
|
|
@@ -1361,7 +1573,7 @@ var PTY_ROWS2 = 40;
|
|
|
1361
1573
|
var SCREEN_SCROLLBACK2 = 1e3;
|
|
1362
1574
|
var CLAUDE_PROMPT_MARKERS = ["\u256D", "\u276F"];
|
|
1363
1575
|
var PROMPT_MARKER_FALLBACK_MS = 1e4;
|
|
1364
|
-
var
|
|
1576
|
+
var QUIET_DETECT_MS2 = 500;
|
|
1365
1577
|
function buildPasteBytes(input) {
|
|
1366
1578
|
return `\x1B[200~${input}\x1B[201~`;
|
|
1367
1579
|
}
|
|
@@ -1861,7 +2073,7 @@ var PTYManager = class {
|
|
|
1861
2073
|
});
|
|
1862
2074
|
let quiet = this.quietCheckers.get(sessionId);
|
|
1863
2075
|
if (!quiet) {
|
|
1864
|
-
quiet = debounce(() => this.handleQuiet(sessionId),
|
|
2076
|
+
quiet = debounce(() => this.handleQuiet(sessionId), QUIET_DETECT_MS2);
|
|
1865
2077
|
this.quietCheckers.set(sessionId, quiet);
|
|
1866
2078
|
}
|
|
1867
2079
|
quiet();
|
|
@@ -2122,7 +2334,7 @@ var LiveSessionManager = class {
|
|
|
2122
2334
|
// src/process-discovery.ts
|
|
2123
2335
|
import { execFile } from "child_process";
|
|
2124
2336
|
import { platform as platform2 } from "os";
|
|
2125
|
-
import { basename as basename4, dirname as
|
|
2337
|
+
import { basename as basename4, dirname as dirname4 } from "path";
|
|
2126
2338
|
async function discoverClaudeProcesses() {
|
|
2127
2339
|
if (platform2() === "win32") return discoverWindows();
|
|
2128
2340
|
return discoverUnix();
|
|
@@ -2244,7 +2456,7 @@ async function getProcessInfoWindows(pid) {
|
|
|
2244
2456
|
const startedAt = /* @__PURE__ */ new Date(`${year}-${month}-${day}T${hour}:${min}:${sec}`);
|
|
2245
2457
|
if (Number.isNaN(startedAt.getTime())) return null;
|
|
2246
2458
|
const exePath = parts[3] ?? "";
|
|
2247
|
-
const cwd = exePath ?
|
|
2459
|
+
const cwd = exePath ? dirname4(exePath) : "";
|
|
2248
2460
|
return { cwd, args, startedAt };
|
|
2249
2461
|
} catch {
|
|
2250
2462
|
return null;
|
|
@@ -2276,16 +2488,16 @@ import {
|
|
|
2276
2488
|
import { EventEmitter } from "events";
|
|
2277
2489
|
import {
|
|
2278
2490
|
createReadStream,
|
|
2279
|
-
existsSync as
|
|
2491
|
+
existsSync as existsSync8,
|
|
2280
2492
|
watch as fsWatch,
|
|
2281
2493
|
readdirSync as readdirSync4,
|
|
2282
|
-
readFileSync as
|
|
2283
|
-
statSync as
|
|
2494
|
+
readFileSync as readFileSync7,
|
|
2495
|
+
statSync as statSync6
|
|
2284
2496
|
} from "fs";
|
|
2285
2497
|
import { realpath as realpath2 } from "fs/promises";
|
|
2286
2498
|
import { createServer } from "http";
|
|
2287
|
-
import { homedir as
|
|
2288
|
-
import { dirname as
|
|
2499
|
+
import { homedir as homedir7 } from "os";
|
|
2500
|
+
import { dirname as dirname8, join as join15 } from "path";
|
|
2289
2501
|
import { createInterface } from "readline";
|
|
2290
2502
|
|
|
2291
2503
|
// node_modules/nanoid/index.js
|
|
@@ -2469,7 +2681,7 @@ async function handleSendAgentInput(sessionId, body, deps) {
|
|
|
2469
2681
|
|
|
2470
2682
|
// src/agent/handle-start-agent-session.ts
|
|
2471
2683
|
import { existsSync as existsSync4 } from "fs";
|
|
2472
|
-
import { join as
|
|
2684
|
+
import { join as join6 } from "path";
|
|
2473
2685
|
function validateBody(body) {
|
|
2474
2686
|
if (body === null || body === void 0 || typeof body !== "object") {
|
|
2475
2687
|
return { ok: false };
|
|
@@ -2499,7 +2711,7 @@ async function handleStartAgentSession(body, deps) {
|
|
|
2499
2711
|
}
|
|
2500
2712
|
let conversationId = parsed.conversationId;
|
|
2501
2713
|
if (conversationId) {
|
|
2502
|
-
const jsonlPath =
|
|
2714
|
+
const jsonlPath = join6(deps.conversationsDir, `${conversationId}.jsonl`);
|
|
2503
2715
|
if (!existsSync4(jsonlPath)) {
|
|
2504
2716
|
return {
|
|
2505
2717
|
status: 404,
|
|
@@ -2544,7 +2756,7 @@ async function handleStartAgentSession(body, deps) {
|
|
|
2544
2756
|
}
|
|
2545
2757
|
|
|
2546
2758
|
// src/api/app.ts
|
|
2547
|
-
import { Hono as
|
|
2759
|
+
import { Hono as Hono12 } from "hono";
|
|
2548
2760
|
|
|
2549
2761
|
// src/api/middleware/auth.middleware.ts
|
|
2550
2762
|
function isLocalRequest(remoteAddr) {
|
|
@@ -2552,6 +2764,7 @@ function isLocalRequest(remoteAddr) {
|
|
|
2552
2764
|
return addr === "127.0.0.1" || addr === "::1" || addr === "::ffff:127.0.0.1";
|
|
2553
2765
|
}
|
|
2554
2766
|
var PUBLIC_PATHS = /* @__PURE__ */ new Set(["/healthz", "/ws"]);
|
|
2767
|
+
var LOCAL_ONLY_PATHS = /* @__PURE__ */ new Set(["/api/logs", "/api/logs/meta"]);
|
|
2555
2768
|
var PUBLIC_POST_PATHS = /* @__PURE__ */ new Set(["/api/pair/exchange", "/api/__update"]);
|
|
2556
2769
|
var PUBLIC_POST_PREFIXES = ["/internal/sessions/"];
|
|
2557
2770
|
var authMiddleware = (deps) => async (c, next) => {
|
|
@@ -2562,8 +2775,12 @@ var authMiddleware = (deps) => async (c, next) => {
|
|
|
2562
2775
|
await next();
|
|
2563
2776
|
return;
|
|
2564
2777
|
}
|
|
2778
|
+
const remoteAddr = c.env.incoming?.socket?.remoteAddress;
|
|
2779
|
+
if (LOCAL_ONLY_PATHS.has(path) && isLocalRequest(remoteAddr)) {
|
|
2780
|
+
await next();
|
|
2781
|
+
return;
|
|
2782
|
+
}
|
|
2565
2783
|
if (deps.localNoAuth) {
|
|
2566
|
-
const remoteAddr = c.env.incoming?.socket?.remoteAddress;
|
|
2567
2784
|
if (isLocalRequest(remoteAddr)) {
|
|
2568
2785
|
await next();
|
|
2569
2786
|
return;
|
|
@@ -2696,8 +2913,8 @@ var createConversationRoutes = (deps) => {
|
|
|
2696
2913
|
import { Hono as Hono4 } from "hono";
|
|
2697
2914
|
|
|
2698
2915
|
// src/version.ts
|
|
2699
|
-
import { readFileSync as
|
|
2700
|
-
import { dirname as
|
|
2916
|
+
import { readFileSync as readFileSync4, realpathSync } from "fs";
|
|
2917
|
+
import { dirname as dirname5, join as join7 } from "path";
|
|
2701
2918
|
var cached;
|
|
2702
2919
|
function getVersion() {
|
|
2703
2920
|
if (cached !== void 0) return cached;
|
|
@@ -2706,22 +2923,22 @@ function getVersion() {
|
|
|
2706
2923
|
}
|
|
2707
2924
|
function resolveVersion() {
|
|
2708
2925
|
const scriptPath = process.argv[1] ?? "";
|
|
2709
|
-
const here = scriptPath ?
|
|
2926
|
+
const here = scriptPath ? dirname5(scriptPath) : process.cwd();
|
|
2710
2927
|
let realHere = here;
|
|
2711
2928
|
try {
|
|
2712
|
-
realHere =
|
|
2929
|
+
realHere = dirname5(realpathSync(scriptPath));
|
|
2713
2930
|
} catch {
|
|
2714
2931
|
}
|
|
2715
|
-
const searchDirs = realHere === here ? [here,
|
|
2932
|
+
const searchDirs = realHere === here ? [here, join7(here, "..")] : [here, join7(here, ".."), realHere, join7(realHere, "..")];
|
|
2716
2933
|
for (const dir of searchDirs) {
|
|
2717
2934
|
try {
|
|
2718
|
-
const v =
|
|
2935
|
+
const v = readFileSync4(join7(dir, "version.txt"), "utf8").trim();
|
|
2719
2936
|
if (v) return v;
|
|
2720
2937
|
} catch {
|
|
2721
2938
|
}
|
|
2722
2939
|
}
|
|
2723
2940
|
try {
|
|
2724
|
-
const pkg = JSON.parse(
|
|
2941
|
+
const pkg = JSON.parse(readFileSync4(join7(here, "..", "package.json"), "utf8"));
|
|
2725
2942
|
if (pkg.version) return `${pkg.version}+source`;
|
|
2726
2943
|
} catch {
|
|
2727
2944
|
}
|
|
@@ -2735,16 +2952,145 @@ var createHealthRoutes = () => {
|
|
|
2735
2952
|
return app;
|
|
2736
2953
|
};
|
|
2737
2954
|
|
|
2955
|
+
// src/api/routes/logs.routes.ts
|
|
2956
|
+
import { closeSync, existsSync as existsSync5, fstatSync, openSync, readSync, statSync } from "fs";
|
|
2957
|
+
import { join as join9 } from "path";
|
|
2958
|
+
import { Hono as Hono5 } from "hono";
|
|
2959
|
+
|
|
2960
|
+
// src/lifecycle/constants.ts
|
|
2961
|
+
import { homedir as homedir4 } from "os";
|
|
2962
|
+
import { join as join8 } from "path";
|
|
2963
|
+
var TASK_NAME = process.env.THREADBASE_TASK_NAME ?? "Threadbase";
|
|
2964
|
+
function installDir() {
|
|
2965
|
+
return process.env.THREADBASE_INSTALL_DIR ?? join8(homedir4(), ".threadbase");
|
|
2966
|
+
}
|
|
2967
|
+
|
|
2968
|
+
// src/api/routes/logs.routes.ts
|
|
2969
|
+
var logger2 = getLogger("logs-api");
|
|
2970
|
+
function resolveLogPath(source) {
|
|
2971
|
+
return join9(installDir(), "logs", `${source}.log`);
|
|
2972
|
+
}
|
|
2973
|
+
function pickDefaultSource() {
|
|
2974
|
+
for (const source of ["stdout", "stderr", "dev"]) {
|
|
2975
|
+
const p = resolveLogPath(source);
|
|
2976
|
+
if (existsSync5(p) && statSync(p).size > 0) return source;
|
|
2977
|
+
}
|
|
2978
|
+
return "stdout";
|
|
2979
|
+
}
|
|
2980
|
+
function readLogLines(filePath, sinceOffset, limit) {
|
|
2981
|
+
if (!existsSync5(filePath)) {
|
|
2982
|
+
return { lines: [], offset: 0, total: 0 };
|
|
2983
|
+
}
|
|
2984
|
+
const fd = openSync(filePath, "r");
|
|
2985
|
+
try {
|
|
2986
|
+
const { size } = fstatSync(fd);
|
|
2987
|
+
if (size === 0) return { lines: [], offset: 0, total: 0 };
|
|
2988
|
+
const maxBytes = Math.min(size, 2 * 1024 * 1024);
|
|
2989
|
+
const start = size - maxBytes;
|
|
2990
|
+
const buf = Buffer.alloc(maxBytes);
|
|
2991
|
+
readSync(fd, buf, 0, maxBytes, start);
|
|
2992
|
+
let text = buf.toString("utf8");
|
|
2993
|
+
if (start > 0) {
|
|
2994
|
+
const firstNl = text.indexOf("\n");
|
|
2995
|
+
if (firstNl >= 0) text = text.slice(firstNl + 1);
|
|
2996
|
+
}
|
|
2997
|
+
const allLines = text.split("\n").filter((line) => line.trim() && !line.startsWith("==="));
|
|
2998
|
+
let lines;
|
|
2999
|
+
let newOffset;
|
|
3000
|
+
if (sinceOffset > 0 && sinceOffset < allLines.length) {
|
|
3001
|
+
lines = allLines.slice(sinceOffset, sinceOffset + limit);
|
|
3002
|
+
newOffset = sinceOffset + lines.length;
|
|
3003
|
+
} else if (sinceOffset >= allLines.length && sinceOffset > 0) {
|
|
3004
|
+
lines = [];
|
|
3005
|
+
newOffset = allLines.length;
|
|
3006
|
+
} else {
|
|
3007
|
+
lines = allLines.slice(-limit);
|
|
3008
|
+
newOffset = allLines.length;
|
|
3009
|
+
}
|
|
3010
|
+
return { lines, offset: newOffset, total: allLines.length };
|
|
3011
|
+
} finally {
|
|
3012
|
+
closeSync(fd);
|
|
3013
|
+
}
|
|
3014
|
+
}
|
|
3015
|
+
function createLogsRoutes() {
|
|
3016
|
+
const app = new Hono5();
|
|
3017
|
+
app.get("/", (c) => {
|
|
3018
|
+
try {
|
|
3019
|
+
const sourceParam = (c.req.query("source") || "").toLowerCase();
|
|
3020
|
+
const source = sourceParam === "stdout" || sourceParam === "stderr" || sourceParam === "dev" ? sourceParam : pickDefaultSource();
|
|
3021
|
+
const logPath = resolveLogPath(source);
|
|
3022
|
+
const sinceOffset = parseInt(c.req.query("since") || "0", 10);
|
|
3023
|
+
const limit = Math.min(parseInt(c.req.query("limit") || "100", 10) || 100, 1e3);
|
|
3024
|
+
if (!existsSync5(logPath)) {
|
|
3025
|
+
return c.json({
|
|
3026
|
+
logs: [],
|
|
3027
|
+
message: `No log file found for source=${source}`,
|
|
3028
|
+
offset: 0,
|
|
3029
|
+
total: 0,
|
|
3030
|
+
source
|
|
3031
|
+
});
|
|
3032
|
+
}
|
|
3033
|
+
const { lines, offset, total } = readLogLines(logPath, sinceOffset, limit);
|
|
3034
|
+
const stats = statSync(logPath);
|
|
3035
|
+
return c.json({
|
|
3036
|
+
logs: lines,
|
|
3037
|
+
offset,
|
|
3038
|
+
total,
|
|
3039
|
+
hasMore: offset < total,
|
|
3040
|
+
source,
|
|
3041
|
+
fileSize: stats.size,
|
|
3042
|
+
fileModified: stats.mtime.toISOString()
|
|
3043
|
+
});
|
|
3044
|
+
} catch (error) {
|
|
3045
|
+
logger2.error("Failed to read logs", { error: String(error) });
|
|
3046
|
+
return c.json(
|
|
3047
|
+
{
|
|
3048
|
+
error: "Failed to read logs",
|
|
3049
|
+
logs: [],
|
|
3050
|
+
offset: 0,
|
|
3051
|
+
total: 0
|
|
3052
|
+
},
|
|
3053
|
+
500
|
|
3054
|
+
);
|
|
3055
|
+
}
|
|
3056
|
+
});
|
|
3057
|
+
app.get("/meta", (c) => {
|
|
3058
|
+
try {
|
|
3059
|
+
const sources = ["stdout", "stderr", "dev"].map((source) => {
|
|
3060
|
+
const logPath = resolveLogPath(source);
|
|
3061
|
+
if (!existsSync5(logPath)) {
|
|
3062
|
+
return { source, exists: false, total: 0, fileSize: 0 };
|
|
3063
|
+
}
|
|
3064
|
+
const stats = statSync(logPath);
|
|
3065
|
+
return {
|
|
3066
|
+
source,
|
|
3067
|
+
exists: true,
|
|
3068
|
+
fileSize: stats.size,
|
|
3069
|
+
fileModified: stats.mtime.toISOString()
|
|
3070
|
+
};
|
|
3071
|
+
});
|
|
3072
|
+
return c.json({
|
|
3073
|
+
defaultSource: pickDefaultSource(),
|
|
3074
|
+
sources
|
|
3075
|
+
});
|
|
3076
|
+
} catch (error) {
|
|
3077
|
+
logger2.error("Failed to read log metadata", { error: String(error) });
|
|
3078
|
+
return c.json({ error: "Failed to read log metadata", exists: false }, 500);
|
|
3079
|
+
}
|
|
3080
|
+
});
|
|
3081
|
+
return app;
|
|
3082
|
+
}
|
|
3083
|
+
|
|
2738
3084
|
// src/api/routes/misc.routes.ts
|
|
2739
3085
|
import { spawn } from "child_process";
|
|
2740
3086
|
import { createHmac, timingSafeEqual as timingSafeEqual2 } from "crypto";
|
|
2741
|
-
import { Hono as
|
|
3087
|
+
import { Hono as Hono6 } from "hono";
|
|
2742
3088
|
import { hostname } from "os";
|
|
2743
3089
|
|
|
2744
3090
|
// src/config/update-config.ts
|
|
2745
|
-
import { readFileSync as
|
|
2746
|
-
import { homedir as
|
|
2747
|
-
import { join as
|
|
3091
|
+
import { readFileSync as readFileSync5 } from "fs";
|
|
3092
|
+
import { homedir as homedir5 } from "os";
|
|
3093
|
+
import { join as join10 } from "path";
|
|
2748
3094
|
import { parse as parseYaml } from "yaml";
|
|
2749
3095
|
|
|
2750
3096
|
// src/schemas/updateConfig.schema.ts
|
|
@@ -2760,12 +3106,12 @@ var UpdateConfigSchema = z.object({
|
|
|
2760
3106
|
}).strict();
|
|
2761
3107
|
|
|
2762
3108
|
// src/config/update-config.ts
|
|
2763
|
-
var DEFAULT_CONFIG_PATH =
|
|
3109
|
+
var DEFAULT_CONFIG_PATH = join10(homedir5(), ".threadbase", "update.yaml");
|
|
2764
3110
|
function loadUpdateConfig(opts = {}) {
|
|
2765
3111
|
const path = opts.path ?? DEFAULT_CONFIG_PATH;
|
|
2766
3112
|
let raw;
|
|
2767
3113
|
try {
|
|
2768
|
-
raw =
|
|
3114
|
+
raw = readFileSync5(path, "utf-8");
|
|
2769
3115
|
} catch (err) {
|
|
2770
3116
|
if (err.code === "ENOENT") return null;
|
|
2771
3117
|
throw err;
|
|
@@ -2812,7 +3158,7 @@ function verifyWebhookSignature(body, header, secret) {
|
|
|
2812
3158
|
}
|
|
2813
3159
|
var clientLog = getLogger("client");
|
|
2814
3160
|
var createMiscRoutes = (deps) => {
|
|
2815
|
-
const app = new
|
|
3161
|
+
const app = new Hono6();
|
|
2816
3162
|
app.get("/api/info", (c) => {
|
|
2817
3163
|
const ptyIds = deps.ptyAttachedIds();
|
|
2818
3164
|
return c.json({
|
|
@@ -2888,11 +3234,11 @@ var createMiscRoutes = (deps) => {
|
|
|
2888
3234
|
};
|
|
2889
3235
|
|
|
2890
3236
|
// src/api/routes/pair.routes.ts
|
|
2891
|
-
import { Hono as
|
|
3237
|
+
import { Hono as Hono7 } from "hono";
|
|
2892
3238
|
var ALREADY_HANDLED3 = 597;
|
|
2893
3239
|
var alreadyHandled3 = () => new Response(null, { status: ALREADY_HANDLED3 });
|
|
2894
3240
|
var createPairRoutes = (deps) => {
|
|
2895
|
-
const app = new
|
|
3241
|
+
const app = new Hono7();
|
|
2896
3242
|
app.post("/start", (c) => {
|
|
2897
3243
|
deps.handlePairStart(c.env.outgoing);
|
|
2898
3244
|
return alreadyHandled3();
|
|
@@ -2905,11 +3251,11 @@ var createPairRoutes = (deps) => {
|
|
|
2905
3251
|
};
|
|
2906
3252
|
|
|
2907
3253
|
// src/api/routes/projects.routes.ts
|
|
2908
|
-
import { Hono as
|
|
3254
|
+
import { Hono as Hono8 } from "hono";
|
|
2909
3255
|
var ALREADY_HANDLED4 = 597;
|
|
2910
3256
|
var alreadyHandled4 = () => new Response(null, { status: ALREADY_HANDLED4 });
|
|
2911
3257
|
var createProjectRoutes = (deps) => {
|
|
2912
|
-
const app = new
|
|
3258
|
+
const app = new Hono8();
|
|
2913
3259
|
app.get("/", (c) => {
|
|
2914
3260
|
const url = new URL(c.req.url);
|
|
2915
3261
|
deps.handleListProjects(url, c.env.outgoing);
|
|
@@ -2924,11 +3270,11 @@ var createProjectRoutes = (deps) => {
|
|
|
2924
3270
|
};
|
|
2925
3271
|
|
|
2926
3272
|
// src/api/routes/scanner.routes.ts
|
|
2927
|
-
import { Hono as
|
|
3273
|
+
import { Hono as Hono9 } from "hono";
|
|
2928
3274
|
var ALREADY_HANDLED5 = 597;
|
|
2929
3275
|
var alreadyHandled5 = () => new Response(null, { status: ALREADY_HANDLED5 });
|
|
2930
3276
|
var createScannerRoutes = (deps) => {
|
|
2931
|
-
const app = new
|
|
3277
|
+
const app = new Hono9();
|
|
2932
3278
|
app.get("/api/search", async (c) => {
|
|
2933
3279
|
const url = new URL(c.req.url);
|
|
2934
3280
|
await deps.handleSearch(url, c.env.outgoing);
|
|
@@ -2938,11 +3284,11 @@ var createScannerRoutes = (deps) => {
|
|
|
2938
3284
|
};
|
|
2939
3285
|
|
|
2940
3286
|
// src/api/routes/sessions.routes.ts
|
|
2941
|
-
import { Hono as
|
|
3287
|
+
import { Hono as Hono10 } from "hono";
|
|
2942
3288
|
var ALREADY_HANDLED6 = 597;
|
|
2943
3289
|
var alreadyHandled6 = () => new Response(null, { status: ALREADY_HANDLED6 });
|
|
2944
3290
|
var createSessionRoutes = (deps) => {
|
|
2945
|
-
const app = new
|
|
3291
|
+
const app = new Hono10();
|
|
2946
3292
|
app.get("/count", (c) => {
|
|
2947
3293
|
deps.handleSessionsCount(c.env.outgoing);
|
|
2948
3294
|
return alreadyHandled6();
|
|
@@ -3009,9 +3355,9 @@ var createSessionRoutes = (deps) => {
|
|
|
3009
3355
|
};
|
|
3010
3356
|
|
|
3011
3357
|
// src/api/routes/ws.routes.ts
|
|
3012
|
-
import { Hono as
|
|
3358
|
+
import { Hono as Hono11 } from "hono";
|
|
3013
3359
|
var createWsRoutes = (deps, upgradeWebSocket) => {
|
|
3014
|
-
const app = new
|
|
3360
|
+
const app = new Hono11();
|
|
3015
3361
|
app.get(
|
|
3016
3362
|
"/ws",
|
|
3017
3363
|
upgradeWebSocket((c) => {
|
|
@@ -3039,7 +3385,7 @@ var createWsRoutes = (deps, upgradeWebSocket) => {
|
|
|
3039
3385
|
|
|
3040
3386
|
// src/api/app.ts
|
|
3041
3387
|
var createHonoApp = (deps, upgradeWebSocket) => {
|
|
3042
|
-
const app = new
|
|
3388
|
+
const app = new Hono12();
|
|
3043
3389
|
const httpLog = getLogger("http");
|
|
3044
3390
|
app.use("*", async (c, next) => {
|
|
3045
3391
|
const start = Date.now();
|
|
@@ -3068,6 +3414,7 @@ var createHonoApp = (deps, upgradeWebSocket) => {
|
|
|
3068
3414
|
app.route("/api", createBrowseRoutes(deps));
|
|
3069
3415
|
app.route("/", createScannerRoutes(deps));
|
|
3070
3416
|
app.route("/internal", createProgressRoutes(deps));
|
|
3417
|
+
app.route("/api/logs", createLogsRoutes());
|
|
3071
3418
|
if (upgradeWebSocket) {
|
|
3072
3419
|
app.route("/", createWsRoutes(deps, upgradeWebSocket));
|
|
3073
3420
|
}
|
|
@@ -3076,7 +3423,7 @@ var createHonoApp = (deps, upgradeWebSocket) => {
|
|
|
3076
3423
|
|
|
3077
3424
|
// src/browse.ts
|
|
3078
3425
|
import { mkdir as mkdir2, readdir, realpath, stat } from "fs/promises";
|
|
3079
|
-
import { join as
|
|
3426
|
+
import { join as join11, resolve, sep } from "path";
|
|
3080
3427
|
var BrowsePathNotFoundError = class extends Error {
|
|
3081
3428
|
constructor(message) {
|
|
3082
3429
|
super(message);
|
|
@@ -3114,7 +3461,7 @@ async function createDirectory(parentAbsolutePath, name) {
|
|
|
3114
3461
|
if (name.includes("/") || name.includes("\\") || name === ".." || name === ".") {
|
|
3115
3462
|
throw new Error("Invalid directory name");
|
|
3116
3463
|
}
|
|
3117
|
-
const target =
|
|
3464
|
+
const target = join11(parentAbsolutePath, name);
|
|
3118
3465
|
try {
|
|
3119
3466
|
const s = await stat(target);
|
|
3120
3467
|
if (s.isDirectory()) throw new Error("Directory already exists");
|
|
@@ -3131,18 +3478,18 @@ import {
|
|
|
3131
3478
|
parseJsonlLine
|
|
3132
3479
|
} from "@threadbase-sh/scanner";
|
|
3133
3480
|
import Database from "better-sqlite3";
|
|
3134
|
-
import { closeSync as
|
|
3481
|
+
import { closeSync as closeSync3, existsSync as existsSync6, mkdirSync as mkdirSync3, openSync as openSync3, readSync as readSync3, statSync as statSync3 } from "fs";
|
|
3135
3482
|
import { open as openAsync } from "fs/promises";
|
|
3136
|
-
import { dirname as
|
|
3483
|
+
import { dirname as dirname7 } from "path";
|
|
3137
3484
|
import { setImmediate as yieldToEventLoop } from "timers/promises";
|
|
3138
3485
|
|
|
3139
3486
|
// src/db/sqlite-migrate.ts
|
|
3140
|
-
import { readdirSync as readdirSync2, readFileSync as
|
|
3141
|
-
import { dirname as
|
|
3487
|
+
import { readdirSync as readdirSync2, readFileSync as readFileSync6 } from "fs";
|
|
3488
|
+
import { dirname as dirname6, join as join12 } from "path";
|
|
3142
3489
|
import { fileURLToPath as fileURLToPath2 } from "url";
|
|
3143
3490
|
function getMigrationsDir2() {
|
|
3144
3491
|
if (typeof import.meta !== "undefined" && import.meta.url) {
|
|
3145
|
-
return
|
|
3492
|
+
return dirname6(fileURLToPath2(import.meta.url));
|
|
3146
3493
|
}
|
|
3147
3494
|
return __dirname;
|
|
3148
3495
|
}
|
|
@@ -3154,7 +3501,7 @@ CREATE TABLE IF NOT EXISTS schema_migrations (
|
|
|
3154
3501
|
`;
|
|
3155
3502
|
function runSqliteMigrations(db, migrationsDir) {
|
|
3156
3503
|
db.exec(SCHEMA_MIGRATIONS_SQL);
|
|
3157
|
-
const dir = migrationsDir ??
|
|
3504
|
+
const dir = migrationsDir ?? join12(getMigrationsDir2(), "migrations");
|
|
3158
3505
|
const files = readdirSync2(dir).filter((f) => f.endsWith(".sql")).sort();
|
|
3159
3506
|
const appliedRows = db.prepare("SELECT id FROM schema_migrations").all();
|
|
3160
3507
|
const appliedSet = new Set(appliedRows.map((r) => r.id));
|
|
@@ -3166,7 +3513,7 @@ function runSqliteMigrations(db, migrationsDir) {
|
|
|
3166
3513
|
skipped.push(file);
|
|
3167
3514
|
continue;
|
|
3168
3515
|
}
|
|
3169
|
-
const sql =
|
|
3516
|
+
const sql = readFileSync6(join12(dir, file), "utf-8");
|
|
3170
3517
|
const tx = db.transaction(() => {
|
|
3171
3518
|
db.exec(sql);
|
|
3172
3519
|
recordApplied.run(file, (/* @__PURE__ */ new Date()).toISOString());
|
|
@@ -3178,7 +3525,7 @@ function runSqliteMigrations(db, migrationsDir) {
|
|
|
3178
3525
|
}
|
|
3179
3526
|
|
|
3180
3527
|
// src/services/conversations/isAgentConversation.ts
|
|
3181
|
-
import { closeSync, openSync, readSync, statSync } from "fs";
|
|
3528
|
+
import { closeSync as closeSync2, openSync as openSync2, readSync as readSync2, statSync as statSync2 } from "fs";
|
|
3182
3529
|
var DEFAULT_AGENT_ENTRYPOINTS = /* @__PURE__ */ new Set(["sdk-cli", "claude-vscode"]);
|
|
3183
3530
|
var CHUNK_BYTES = 64 * 1024;
|
|
3184
3531
|
var ENTRYPOINT_PROBE = `"entrypoint":`;
|
|
@@ -3200,12 +3547,12 @@ function isAgentFile(filePath, entrypoints = DEFAULT_AGENT_ENTRYPOINTS) {
|
|
|
3200
3547
|
if (cached2 !== void 0) return cached2;
|
|
3201
3548
|
let fd;
|
|
3202
3549
|
try {
|
|
3203
|
-
fd =
|
|
3550
|
+
fd = openSync2(filePath, "r");
|
|
3204
3551
|
} catch {
|
|
3205
3552
|
return false;
|
|
3206
3553
|
}
|
|
3207
3554
|
try {
|
|
3208
|
-
const fileSize =
|
|
3555
|
+
const fileSize = statSync2(filePath).size;
|
|
3209
3556
|
if (fileSize === 0) {
|
|
3210
3557
|
fileDecisionCache.set(key, false);
|
|
3211
3558
|
return false;
|
|
@@ -3216,7 +3563,7 @@ function isAgentFile(filePath, entrypoints = DEFAULT_AGENT_ENTRYPOINTS) {
|
|
|
3216
3563
|
let carry = "";
|
|
3217
3564
|
while (offset < fileSize) {
|
|
3218
3565
|
const toRead = Math.min(CHUNK_BYTES, fileSize - offset);
|
|
3219
|
-
const got =
|
|
3566
|
+
const got = readSync2(fd, buf, 0, toRead, offset);
|
|
3220
3567
|
if (got <= 0) break;
|
|
3221
3568
|
const chunk = carry + buf.toString("utf8", 0, got);
|
|
3222
3569
|
for (const marker of markers) {
|
|
@@ -3237,7 +3584,7 @@ function isAgentFile(filePath, entrypoints = DEFAULT_AGENT_ENTRYPOINTS) {
|
|
|
3237
3584
|
} catch {
|
|
3238
3585
|
return false;
|
|
3239
3586
|
} finally {
|
|
3240
|
-
|
|
3587
|
+
closeSync2(fd);
|
|
3241
3588
|
}
|
|
3242
3589
|
}
|
|
3243
3590
|
function parseAgentEntrypointsEnv(raw) {
|
|
@@ -3774,7 +4121,7 @@ var ConversationCache = class _ConversationCache {
|
|
|
3774
4121
|
if (!fileState) return null;
|
|
3775
4122
|
let stat3;
|
|
3776
4123
|
try {
|
|
3777
|
-
stat3 =
|
|
4124
|
+
stat3 = statSync3(filePath);
|
|
3778
4125
|
} catch {
|
|
3779
4126
|
return null;
|
|
3780
4127
|
}
|
|
@@ -3795,17 +4142,17 @@ var ConversationCache = class _ConversationCache {
|
|
|
3795
4142
|
);
|
|
3796
4143
|
if (rows.length === 0) return { messages: [], total, fromIndex: from };
|
|
3797
4144
|
const messages = [];
|
|
3798
|
-
const fd =
|
|
4145
|
+
const fd = openSync3(filePath, "r");
|
|
3799
4146
|
try {
|
|
3800
4147
|
const state = createJsonlParseState();
|
|
3801
4148
|
for (const row of rows) {
|
|
3802
4149
|
const buf = Buffer.alloc(row.byte_length);
|
|
3803
|
-
|
|
4150
|
+
readSync3(fd, buf, 0, row.byte_length, row.byte_offset);
|
|
3804
4151
|
const msg = parseJsonlLine(buf.toString("utf-8"), state);
|
|
3805
4152
|
if (msg) messages.push(msg);
|
|
3806
4153
|
}
|
|
3807
4154
|
} finally {
|
|
3808
|
-
|
|
4155
|
+
closeSync3(fd);
|
|
3809
4156
|
}
|
|
3810
4157
|
return { messages, total, fromIndex: from };
|
|
3811
4158
|
}
|
|
@@ -3833,14 +4180,14 @@ var ConversationCache = class _ConversationCache {
|
|
|
3833
4180
|
isAgentFileCached(filePath) {
|
|
3834
4181
|
let s;
|
|
3835
4182
|
try {
|
|
3836
|
-
s =
|
|
4183
|
+
s = statSync3(filePath);
|
|
3837
4184
|
} catch {
|
|
3838
4185
|
return false;
|
|
3839
4186
|
}
|
|
3840
4187
|
return this.classifyAgentFile(filePath, s.mtimeMs, s.size);
|
|
3841
4188
|
}
|
|
3842
4189
|
static open(dbPath, tailSize = 10, migrationsDir, options) {
|
|
3843
|
-
|
|
4190
|
+
mkdirSync3(dirname7(dbPath), { recursive: true });
|
|
3844
4191
|
const db = new Database(dbPath);
|
|
3845
4192
|
db.pragma("journal_mode = WAL");
|
|
3846
4193
|
db.pragma("foreign_keys = ON");
|
|
@@ -4021,7 +4368,7 @@ var ConversationCache = class _ConversationCache {
|
|
|
4021
4368
|
let mtimeMs = null;
|
|
4022
4369
|
let fileSize = null;
|
|
4023
4370
|
try {
|
|
4024
|
-
const s =
|
|
4371
|
+
const s = statSync3(m.filePath);
|
|
4025
4372
|
mtimeMs = s.mtimeMs;
|
|
4026
4373
|
fileSize = s.size;
|
|
4027
4374
|
} catch {
|
|
@@ -4080,8 +4427,8 @@ var ConversationCache = class _ConversationCache {
|
|
|
4080
4427
|
let fileSize;
|
|
4081
4428
|
let fd;
|
|
4082
4429
|
try {
|
|
4083
|
-
fileSize =
|
|
4084
|
-
fd =
|
|
4430
|
+
fileSize = statSync3(filePath).size;
|
|
4431
|
+
fd = openSync3(filePath, "r");
|
|
4085
4432
|
} catch {
|
|
4086
4433
|
return false;
|
|
4087
4434
|
}
|
|
@@ -4094,7 +4441,7 @@ var ConversationCache = class _ConversationCache {
|
|
|
4094
4441
|
while (pos > 0 && lines.length < this.tailSize * 4) {
|
|
4095
4442
|
const toRead = Math.min(CHUNK, pos);
|
|
4096
4443
|
pos -= toRead;
|
|
4097
|
-
|
|
4444
|
+
readSync3(fd, buf, 0, toRead, pos);
|
|
4098
4445
|
const chunk = buf.subarray(0, toRead).toString("utf8");
|
|
4099
4446
|
const combined = chunk + partial;
|
|
4100
4447
|
const parts = combined.split("\n");
|
|
@@ -4105,7 +4452,7 @@ var ConversationCache = class _ConversationCache {
|
|
|
4105
4452
|
}
|
|
4106
4453
|
if (partial) lines.push(partial);
|
|
4107
4454
|
} finally {
|
|
4108
|
-
|
|
4455
|
+
closeSync3(fd);
|
|
4109
4456
|
}
|
|
4110
4457
|
const msgs = [];
|
|
4111
4458
|
for (let i = 0; i < lines.length && msgs.length < this.tailSize; i++) {
|
|
@@ -4307,7 +4654,7 @@ var ConversationCache = class _ConversationCache {
|
|
|
4307
4654
|
* `handleGetConversation` can still serve the cached tail even when the
|
|
4308
4655
|
* JSONL has been deleted.
|
|
4309
4656
|
*/
|
|
4310
|
-
pruneGhostFiles(exists =
|
|
4657
|
+
pruneGhostFiles(exists = existsSync6) {
|
|
4311
4658
|
const rows = this.stmts.allFilePaths.all();
|
|
4312
4659
|
const ghosts = [];
|
|
4313
4660
|
const prune = this.db.transaction((ids) => {
|
|
@@ -4362,7 +4709,7 @@ var ConversationCache = class _ConversationCache {
|
|
|
4362
4709
|
* Returns the removed IDs.
|
|
4363
4710
|
*/
|
|
4364
4711
|
reconcileDeletions(livePaths, opts) {
|
|
4365
|
-
const exists = opts?.exists ??
|
|
4712
|
+
const exists = opts?.exists ?? existsSync6;
|
|
4366
4713
|
const rows = this.stmts.allFilePaths.all();
|
|
4367
4714
|
const removed = [];
|
|
4368
4715
|
const drop = this.db.transaction((ids) => {
|
|
@@ -4591,23 +4938,23 @@ async function recordUpload(pool2, instanceId, row) {
|
|
|
4591
4938
|
}
|
|
4592
4939
|
|
|
4593
4940
|
// src/handlers/handleListProjects.ts
|
|
4594
|
-
import { readdirSync as readdirSync3, statSync as
|
|
4595
|
-
import { homedir as
|
|
4596
|
-
import { join as
|
|
4941
|
+
import { readdirSync as readdirSync3, statSync as statSync4 } from "fs";
|
|
4942
|
+
import { homedir as homedir6 } from "os";
|
|
4943
|
+
import { join as join13 } from "path";
|
|
4597
4944
|
function decodeProjectPath(dirName) {
|
|
4598
4945
|
return dirName.replace(/-/g, "/");
|
|
4599
4946
|
}
|
|
4600
4947
|
function handleListProjects(url, res) {
|
|
4601
4948
|
const limit = Math.max(1, parseInt(url.searchParams.get("limit") ?? "50", 10) || 50);
|
|
4602
4949
|
const offset = Math.max(0, parseInt(url.searchParams.get("offset") ?? "0", 10) || 0);
|
|
4603
|
-
const projectsDir =
|
|
4950
|
+
const projectsDir = join13(homedir6(), ".claude", "projects");
|
|
4604
4951
|
let entries;
|
|
4605
4952
|
try {
|
|
4606
4953
|
entries = readdirSync3(projectsDir).map((dirName) => {
|
|
4607
|
-
const fullPath =
|
|
4954
|
+
const fullPath = join13(projectsDir, dirName);
|
|
4608
4955
|
let mtime = 0;
|
|
4609
4956
|
try {
|
|
4610
|
-
mtime =
|
|
4957
|
+
mtime = statSync4(fullPath).mtimeMs;
|
|
4611
4958
|
} catch {
|
|
4612
4959
|
}
|
|
4613
4960
|
const path = decodeProjectPath(dirName);
|
|
@@ -4702,7 +5049,7 @@ function seal(plaintext, recipientPublicKeyBase64) {
|
|
|
4702
5049
|
|
|
4703
5050
|
// src/services/conversations/conversationWatcher.ts
|
|
4704
5051
|
import chokidar from "chokidar";
|
|
4705
|
-
import { statSync as
|
|
5052
|
+
import { statSync as statSync5 } from "fs";
|
|
4706
5053
|
import { open, stat as stat2 } from "fs/promises";
|
|
4707
5054
|
var ConversationWatcher = class {
|
|
4708
5055
|
files = /* @__PURE__ */ new Map();
|
|
@@ -4725,7 +5072,7 @@ var ConversationWatcher = class {
|
|
|
4725
5072
|
if (this.files.has(filePath)) return;
|
|
4726
5073
|
let offset;
|
|
4727
5074
|
try {
|
|
4728
|
-
offset =
|
|
5075
|
+
offset = statSync5(filePath).size;
|
|
4729
5076
|
} catch {
|
|
4730
5077
|
offset = 0;
|
|
4731
5078
|
}
|
|
@@ -4900,14 +5247,14 @@ function findSearchTarget(messages, query) {
|
|
|
4900
5247
|
}
|
|
4901
5248
|
|
|
4902
5249
|
// src/services/conversations/pruneAgentConversations.ts
|
|
4903
|
-
import { existsSync as
|
|
5250
|
+
import { existsSync as existsSync7 } from "fs";
|
|
4904
5251
|
function pruneAgentConversations(cache) {
|
|
4905
5252
|
const db = cache.getDatabase();
|
|
4906
5253
|
const rows = db.prepare("SELECT id, file_path FROM conversation_meta").all();
|
|
4907
5254
|
let pruned = 0;
|
|
4908
5255
|
let missing = 0;
|
|
4909
5256
|
for (const row of rows) {
|
|
4910
|
-
if (!
|
|
5257
|
+
if (!existsSync7(row.file_path)) {
|
|
4911
5258
|
missing += 1;
|
|
4912
5259
|
continue;
|
|
4913
5260
|
}
|
|
@@ -5250,7 +5597,7 @@ function discoveredToResponse(d, conversationId) {
|
|
|
5250
5597
|
import { randomBytes as randomBytes3 } from "crypto";
|
|
5251
5598
|
import { mkdir as mkdir3, writeFile } from "fs/promises";
|
|
5252
5599
|
import heicConvert from "heic-convert";
|
|
5253
|
-
import { join as
|
|
5600
|
+
import { join as join14 } from "path";
|
|
5254
5601
|
var UPLOAD_DIR_NAME = ".threadbase-uploads";
|
|
5255
5602
|
var MAX_BYTES = 25 * 1024 * 1024;
|
|
5256
5603
|
var HEIC_MIMES = /* @__PURE__ */ new Set(["image/heic", "image/heif"]);
|
|
@@ -5283,9 +5630,9 @@ async function saveUploadFile(input) {
|
|
|
5283
5630
|
}
|
|
5284
5631
|
const id = `up_${randomBytes3(8).toString("hex")}`;
|
|
5285
5632
|
const safeName = sanitizeFilename(originalName) || `file${MIME_TO_EXT[mimeType] ?? ""}`;
|
|
5286
|
-
const dir =
|
|
5633
|
+
const dir = join14(input.projectPath, UPLOAD_DIR_NAME, input.sessionId);
|
|
5287
5634
|
await mkdir3(dir, { recursive: true });
|
|
5288
|
-
const filePath =
|
|
5635
|
+
const filePath = join14(dir, `${Date.now()}-${id}-${safeName}`);
|
|
5289
5636
|
await writeFile(filePath, buffer);
|
|
5290
5637
|
return {
|
|
5291
5638
|
id,
|
|
@@ -5544,7 +5891,7 @@ var DEFAULT_PTY_GRACE_PERIOD_MS = 27e4;
|
|
|
5544
5891
|
var DEFAULT_WS_AUTH_TIMEOUT_MS = 5e3;
|
|
5545
5892
|
var WS_CLOSE_UNAUTHORIZED = 4401;
|
|
5546
5893
|
var REFRESH_TTL_MS = 2e3;
|
|
5547
|
-
var START_READY_TIMEOUT_MS =
|
|
5894
|
+
var START_READY_TIMEOUT_MS = 1e4;
|
|
5548
5895
|
function parseIncludeAgentsEnv(raw) {
|
|
5549
5896
|
if (raw === void 0) return false;
|
|
5550
5897
|
const v = raw.trim().toLowerCase();
|
|
@@ -5678,12 +6025,12 @@ var StreamerServer = class {
|
|
|
5678
6025
|
this.disableDb = config.disableDb ?? false;
|
|
5679
6026
|
this.scannerPersistenceDisabled = config.scannerPersistent === false;
|
|
5680
6027
|
this.scanProfiles = config.scanProfiles;
|
|
5681
|
-
this.codexRoots = config.codexRoots ?? [
|
|
6028
|
+
this.codexRoots = config.codexRoots ?? [join15(homedir7(), ".codex", "sessions")];
|
|
5682
6029
|
this.ptyGracePeriodMs = config.ptyGracePeriodMs ?? DEFAULT_PTY_GRACE_PERIOD_MS;
|
|
5683
6030
|
this.wsAuthTimeoutMs = config.wsAuthTimeoutMs ?? DEFAULT_WS_AUTH_TIMEOUT_MS;
|
|
5684
6031
|
this.defaultSystemPrompt = config.defaultSystemPrompt ?? DEFAULT_SYSTEM_PROMPT;
|
|
5685
6032
|
this.defaultPermissionMode = config.defaultPermissionMode ?? loadDefaultPermissionMode() ?? "acceptEdits";
|
|
5686
|
-
this.cacheDir = config.cacheDir ?? loadCacheDir() ??
|
|
6033
|
+
this.cacheDir = config.cacheDir ?? loadCacheDir() ?? join15(homedir7(), ".threadbase", "cache");
|
|
5687
6034
|
this.tailSize = config.tailSize ?? loadTailSize() ?? 10;
|
|
5688
6035
|
this.directoryDebounceMs = parseDirScanDebounceEnv(process.env.THREADBASE_DIR_SCAN_DEBOUNCE_MS) ?? config.directoryScanDebounceMs ?? 1e3;
|
|
5689
6036
|
this.markScannerStaleDebounced = debounce(() => {
|
|
@@ -5724,7 +6071,7 @@ var StreamerServer = class {
|
|
|
5724
6071
|
const seqs = cache.extendMessageIndex(
|
|
5725
6072
|
filePath,
|
|
5726
6073
|
spans,
|
|
5727
|
-
|
|
6074
|
+
statSync6(filePath),
|
|
5728
6075
|
readFrom,
|
|
5729
6076
|
endOffset
|
|
5730
6077
|
);
|
|
@@ -5885,7 +6232,7 @@ var StreamerServer = class {
|
|
|
5885
6232
|
temporalClient,
|
|
5886
6233
|
taskQueue: agentConfig.temporal.taskQueue
|
|
5887
6234
|
});
|
|
5888
|
-
const conversationsBaseDir = agentConfig.conversationsDir ||
|
|
6235
|
+
const conversationsBaseDir = agentConfig.conversationsDir || join15(dirname8(this.cacheDir), "conversations");
|
|
5889
6236
|
conversationWriter = createConversationWriter({
|
|
5890
6237
|
baseDir: conversationsBaseDir
|
|
5891
6238
|
});
|
|
@@ -5983,6 +6330,38 @@ var StreamerServer = class {
|
|
|
5983
6330
|
const lines = await this.ptyManager.getOutputLines(msg.sessionId, 200);
|
|
5984
6331
|
ws.send(JSON.stringify({ type: "terminal_replay", sessionId: msg.sessionId, lines }));
|
|
5985
6332
|
}
|
|
6333
|
+
const pendingGate = this.pendingPermission.get(msg.sessionId);
|
|
6334
|
+
if (pendingGate) {
|
|
6335
|
+
this.log.info(`[ws.replay_permission] ${msg.sessionId.slice(0, 8)}`, {
|
|
6336
|
+
event: "ws.replay_permission",
|
|
6337
|
+
sessionId: msg.sessionId
|
|
6338
|
+
});
|
|
6339
|
+
ws.send(
|
|
6340
|
+
JSON.stringify({
|
|
6341
|
+
type: "permission",
|
|
6342
|
+
sessionId: msg.sessionId,
|
|
6343
|
+
...pendingGate.prompt ? { prompt: pendingGate.prompt } : {},
|
|
6344
|
+
...pendingGate.detail ? { detail: pendingGate.detail } : {},
|
|
6345
|
+
options: pendingGate.options,
|
|
6346
|
+
...pendingGate.cursor !== void 0 ? { cursor: pendingGate.cursor } : {}
|
|
6347
|
+
})
|
|
6348
|
+
);
|
|
6349
|
+
}
|
|
6350
|
+
const pendingQuestion = this.pendingQuestions.get(msg.sessionId);
|
|
6351
|
+
if (pendingQuestion) {
|
|
6352
|
+
this.log.info(`[ws.replay_question] ${msg.sessionId.slice(0, 8)}`, {
|
|
6353
|
+
event: "ws.replay_question",
|
|
6354
|
+
sessionId: msg.sessionId
|
|
6355
|
+
});
|
|
6356
|
+
ws.send(
|
|
6357
|
+
JSON.stringify({
|
|
6358
|
+
type: "question",
|
|
6359
|
+
sessionId: msg.sessionId,
|
|
6360
|
+
toolUseId: pendingQuestion.toolUseId,
|
|
6361
|
+
questions: pendingQuestion.questions
|
|
6362
|
+
})
|
|
6363
|
+
);
|
|
6364
|
+
}
|
|
5986
6365
|
}
|
|
5987
6366
|
if (msg.type === "hold_session" && typeof msg.sessionId === "string") {
|
|
5988
6367
|
if (this.sessionSubscribers.get(msg.sessionId)?.has(ws)) {
|
|
@@ -6153,7 +6532,7 @@ var StreamerServer = class {
|
|
|
6153
6532
|
});
|
|
6154
6533
|
try {
|
|
6155
6534
|
this.cache = ConversationCache.open(
|
|
6156
|
-
|
|
6535
|
+
join15(this.cacheDir, "cache.db"),
|
|
6157
6536
|
this.tailSize,
|
|
6158
6537
|
void 0,
|
|
6159
6538
|
{
|
|
@@ -6179,11 +6558,11 @@ var StreamerServer = class {
|
|
|
6179
6558
|
if (this.scanProfiles && this.scanProfiles.length > 0) {
|
|
6180
6559
|
for (const profile of this.scanProfiles) {
|
|
6181
6560
|
if (profile.enabled) {
|
|
6182
|
-
this.fileWatcher.watchDirectory(
|
|
6561
|
+
this.fileWatcher.watchDirectory(join15(profile.configDir, "projects"));
|
|
6183
6562
|
}
|
|
6184
6563
|
}
|
|
6185
6564
|
} else {
|
|
6186
|
-
this.fileWatcher.watchDirectory(
|
|
6565
|
+
this.fileWatcher.watchDirectory(join15(homedir7(), ".claude", "projects"));
|
|
6187
6566
|
}
|
|
6188
6567
|
} catch (err) {
|
|
6189
6568
|
const message = err instanceof Error ? err.message : String(err);
|
|
@@ -6768,17 +7147,17 @@ var StreamerServer = class {
|
|
|
6768
7147
|
return scanner;
|
|
6769
7148
|
}
|
|
6770
7149
|
findJsonlPath(uuid) {
|
|
6771
|
-
const projectsDir =
|
|
6772
|
-
if (!
|
|
7150
|
+
const projectsDir = join15(homedir7(), ".claude", "projects");
|
|
7151
|
+
if (!existsSync8(projectsDir)) return null;
|
|
6773
7152
|
const filename = `${uuid}.jsonl`;
|
|
6774
7153
|
for (const dir of readdirSync4(projectsDir)) {
|
|
6775
|
-
const fp =
|
|
6776
|
-
if (
|
|
6777
|
-
const projectDir =
|
|
7154
|
+
const fp = join15(projectsDir, dir, filename);
|
|
7155
|
+
if (existsSync8(fp)) return fp;
|
|
7156
|
+
const projectDir = join15(projectsDir, dir);
|
|
6778
7157
|
try {
|
|
6779
7158
|
for (const sub of readdirSync4(projectDir)) {
|
|
6780
|
-
const subagentPath =
|
|
6781
|
-
if (
|
|
7159
|
+
const subagentPath = join15(projectDir, sub, "subagents", filename);
|
|
7160
|
+
if (existsSync8(subagentPath)) return subagentPath;
|
|
6782
7161
|
}
|
|
6783
7162
|
} catch {
|
|
6784
7163
|
}
|
|
@@ -6925,7 +7304,7 @@ var StreamerServer = class {
|
|
|
6925
7304
|
if (!conv.filePath) return false;
|
|
6926
7305
|
let mtimeMs = null;
|
|
6927
7306
|
try {
|
|
6928
|
-
mtimeMs =
|
|
7307
|
+
mtimeMs = statSync6(conv.filePath).mtimeMs;
|
|
6929
7308
|
} catch {
|
|
6930
7309
|
return false;
|
|
6931
7310
|
}
|
|
@@ -7293,7 +7672,7 @@ var StreamerServer = class {
|
|
|
7293
7672
|
handleGetSession(sessionId, res) {
|
|
7294
7673
|
const session = this.sessionStore.get(sessionId, this.ptyAttachedIds());
|
|
7295
7674
|
if (session) {
|
|
7296
|
-
if (!
|
|
7675
|
+
if (!existsSync8(session.projectPath)) {
|
|
7297
7676
|
session.failureReason = `Project directory not found: ${session.projectPath}`;
|
|
7298
7677
|
}
|
|
7299
7678
|
json(res, 200, session);
|
|
@@ -7501,6 +7880,11 @@ var StreamerServer = class {
|
|
|
7501
7880
|
const answerKeys = sanitizeAnswerKeys(o.answerKeys);
|
|
7502
7881
|
return answerKeys === void 0 ? { index: o.index, label: o.label } : { ...o, answerKeys };
|
|
7503
7882
|
});
|
|
7883
|
+
const subscriberCount = this.sessionSubscribers.get(sessionId)?.size ?? 0;
|
|
7884
|
+
this.log.info(
|
|
7885
|
+
`[ws.broadcast_permission] ${sessionId.slice(0, 8)} subscribers=${subscriberCount}`,
|
|
7886
|
+
{ event: "ws.broadcast_permission", sessionId, subscriberCount }
|
|
7887
|
+
);
|
|
7504
7888
|
this.wsHub.broadcast({
|
|
7505
7889
|
type: "permission",
|
|
7506
7890
|
sessionId,
|
|
@@ -7693,7 +8077,7 @@ var StreamerServer = class {
|
|
|
7693
8077
|
sessionStore: this.sessionStore,
|
|
7694
8078
|
// biome-ignore lint/style/noNonNullAssertion: agentClient is set when agentConfig.enabled is true
|
|
7695
8079
|
agentClient: this.agentClient,
|
|
7696
|
-
conversationsDir: this.cacheDir ?
|
|
8080
|
+
conversationsDir: this.cacheDir ? join15(dirname8(this.cacheDir), "conversations") : "",
|
|
7697
8081
|
agentConfig: this.agentConfig
|
|
7698
8082
|
});
|
|
7699
8083
|
json(res, result.status, result.body);
|
|
@@ -7834,9 +8218,9 @@ var StreamerServer = class {
|
|
|
7834
8218
|
// was passed to Claude via --session-id so the filename matches from the start.
|
|
7835
8219
|
watchForJsonl(sessionId, projectPath) {
|
|
7836
8220
|
const encoded = projectPath.replace(/[/\\:.]/g, "-");
|
|
7837
|
-
const projectsDir =
|
|
8221
|
+
const projectsDir = join15(homedir7(), ".claude", "projects", encoded);
|
|
7838
8222
|
const expectedFile = `${sessionId}.jsonl`;
|
|
7839
|
-
const filePath =
|
|
8223
|
+
const filePath = join15(projectsDir, expectedFile);
|
|
7840
8224
|
const deadline = Date.now() + 12e4;
|
|
7841
8225
|
let watcher = null;
|
|
7842
8226
|
const cleanup = () => {
|
|
@@ -7854,12 +8238,12 @@ var StreamerServer = class {
|
|
|
7854
8238
|
cleanup();
|
|
7855
8239
|
return;
|
|
7856
8240
|
}
|
|
7857
|
-
let resolvedFilePath =
|
|
7858
|
-
if (!resolvedFilePath &&
|
|
8241
|
+
let resolvedFilePath = existsSync8(filePath) ? filePath : null;
|
|
8242
|
+
if (!resolvedFilePath && existsSync8(projectsDir)) {
|
|
7859
8243
|
try {
|
|
7860
8244
|
const now = Date.now();
|
|
7861
|
-
const recent = readdirSync4(projectsDir).filter((f) => f.endsWith(".jsonl")).map((f) => ({ f, mtime:
|
|
7862
|
-
if (recent) resolvedFilePath =
|
|
8245
|
+
const recent = readdirSync4(projectsDir).filter((f) => f.endsWith(".jsonl")).map((f) => ({ f, mtime: statSync6(join15(projectsDir, f)).mtimeMs })).filter(({ mtime }) => now - mtime < 5e3).sort((a, b) => b.mtime - a.mtime)[0];
|
|
8246
|
+
if (recent) resolvedFilePath = join15(projectsDir, recent.f);
|
|
7863
8247
|
} catch {
|
|
7864
8248
|
}
|
|
7865
8249
|
}
|
|
@@ -7868,7 +8252,7 @@ var StreamerServer = class {
|
|
|
7868
8252
|
this.sessionFileMap.set(sessionId, resolvedFilePath);
|
|
7869
8253
|
this.fileWatcher.watch(resolvedFilePath);
|
|
7870
8254
|
try {
|
|
7871
|
-
const existing =
|
|
8255
|
+
const existing = readFileSync7(resolvedFilePath, "utf8").split("\n").filter(Boolean);
|
|
7872
8256
|
if (existing.length > 0) {
|
|
7873
8257
|
this.broadcastConversationLines(sessionId, existing);
|
|
7874
8258
|
}
|
|
@@ -7907,7 +8291,7 @@ var StreamerServer = class {
|
|
|
7907
8291
|
watchForCodexRollout(sessionId, projectPath) {
|
|
7908
8292
|
const deadline = Date.now() + 12e4;
|
|
7909
8293
|
const now = /* @__PURE__ */ new Date();
|
|
7910
|
-
const dateDir =
|
|
8294
|
+
const dateDir = join15(
|
|
7911
8295
|
String(now.getFullYear()),
|
|
7912
8296
|
String(now.getMonth() + 1).padStart(2, "0"),
|
|
7913
8297
|
String(now.getDate()).padStart(2, "0")
|
|
@@ -7920,7 +8304,7 @@ var StreamerServer = class {
|
|
|
7920
8304
|
};
|
|
7921
8305
|
const matchesProjectPath = (candidatePath) => {
|
|
7922
8306
|
try {
|
|
7923
|
-
const firstLine =
|
|
8307
|
+
const firstLine = readFileSync7(candidatePath, "utf8").split("\n", 1)[0];
|
|
7924
8308
|
if (!firstLine) return null;
|
|
7925
8309
|
const parsed = JSON.parse(firstLine);
|
|
7926
8310
|
if (parsed?.type !== "session_meta") return null;
|
|
@@ -7948,8 +8332,8 @@ var StreamerServer = class {
|
|
|
7948
8332
|
this.sessionStore.listManaged().filter((s) => s.id !== sessionId && s.boundConversationId != null).map((s) => s.boundConversationId)
|
|
7949
8333
|
);
|
|
7950
8334
|
for (const root of this.codexRoots) {
|
|
7951
|
-
const sessionsDir =
|
|
7952
|
-
if (!
|
|
8335
|
+
const sessionsDir = join15(root, dateDir);
|
|
8336
|
+
if (!existsSync8(sessionsDir)) continue;
|
|
7953
8337
|
let candidateFiles;
|
|
7954
8338
|
try {
|
|
7955
8339
|
candidateFiles = readdirSync4(sessionsDir).filter((f) => f.endsWith(".jsonl"));
|
|
@@ -7957,9 +8341,9 @@ var StreamerServer = class {
|
|
|
7957
8341
|
continue;
|
|
7958
8342
|
}
|
|
7959
8343
|
const nowMs = Date.now();
|
|
7960
|
-
const recentCandidates = candidateFiles.map((f) => ({ f, mtime:
|
|
8344
|
+
const recentCandidates = candidateFiles.map((f) => ({ f, mtime: statSync6(join15(sessionsDir, f)).mtimeMs })).filter(({ mtime }) => nowMs - mtime < 1e4).sort((a, b) => b.mtime - a.mtime);
|
|
7961
8345
|
for (const { f } of recentCandidates) {
|
|
7962
|
-
const candidatePath =
|
|
8346
|
+
const candidatePath = join15(sessionsDir, f);
|
|
7963
8347
|
const match = matchesProjectPath(candidatePath);
|
|
7964
8348
|
if (!match) continue;
|
|
7965
8349
|
if (boundElsewhere.has(match.id)) continue;
|
|
@@ -7969,7 +8353,7 @@ var StreamerServer = class {
|
|
|
7969
8353
|
this.sessionFileMap.set(sessionId, candidatePath);
|
|
7970
8354
|
this.fileWatcher.watch(candidatePath);
|
|
7971
8355
|
try {
|
|
7972
|
-
const existing =
|
|
8356
|
+
const existing = readFileSync7(candidatePath, "utf8").split("\n").filter(Boolean);
|
|
7973
8357
|
if (existing.length > 0) {
|
|
7974
8358
|
this.broadcastConversationLines(sessionId, existing);
|
|
7975
8359
|
}
|
|
@@ -8091,7 +8475,7 @@ var StreamerServer = class {
|
|
|
8091
8475
|
};
|
|
8092
8476
|
function classifyResumability(cwd) {
|
|
8093
8477
|
if (!cwd) return { resumable: true };
|
|
8094
|
-
if (
|
|
8478
|
+
if (existsSync8(cwd)) return { resumable: true };
|
|
8095
8479
|
const ranInWorktree = /\/\.worktrees\//.test(cwd) || /\/\.claude\/worktrees\//.test(cwd);
|
|
8096
8480
|
return {
|
|
8097
8481
|
resumable: false,
|