@threadbase-sh/streamer 1.23.0 → 1.24.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 +1194 -354
- package/dist/cli.cjs.map +1 -1
- package/dist/index.cjs +1176 -378
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +107 -29
- package/dist/index.d.ts +107 -29
- package/dist/index.js +1046 -253
- package/dist/index.js.map +1 -1
- package/package.json +2 -1
package/dist/index.cjs
CHANGED
|
@@ -30,7 +30,10 @@ var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: tru
|
|
|
30
30
|
// src/index.ts
|
|
31
31
|
var index_exports = {};
|
|
32
32
|
__export(index_exports, {
|
|
33
|
+
CLAUDE_CODE_PROVIDER: () => CLAUDE_CODE_PROVIDER,
|
|
34
|
+
CODEX_CLI_PROVIDER: () => CODEX_CLI_PROVIDER,
|
|
33
35
|
ConversationWatcher: () => ConversationWatcher,
|
|
36
|
+
LiveSessionManager: () => LiveSessionManager,
|
|
34
37
|
PTYManager: () => PTYManager,
|
|
35
38
|
SessionStore: () => SessionStore,
|
|
36
39
|
StreamerServer: () => StreamerServer,
|
|
@@ -44,6 +47,8 @@ __export(index_exports, {
|
|
|
44
47
|
generateApiKey: () => generateApiKey,
|
|
45
48
|
getDbConfig: () => getDbConfig,
|
|
46
49
|
isDbEnabled: () => isDbEnabled,
|
|
50
|
+
isProviderName: () => isProviderName,
|
|
51
|
+
isProviderResumable: () => isProviderResumable,
|
|
47
52
|
loadOrCreateApiKey: () => loadOrCreateApiKey,
|
|
48
53
|
maskConnectionString: () => maskConnectionString,
|
|
49
54
|
readAgentConfig: () => readAgentConfig,
|
|
@@ -527,11 +532,51 @@ async function createPool(config) {
|
|
|
527
532
|
return new Pool(poolConfig);
|
|
528
533
|
}
|
|
529
534
|
|
|
530
|
-
// src/
|
|
531
|
-
var
|
|
532
|
-
|
|
535
|
+
// src/live-session-manager.ts
|
|
536
|
+
var import_path6 = require("path");
|
|
537
|
+
|
|
538
|
+
// src/codex-pty-runner.ts
|
|
539
|
+
var import_headless = require("@xterm/headless");
|
|
540
|
+
var import_crypto2 = require("crypto");
|
|
541
|
+
var import_fs4 = require("fs");
|
|
533
542
|
var import_path4 = require("path");
|
|
534
543
|
|
|
544
|
+
// src/logger.ts
|
|
545
|
+
var import_pino = __toESM(require("pino"), 1);
|
|
546
|
+
var baseLogger = (0, import_pino.default)({
|
|
547
|
+
level: process.env.LOG_LEVEL ?? "info",
|
|
548
|
+
base: { service: "tb-streamer" },
|
|
549
|
+
timestamp: import_pino.default.stdTimeFunctions.isoTime,
|
|
550
|
+
redact: {
|
|
551
|
+
paths: ["req.headers.authorization", "req.headers.cookie", 'req.headers["x-api-key"]'],
|
|
552
|
+
censor: "[redacted]"
|
|
553
|
+
}
|
|
554
|
+
});
|
|
555
|
+
function emit(pinoChild, level, msg, fields, dest) {
|
|
556
|
+
if (dest === "pino" || dest === "both") {
|
|
557
|
+
if (fields && Object.keys(fields).length > 0) pinoChild[level](fields, msg);
|
|
558
|
+
else pinoChild[level](msg);
|
|
559
|
+
}
|
|
560
|
+
if (dest === "console" || dest === "both") {
|
|
561
|
+
const consoleMethod = level === "error" ? "error" : level === "warn" ? "warn" : "log";
|
|
562
|
+
console[consoleMethod](msg);
|
|
563
|
+
}
|
|
564
|
+
}
|
|
565
|
+
function build(pinoChild) {
|
|
566
|
+
return {
|
|
567
|
+
debug: (m, f, d = "both") => emit(pinoChild, "debug", m, f, d),
|
|
568
|
+
info: (m, f, d = "both") => emit(pinoChild, "info", m, f, d),
|
|
569
|
+
warn: (m, f, d = "both") => emit(pinoChild, "warn", m, f, d),
|
|
570
|
+
error: (m, f, d = "both") => emit(pinoChild, "error", m, f, d),
|
|
571
|
+
log: (lvl, m, f, d = "both") => emit(pinoChild, lvl, m, f, d),
|
|
572
|
+
pino: pinoChild
|
|
573
|
+
};
|
|
574
|
+
}
|
|
575
|
+
function getLogger(component) {
|
|
576
|
+
return build(component ? baseLogger.child({ component }) : baseLogger);
|
|
577
|
+
}
|
|
578
|
+
var logger = build(baseLogger);
|
|
579
|
+
|
|
535
580
|
// src/platform.ts
|
|
536
581
|
var import_child_process = require("child_process");
|
|
537
582
|
var import_fs3 = require("fs");
|
|
@@ -596,188 +641,534 @@ function resolveClaudeExe() {
|
|
|
596
641
|
_claudeExe = "claude";
|
|
597
642
|
return _claudeExe;
|
|
598
643
|
}
|
|
599
|
-
|
|
600
|
-
|
|
601
|
-
|
|
602
|
-
if (
|
|
603
|
-
|
|
604
|
-
|
|
605
|
-
|
|
606
|
-
|
|
607
|
-
|
|
608
|
-
|
|
609
|
-
|
|
610
|
-
|
|
611
|
-
|
|
612
|
-
getProcessArgsUnix(pid),
|
|
613
|
-
getProcessStartTimeUnix(pid)
|
|
614
|
-
]);
|
|
615
|
-
const conversationId = extractResumeId(args);
|
|
616
|
-
return {
|
|
617
|
-
pid,
|
|
618
|
-
projectPath: cwd,
|
|
619
|
-
projectName: (0, import_path4.basename)(cwd),
|
|
620
|
-
branch: await readGitBranch(cwd),
|
|
621
|
-
conversationId,
|
|
622
|
-
startedAt
|
|
623
|
-
};
|
|
624
|
-
} catch {
|
|
625
|
-
return null;
|
|
644
|
+
var _codexExe;
|
|
645
|
+
function resolveCodexExe() {
|
|
646
|
+
if (_codexExe !== void 0) return _codexExe;
|
|
647
|
+
if (isWindows) {
|
|
648
|
+
try {
|
|
649
|
+
const found = (0, import_child_process.execFileSync)("where.exe", ["codex"], {
|
|
650
|
+
encoding: "utf-8",
|
|
651
|
+
windowsHide: true,
|
|
652
|
+
timeout: 3e3
|
|
653
|
+
}).trim().split("\n")[0].trim();
|
|
654
|
+
if (found) {
|
|
655
|
+
_codexExe = found;
|
|
656
|
+
return _codexExe;
|
|
626
657
|
}
|
|
627
|
-
}
|
|
628
|
-
|
|
629
|
-
|
|
630
|
-
|
|
631
|
-
|
|
632
|
-
|
|
633
|
-
|
|
634
|
-
|
|
635
|
-
|
|
636
|
-
|
|
637
|
-
|
|
638
|
-
|
|
639
|
-
|
|
640
|
-
|
|
641
|
-
|
|
642
|
-
branch: await readGitBranch(info.cwd),
|
|
643
|
-
conversationId: extractResumeId(info.args),
|
|
644
|
-
startedAt: info.startedAt
|
|
645
|
-
};
|
|
646
|
-
} catch {
|
|
647
|
-
return null;
|
|
658
|
+
} catch {
|
|
659
|
+
}
|
|
660
|
+
const candidates = [
|
|
661
|
+
(0, import_path3.join)((0, import_os2.homedir)(), ".local", "bin", "codex.exe"),
|
|
662
|
+
(0, import_path3.join)(
|
|
663
|
+
process.env.LOCALAPPDATA ?? (0, import_path3.join)((0, import_os2.homedir)(), "AppData", "Local"),
|
|
664
|
+
"Microsoft",
|
|
665
|
+
"WindowsApps",
|
|
666
|
+
"codex.exe"
|
|
667
|
+
)
|
|
668
|
+
];
|
|
669
|
+
for (const p of candidates) {
|
|
670
|
+
if ((0, import_fs3.existsSync)(p)) {
|
|
671
|
+
_codexExe = p;
|
|
672
|
+
return _codexExe;
|
|
648
673
|
}
|
|
649
|
-
}
|
|
650
|
-
|
|
651
|
-
|
|
652
|
-
|
|
653
|
-
|
|
654
|
-
|
|
655
|
-
|
|
656
|
-
|
|
657
|
-
|
|
658
|
-
|
|
659
|
-
(err, stdout) => {
|
|
660
|
-
if (err) reject(err);
|
|
661
|
-
else resolve2(stdout);
|
|
674
|
+
}
|
|
675
|
+
} else {
|
|
676
|
+
try {
|
|
677
|
+
const found = (0, import_child_process.execFileSync)("/usr/bin/which", ["codex"], {
|
|
678
|
+
encoding: "utf-8",
|
|
679
|
+
timeout: 3e3
|
|
680
|
+
}).trim().split("\n")[0].trim();
|
|
681
|
+
if (found && (0, import_fs3.existsSync)(found)) {
|
|
682
|
+
_codexExe = found;
|
|
683
|
+
return _codexExe;
|
|
662
684
|
}
|
|
663
|
-
|
|
664
|
-
|
|
665
|
-
|
|
666
|
-
|
|
667
|
-
|
|
668
|
-
|
|
669
|
-
|
|
670
|
-
|
|
671
|
-
|
|
685
|
+
} catch {
|
|
686
|
+
}
|
|
687
|
+
const candidates = [
|
|
688
|
+
"/opt/homebrew/bin/codex",
|
|
689
|
+
"/usr/local/bin/codex",
|
|
690
|
+
(0, import_path3.join)((0, import_os2.homedir)(), ".local", "bin", "codex")
|
|
691
|
+
];
|
|
692
|
+
for (const p of candidates) {
|
|
693
|
+
if ((0, import_fs3.existsSync)(p)) {
|
|
694
|
+
_codexExe = p;
|
|
695
|
+
return _codexExe;
|
|
696
|
+
}
|
|
697
|
+
}
|
|
672
698
|
}
|
|
699
|
+
_codexExe = "codex";
|
|
700
|
+
return _codexExe;
|
|
673
701
|
}
|
|
674
|
-
|
|
675
|
-
|
|
676
|
-
|
|
677
|
-
|
|
678
|
-
|
|
679
|
-
|
|
680
|
-
return (await run("ps", ["-p", String(pid), "-o", "args="])).trim();
|
|
702
|
+
|
|
703
|
+
// src/providers.ts
|
|
704
|
+
var CLAUDE_CODE_PROVIDER = "claude-code";
|
|
705
|
+
var CODEX_CLI_PROVIDER = "codex-cli";
|
|
706
|
+
function isProviderName(value) {
|
|
707
|
+
return value === CLAUDE_CODE_PROVIDER || value === CODEX_CLI_PROVIDER;
|
|
681
708
|
}
|
|
682
|
-
|
|
683
|
-
|
|
684
|
-
const d = new Date(raw);
|
|
685
|
-
return Number.isNaN(d.getTime()) ? /* @__PURE__ */ new Date() : d;
|
|
709
|
+
function isProviderResumable(_provider, availabilityResumable) {
|
|
710
|
+
return availabilityResumable;
|
|
686
711
|
}
|
|
687
|
-
|
|
688
|
-
|
|
689
|
-
|
|
690
|
-
|
|
691
|
-
|
|
692
|
-
|
|
693
|
-
|
|
694
|
-
|
|
695
|
-
|
|
696
|
-
|
|
712
|
+
|
|
713
|
+
// src/codex-pty-runner.ts
|
|
714
|
+
var OUTPUT_BUFFER_MAX = 65536;
|
|
715
|
+
var PTY_COLS = 120;
|
|
716
|
+
var PTY_ROWS = 40;
|
|
717
|
+
var SCREEN_SCROLLBACK = 1e3;
|
|
718
|
+
var CODEX_PROMPT_READY_TEXT = "Ready";
|
|
719
|
+
var CODEX_TRUST_GATE_REGEX = /trust the contents/i;
|
|
720
|
+
var SUBMIT_BYTES = "\r";
|
|
721
|
+
var CODEX_SUBMIT_DELAY_MS = 16;
|
|
722
|
+
function digestBytes(s) {
|
|
723
|
+
const escaped = s.replace(new RegExp(String.fromCharCode(27), "g"), "\\x1b").replace(/\r/g, "\\r").replace(/\n/g, "\\n").replace(/\t/g, "\\t");
|
|
724
|
+
if (escaped.length <= 200) return escaped;
|
|
725
|
+
return `${escaped.slice(0, 100)}\u2026[${escaped.length - 200}B omitted]\u2026${escaped.slice(-100)}`;
|
|
697
726
|
}
|
|
698
|
-
|
|
727
|
+
var pty = null;
|
|
728
|
+
async function loadPty() {
|
|
729
|
+
if (pty) return pty;
|
|
699
730
|
try {
|
|
700
|
-
|
|
701
|
-
|
|
702
|
-
|
|
703
|
-
|
|
704
|
-
|
|
705
|
-
|
|
706
|
-
|
|
707
|
-
]);
|
|
708
|
-
const lines = output.trim().split(/\r?\n/).filter((l) => l.trim().length > 0);
|
|
709
|
-
if (lines.length < 2) return null;
|
|
710
|
-
const parts = lines[1].split(",");
|
|
711
|
-
const args = parts[1] ?? "";
|
|
712
|
-
const creationDate = parts[2] ?? "";
|
|
713
|
-
const year = creationDate.slice(0, 4);
|
|
714
|
-
const month = creationDate.slice(4, 6);
|
|
715
|
-
const day = creationDate.slice(6, 8);
|
|
716
|
-
const hour = creationDate.slice(8, 10);
|
|
717
|
-
const min = creationDate.slice(10, 12);
|
|
718
|
-
const sec = creationDate.slice(12, 14);
|
|
719
|
-
const startedAt = /* @__PURE__ */ new Date(`${year}-${month}-${day}T${hour}:${min}:${sec}`);
|
|
720
|
-
if (Number.isNaN(startedAt.getTime())) return null;
|
|
721
|
-
const exePath = parts[3] ?? "";
|
|
722
|
-
const cwd = exePath ? (0, import_path4.dirname)(exePath) : "";
|
|
723
|
-
return { cwd, args, startedAt };
|
|
724
|
-
} catch {
|
|
725
|
-
return null;
|
|
731
|
+
pty = await import("node-pty");
|
|
732
|
+
return pty;
|
|
733
|
+
} catch (err) {
|
|
734
|
+
throw new Error(
|
|
735
|
+
`node-pty is required for PTY management but failed to load. Ensure it is installed: npm install node-pty
|
|
736
|
+
Original error: ${err}`
|
|
737
|
+
);
|
|
726
738
|
}
|
|
727
739
|
}
|
|
728
|
-
function
|
|
729
|
-
|
|
730
|
-
|
|
740
|
+
function createScreen() {
|
|
741
|
+
return new import_headless.Terminal({
|
|
742
|
+
cols: PTY_COLS,
|
|
743
|
+
rows: PTY_ROWS,
|
|
744
|
+
scrollback: SCREEN_SCROLLBACK,
|
|
745
|
+
allowProposedApi: true
|
|
746
|
+
});
|
|
731
747
|
}
|
|
732
|
-
|
|
733
|
-
|
|
734
|
-
|
|
735
|
-
|
|
736
|
-
|
|
748
|
+
var CodexPtyRunner = class {
|
|
749
|
+
sessions = /* @__PURE__ */ new Map();
|
|
750
|
+
onOutput;
|
|
751
|
+
onStatusChange;
|
|
752
|
+
onReady;
|
|
753
|
+
// Accepted for shape-compatibility with PTYManagerOptions; Codex has no
|
|
754
|
+
// detected equivalent yet (Phase 0) — never invoked.
|
|
755
|
+
onPermissionChange;
|
|
756
|
+
onLiveQuestion;
|
|
757
|
+
onLiveQuestionGone;
|
|
758
|
+
log;
|
|
759
|
+
// Tracks sessions whose PTY has spawned but Codex hasn't yet reached its
|
|
760
|
+
// "Ready" status bar — i.e. onReady hasn't fired.
|
|
761
|
+
pendingReady = /* @__PURE__ */ new Set();
|
|
762
|
+
// Inputs received via sendInput() while the session was still pendingReady.
|
|
763
|
+
// Flushed in arrival order once Codex reaches Ready.
|
|
764
|
+
queuedInputs = /* @__PURE__ */ new Map();
|
|
765
|
+
// Per-session debounce so the directory-trust gate's \r is only written once.
|
|
766
|
+
trustGateAnswered = /* @__PURE__ */ new Set();
|
|
767
|
+
// In-flight start()/startFresh() calls keyed by sessionId. A second
|
|
768
|
+
// concurrent resume for the same session (double-tap, client retry) awaits
|
|
769
|
+
// the first call's promise instead of spawning a duplicate PTY (CRITICAL #3).
|
|
770
|
+
startPromises = /* @__PURE__ */ new Map();
|
|
771
|
+
constructor(options = {}) {
|
|
772
|
+
this.onOutput = options.onOutput;
|
|
773
|
+
this.onStatusChange = options.onStatusChange;
|
|
774
|
+
this.onReady = options.onReady;
|
|
775
|
+
this.onPermissionChange = options.onPermissionChange;
|
|
776
|
+
this.onLiveQuestion = options.onLiveQuestion;
|
|
777
|
+
this.onLiveQuestionGone = options.onLiveQuestionGone;
|
|
778
|
+
this.log = options.logger ?? getLogger("codex-pty");
|
|
737
779
|
}
|
|
738
|
-
|
|
739
|
-
|
|
740
|
-
//
|
|
741
|
-
|
|
742
|
-
|
|
743
|
-
|
|
744
|
-
|
|
745
|
-
|
|
746
|
-
|
|
747
|
-
|
|
748
|
-
|
|
749
|
-
|
|
750
|
-
|
|
751
|
-
timestamp: import_pino.default.stdTimeFunctions.isoTime,
|
|
752
|
-
redact: {
|
|
753
|
-
paths: ["req.headers.authorization", "req.headers.cookie", 'req.headers["x-api-key"]'],
|
|
754
|
-
censor: "[redacted]"
|
|
780
|
+
// Resume an existing Codex session. sessionId is the Codex-persisted
|
|
781
|
+
// session_meta.payload.id (Phase 0, Section 8) — Codex has no fresh-session
|
|
782
|
+
// equivalent of --session-id, so start() always means "resume".
|
|
783
|
+
async start(sessionId, options) {
|
|
784
|
+
const existing = this.sessions.get(sessionId);
|
|
785
|
+
if (existing) return toPublicSession(existing);
|
|
786
|
+
const inFlight = this.startPromises.get(sessionId);
|
|
787
|
+
if (inFlight) return inFlight;
|
|
788
|
+
const promise = this.doStart(sessionId, options).finally(() => {
|
|
789
|
+
this.startPromises.delete(sessionId);
|
|
790
|
+
});
|
|
791
|
+
this.startPromises.set(sessionId, promise);
|
|
792
|
+
return promise;
|
|
755
793
|
}
|
|
756
|
-
|
|
757
|
-
|
|
758
|
-
|
|
759
|
-
|
|
760
|
-
|
|
794
|
+
async doStart(sessionId, options) {
|
|
795
|
+
const nodePty = await loadPty();
|
|
796
|
+
const projectName = options.projectName ?? (0, import_path4.basename)(options.projectPath);
|
|
797
|
+
const proc = nodePty.spawn(
|
|
798
|
+
resolveCodexExe(),
|
|
799
|
+
["resume", sessionId, "--cd", options.projectPath, "--no-alt-screen"],
|
|
800
|
+
{
|
|
801
|
+
name: "xterm-256color",
|
|
802
|
+
cols: PTY_COLS,
|
|
803
|
+
rows: PTY_ROWS,
|
|
804
|
+
cwd: options.projectPath,
|
|
805
|
+
env: process.env
|
|
806
|
+
}
|
|
807
|
+
);
|
|
808
|
+
const session = {
|
|
809
|
+
id: sessionId,
|
|
810
|
+
provider: CODEX_CLI_PROVIDER,
|
|
811
|
+
projectPath: options.projectPath,
|
|
812
|
+
projectName,
|
|
813
|
+
branch: options.branch ?? "",
|
|
814
|
+
status: "running",
|
|
815
|
+
startedAt: /* @__PURE__ */ new Date(),
|
|
816
|
+
completedAt: null,
|
|
817
|
+
promptCount: 0,
|
|
818
|
+
lastOutput: "",
|
|
819
|
+
process: proc,
|
|
820
|
+
outputBuffer: Buffer.alloc(0),
|
|
821
|
+
screen: createScreen()
|
|
822
|
+
};
|
|
823
|
+
this.sessions.set(sessionId, session);
|
|
824
|
+
this.pendingReady.add(sessionId);
|
|
825
|
+
proc.onData((data) => {
|
|
826
|
+
this.handleOutput(sessionId, data);
|
|
827
|
+
});
|
|
828
|
+
proc.onExit(({ exitCode }) => {
|
|
829
|
+
this.pendingReady.delete(sessionId);
|
|
830
|
+
this.handleExit(sessionId, exitCode);
|
|
831
|
+
});
|
|
832
|
+
return toPublicSession(session);
|
|
833
|
+
}
|
|
834
|
+
// Start a brand-new Codex session. Codex has no --session-id equivalent for
|
|
835
|
+
// a fresh launch — it assigns its own id, discovered later (Task 3's
|
|
836
|
+
// binding logic). This runner generates a local placeholder id for the
|
|
837
|
+
// ManagedSession handle only.
|
|
838
|
+
async startFresh(options) {
|
|
839
|
+
const nodePty = await loadPty();
|
|
840
|
+
const sessionId = (0, import_crypto2.randomUUID)();
|
|
841
|
+
const projectName = options.projectName ?? (0, import_path4.basename)(options.projectPath);
|
|
842
|
+
const args = ["--cd", options.projectPath, "--no-alt-screen"];
|
|
843
|
+
if (options.systemPrompt) {
|
|
844
|
+
args.push(options.systemPrompt);
|
|
845
|
+
}
|
|
846
|
+
const proc = nodePty.spawn(resolveCodexExe(), args, {
|
|
847
|
+
name: "xterm-256color",
|
|
848
|
+
cols: PTY_COLS,
|
|
849
|
+
rows: PTY_ROWS,
|
|
850
|
+
cwd: options.projectPath,
|
|
851
|
+
env: process.env
|
|
852
|
+
});
|
|
853
|
+
const session = {
|
|
854
|
+
id: sessionId,
|
|
855
|
+
provider: CODEX_CLI_PROVIDER,
|
|
856
|
+
projectPath: options.projectPath,
|
|
857
|
+
projectName,
|
|
858
|
+
branch: "",
|
|
859
|
+
status: "running",
|
|
860
|
+
startedAt: /* @__PURE__ */ new Date(),
|
|
861
|
+
completedAt: null,
|
|
862
|
+
promptCount: 0,
|
|
863
|
+
lastOutput: "",
|
|
864
|
+
process: proc,
|
|
865
|
+
outputBuffer: Buffer.alloc(0),
|
|
866
|
+
screen: createScreen()
|
|
867
|
+
};
|
|
868
|
+
this.sessions.set(sessionId, session);
|
|
869
|
+
this.pendingReady.add(sessionId);
|
|
870
|
+
proc.onData((data) => {
|
|
871
|
+
this.handleOutput(sessionId, data);
|
|
872
|
+
});
|
|
873
|
+
proc.onExit(({ exitCode }) => {
|
|
874
|
+
this.pendingReady.delete(sessionId);
|
|
875
|
+
this.handleExit(sessionId, exitCode);
|
|
876
|
+
});
|
|
877
|
+
return toPublicSession(session);
|
|
878
|
+
}
|
|
879
|
+
// Write raw key bytes directly to the PTY, same as PTYManager.sendKeys.
|
|
880
|
+
sendKeys(sessionId, keys) {
|
|
881
|
+
const session = this.sessions.get(sessionId);
|
|
882
|
+
if (!session) throw new Error(`Session not found: ${sessionId}`);
|
|
883
|
+
if (session.status === "idle") {
|
|
884
|
+
throw new Error(`Session is idle (no active PTY): ${sessionId}`);
|
|
885
|
+
}
|
|
886
|
+
if (session.status === "waiting_input") {
|
|
887
|
+
session.status = "running";
|
|
888
|
+
this.onStatusChange?.(toPublicSession(session));
|
|
889
|
+
}
|
|
890
|
+
this.log.info(
|
|
891
|
+
`[codex.keys.write] ${sessionId.slice(0, 8)} bytes=${keys.length} digest=${digestBytes(keys)}`,
|
|
892
|
+
{ event: "codex.keys_write", sessionId, byteLen: keys.length }
|
|
893
|
+
);
|
|
894
|
+
session.process.write(keys);
|
|
895
|
+
session.lastActivityAt = /* @__PURE__ */ new Date();
|
|
896
|
+
}
|
|
897
|
+
sendInput(sessionId, input) {
|
|
898
|
+
const session = this.sessions.get(sessionId);
|
|
899
|
+
if (!session) throw new Error(`Session not found: ${sessionId}`);
|
|
900
|
+
if (session.status === "idle") {
|
|
901
|
+
throw new Error(`Session is idle (no active PTY): ${sessionId}`);
|
|
902
|
+
}
|
|
903
|
+
if (this.pendingReady.has(sessionId)) {
|
|
904
|
+
const queue = this.queuedInputs.get(sessionId) ?? [];
|
|
905
|
+
queue.push(input);
|
|
906
|
+
this.queuedInputs.set(sessionId, queue);
|
|
907
|
+
session.lastActivityAt = /* @__PURE__ */ new Date();
|
|
908
|
+
session.promptCount++;
|
|
909
|
+
this.log.warn(
|
|
910
|
+
`[codex.input.queued] ${sessionId.slice(0, 8)} promptCount=${session.promptCount} queueLen=${queue.length}`,
|
|
911
|
+
{
|
|
912
|
+
event: "codex.input_queued",
|
|
913
|
+
sessionId,
|
|
914
|
+
promptCount: session.promptCount,
|
|
915
|
+
queueLen: queue.length,
|
|
916
|
+
inputLen: input.length
|
|
917
|
+
}
|
|
918
|
+
);
|
|
919
|
+
return session.promptCount;
|
|
920
|
+
}
|
|
921
|
+
if (session.status === "waiting_input") {
|
|
922
|
+
session.status = "running";
|
|
923
|
+
this.onStatusChange?.(toPublicSession(session));
|
|
924
|
+
}
|
|
925
|
+
this.writeSubmit(sessionId, session, input, "direct", session.promptCount + 1);
|
|
926
|
+
session.lastActivityAt = /* @__PURE__ */ new Date();
|
|
927
|
+
session.promptCount++;
|
|
928
|
+
return session.promptCount;
|
|
929
|
+
}
|
|
930
|
+
// Write the input as plain bytes (no bracketed-paste wrap — Phase 0
|
|
931
|
+
// confirmed Codex accepts plain keystrokes), then submit \r after a short
|
|
932
|
+
// delay so Codex's TUI gets an event-loop tick to process the input first.
|
|
933
|
+
writeSubmit(sessionId, session, input, path, promptCount) {
|
|
934
|
+
this.log.info(
|
|
935
|
+
`[codex.input.write] ${sessionId.slice(0, 8)} promptCount=${promptCount} bytes=${input.length} digest=${digestBytes(input)}`,
|
|
936
|
+
{
|
|
937
|
+
event: "codex.input_write",
|
|
938
|
+
sessionId,
|
|
939
|
+
promptCount,
|
|
940
|
+
byteLen: input.length,
|
|
941
|
+
digest: digestBytes(input),
|
|
942
|
+
path,
|
|
943
|
+
phase: "input"
|
|
944
|
+
}
|
|
945
|
+
);
|
|
946
|
+
session.process.write(input);
|
|
947
|
+
setTimeout(() => {
|
|
948
|
+
const current = this.sessions.get(sessionId);
|
|
949
|
+
if (!current || current !== session) return;
|
|
950
|
+
this.log.info(
|
|
951
|
+
`[codex.input.submit] ${sessionId.slice(0, 8)} promptCount=${promptCount} digest=\\r`,
|
|
952
|
+
{
|
|
953
|
+
event: "codex.input_write",
|
|
954
|
+
sessionId,
|
|
955
|
+
promptCount,
|
|
956
|
+
byteLen: SUBMIT_BYTES.length,
|
|
957
|
+
digest: "\\r",
|
|
958
|
+
path,
|
|
959
|
+
phase: "submit"
|
|
960
|
+
}
|
|
961
|
+
);
|
|
962
|
+
current.process.write(SUBMIT_BYTES);
|
|
963
|
+
}, CODEX_SUBMIT_DELAY_MS);
|
|
964
|
+
}
|
|
965
|
+
// Drain any inputs sent while the session was still pendingReady, writing
|
|
966
|
+
// them in arrival order now that Codex is Ready.
|
|
967
|
+
flushQueuedInputs(sessionId) {
|
|
968
|
+
const queue = this.queuedInputs.get(sessionId);
|
|
969
|
+
if (!queue || queue.length === 0) return;
|
|
970
|
+
this.queuedInputs.delete(sessionId);
|
|
971
|
+
const session = this.sessions.get(sessionId);
|
|
972
|
+
if (!session) return;
|
|
973
|
+
this.log.info(
|
|
974
|
+
`[codex.flush] ${sessionId.slice(0, 8)} flushing ${queue.length} queued input(s)`,
|
|
975
|
+
{
|
|
976
|
+
event: "codex.flush_queued",
|
|
977
|
+
sessionId,
|
|
978
|
+
queueLen: queue.length
|
|
979
|
+
}
|
|
980
|
+
);
|
|
981
|
+
queue.forEach((input, i) => {
|
|
982
|
+
const writeAt = i * CODEX_SUBMIT_DELAY_MS * 2;
|
|
983
|
+
if (writeAt === 0) {
|
|
984
|
+
this.writeSubmit(sessionId, session, input, "flush", session.promptCount);
|
|
985
|
+
} else {
|
|
986
|
+
setTimeout(() => {
|
|
987
|
+
const current = this.sessions.get(sessionId);
|
|
988
|
+
if (!current || current !== session) return;
|
|
989
|
+
this.writeSubmit(sessionId, session, input, "flush", session.promptCount);
|
|
990
|
+
}, writeAt);
|
|
991
|
+
}
|
|
992
|
+
});
|
|
993
|
+
}
|
|
994
|
+
// SIGINT produces a clean exitCode=0 exit (Phase 0 — confirmed).
|
|
995
|
+
cancel(sessionId) {
|
|
996
|
+
const session = this.sessions.get(sessionId);
|
|
997
|
+
if (!session) throw new Error(`Session not found: ${sessionId}`);
|
|
998
|
+
session.process.kill("SIGINT");
|
|
999
|
+
}
|
|
1000
|
+
killPid(pid) {
|
|
1001
|
+
try {
|
|
1002
|
+
process.kill(pid, "SIGTERM");
|
|
1003
|
+
} catch {
|
|
1004
|
+
}
|
|
1005
|
+
}
|
|
1006
|
+
// Kill the PTY and mark the session idle. Mirrors PTYManager.putOnHold.
|
|
1007
|
+
putOnHold(sessionId) {
|
|
1008
|
+
const session = this.sessions.get(sessionId);
|
|
1009
|
+
if (!session) return;
|
|
1010
|
+
this.pendingReady.delete(sessionId);
|
|
1011
|
+
this.queuedInputs.delete(sessionId);
|
|
1012
|
+
this.trustGateAnswered.delete(sessionId);
|
|
1013
|
+
try {
|
|
1014
|
+
session.process.kill("SIGINT");
|
|
1015
|
+
} catch {
|
|
1016
|
+
}
|
|
1017
|
+
session.status = "idle";
|
|
1018
|
+
session.completedAt = /* @__PURE__ */ new Date();
|
|
1019
|
+
session.screen.dispose();
|
|
1020
|
+
this.sessions.delete(sessionId);
|
|
1021
|
+
this.onStatusChange?.(toPublicSession(session));
|
|
1022
|
+
}
|
|
1023
|
+
getOutput(sessionId) {
|
|
1024
|
+
const session = this.sessions.get(sessionId);
|
|
1025
|
+
if (!session) throw new Error(`Session not found: ${sessionId}`);
|
|
1026
|
+
return session.outputBuffer.toString("utf-8");
|
|
1027
|
+
}
|
|
1028
|
+
// Render the last `maxLines` rows of the session's screen in true on-screen
|
|
1029
|
+
// order — same flush-then-read technique as PTYManager.getOutputLines.
|
|
1030
|
+
async getOutputLines(sessionId, maxLines) {
|
|
1031
|
+
const session = this.sessions.get(sessionId);
|
|
1032
|
+
if (!session) throw new Error(`Session not found: ${sessionId}`);
|
|
1033
|
+
await new Promise((resolve2) => session.screen.write("", () => resolve2()));
|
|
1034
|
+
const buf = session.screen.buffer.active;
|
|
1035
|
+
const lines = [];
|
|
1036
|
+
for (let y = 0; y < buf.length; y++) {
|
|
1037
|
+
lines.push(buf.getLine(y)?.translateToString(true) ?? "");
|
|
1038
|
+
}
|
|
1039
|
+
while (lines.length > 0 && lines[lines.length - 1] === "") {
|
|
1040
|
+
lines.pop();
|
|
1041
|
+
}
|
|
1042
|
+
return lines.slice(-maxLines);
|
|
1043
|
+
}
|
|
1044
|
+
getSession(sessionId) {
|
|
1045
|
+
const session = this.sessions.get(sessionId);
|
|
1046
|
+
return session ? toPublicSession(session) : null;
|
|
1047
|
+
}
|
|
1048
|
+
hasSession(sessionId) {
|
|
1049
|
+
return this.sessions.has(sessionId);
|
|
1050
|
+
}
|
|
1051
|
+
listSessions() {
|
|
1052
|
+
return Array.from(this.sessions.values()).map(toPublicSession);
|
|
1053
|
+
}
|
|
1054
|
+
dispose() {
|
|
1055
|
+
for (const session of this.sessions.values()) {
|
|
1056
|
+
try {
|
|
1057
|
+
session.process.kill();
|
|
1058
|
+
} catch {
|
|
1059
|
+
}
|
|
1060
|
+
session.screen.dispose();
|
|
1061
|
+
}
|
|
1062
|
+
this.sessions.clear();
|
|
1063
|
+
this.pendingReady.clear();
|
|
1064
|
+
this.queuedInputs.clear();
|
|
1065
|
+
this.trustGateAnswered.clear();
|
|
1066
|
+
}
|
|
1067
|
+
handleOutput(sessionId, data) {
|
|
1068
|
+
const session = this.sessions.get(sessionId);
|
|
1069
|
+
if (!session) return;
|
|
1070
|
+
const chunk = Buffer.from(data, "utf-8");
|
|
1071
|
+
session.outputBuffer = Buffer.concat([session.outputBuffer, chunk]);
|
|
1072
|
+
if (session.outputBuffer.length > OUTPUT_BUFFER_MAX) {
|
|
1073
|
+
session.outputBuffer = session.outputBuffer.subarray(
|
|
1074
|
+
session.outputBuffer.length - OUTPUT_BUFFER_MAX
|
|
1075
|
+
);
|
|
1076
|
+
}
|
|
1077
|
+
session.screen.write(data);
|
|
1078
|
+
session.lastOutput = stripAnsi(data);
|
|
1079
|
+
this.onOutput?.(sessionId, data);
|
|
1080
|
+
this.detectReady(sessionId, session).catch((err) => {
|
|
1081
|
+
this.log.warn("[codex.ready_detect] failed", {
|
|
1082
|
+
event: "codex.ready_detect_failed",
|
|
1083
|
+
sessionId,
|
|
1084
|
+
err
|
|
1085
|
+
});
|
|
1086
|
+
});
|
|
1087
|
+
}
|
|
1088
|
+
// Renders the session's headless screen and checks for the directory-trust
|
|
1089
|
+
// gate (answered once, debounced) and the "Ready" status-bar text. Only
|
|
1090
|
+
// transitions to waiting_input / fires onReady when the rendered status
|
|
1091
|
+
// line literally contains "Ready" — `›` alone (visible during "Starting")
|
|
1092
|
+
// is NOT a valid readiness signal (Phase 0).
|
|
1093
|
+
async detectReady(sessionId, session) {
|
|
1094
|
+
if (session.status !== "running" || !this.pendingReady.has(sessionId)) return;
|
|
1095
|
+
const lines = await this.getOutputLines(sessionId, PTY_ROWS);
|
|
1096
|
+
const screenText = lines.join("\n");
|
|
1097
|
+
if (CODEX_TRUST_GATE_REGEX.test(screenText)) {
|
|
1098
|
+
if (!this.trustGateAnswered.has(sessionId)) {
|
|
1099
|
+
this.trustGateAnswered.add(sessionId);
|
|
1100
|
+
this.log.info(`[codex.trust_gate] ${sessionId.slice(0, 8)} auto-answering`, {
|
|
1101
|
+
event: "codex.trust_gate",
|
|
1102
|
+
sessionId
|
|
1103
|
+
});
|
|
1104
|
+
session.process.write("\r");
|
|
1105
|
+
}
|
|
1106
|
+
return;
|
|
1107
|
+
}
|
|
1108
|
+
const lastNonBlank2 = [...lines].reverse().find((l) => l.trim() !== "") ?? "";
|
|
1109
|
+
if (!lastNonBlank2.includes(CODEX_PROMPT_READY_TEXT)) return;
|
|
1110
|
+
this.markReady(sessionId, session);
|
|
1111
|
+
}
|
|
1112
|
+
markReady(sessionId, session) {
|
|
1113
|
+
session.lastActivityAt = /* @__PURE__ */ new Date();
|
|
1114
|
+
session.status = "waiting_input";
|
|
1115
|
+
this.log.info(`[codex.ready] ${sessionId.slice(0, 8)}`, {
|
|
1116
|
+
event: "codex.ready",
|
|
1117
|
+
sessionId
|
|
1118
|
+
});
|
|
1119
|
+
this.onStatusChange?.(toPublicSession(session));
|
|
1120
|
+
if (this.pendingReady.has(sessionId)) {
|
|
1121
|
+
this.pendingReady.delete(sessionId);
|
|
1122
|
+
this.flushQueuedInputs(sessionId);
|
|
1123
|
+
this.onReady?.(toPublicSession(session));
|
|
1124
|
+
}
|
|
761
1125
|
}
|
|
762
|
-
|
|
763
|
-
const
|
|
764
|
-
|
|
1126
|
+
handleExit(sessionId, exitCode) {
|
|
1127
|
+
const session = this.sessions.get(sessionId);
|
|
1128
|
+
if (!session) return;
|
|
1129
|
+
session.completedAt = /* @__PURE__ */ new Date();
|
|
1130
|
+
session.status = "idle";
|
|
1131
|
+
const elapsedMs = session.completedAt.getTime() - session.startedAt.getTime();
|
|
1132
|
+
if (exitCode !== 0 && elapsedMs < 2e3 && session.lastOutput === "") {
|
|
1133
|
+
if (!(0, import_fs4.existsSync)(session.projectPath)) {
|
|
1134
|
+
session.failureReason = `Project directory not found: ${session.projectPath}`;
|
|
1135
|
+
} else {
|
|
1136
|
+
session.failureReason = `Codex process exited immediately (code ${exitCode}).`;
|
|
1137
|
+
}
|
|
1138
|
+
}
|
|
1139
|
+
this.onStatusChange?.(toPublicSession(session));
|
|
1140
|
+
session.screen.dispose();
|
|
1141
|
+
this.sessions.delete(sessionId);
|
|
1142
|
+
this.queuedInputs.delete(sessionId);
|
|
1143
|
+
this.trustGateAnswered.delete(sessionId);
|
|
765
1144
|
}
|
|
766
|
-
}
|
|
767
|
-
function
|
|
1145
|
+
};
|
|
1146
|
+
function toPublicSession(s) {
|
|
768
1147
|
return {
|
|
769
|
-
|
|
770
|
-
|
|
771
|
-
|
|
772
|
-
|
|
773
|
-
|
|
774
|
-
|
|
1148
|
+
id: s.id,
|
|
1149
|
+
provider: s.provider ?? CODEX_CLI_PROVIDER,
|
|
1150
|
+
projectPath: s.projectPath,
|
|
1151
|
+
projectName: s.projectName,
|
|
1152
|
+
branch: s.branch,
|
|
1153
|
+
status: s.status,
|
|
1154
|
+
startedAt: s.startedAt,
|
|
1155
|
+
completedAt: s.completedAt,
|
|
1156
|
+
promptCount: s.promptCount,
|
|
1157
|
+
lastOutput: s.lastOutput,
|
|
1158
|
+
...s.failureReason != null && { failureReason: s.failureReason },
|
|
1159
|
+
...s.lastActivityAt != null && { lastActivityAt: s.lastActivityAt },
|
|
1160
|
+
...s.filePath != null && { filePath: s.filePath }
|
|
775
1161
|
};
|
|
776
1162
|
}
|
|
777
|
-
function
|
|
778
|
-
return
|
|
1163
|
+
function stripAnsi(str) {
|
|
1164
|
+
return str.replace(/\x1b\[[0-9;]*[a-zA-Z]/g, "").replace(/\x1b\][^\x07]*\x07/g, "");
|
|
779
1165
|
}
|
|
780
|
-
|
|
1166
|
+
|
|
1167
|
+
// src/pty-manager.ts
|
|
1168
|
+
var import_headless2 = require("@xterm/headless");
|
|
1169
|
+
var import_crypto3 = require("crypto");
|
|
1170
|
+
var import_fs5 = require("fs");
|
|
1171
|
+
var import_path5 = require("path");
|
|
781
1172
|
|
|
782
1173
|
// src/services/questions/detectPermissionGate.ts
|
|
783
1174
|
var OSC_777_RE = /\x1b\]777;notify;Claude Code;[^\x07\x1b]*/;
|
|
@@ -936,28 +1327,28 @@ function detectShellPrompt(lines) {
|
|
|
936
1327
|
}
|
|
937
1328
|
|
|
938
1329
|
// src/pty-manager.ts
|
|
939
|
-
var
|
|
940
|
-
var
|
|
941
|
-
var
|
|
942
|
-
var
|
|
1330
|
+
var OUTPUT_BUFFER_MAX2 = 65536;
|
|
1331
|
+
var PTY_COLS2 = 120;
|
|
1332
|
+
var PTY_ROWS2 = 40;
|
|
1333
|
+
var SCREEN_SCROLLBACK2 = 1e3;
|
|
943
1334
|
var CLAUDE_PROMPT_MARKERS = ["\u256D", "\u276F"];
|
|
944
1335
|
var PROMPT_MARKER_FALLBACK_MS = 1e4;
|
|
945
1336
|
function buildPasteBytes(input) {
|
|
946
1337
|
return `\x1B[200~${input}\x1B[201~`;
|
|
947
1338
|
}
|
|
948
|
-
var
|
|
1339
|
+
var SUBMIT_BYTES2 = "\r";
|
|
949
1340
|
var SUBMIT_DELAY_MS = 16;
|
|
950
|
-
function
|
|
1341
|
+
function digestBytes2(s) {
|
|
951
1342
|
const escaped = s.replace(new RegExp(String.fromCharCode(27), "g"), "\\x1b").replace(/\r/g, "\\r").replace(/\n/g, "\\n").replace(/\t/g, "\\t");
|
|
952
1343
|
if (escaped.length <= 200) return escaped;
|
|
953
1344
|
return `${escaped.slice(0, 100)}\u2026[${escaped.length - 200}B omitted]\u2026${escaped.slice(-100)}`;
|
|
954
1345
|
}
|
|
955
|
-
var
|
|
956
|
-
async function
|
|
957
|
-
if (
|
|
1346
|
+
var pty2 = null;
|
|
1347
|
+
async function loadPty2() {
|
|
1348
|
+
if (pty2) return pty2;
|
|
958
1349
|
try {
|
|
959
|
-
|
|
960
|
-
return
|
|
1350
|
+
pty2 = await import("node-pty");
|
|
1351
|
+
return pty2;
|
|
961
1352
|
} catch (err) {
|
|
962
1353
|
throw new Error(
|
|
963
1354
|
`node-pty is required for PTY management but failed to load. Ensure it is installed: npm install node-pty
|
|
@@ -965,11 +1356,11 @@ Original error: ${err}`
|
|
|
965
1356
|
);
|
|
966
1357
|
}
|
|
967
1358
|
}
|
|
968
|
-
function
|
|
969
|
-
return new
|
|
970
|
-
cols:
|
|
971
|
-
rows:
|
|
972
|
-
scrollback:
|
|
1359
|
+
function createScreen2() {
|
|
1360
|
+
return new import_headless2.Terminal({
|
|
1361
|
+
cols: PTY_COLS2,
|
|
1362
|
+
rows: PTY_ROWS2,
|
|
1363
|
+
scrollback: SCREEN_SCROLLBACK2,
|
|
973
1364
|
allowProposedApi: true
|
|
974
1365
|
});
|
|
975
1366
|
}
|
|
@@ -1018,6 +1409,10 @@ var PTYManager = class {
|
|
|
1018
1409
|
// to a given input or fell silent. Reset on dispose().
|
|
1019
1410
|
chunkIndex = /* @__PURE__ */ new Map();
|
|
1020
1411
|
lastChunkAt = /* @__PURE__ */ new Map();
|
|
1412
|
+
// In-flight start()/startFresh() calls keyed by sessionId. A second
|
|
1413
|
+
// concurrent resume for the same session (double-tap, client retry) awaits
|
|
1414
|
+
// the first call's promise instead of spawning a duplicate PTY (CRITICAL #3).
|
|
1415
|
+
startPromises = /* @__PURE__ */ new Map();
|
|
1021
1416
|
constructor(options = {}) {
|
|
1022
1417
|
this.onOutput = options.onOutput;
|
|
1023
1418
|
this.onStatusChange = options.onStatusChange;
|
|
@@ -1040,7 +1435,18 @@ var PTYManager = class {
|
|
|
1040
1435
|
// custom-API-key — are cleared by the seeded ~/.claude.json in
|
|
1041
1436
|
// docker/entrypoint.sh.) startFresh() uses the same flag for the same reason.
|
|
1042
1437
|
async start(sessionId, options) {
|
|
1043
|
-
const
|
|
1438
|
+
const existing = this.sessions.get(sessionId);
|
|
1439
|
+
if (existing) return toPublicSession2(existing);
|
|
1440
|
+
const inFlight = this.startPromises.get(sessionId);
|
|
1441
|
+
if (inFlight) return inFlight;
|
|
1442
|
+
const promise = this.doStart(sessionId, options).finally(() => {
|
|
1443
|
+
this.startPromises.delete(sessionId);
|
|
1444
|
+
});
|
|
1445
|
+
this.startPromises.set(sessionId, promise);
|
|
1446
|
+
return promise;
|
|
1447
|
+
}
|
|
1448
|
+
async doStart(sessionId, options) {
|
|
1449
|
+
const nodePty = await loadPty2();
|
|
1044
1450
|
const projectName = options.projectName ?? (0, import_path5.basename)(options.projectPath);
|
|
1045
1451
|
const proc = nodePty.spawn(
|
|
1046
1452
|
resolveClaudeExe(),
|
|
@@ -1062,6 +1468,7 @@ var PTYManager = class {
|
|
|
1062
1468
|
);
|
|
1063
1469
|
const session = {
|
|
1064
1470
|
id: sessionId,
|
|
1471
|
+
provider: CLAUDE_CODE_PROVIDER,
|
|
1065
1472
|
projectPath: options.projectPath,
|
|
1066
1473
|
projectName,
|
|
1067
1474
|
branch: options.branch ?? "",
|
|
@@ -1072,7 +1479,7 @@ var PTYManager = class {
|
|
|
1072
1479
|
lastOutput: "",
|
|
1073
1480
|
process: proc,
|
|
1074
1481
|
outputBuffer: Buffer.alloc(0),
|
|
1075
|
-
screen:
|
|
1482
|
+
screen: createScreen2()
|
|
1076
1483
|
};
|
|
1077
1484
|
this.sessions.set(sessionId, session);
|
|
1078
1485
|
this.pendingReady.add(sessionId);
|
|
@@ -1083,14 +1490,14 @@ var PTYManager = class {
|
|
|
1083
1490
|
this.pendingReady.delete(sessionId);
|
|
1084
1491
|
this.handleExit(sessionId, exitCode);
|
|
1085
1492
|
});
|
|
1086
|
-
return
|
|
1493
|
+
return toPublicSession2(session);
|
|
1087
1494
|
}
|
|
1088
1495
|
// Start a brand-new Claude session. A stable UUID is generated here and passed
|
|
1089
1496
|
// to Claude via --session-id so the JSONL filename matches from the start.
|
|
1090
1497
|
// onReady fires once Claude reaches its first prompt (waiting_input).
|
|
1091
1498
|
async startFresh(options) {
|
|
1092
|
-
const nodePty = await
|
|
1093
|
-
const sessionId = (0,
|
|
1499
|
+
const nodePty = await loadPty2();
|
|
1500
|
+
const sessionId = (0, import_crypto3.randomUUID)();
|
|
1094
1501
|
const projectName = options.projectName ?? (0, import_path5.basename)(options.projectPath);
|
|
1095
1502
|
const args = [
|
|
1096
1503
|
"--permission-mode",
|
|
@@ -1112,6 +1519,7 @@ var PTYManager = class {
|
|
|
1112
1519
|
});
|
|
1113
1520
|
const session = {
|
|
1114
1521
|
id: sessionId,
|
|
1522
|
+
provider: CLAUDE_CODE_PROVIDER,
|
|
1115
1523
|
projectPath: options.projectPath,
|
|
1116
1524
|
projectName,
|
|
1117
1525
|
branch: "",
|
|
@@ -1122,7 +1530,7 @@ var PTYManager = class {
|
|
|
1122
1530
|
lastOutput: "",
|
|
1123
1531
|
process: proc,
|
|
1124
1532
|
outputBuffer: Buffer.alloc(0),
|
|
1125
|
-
screen:
|
|
1533
|
+
screen: createScreen2()
|
|
1126
1534
|
};
|
|
1127
1535
|
this.sessions.set(sessionId, session);
|
|
1128
1536
|
this.pendingReady.add(sessionId);
|
|
@@ -1133,7 +1541,7 @@ var PTYManager = class {
|
|
|
1133
1541
|
this.pendingReady.delete(sessionId);
|
|
1134
1542
|
this.handleExit(sessionId, exitCode);
|
|
1135
1543
|
});
|
|
1136
|
-
return
|
|
1544
|
+
return toPublicSession2(session);
|
|
1137
1545
|
}
|
|
1138
1546
|
// Write raw key bytes directly to the PTY without bracketed-paste wrapping.
|
|
1139
1547
|
// Use for control sequences (arrow keys, Enter) that must not be quoted.
|
|
@@ -1145,10 +1553,10 @@ var PTYManager = class {
|
|
|
1145
1553
|
}
|
|
1146
1554
|
if (session.status === "waiting_input") {
|
|
1147
1555
|
session.status = "running";
|
|
1148
|
-
this.onStatusChange?.(
|
|
1556
|
+
this.onStatusChange?.(toPublicSession2(session));
|
|
1149
1557
|
}
|
|
1150
1558
|
this.log.info(
|
|
1151
|
-
`[pty.keys.write] ${sessionId.slice(0, 8)} bytes=${keys.length} digest=${
|
|
1559
|
+
`[pty.keys.write] ${sessionId.slice(0, 8)} bytes=${keys.length} digest=${digestBytes2(keys)}`,
|
|
1152
1560
|
{ event: "pty.keys_write", sessionId, byteLen: keys.length }
|
|
1153
1561
|
);
|
|
1154
1562
|
session.process.write(keys);
|
|
@@ -1180,7 +1588,7 @@ var PTYManager = class {
|
|
|
1180
1588
|
}
|
|
1181
1589
|
if (session.status === "waiting_input") {
|
|
1182
1590
|
session.status = "running";
|
|
1183
|
-
this.onStatusChange?.(
|
|
1591
|
+
this.onStatusChange?.(toPublicSession2(session));
|
|
1184
1592
|
}
|
|
1185
1593
|
this.writeSubmit(sessionId, session, input, "direct", session.promptCount + 1);
|
|
1186
1594
|
session.lastActivityAt = /* @__PURE__ */ new Date();
|
|
@@ -1193,13 +1601,13 @@ var PTYManager = class {
|
|
|
1193
1601
|
writeSubmit(sessionId, session, input, path, promptCount) {
|
|
1194
1602
|
const pasteBytes = buildPasteBytes(input);
|
|
1195
1603
|
this.log.info(
|
|
1196
|
-
`[pty.input.write] ${sessionId.slice(0, 8)} promptCount=${promptCount} bytes=${pasteBytes.length} digest=${
|
|
1604
|
+
`[pty.input.write] ${sessionId.slice(0, 8)} promptCount=${promptCount} bytes=${pasteBytes.length} digest=${digestBytes2(pasteBytes)}`,
|
|
1197
1605
|
{
|
|
1198
1606
|
event: "pty.input_write",
|
|
1199
1607
|
sessionId,
|
|
1200
1608
|
promptCount,
|
|
1201
1609
|
byteLen: pasteBytes.length,
|
|
1202
|
-
digest:
|
|
1610
|
+
digest: digestBytes2(pasteBytes),
|
|
1203
1611
|
path,
|
|
1204
1612
|
phase: "paste"
|
|
1205
1613
|
}
|
|
@@ -1214,13 +1622,13 @@ var PTYManager = class {
|
|
|
1214
1622
|
event: "pty.input_write",
|
|
1215
1623
|
sessionId,
|
|
1216
1624
|
promptCount,
|
|
1217
|
-
byteLen:
|
|
1625
|
+
byteLen: SUBMIT_BYTES2.length,
|
|
1218
1626
|
digest: "\\r",
|
|
1219
1627
|
path,
|
|
1220
1628
|
phase: "submit"
|
|
1221
1629
|
}
|
|
1222
1630
|
);
|
|
1223
|
-
current.process.write(
|
|
1631
|
+
current.process.write(SUBMIT_BYTES2);
|
|
1224
1632
|
}, SUBMIT_DELAY_MS);
|
|
1225
1633
|
}
|
|
1226
1634
|
// Drain any inputs that were sent while the session was still pendingReady,
|
|
@@ -1278,7 +1686,7 @@ var PTYManager = class {
|
|
|
1278
1686
|
session.completedAt = /* @__PURE__ */ new Date();
|
|
1279
1687
|
session.screen.dispose();
|
|
1280
1688
|
this.sessions.delete(sessionId);
|
|
1281
|
-
this.onStatusChange?.(
|
|
1689
|
+
this.onStatusChange?.(toPublicSession2(session));
|
|
1282
1690
|
}
|
|
1283
1691
|
getOutput(sessionId) {
|
|
1284
1692
|
const session = this.sessions.get(sessionId);
|
|
@@ -1309,13 +1717,13 @@ var PTYManager = class {
|
|
|
1309
1717
|
}
|
|
1310
1718
|
getSession(sessionId) {
|
|
1311
1719
|
const session = this.sessions.get(sessionId);
|
|
1312
|
-
return session ?
|
|
1720
|
+
return session ? toPublicSession2(session) : null;
|
|
1313
1721
|
}
|
|
1314
1722
|
hasSession(sessionId) {
|
|
1315
1723
|
return this.sessions.has(sessionId);
|
|
1316
1724
|
}
|
|
1317
1725
|
listSessions() {
|
|
1318
|
-
return Array.from(this.sessions.values()).map(
|
|
1726
|
+
return Array.from(this.sessions.values()).map(toPublicSession2);
|
|
1319
1727
|
}
|
|
1320
1728
|
dispose() {
|
|
1321
1729
|
for (const session of this.sessions.values()) {
|
|
@@ -1347,7 +1755,7 @@ var PTYManager = class {
|
|
|
1347
1755
|
this.lastChunkAt.set(sessionId, now);
|
|
1348
1756
|
const gapMs = last == null ? 0 : now - last;
|
|
1349
1757
|
this.log.info(
|
|
1350
|
-
`[pty.chunk] ${sessionId.slice(0, 8)} #${idx} +${chunk.length}B gap=${gapMs}ms status=${session.status} digest=${
|
|
1758
|
+
`[pty.chunk] ${sessionId.slice(0, 8)} #${idx} +${chunk.length}B gap=${gapMs}ms status=${session.status} digest=${digestBytes2(data)}`,
|
|
1351
1759
|
{
|
|
1352
1760
|
event: "pty.chunk",
|
|
1353
1761
|
sessionId,
|
|
@@ -1356,17 +1764,17 @@ var PTYManager = class {
|
|
|
1356
1764
|
gapMs,
|
|
1357
1765
|
status: session.status,
|
|
1358
1766
|
pendingReady: this.pendingReady.has(sessionId),
|
|
1359
|
-
digest:
|
|
1767
|
+
digest: digestBytes2(data)
|
|
1360
1768
|
}
|
|
1361
1769
|
);
|
|
1362
1770
|
session.outputBuffer = Buffer.concat([session.outputBuffer, chunk]);
|
|
1363
|
-
if (session.outputBuffer.length >
|
|
1771
|
+
if (session.outputBuffer.length > OUTPUT_BUFFER_MAX2) {
|
|
1364
1772
|
session.outputBuffer = session.outputBuffer.subarray(
|
|
1365
|
-
session.outputBuffer.length -
|
|
1773
|
+
session.outputBuffer.length - OUTPUT_BUFFER_MAX2
|
|
1366
1774
|
);
|
|
1367
1775
|
}
|
|
1368
1776
|
session.screen.write(data);
|
|
1369
|
-
const stripped =
|
|
1777
|
+
const stripped = stripAnsi2(data);
|
|
1370
1778
|
session.lastOutput = stripped;
|
|
1371
1779
|
const matchedMarker = CLAUDE_PROMPT_MARKERS.find((m) => stripped.includes(m));
|
|
1372
1780
|
if (session.status === "running" && matchedMarker) {
|
|
@@ -1436,87 +1844,327 @@ var PTYManager = class {
|
|
|
1436
1844
|
this.onLiveQuestion?.(sessionId, detected.questions);
|
|
1437
1845
|
}
|
|
1438
1846
|
}
|
|
1439
|
-
} else if (this.lastScreenQuestionKey.has(sessionId) && hasPromptMarker) {
|
|
1440
|
-
this.lastScreenQuestionKey.delete(sessionId);
|
|
1441
|
-
this.onLiveQuestionGone?.(sessionId);
|
|
1442
|
-
}
|
|
1443
|
-
if (!oscPermission && !askFooterOnScreen && !this.permissionOpen.has(sessionId)) {
|
|
1444
|
-
const shell = detectShellPrompt(lines);
|
|
1445
|
-
if (shell) {
|
|
1446
|
-
const key = `${shell.prompt}\0${shell.options.map((o) => o.label).join("\0")}`;
|
|
1447
|
-
if (this.shellPromptOpen.get(sessionId) !== key) {
|
|
1448
|
-
this.shellPromptOpen.set(sessionId, key);
|
|
1449
|
-
this.onPermissionChange?.(sessionId, {
|
|
1450
|
-
prompt: shell.prompt,
|
|
1451
|
-
options: shell.options
|
|
1452
|
-
});
|
|
1453
|
-
}
|
|
1454
|
-
} else if (this.shellPromptOpen.has(sessionId) && hasPromptMarker) {
|
|
1455
|
-
this.shellPromptOpen.delete(sessionId);
|
|
1456
|
-
this.onPermissionChange?.(sessionId, null);
|
|
1847
|
+
} else if (this.lastScreenQuestionKey.has(sessionId) && hasPromptMarker) {
|
|
1848
|
+
this.lastScreenQuestionKey.delete(sessionId);
|
|
1849
|
+
this.onLiveQuestionGone?.(sessionId);
|
|
1850
|
+
}
|
|
1851
|
+
if (!oscPermission && !askFooterOnScreen && !this.permissionOpen.has(sessionId)) {
|
|
1852
|
+
const shell = detectShellPrompt(lines);
|
|
1853
|
+
if (shell) {
|
|
1854
|
+
const key = `${shell.prompt}\0${shell.options.map((o) => o.label).join("\0")}`;
|
|
1855
|
+
if (this.shellPromptOpen.get(sessionId) !== key) {
|
|
1856
|
+
this.shellPromptOpen.set(sessionId, key);
|
|
1857
|
+
this.onPermissionChange?.(sessionId, {
|
|
1858
|
+
prompt: shell.prompt,
|
|
1859
|
+
options: shell.options
|
|
1860
|
+
});
|
|
1861
|
+
}
|
|
1862
|
+
} else if (this.shellPromptOpen.has(sessionId) && hasPromptMarker) {
|
|
1863
|
+
this.shellPromptOpen.delete(sessionId);
|
|
1864
|
+
this.onPermissionChange?.(sessionId, null);
|
|
1865
|
+
}
|
|
1866
|
+
}
|
|
1867
|
+
}
|
|
1868
|
+
// Transition a session from "running" to "waiting_input", clear pendingReady,
|
|
1869
|
+
// and flush any queued input. Idempotent: callers can invoke at any chunk.
|
|
1870
|
+
markReady(sessionId, session, reason) {
|
|
1871
|
+
session.lastActivityAt = /* @__PURE__ */ new Date();
|
|
1872
|
+
session.status = "waiting_input";
|
|
1873
|
+
const elapsedMs = Date.now() - (this.firstChunkAt.get(sessionId) ?? Date.now());
|
|
1874
|
+
this.log.info(`[pty.ready] ${sessionId.slice(0, 8)} ${reason} (elapsed=${elapsedMs}ms)`, {
|
|
1875
|
+
event: "pty.ready",
|
|
1876
|
+
sessionId,
|
|
1877
|
+
reason,
|
|
1878
|
+
elapsedMs
|
|
1879
|
+
});
|
|
1880
|
+
this.onStatusChange?.(toPublicSession2(session));
|
|
1881
|
+
if (this.pendingReady.has(sessionId)) {
|
|
1882
|
+
this.pendingReady.delete(sessionId);
|
|
1883
|
+
this.flushQueuedInputs(sessionId);
|
|
1884
|
+
this.onReady?.(toPublicSession2(session));
|
|
1885
|
+
}
|
|
1886
|
+
}
|
|
1887
|
+
handleExit(sessionId, exitCode) {
|
|
1888
|
+
const session = this.sessions.get(sessionId);
|
|
1889
|
+
if (!session) return;
|
|
1890
|
+
session.completedAt = /* @__PURE__ */ new Date();
|
|
1891
|
+
session.status = "idle";
|
|
1892
|
+
const elapsedMs = session.completedAt.getTime() - session.startedAt.getTime();
|
|
1893
|
+
if (exitCode !== 0 && elapsedMs < 2e3 && session.lastOutput === "") {
|
|
1894
|
+
if (!(0, import_fs5.existsSync)(session.projectPath)) {
|
|
1895
|
+
session.failureReason = `Project directory not found: ${session.projectPath}`;
|
|
1896
|
+
} else {
|
|
1897
|
+
session.failureReason = `Process exited immediately (code ${exitCode}). Check that the Claude binary is installed and accessible.`;
|
|
1898
|
+
}
|
|
1899
|
+
}
|
|
1900
|
+
this.onStatusChange?.(toPublicSession2(session));
|
|
1901
|
+
session.screen.dispose();
|
|
1902
|
+
this.sessions.delete(sessionId);
|
|
1903
|
+
this.queuedInputs.delete(sessionId);
|
|
1904
|
+
this.firstChunkAt.delete(sessionId);
|
|
1905
|
+
this.permissionOpen.delete(sessionId);
|
|
1906
|
+
this.lastScreenQuestionKey.delete(sessionId);
|
|
1907
|
+
this.shellPromptOpen.delete(sessionId);
|
|
1908
|
+
}
|
|
1909
|
+
};
|
|
1910
|
+
function toPublicSession2(s) {
|
|
1911
|
+
return {
|
|
1912
|
+
id: s.id,
|
|
1913
|
+
provider: s.provider ?? CLAUDE_CODE_PROVIDER,
|
|
1914
|
+
projectPath: s.projectPath,
|
|
1915
|
+
projectName: s.projectName,
|
|
1916
|
+
branch: s.branch,
|
|
1917
|
+
status: s.status,
|
|
1918
|
+
startedAt: s.startedAt,
|
|
1919
|
+
completedAt: s.completedAt,
|
|
1920
|
+
promptCount: s.promptCount,
|
|
1921
|
+
lastOutput: s.lastOutput,
|
|
1922
|
+
...s.failureReason != null && { failureReason: s.failureReason },
|
|
1923
|
+
...s.lastActivityAt != null && { lastActivityAt: s.lastActivityAt },
|
|
1924
|
+
...s.filePath != null && { filePath: s.filePath }
|
|
1925
|
+
};
|
|
1926
|
+
}
|
|
1927
|
+
function stripAnsi2(str) {
|
|
1928
|
+
return str.replace(/\x1b\[[0-9;]*[a-zA-Z]/g, "").replace(/\x1b\][^\x07]*\x07/g, "");
|
|
1929
|
+
}
|
|
1930
|
+
|
|
1931
|
+
// src/live-session-manager.ts
|
|
1932
|
+
var LiveSessionManager = class {
|
|
1933
|
+
runners;
|
|
1934
|
+
constructor(options = {}) {
|
|
1935
|
+
this.runners = /* @__PURE__ */ new Map([
|
|
1936
|
+
[CLAUDE_CODE_PROVIDER, new PTYManager(options)],
|
|
1937
|
+
[CODEX_CLI_PROVIDER, new CodexPtyRunner(options)]
|
|
1938
|
+
]);
|
|
1939
|
+
}
|
|
1940
|
+
async start(sessionId, options) {
|
|
1941
|
+
const provider = options.provider ?? CLAUDE_CODE_PROVIDER;
|
|
1942
|
+
const runner = this.assertSupportedProvider(provider, options.projectPath);
|
|
1943
|
+
return runner.start(sessionId, options);
|
|
1944
|
+
}
|
|
1945
|
+
async startFresh(options) {
|
|
1946
|
+
const provider = options.provider ?? CLAUDE_CODE_PROVIDER;
|
|
1947
|
+
const runner = this.assertSupportedProvider(provider, options.projectPath);
|
|
1948
|
+
return runner.startFresh(options);
|
|
1949
|
+
}
|
|
1950
|
+
sendInput(sessionId, input) {
|
|
1951
|
+
return this.runnerFor(sessionId).sendInput(sessionId, input);
|
|
1952
|
+
}
|
|
1953
|
+
sendKeys(sessionId, keys) {
|
|
1954
|
+
this.runnerFor(sessionId).sendKeys(sessionId, keys);
|
|
1955
|
+
}
|
|
1956
|
+
cancel(sessionId) {
|
|
1957
|
+
this.runnerFor(sessionId).cancel(sessionId);
|
|
1958
|
+
}
|
|
1959
|
+
killPid(pid) {
|
|
1960
|
+
for (const runner of this.runners.values()) {
|
|
1961
|
+
runner.killPid(pid);
|
|
1962
|
+
}
|
|
1963
|
+
}
|
|
1964
|
+
// putOnHold tolerates an unknown sessionId (PTYManager.putOnHold is a no-op
|
|
1965
|
+
// when the session isn't in its map), so — unlike the other session-keyed
|
|
1966
|
+
// methods — route to the owning runner when found, otherwise broadcast to
|
|
1967
|
+
// every runner rather than throwing; this matches the pre-extraction
|
|
1968
|
+
// behavior of delegating straight through with no existence check.
|
|
1969
|
+
putOnHold(sessionId) {
|
|
1970
|
+
for (const runner of this.runners.values()) {
|
|
1971
|
+
if (runner.hasSession(sessionId) || runner.getSession(sessionId)) {
|
|
1972
|
+
runner.putOnHold(sessionId);
|
|
1973
|
+
return;
|
|
1974
|
+
}
|
|
1975
|
+
}
|
|
1976
|
+
for (const runner of this.runners.values()) {
|
|
1977
|
+
runner.putOnHold(sessionId);
|
|
1978
|
+
}
|
|
1979
|
+
}
|
|
1980
|
+
getOutput(sessionId) {
|
|
1981
|
+
return this.runnerFor(sessionId).getOutput(sessionId);
|
|
1982
|
+
}
|
|
1983
|
+
getOutputLines(sessionId, maxLines) {
|
|
1984
|
+
return this.runnerFor(sessionId).getOutputLines(sessionId, maxLines);
|
|
1985
|
+
}
|
|
1986
|
+
getSession(sessionId) {
|
|
1987
|
+
for (const runner of this.runners.values()) {
|
|
1988
|
+
const session = runner.getSession(sessionId);
|
|
1989
|
+
if (session) return session;
|
|
1990
|
+
}
|
|
1991
|
+
return null;
|
|
1992
|
+
}
|
|
1993
|
+
hasSession(sessionId) {
|
|
1994
|
+
for (const runner of this.runners.values()) {
|
|
1995
|
+
if (runner.hasSession(sessionId)) return true;
|
|
1996
|
+
}
|
|
1997
|
+
return false;
|
|
1998
|
+
}
|
|
1999
|
+
listSessions() {
|
|
2000
|
+
return Array.from(this.runners.values()).flatMap((runner) => runner.listSessions());
|
|
2001
|
+
}
|
|
2002
|
+
dispose() {
|
|
2003
|
+
for (const runner of this.runners.values()) {
|
|
2004
|
+
runner.dispose();
|
|
2005
|
+
}
|
|
2006
|
+
}
|
|
2007
|
+
// Look up which runner owns a session. Only one runner exists today, so
|
|
2008
|
+
// this is a linear scan across hasSession()/getSession() rather than a
|
|
2009
|
+
// separate session→provider index — see task-1-brief.md.
|
|
2010
|
+
runnerFor(sessionId) {
|
|
2011
|
+
for (const runner of this.runners.values()) {
|
|
2012
|
+
if (runner.hasSession(sessionId) || runner.getSession(sessionId)) return runner;
|
|
2013
|
+
}
|
|
2014
|
+
throw new Error(`Session not found: ${sessionId}`);
|
|
2015
|
+
}
|
|
2016
|
+
assertSupportedProvider(provider, projectPath) {
|
|
2017
|
+
const runner = this.runners.get(provider);
|
|
2018
|
+
if (runner) return runner;
|
|
2019
|
+
const err = new Error(
|
|
2020
|
+
`Live ${provider} sessions are not implemented yet for ${(0, import_path6.basename)(projectPath)}`
|
|
2021
|
+
);
|
|
2022
|
+
err.statusCode = 501;
|
|
2023
|
+
throw err;
|
|
2024
|
+
}
|
|
2025
|
+
};
|
|
2026
|
+
|
|
2027
|
+
// src/process-discovery.ts
|
|
2028
|
+
var import_child_process2 = require("child_process");
|
|
2029
|
+
var import_os3 = require("os");
|
|
2030
|
+
var import_path7 = require("path");
|
|
2031
|
+
async function discoverClaudeProcesses() {
|
|
2032
|
+
if ((0, import_os3.platform)() === "win32") return discoverWindows();
|
|
2033
|
+
return discoverUnix();
|
|
2034
|
+
}
|
|
2035
|
+
async function discoverUnix() {
|
|
2036
|
+
const pids = await getPidsUnix();
|
|
2037
|
+
const results = await Promise.all(
|
|
2038
|
+
pids.map(async (pid) => {
|
|
2039
|
+
try {
|
|
2040
|
+
const [cwd, args, startedAt] = await Promise.all([
|
|
2041
|
+
getProcessCwdUnix(pid),
|
|
2042
|
+
getProcessArgsUnix(pid),
|
|
2043
|
+
getProcessStartTimeUnix(pid)
|
|
2044
|
+
]);
|
|
2045
|
+
const conversationId = extractResumeId(args);
|
|
2046
|
+
return {
|
|
2047
|
+
pid,
|
|
2048
|
+
projectPath: cwd,
|
|
2049
|
+
projectName: (0, import_path7.basename)(cwd),
|
|
2050
|
+
branch: await readGitBranch(cwd),
|
|
2051
|
+
conversationId,
|
|
2052
|
+
startedAt
|
|
2053
|
+
};
|
|
2054
|
+
} catch {
|
|
2055
|
+
return null;
|
|
2056
|
+
}
|
|
2057
|
+
})
|
|
2058
|
+
);
|
|
2059
|
+
return results.filter((r) => r !== null);
|
|
2060
|
+
}
|
|
2061
|
+
async function discoverWindows() {
|
|
2062
|
+
const pids = await getPidsWindows();
|
|
2063
|
+
const results = await Promise.all(
|
|
2064
|
+
pids.map(async (pid) => {
|
|
2065
|
+
try {
|
|
2066
|
+
const info = await getProcessInfoWindows(pid);
|
|
2067
|
+
if (!info) return null;
|
|
2068
|
+
return {
|
|
2069
|
+
pid,
|
|
2070
|
+
projectPath: info.cwd,
|
|
2071
|
+
projectName: (0, import_path7.basename)(info.cwd),
|
|
2072
|
+
branch: await readGitBranch(info.cwd),
|
|
2073
|
+
conversationId: extractResumeId(info.args),
|
|
2074
|
+
startedAt: info.startedAt
|
|
2075
|
+
};
|
|
2076
|
+
} catch {
|
|
2077
|
+
return null;
|
|
2078
|
+
}
|
|
2079
|
+
})
|
|
2080
|
+
);
|
|
2081
|
+
return results.filter((r) => r !== null);
|
|
2082
|
+
}
|
|
2083
|
+
function run(cmd, args, opts = {}) {
|
|
2084
|
+
return new Promise((resolve2, reject) => {
|
|
2085
|
+
(0, import_child_process2.execFile)(
|
|
2086
|
+
cmd,
|
|
2087
|
+
args,
|
|
2088
|
+
{ windowsHide: isWindows, encoding: "utf-8", timeout: opts.timeout ?? 5e3, cwd: opts.cwd },
|
|
2089
|
+
(err, stdout) => {
|
|
2090
|
+
if (err) reject(err);
|
|
2091
|
+
else resolve2(stdout);
|
|
1457
2092
|
}
|
|
1458
|
-
|
|
2093
|
+
);
|
|
2094
|
+
});
|
|
2095
|
+
}
|
|
2096
|
+
async function getPidsUnix() {
|
|
2097
|
+
try {
|
|
2098
|
+
const output = await run("pgrep", ["-x", "claude"]);
|
|
2099
|
+
return output.trim().split("\n").filter(Boolean).map((s) => Number.parseInt(s, 10));
|
|
2100
|
+
} catch {
|
|
2101
|
+
return [];
|
|
1459
2102
|
}
|
|
1460
|
-
|
|
1461
|
-
|
|
1462
|
-
|
|
1463
|
-
|
|
1464
|
-
|
|
1465
|
-
|
|
1466
|
-
|
|
1467
|
-
|
|
1468
|
-
|
|
1469
|
-
|
|
1470
|
-
|
|
1471
|
-
|
|
1472
|
-
|
|
1473
|
-
|
|
1474
|
-
|
|
1475
|
-
|
|
1476
|
-
|
|
1477
|
-
|
|
2103
|
+
}
|
|
2104
|
+
async function getProcessCwdUnix(pid) {
|
|
2105
|
+
const output = await run("lsof", ["-p", String(pid), "-a", "-d", "cwd", "-Fn"]);
|
|
2106
|
+
const match = output.match(/n(.+)/);
|
|
2107
|
+
return match?.[1] ?? "";
|
|
2108
|
+
}
|
|
2109
|
+
async function getProcessArgsUnix(pid) {
|
|
2110
|
+
return (await run("ps", ["-p", String(pid), "-o", "args="])).trim();
|
|
2111
|
+
}
|
|
2112
|
+
async function getProcessStartTimeUnix(pid) {
|
|
2113
|
+
const raw = (await run("ps", ["-p", String(pid), "-o", "lstart="])).trim();
|
|
2114
|
+
const d = new Date(raw);
|
|
2115
|
+
return Number.isNaN(d.getTime()) ? /* @__PURE__ */ new Date() : d;
|
|
2116
|
+
}
|
|
2117
|
+
async function getPidsWindows() {
|
|
2118
|
+
try {
|
|
2119
|
+
const output = await run("tasklist", ["/FI", "IMAGENAME eq claude.exe", "/FO", "CSV", "/NH"]);
|
|
2120
|
+
return output.trim().split("\n").filter(Boolean).map((line) => {
|
|
2121
|
+
const parts = line.split(",");
|
|
2122
|
+
return Number.parseInt(parts[1]?.replace(/"/g, "") ?? "0", 10);
|
|
2123
|
+
}).filter((pid) => pid > 0);
|
|
2124
|
+
} catch {
|
|
2125
|
+
return [];
|
|
1478
2126
|
}
|
|
1479
|
-
|
|
1480
|
-
|
|
1481
|
-
|
|
1482
|
-
|
|
1483
|
-
|
|
1484
|
-
|
|
1485
|
-
|
|
1486
|
-
|
|
1487
|
-
|
|
1488
|
-
|
|
1489
|
-
|
|
1490
|
-
|
|
1491
|
-
|
|
1492
|
-
|
|
1493
|
-
|
|
1494
|
-
|
|
1495
|
-
|
|
1496
|
-
|
|
1497
|
-
|
|
1498
|
-
|
|
1499
|
-
|
|
2127
|
+
}
|
|
2128
|
+
async function getProcessInfoWindows(pid) {
|
|
2129
|
+
try {
|
|
2130
|
+
const output = await run("wmic", [
|
|
2131
|
+
"process",
|
|
2132
|
+
"where",
|
|
2133
|
+
`ProcessId=${pid}`,
|
|
2134
|
+
"get",
|
|
2135
|
+
"CommandLine,CreationDate,ExecutablePath",
|
|
2136
|
+
"/FORMAT:CSV"
|
|
2137
|
+
]);
|
|
2138
|
+
const lines = output.trim().split(/\r?\n/).filter((l) => l.trim().length > 0);
|
|
2139
|
+
if (lines.length < 2) return null;
|
|
2140
|
+
const parts = lines[1].split(",");
|
|
2141
|
+
const args = parts[1] ?? "";
|
|
2142
|
+
const creationDate = parts[2] ?? "";
|
|
2143
|
+
const year = creationDate.slice(0, 4);
|
|
2144
|
+
const month = creationDate.slice(4, 6);
|
|
2145
|
+
const day = creationDate.slice(6, 8);
|
|
2146
|
+
const hour = creationDate.slice(8, 10);
|
|
2147
|
+
const min = creationDate.slice(10, 12);
|
|
2148
|
+
const sec = creationDate.slice(12, 14);
|
|
2149
|
+
const startedAt = /* @__PURE__ */ new Date(`${year}-${month}-${day}T${hour}:${min}:${sec}`);
|
|
2150
|
+
if (Number.isNaN(startedAt.getTime())) return null;
|
|
2151
|
+
const exePath = parts[3] ?? "";
|
|
2152
|
+
const cwd = exePath ? (0, import_path7.dirname)(exePath) : "";
|
|
2153
|
+
return { cwd, args, startedAt };
|
|
2154
|
+
} catch {
|
|
2155
|
+
return null;
|
|
1500
2156
|
}
|
|
1501
|
-
};
|
|
1502
|
-
function toPublicSession(s) {
|
|
1503
|
-
return {
|
|
1504
|
-
id: s.id,
|
|
1505
|
-
projectPath: s.projectPath,
|
|
1506
|
-
projectName: s.projectName,
|
|
1507
|
-
branch: s.branch,
|
|
1508
|
-
status: s.status,
|
|
1509
|
-
startedAt: s.startedAt,
|
|
1510
|
-
completedAt: s.completedAt,
|
|
1511
|
-
promptCount: s.promptCount,
|
|
1512
|
-
lastOutput: s.lastOutput,
|
|
1513
|
-
...s.failureReason != null && { failureReason: s.failureReason },
|
|
1514
|
-
...s.lastActivityAt != null && { lastActivityAt: s.lastActivityAt },
|
|
1515
|
-
...s.filePath != null && { filePath: s.filePath }
|
|
1516
|
-
};
|
|
1517
2157
|
}
|
|
1518
|
-
function
|
|
1519
|
-
|
|
2158
|
+
function extractResumeId(args) {
|
|
2159
|
+
const match = args.match(/--resume\s+(\S+)/);
|
|
2160
|
+
return match?.[1] ?? null;
|
|
2161
|
+
}
|
|
2162
|
+
async function readGitBranch(dir) {
|
|
2163
|
+
try {
|
|
2164
|
+
return (await run("git", ["rev-parse", "--abbrev-ref", "HEAD"], { cwd: dir, timeout: 3e3 })).trim();
|
|
2165
|
+
} catch {
|
|
2166
|
+
return "";
|
|
2167
|
+
}
|
|
1520
2168
|
}
|
|
1521
2169
|
|
|
1522
2170
|
// src/server.ts
|
|
@@ -1524,15 +2172,15 @@ var import_node_ws = require("@hono/node-ws");
|
|
|
1524
2172
|
var import_client = require("@temporalio/client");
|
|
1525
2173
|
var import_scanner2 = require("@threadbase-sh/scanner");
|
|
1526
2174
|
var import_events = require("events");
|
|
1527
|
-
var
|
|
2175
|
+
var import_fs12 = require("fs");
|
|
1528
2176
|
var import_promises5 = require("fs/promises");
|
|
1529
2177
|
var import_http = require("http");
|
|
1530
2178
|
var import_os6 = require("os");
|
|
1531
|
-
var
|
|
2179
|
+
var import_path13 = require("path");
|
|
1532
2180
|
var import_readline = require("readline");
|
|
1533
2181
|
|
|
1534
2182
|
// node_modules/nanoid/index.js
|
|
1535
|
-
var
|
|
2183
|
+
var import_crypto4 = __toESM(require("crypto"), 1);
|
|
1536
2184
|
|
|
1537
2185
|
// node_modules/nanoid/url-alphabet/index.js
|
|
1538
2186
|
var urlAlphabet = "useandom-26T198340PX75pxJACKVERYMINDBUSHWOLF_GQZbfghjklqvwyzrict";
|
|
@@ -1546,10 +2194,10 @@ var fillPool = (bytes) => {
|
|
|
1546
2194
|
try {
|
|
1547
2195
|
if (!pool || pool.length < bytes) {
|
|
1548
2196
|
pool = Buffer.allocUnsafe(bytes * POOL_SIZE_MULTIPLIER);
|
|
1549
|
-
|
|
2197
|
+
import_crypto4.default.randomFillSync(pool);
|
|
1550
2198
|
poolOffset = 0;
|
|
1551
2199
|
} else if (poolOffset + bytes > pool.length) {
|
|
1552
|
-
|
|
2200
|
+
import_crypto4.default.randomFillSync(pool);
|
|
1553
2201
|
poolOffset = 0;
|
|
1554
2202
|
}
|
|
1555
2203
|
} catch (e) {
|
|
@@ -2282,7 +2930,7 @@ var createHonoApp = (deps, upgradeWebSocket) => {
|
|
|
2282
2930
|
|
|
2283
2931
|
// src/browse.ts
|
|
2284
2932
|
var import_promises2 = require("fs/promises");
|
|
2285
|
-
var
|
|
2933
|
+
var import_path8 = require("path");
|
|
2286
2934
|
var BrowsePathNotFoundError = class extends Error {
|
|
2287
2935
|
constructor(message) {
|
|
2288
2936
|
super(message);
|
|
@@ -2290,15 +2938,15 @@ var BrowsePathNotFoundError = class extends Error {
|
|
|
2290
2938
|
}
|
|
2291
2939
|
};
|
|
2292
2940
|
async function resolveBrowsePath(browseRoot, relativePath) {
|
|
2293
|
-
const normalizedRoot = (0,
|
|
2941
|
+
const normalizedRoot = (0, import_path8.resolve)(browseRoot);
|
|
2294
2942
|
let sanitized;
|
|
2295
2943
|
if (process.platform !== "win32" && relativePath.startsWith("/") && relativePath.length > 1 && relativePath.includes("/", 1)) {
|
|
2296
2944
|
sanitized = relativePath;
|
|
2297
2945
|
} else {
|
|
2298
2946
|
sanitized = relativePath.replace(/^[/\\]+/, "");
|
|
2299
2947
|
}
|
|
2300
|
-
const target = sanitized ? (0,
|
|
2301
|
-
const rootPrefix = normalizedRoot.endsWith(
|
|
2948
|
+
const target = sanitized ? (0, import_path8.resolve)(normalizedRoot, sanitized) : normalizedRoot;
|
|
2949
|
+
const rootPrefix = normalizedRoot.endsWith(import_path8.sep) ? normalizedRoot : `${normalizedRoot}${import_path8.sep}`;
|
|
2302
2950
|
if (!target.startsWith(rootPrefix) && target !== normalizedRoot) {
|
|
2303
2951
|
throw new Error("Path outside browse root");
|
|
2304
2952
|
}
|
|
@@ -2320,7 +2968,7 @@ async function createDirectory(parentAbsolutePath, name) {
|
|
|
2320
2968
|
if (name.includes("/") || name.includes("\\") || name === ".." || name === ".") {
|
|
2321
2969
|
throw new Error("Invalid directory name");
|
|
2322
2970
|
}
|
|
2323
|
-
const target = (0,
|
|
2971
|
+
const target = (0, import_path8.join)(parentAbsolutePath, name);
|
|
2324
2972
|
try {
|
|
2325
2973
|
const s = await (0, import_promises2.stat)(target);
|
|
2326
2974
|
if (s.isDirectory()) throw new Error("Directory already exists");
|
|
@@ -2333,17 +2981,17 @@ async function createDirectory(parentAbsolutePath, name) {
|
|
|
2333
2981
|
|
|
2334
2982
|
// src/conversation-cache.ts
|
|
2335
2983
|
var import_better_sqlite3 = __toESM(require("better-sqlite3"), 1);
|
|
2336
|
-
var
|
|
2337
|
-
var
|
|
2984
|
+
var import_fs8 = require("fs");
|
|
2985
|
+
var import_path10 = require("path");
|
|
2338
2986
|
|
|
2339
2987
|
// src/db/sqlite-migrate.ts
|
|
2340
|
-
var
|
|
2341
|
-
var
|
|
2988
|
+
var import_fs6 = require("fs");
|
|
2989
|
+
var import_path9 = require("path");
|
|
2342
2990
|
var import_url2 = require("url");
|
|
2343
2991
|
var import_meta2 = {};
|
|
2344
2992
|
function getMigrationsDir2() {
|
|
2345
2993
|
if (typeof import_meta2 !== "undefined" && import_meta2.url) {
|
|
2346
|
-
return (0,
|
|
2994
|
+
return (0, import_path9.dirname)((0, import_url2.fileURLToPath)(import_meta2.url));
|
|
2347
2995
|
}
|
|
2348
2996
|
return __dirname;
|
|
2349
2997
|
}
|
|
@@ -2355,8 +3003,8 @@ CREATE TABLE IF NOT EXISTS schema_migrations (
|
|
|
2355
3003
|
`;
|
|
2356
3004
|
function runSqliteMigrations(db, migrationsDir) {
|
|
2357
3005
|
db.exec(SCHEMA_MIGRATIONS_SQL);
|
|
2358
|
-
const dir = migrationsDir ?? (0,
|
|
2359
|
-
const files = (0,
|
|
3006
|
+
const dir = migrationsDir ?? (0, import_path9.join)(getMigrationsDir2(), "migrations");
|
|
3007
|
+
const files = (0, import_fs6.readdirSync)(dir).filter((f) => f.endsWith(".sql")).sort();
|
|
2360
3008
|
const appliedRows = db.prepare("SELECT id FROM schema_migrations").all();
|
|
2361
3009
|
const appliedSet = new Set(appliedRows.map((r) => r.id));
|
|
2362
3010
|
const recordApplied = db.prepare("INSERT INTO schema_migrations (id, applied_at) VALUES (?, ?)");
|
|
@@ -2367,7 +3015,7 @@ function runSqliteMigrations(db, migrationsDir) {
|
|
|
2367
3015
|
skipped.push(file);
|
|
2368
3016
|
continue;
|
|
2369
3017
|
}
|
|
2370
|
-
const sql = (0,
|
|
3018
|
+
const sql = (0, import_fs6.readFileSync)((0, import_path9.join)(dir, file), "utf-8");
|
|
2371
3019
|
const tx = db.transaction(() => {
|
|
2372
3020
|
db.exec(sql);
|
|
2373
3021
|
recordApplied.run(file, (/* @__PURE__ */ new Date()).toISOString());
|
|
@@ -2378,16 +3026,8 @@ function runSqliteMigrations(db, migrationsDir) {
|
|
|
2378
3026
|
return { applied, skipped };
|
|
2379
3027
|
}
|
|
2380
3028
|
|
|
2381
|
-
// src/providers.ts
|
|
2382
|
-
var CLAUDE_CODE_PROVIDER = "claude-code";
|
|
2383
|
-
var CODEX_CLI_PROVIDER = "codex-cli";
|
|
2384
|
-
function isProviderResumable(provider, availabilityResumable) {
|
|
2385
|
-
if (provider === CODEX_CLI_PROVIDER) return false;
|
|
2386
|
-
return availabilityResumable;
|
|
2387
|
-
}
|
|
2388
|
-
|
|
2389
3029
|
// src/services/conversations/isAgentConversation.ts
|
|
2390
|
-
var
|
|
3030
|
+
var import_fs7 = require("fs");
|
|
2391
3031
|
var DEFAULT_AGENT_ENTRYPOINTS = /* @__PURE__ */ new Set(["sdk-cli", "claude-vscode"]);
|
|
2392
3032
|
var CHUNK_BYTES = 64 * 1024;
|
|
2393
3033
|
var ENTRYPOINT_PROBE = `"entrypoint":`;
|
|
@@ -2409,12 +3049,12 @@ function isAgentFile(filePath, entrypoints = DEFAULT_AGENT_ENTRYPOINTS) {
|
|
|
2409
3049
|
if (cached2 !== void 0) return cached2;
|
|
2410
3050
|
let fd;
|
|
2411
3051
|
try {
|
|
2412
|
-
fd = (0,
|
|
3052
|
+
fd = (0, import_fs7.openSync)(filePath, "r");
|
|
2413
3053
|
} catch {
|
|
2414
3054
|
return false;
|
|
2415
3055
|
}
|
|
2416
3056
|
try {
|
|
2417
|
-
const fileSize = (0,
|
|
3057
|
+
const fileSize = (0, import_fs7.statSync)(filePath).size;
|
|
2418
3058
|
if (fileSize === 0) {
|
|
2419
3059
|
fileDecisionCache.set(key, false);
|
|
2420
3060
|
return false;
|
|
@@ -2425,7 +3065,7 @@ function isAgentFile(filePath, entrypoints = DEFAULT_AGENT_ENTRYPOINTS) {
|
|
|
2425
3065
|
let carry = "";
|
|
2426
3066
|
while (offset < fileSize) {
|
|
2427
3067
|
const toRead = Math.min(CHUNK_BYTES, fileSize - offset);
|
|
2428
|
-
const got = (0,
|
|
3068
|
+
const got = (0, import_fs7.readSync)(fd, buf, 0, toRead, offset);
|
|
2429
3069
|
if (got <= 0) break;
|
|
2430
3070
|
const chunk = carry + buf.toString("utf8", 0, got);
|
|
2431
3071
|
for (const marker of markers) {
|
|
@@ -2446,7 +3086,7 @@ function isAgentFile(filePath, entrypoints = DEFAULT_AGENT_ENTRYPOINTS) {
|
|
|
2446
3086
|
} catch {
|
|
2447
3087
|
return false;
|
|
2448
3088
|
} finally {
|
|
2449
|
-
(0,
|
|
3089
|
+
(0, import_fs7.closeSync)(fd);
|
|
2450
3090
|
}
|
|
2451
3091
|
}
|
|
2452
3092
|
function parseAgentEntrypointsEnv(raw) {
|
|
@@ -2566,7 +3206,10 @@ var ConversationCache = class _ConversationCache {
|
|
|
2566
3206
|
model = excluded.model,
|
|
2567
3207
|
account = excluded.account,
|
|
2568
3208
|
branch = excluded.branch,
|
|
2569
|
-
message_count
|
|
3209
|
+
-- message_count is incremented by live tailing but recounted from
|
|
3210
|
+
-- scratch by a scanner rescan; a stale rescan must not carry a live
|
|
3211
|
+
-- session's count backwards, so take the max instead of overwriting.
|
|
3212
|
+
message_count = MAX(conversation_meta.message_count, excluded.message_count),
|
|
2570
3213
|
last_activity = excluded.last_activity,
|
|
2571
3214
|
first_message = excluded.first_message,
|
|
2572
3215
|
last_message = excluded.last_message,
|
|
@@ -2655,7 +3298,7 @@ var ConversationCache = class _ConversationCache {
|
|
|
2655
3298
|
return this.agentEntrypoints;
|
|
2656
3299
|
}
|
|
2657
3300
|
static open(dbPath, tailSize = 10, migrationsDir, options) {
|
|
2658
|
-
(0,
|
|
3301
|
+
(0, import_fs8.mkdirSync)((0, import_path10.dirname)(dbPath), { recursive: true });
|
|
2659
3302
|
const db = new import_better_sqlite3.default(dbPath);
|
|
2660
3303
|
db.pragma("journal_mode = WAL");
|
|
2661
3304
|
db.pragma("foreign_keys = ON");
|
|
@@ -2838,11 +3481,12 @@ var ConversationCache = class _ConversationCache {
|
|
|
2838
3481
|
let mtimeMs = null;
|
|
2839
3482
|
let fileSize = null;
|
|
2840
3483
|
try {
|
|
2841
|
-
const s = (0,
|
|
3484
|
+
const s = (0, import_fs8.statSync)(m.filePath);
|
|
2842
3485
|
mtimeMs = s.mtimeMs;
|
|
2843
3486
|
fileSize = s.size;
|
|
2844
3487
|
} catch {
|
|
2845
3488
|
}
|
|
3489
|
+
const seq = ++this.tailSeq;
|
|
2846
3490
|
this.stmts.upsertFull.run({
|
|
2847
3491
|
id,
|
|
2848
3492
|
file_path: m.filePath,
|
|
@@ -2857,7 +3501,7 @@ var ConversationCache = class _ConversationCache {
|
|
|
2857
3501
|
first_message: m.firstMessage ? JSON.stringify(m.firstMessage) : null,
|
|
2858
3502
|
last_message: m.lastMessage ? JSON.stringify(m.lastMessage) : null,
|
|
2859
3503
|
preview: m.preview ?? null,
|
|
2860
|
-
updated_at:
|
|
3504
|
+
updated_at: seq,
|
|
2861
3505
|
mtime_ms: mtimeMs,
|
|
2862
3506
|
file_size: fileSize,
|
|
2863
3507
|
provider: m.provider ?? CLAUDE_CODE_PROVIDER
|
|
@@ -2890,8 +3534,8 @@ var ConversationCache = class _ConversationCache {
|
|
|
2890
3534
|
let fileSize;
|
|
2891
3535
|
let fd;
|
|
2892
3536
|
try {
|
|
2893
|
-
fileSize = (0,
|
|
2894
|
-
fd = (0,
|
|
3537
|
+
fileSize = (0, import_fs8.statSync)(filePath).size;
|
|
3538
|
+
fd = (0, import_fs8.openSync)(filePath, "r");
|
|
2895
3539
|
} catch {
|
|
2896
3540
|
return false;
|
|
2897
3541
|
}
|
|
@@ -2904,7 +3548,7 @@ var ConversationCache = class _ConversationCache {
|
|
|
2904
3548
|
while (pos > 0 && lines.length < this.tailSize * 4) {
|
|
2905
3549
|
const toRead = Math.min(CHUNK, pos);
|
|
2906
3550
|
pos -= toRead;
|
|
2907
|
-
(0,
|
|
3551
|
+
(0, import_fs8.readSync)(fd, buf, 0, toRead, pos);
|
|
2908
3552
|
const chunk = buf.subarray(0, toRead).toString("utf8");
|
|
2909
3553
|
const combined = chunk + partial;
|
|
2910
3554
|
const parts = combined.split("\n");
|
|
@@ -2915,7 +3559,7 @@ var ConversationCache = class _ConversationCache {
|
|
|
2915
3559
|
}
|
|
2916
3560
|
if (partial) lines.push(partial);
|
|
2917
3561
|
} finally {
|
|
2918
|
-
(0,
|
|
3562
|
+
(0, import_fs8.closeSync)(fd);
|
|
2919
3563
|
}
|
|
2920
3564
|
const msgs = [];
|
|
2921
3565
|
for (let i = 0; i < lines.length && msgs.length < this.tailSize; i++) {
|
|
@@ -3074,9 +3718,25 @@ var ConversationCache = class _ConversationCache {
|
|
|
3074
3718
|
this.fileIndex.clear();
|
|
3075
3719
|
}
|
|
3076
3720
|
}
|
|
3077
|
-
|
|
3721
|
+
/**
|
|
3722
|
+
* Drop the cached row for a file. Two callers with opposite intent:
|
|
3723
|
+
* - a directory-watch "change" event (the file was appended to) — pass
|
|
3724
|
+
* `skipIfTailed: true`. A cached tail means the row is being actively
|
|
3725
|
+
* maintained from the file's real content by the live-tail
|
|
3726
|
+
* (updateFromLines) or warm-up path, fresher than any scanner-derived
|
|
3727
|
+
* view. Both watchers fire on the same append with no ordering guarantee;
|
|
3728
|
+
* without this guard the invalidate can land after the tail write and wipe
|
|
3729
|
+
* the just-cached row, flickering the conversation out of
|
|
3730
|
+
* /api/conversations on nearly every message (CRITICAL #2). The debounced
|
|
3731
|
+
* rescan still re-derives metadata, so skipping the eager drop loses
|
|
3732
|
+
* nothing.
|
|
3733
|
+
* - a genuine unlink (the file is gone) — leave `skipIfTailed` false so the
|
|
3734
|
+
* row is always removed, otherwise a deleted session ghosts in the cache.
|
|
3735
|
+
*/
|
|
3736
|
+
invalidateByFilePath(filePath, opts) {
|
|
3078
3737
|
const row = this.stmts.getIdByFilePath.get(filePath);
|
|
3079
3738
|
if (!row) return null;
|
|
3739
|
+
if (opts?.skipIfTailed && this.stmts.hasTail.get(row.id)) return null;
|
|
3080
3740
|
this.invalidate(row.id);
|
|
3081
3741
|
return row.id;
|
|
3082
3742
|
}
|
|
@@ -3086,7 +3746,7 @@ var ConversationCache = class _ConversationCache {
|
|
|
3086
3746
|
* `handleGetConversation` can still serve the cached tail even when the
|
|
3087
3747
|
* JSONL has been deleted.
|
|
3088
3748
|
*/
|
|
3089
|
-
pruneGhostFiles(exists =
|
|
3749
|
+
pruneGhostFiles(exists = import_fs8.existsSync) {
|
|
3090
3750
|
const rows = this.stmts.allFilePaths.all();
|
|
3091
3751
|
const ghosts = [];
|
|
3092
3752
|
const prune = this.db.transaction((ids) => {
|
|
@@ -3166,7 +3826,7 @@ var ConversationsRepository = class {
|
|
|
3166
3826
|
};
|
|
3167
3827
|
|
|
3168
3828
|
// src/db/repositories/projects.repository.ts
|
|
3169
|
-
var
|
|
3829
|
+
var import_crypto5 = require("crypto");
|
|
3170
3830
|
|
|
3171
3831
|
// src/utils/canonicalizeProjectPath.ts
|
|
3172
3832
|
function canonicalizeProjectPath(projectPath) {
|
|
@@ -3259,7 +3919,7 @@ var ProjectsRepository = class {
|
|
|
3259
3919
|
});
|
|
3260
3920
|
return rowToProject(this.getById.get(existing.id));
|
|
3261
3921
|
}
|
|
3262
|
-
const id = (0,
|
|
3922
|
+
const id = (0, import_crypto5.randomUUID)();
|
|
3263
3923
|
this.insert.run({
|
|
3264
3924
|
id,
|
|
3265
3925
|
path,
|
|
@@ -3315,23 +3975,23 @@ async function recordUpload(pool2, instanceId, row) {
|
|
|
3315
3975
|
}
|
|
3316
3976
|
|
|
3317
3977
|
// src/handlers/handleListProjects.ts
|
|
3318
|
-
var
|
|
3978
|
+
var import_fs9 = require("fs");
|
|
3319
3979
|
var import_os5 = require("os");
|
|
3320
|
-
var
|
|
3980
|
+
var import_path11 = require("path");
|
|
3321
3981
|
function decodeProjectPath(dirName) {
|
|
3322
3982
|
return dirName.replace(/-/g, "/");
|
|
3323
3983
|
}
|
|
3324
3984
|
function handleListProjects(url, res) {
|
|
3325
3985
|
const limit = Math.max(1, parseInt(url.searchParams.get("limit") ?? "50", 10) || 50);
|
|
3326
3986
|
const offset = Math.max(0, parseInt(url.searchParams.get("offset") ?? "0", 10) || 0);
|
|
3327
|
-
const projectsDir = (0,
|
|
3987
|
+
const projectsDir = (0, import_path11.join)((0, import_os5.homedir)(), ".claude", "projects");
|
|
3328
3988
|
let entries;
|
|
3329
3989
|
try {
|
|
3330
|
-
entries = (0,
|
|
3331
|
-
const fullPath = (0,
|
|
3990
|
+
entries = (0, import_fs9.readdirSync)(projectsDir).map((dirName) => {
|
|
3991
|
+
const fullPath = (0, import_path11.join)(projectsDir, dirName);
|
|
3332
3992
|
let mtime = 0;
|
|
3333
3993
|
try {
|
|
3334
|
-
mtime = (0,
|
|
3994
|
+
mtime = (0, import_fs9.statSync)(fullPath).mtimeMs;
|
|
3335
3995
|
} catch {
|
|
3336
3996
|
}
|
|
3337
3997
|
const path = decodeProjectPath(dirName);
|
|
@@ -3350,7 +4010,7 @@ function handleListProjects(url, res) {
|
|
|
3350
4010
|
}
|
|
3351
4011
|
|
|
3352
4012
|
// src/pair-store.ts
|
|
3353
|
-
var
|
|
4013
|
+
var import_crypto6 = require("crypto");
|
|
3354
4014
|
var DEFAULT_TTL_SECONDS = 180;
|
|
3355
4015
|
var SWEEP_INTERVAL_MS = 6e4;
|
|
3356
4016
|
var PairTokenStore = class {
|
|
@@ -3365,7 +4025,7 @@ var PairTokenStore = class {
|
|
|
3365
4025
|
}
|
|
3366
4026
|
}
|
|
3367
4027
|
mint() {
|
|
3368
|
-
const token = `pt_${(0,
|
|
4028
|
+
const token = `pt_${(0, import_crypto6.randomBytes)(16).toString("hex")}`;
|
|
3369
4029
|
const expiresAt = Date.now() + this.ttlMs;
|
|
3370
4030
|
this.current = { token, expiresAt, used: false };
|
|
3371
4031
|
return {
|
|
@@ -3426,7 +4086,7 @@ function seal(plaintext, recipientPublicKeyBase64) {
|
|
|
3426
4086
|
|
|
3427
4087
|
// src/services/conversations/conversationWatcher.ts
|
|
3428
4088
|
var import_chokidar = __toESM(require("chokidar"), 1);
|
|
3429
|
-
var
|
|
4089
|
+
var import_fs10 = require("fs");
|
|
3430
4090
|
var import_promises3 = require("fs/promises");
|
|
3431
4091
|
var ConversationWatcher = class {
|
|
3432
4092
|
files = /* @__PURE__ */ new Map();
|
|
@@ -3447,7 +4107,7 @@ var ConversationWatcher = class {
|
|
|
3447
4107
|
if (this.files.has(filePath)) return;
|
|
3448
4108
|
let offset;
|
|
3449
4109
|
try {
|
|
3450
|
-
offset = (0,
|
|
4110
|
+
offset = (0, import_fs10.statSync)(filePath).size;
|
|
3451
4111
|
} catch {
|
|
3452
4112
|
offset = 0;
|
|
3453
4113
|
}
|
|
@@ -3556,14 +4216,14 @@ var ConversationWatcher = class {
|
|
|
3556
4216
|
};
|
|
3557
4217
|
|
|
3558
4218
|
// src/services/conversations/pruneAgentConversations.ts
|
|
3559
|
-
var
|
|
4219
|
+
var import_fs11 = require("fs");
|
|
3560
4220
|
function pruneAgentConversations(cache) {
|
|
3561
4221
|
const db = cache.getDatabase();
|
|
3562
4222
|
const rows = db.prepare("SELECT id, file_path FROM conversation_meta").all();
|
|
3563
4223
|
let pruned = 0;
|
|
3564
4224
|
let missing = 0;
|
|
3565
4225
|
for (const row of rows) {
|
|
3566
|
-
if (!(0,
|
|
4226
|
+
if (!(0, import_fs11.existsSync)(row.file_path)) {
|
|
3567
4227
|
missing += 1;
|
|
3568
4228
|
continue;
|
|
3569
4229
|
}
|
|
@@ -3846,6 +4506,7 @@ function managedToResponse(s, ptyAttached) {
|
|
|
3846
4506
|
return {
|
|
3847
4507
|
id: s.id,
|
|
3848
4508
|
conversationId: s.id,
|
|
4509
|
+
provider: s.provider ?? CLAUDE_CODE_PROVIDER,
|
|
3849
4510
|
status: s.status,
|
|
3850
4511
|
projectPath: s.projectPath,
|
|
3851
4512
|
projectName: s.projectName,
|
|
@@ -3871,13 +4532,15 @@ function managedToResponse(s, ptyAttached) {
|
|
|
3871
4532
|
...s.failureReason != null && { failureReason: s.failureReason },
|
|
3872
4533
|
...s.resumedFromConversationId != null && {
|
|
3873
4534
|
resumedFromConversationId: s.resumedFromConversationId
|
|
3874
|
-
}
|
|
4535
|
+
},
|
|
4536
|
+
...s.boundConversationId != null && { boundConversationId: s.boundConversationId }
|
|
3875
4537
|
};
|
|
3876
4538
|
}
|
|
3877
4539
|
function discoveredToResponse(d, conversationId) {
|
|
3878
4540
|
return {
|
|
3879
4541
|
id: conversationId,
|
|
3880
4542
|
conversationId,
|
|
4543
|
+
provider: CLAUDE_CODE_PROVIDER,
|
|
3881
4544
|
status: "idle",
|
|
3882
4545
|
projectPath: d.projectPath,
|
|
3883
4546
|
projectName: d.projectName,
|
|
@@ -3893,10 +4556,10 @@ function discoveredToResponse(d, conversationId) {
|
|
|
3893
4556
|
}
|
|
3894
4557
|
|
|
3895
4558
|
// src/uploads.ts
|
|
3896
|
-
var
|
|
4559
|
+
var import_crypto7 = require("crypto");
|
|
3897
4560
|
var import_promises4 = require("fs/promises");
|
|
3898
4561
|
var import_heic_convert = __toESM(require("heic-convert"), 1);
|
|
3899
|
-
var
|
|
4562
|
+
var import_path12 = require("path");
|
|
3900
4563
|
var UPLOAD_DIR_NAME = ".threadbase-uploads";
|
|
3901
4564
|
var MAX_BYTES = 25 * 1024 * 1024;
|
|
3902
4565
|
var HEIC_MIMES = /* @__PURE__ */ new Set(["image/heic", "image/heif"]);
|
|
@@ -3927,11 +4590,11 @@ async function saveUploadFile(input) {
|
|
|
3927
4590
|
mimeType = "image/jpeg";
|
|
3928
4591
|
originalName = originalName.replace(/\.(heic|heif)$/i, ".jpg");
|
|
3929
4592
|
}
|
|
3930
|
-
const id = `up_${(0,
|
|
4593
|
+
const id = `up_${(0, import_crypto7.randomBytes)(8).toString("hex")}`;
|
|
3931
4594
|
const safeName = sanitizeFilename(originalName) || `file${MIME_TO_EXT[mimeType] ?? ""}`;
|
|
3932
|
-
const dir = (0,
|
|
4595
|
+
const dir = (0, import_path12.join)(input.projectPath, UPLOAD_DIR_NAME, input.sessionId);
|
|
3933
4596
|
await (0, import_promises4.mkdir)(dir, { recursive: true });
|
|
3934
|
-
const filePath = (0,
|
|
4597
|
+
const filePath = (0, import_path12.join)(dir, `${Date.now()}-${id}-${safeName}`);
|
|
3935
4598
|
await (0, import_promises4.writeFile)(filePath, buffer);
|
|
3936
4599
|
return {
|
|
3937
4600
|
id,
|
|
@@ -4232,10 +4895,10 @@ var StreamerServer = class {
|
|
|
4232
4895
|
this.verbose = config.verbose ?? false;
|
|
4233
4896
|
this.disableDb = config.disableDb ?? false;
|
|
4234
4897
|
this.scanProfiles = config.scanProfiles;
|
|
4235
|
-
this.codexRoots = config.codexRoots ?? [(0,
|
|
4898
|
+
this.codexRoots = config.codexRoots ?? [(0, import_path13.join)((0, import_os6.homedir)(), ".codex", "sessions")];
|
|
4236
4899
|
this.ptyGracePeriodMs = config.ptyGracePeriodMs ?? DEFAULT_PTY_GRACE_PERIOD_MS;
|
|
4237
4900
|
this.defaultSystemPrompt = config.defaultSystemPrompt ?? DEFAULT_SYSTEM_PROMPT;
|
|
4238
|
-
this.cacheDir = config.cacheDir ?? loadCacheDir() ?? (0,
|
|
4901
|
+
this.cacheDir = config.cacheDir ?? loadCacheDir() ?? (0, import_path13.join)((0, import_os6.homedir)(), ".threadbase", "cache");
|
|
4239
4902
|
this.tailSize = config.tailSize ?? loadTailSize() ?? 10;
|
|
4240
4903
|
this.directoryDebounceMs = parseDirScanDebounceEnv(process.env.THREADBASE_DIR_SCAN_DEBOUNCE_MS) ?? config.directoryScanDebounceMs ?? 1e3;
|
|
4241
4904
|
this.markScannerStaleDebounced = debounce(() => {
|
|
@@ -4302,7 +4965,7 @@ var StreamerServer = class {
|
|
|
4302
4965
|
}
|
|
4303
4966
|
},
|
|
4304
4967
|
onConversationChanged: (filePath) => {
|
|
4305
|
-
this.cache?.invalidateByFilePath(filePath);
|
|
4968
|
+
this.cache?.invalidateByFilePath(filePath, { skipIfTailed: true });
|
|
4306
4969
|
this.markScannerStaleDebounced();
|
|
4307
4970
|
this.log.debug?.(`Scanner invalidated by directory event: ${filePath}`, {
|
|
4308
4971
|
filePath,
|
|
@@ -4319,7 +4982,7 @@ var StreamerServer = class {
|
|
|
4319
4982
|
});
|
|
4320
4983
|
}
|
|
4321
4984
|
});
|
|
4322
|
-
this.ptyManager = new
|
|
4985
|
+
this.ptyManager = new LiveSessionManager({
|
|
4323
4986
|
logger: getLogger("pty"),
|
|
4324
4987
|
onOutput: (sessionId, data) => {
|
|
4325
4988
|
this.wsHub.broadcast({ type: "terminal_output", sessionId, data });
|
|
@@ -4400,7 +5063,7 @@ var StreamerServer = class {
|
|
|
4400
5063
|
temporalClient,
|
|
4401
5064
|
taskQueue: agentConfig.temporal.taskQueue
|
|
4402
5065
|
});
|
|
4403
|
-
const conversationsBaseDir = agentConfig.conversationsDir || (0,
|
|
5066
|
+
const conversationsBaseDir = agentConfig.conversationsDir || (0, import_path13.join)((0, import_path13.dirname)(this.cacheDir), "conversations");
|
|
4404
5067
|
conversationWriter = createConversationWriter({
|
|
4405
5068
|
baseDir: conversationsBaseDir
|
|
4406
5069
|
});
|
|
@@ -4622,7 +5285,7 @@ var StreamerServer = class {
|
|
|
4622
5285
|
});
|
|
4623
5286
|
try {
|
|
4624
5287
|
this.cache = ConversationCache.open(
|
|
4625
|
-
(0,
|
|
5288
|
+
(0, import_path13.join)(this.cacheDir, "cache.db"),
|
|
4626
5289
|
this.tailSize,
|
|
4627
5290
|
void 0,
|
|
4628
5291
|
{
|
|
@@ -4648,18 +5311,19 @@ var StreamerServer = class {
|
|
|
4648
5311
|
if (this.scanProfiles && this.scanProfiles.length > 0) {
|
|
4649
5312
|
for (const profile of this.scanProfiles) {
|
|
4650
5313
|
if (profile.enabled) {
|
|
4651
|
-
this.fileWatcher.watchDirectory((0,
|
|
5314
|
+
this.fileWatcher.watchDirectory((0, import_path13.join)(profile.configDir, "projects"));
|
|
4652
5315
|
}
|
|
4653
5316
|
}
|
|
4654
5317
|
} else {
|
|
4655
|
-
this.fileWatcher.watchDirectory((0,
|
|
5318
|
+
this.fileWatcher.watchDirectory((0, import_path13.join)((0, import_os6.homedir)(), ".claude", "projects"));
|
|
4656
5319
|
}
|
|
4657
5320
|
} catch (err) {
|
|
4658
5321
|
const message = err instanceof Error ? err.message : String(err);
|
|
4659
|
-
|
|
4660
|
-
|
|
4661
|
-
|
|
4662
|
-
|
|
5322
|
+
const abiMismatch = message.includes("NODE_MODULE_VERSION") || message.includes("was compiled against a different Node.js version");
|
|
5323
|
+
this.log.error(
|
|
5324
|
+
`ConversationCache failed to open \u2014 running WITHOUT cache; /api/conversations, /api/conversations/count and /project-chats will 500.` + (abiMismatch ? ` Fix: npm rebuild better-sqlite3` : "") + ` (${message})`,
|
|
5325
|
+
{ error: message, abiMismatch, event: "cache.open_failed" }
|
|
5326
|
+
);
|
|
4663
5327
|
}
|
|
4664
5328
|
const warmupScanner = new import_scanner2.ConversationScanner();
|
|
4665
5329
|
this.allScanners.add(warmupScanner);
|
|
@@ -5146,17 +5810,17 @@ var StreamerServer = class {
|
|
|
5146
5810
|
return this.getScanner();
|
|
5147
5811
|
}
|
|
5148
5812
|
findJsonlPath(uuid) {
|
|
5149
|
-
const projectsDir = (0,
|
|
5150
|
-
if (!(0,
|
|
5813
|
+
const projectsDir = (0, import_path13.join)((0, import_os6.homedir)(), ".claude", "projects");
|
|
5814
|
+
if (!(0, import_fs12.existsSync)(projectsDir)) return null;
|
|
5151
5815
|
const filename = `${uuid}.jsonl`;
|
|
5152
|
-
for (const dir of (0,
|
|
5153
|
-
const fp = (0,
|
|
5154
|
-
if ((0,
|
|
5155
|
-
const projectDir = (0,
|
|
5816
|
+
for (const dir of (0, import_fs12.readdirSync)(projectsDir)) {
|
|
5817
|
+
const fp = (0, import_path13.join)(projectsDir, dir, filename);
|
|
5818
|
+
if ((0, import_fs12.existsSync)(fp)) return fp;
|
|
5819
|
+
const projectDir = (0, import_path13.join)(projectsDir, dir);
|
|
5156
5820
|
try {
|
|
5157
|
-
for (const sub of (0,
|
|
5158
|
-
const subagentPath = (0,
|
|
5159
|
-
if ((0,
|
|
5821
|
+
for (const sub of (0, import_fs12.readdirSync)(projectDir)) {
|
|
5822
|
+
const subagentPath = (0, import_path13.join)(projectDir, sub, "subagents", filename);
|
|
5823
|
+
if ((0, import_fs12.existsSync)(subagentPath)) return subagentPath;
|
|
5160
5824
|
}
|
|
5161
5825
|
} catch {
|
|
5162
5826
|
}
|
|
@@ -5165,7 +5829,7 @@ var StreamerServer = class {
|
|
|
5165
5829
|
}
|
|
5166
5830
|
async readCwdFromJsonl(filePath) {
|
|
5167
5831
|
return new Promise((resolve2) => {
|
|
5168
|
-
const rl = (0, import_readline.createInterface)({ input: (0,
|
|
5832
|
+
const rl = (0, import_readline.createInterface)({ input: (0, import_fs12.createReadStream)(filePath), crlfDelay: Infinity });
|
|
5169
5833
|
let found = false;
|
|
5170
5834
|
rl.on("line", (line) => {
|
|
5171
5835
|
if (found) return;
|
|
@@ -5222,7 +5886,7 @@ var StreamerServer = class {
|
|
|
5222
5886
|
if (!conv.filePath) return false;
|
|
5223
5887
|
let mtimeMs = null;
|
|
5224
5888
|
try {
|
|
5225
|
-
mtimeMs = (0,
|
|
5889
|
+
mtimeMs = (0, import_fs12.statSync)(conv.filePath).mtimeMs;
|
|
5226
5890
|
} catch {
|
|
5227
5891
|
return false;
|
|
5228
5892
|
}
|
|
@@ -5465,7 +6129,7 @@ var StreamerServer = class {
|
|
|
5465
6129
|
handleGetSession(sessionId, res) {
|
|
5466
6130
|
const session = this.sessionStore.get(sessionId, this.ptyAttachedIds());
|
|
5467
6131
|
if (session) {
|
|
5468
|
-
if (!(0,
|
|
6132
|
+
if (!(0, import_fs12.existsSync)(session.projectPath)) {
|
|
5469
6133
|
session.failureReason = `Project directory not found: ${session.projectPath}`;
|
|
5470
6134
|
}
|
|
5471
6135
|
json(res, 200, session);
|
|
@@ -5505,7 +6169,10 @@ var StreamerServer = class {
|
|
|
5505
6169
|
json(res, 400, { error: "Could not determine project path" });
|
|
5506
6170
|
return;
|
|
5507
6171
|
}
|
|
6172
|
+
const cachedConvMeta = this.cache?.getMetaById(sessionId);
|
|
6173
|
+
const provider = conv?.provider ?? cachedConvMeta?.provider ?? CLAUDE_CODE_PROVIDER;
|
|
5508
6174
|
const session = await this.ptyManager.start(sessionId, {
|
|
6175
|
+
provider,
|
|
5509
6176
|
projectPath,
|
|
5510
6177
|
projectName: body.projectName,
|
|
5511
6178
|
branch: body.branch
|
|
@@ -5855,7 +6522,7 @@ var StreamerServer = class {
|
|
|
5855
6522
|
sessionStore: this.sessionStore,
|
|
5856
6523
|
// biome-ignore lint/style/noNonNullAssertion: agentClient is set when agentConfig.enabled is true
|
|
5857
6524
|
agentClient: this.agentClient,
|
|
5858
|
-
conversationsDir: this.cacheDir ? (0,
|
|
6525
|
+
conversationsDir: this.cacheDir ? (0, import_path13.join)((0, import_path13.dirname)(this.cacheDir), "conversations") : "",
|
|
5859
6526
|
agentConfig: this.agentConfig
|
|
5860
6527
|
});
|
|
5861
6528
|
json(res, result.status, result.body);
|
|
@@ -5864,6 +6531,13 @@ var StreamerServer = class {
|
|
|
5864
6531
|
}
|
|
5865
6532
|
return;
|
|
5866
6533
|
}
|
|
6534
|
+
const body = await readBody(req);
|
|
6535
|
+
const { path: relativePath, provider: requestedProvider, systemPrompt: clientPrompt } = body;
|
|
6536
|
+
if (requestedProvider !== void 0 && !isProviderName(requestedProvider)) {
|
|
6537
|
+
json(res, 400, { error: "Invalid provider" });
|
|
6538
|
+
return;
|
|
6539
|
+
}
|
|
6540
|
+
const provider = requestedProvider ?? CLAUDE_CODE_PROVIDER;
|
|
5867
6541
|
if (!this.browseRoot) {
|
|
5868
6542
|
json(res, 403, {
|
|
5869
6543
|
error: "File browsing not configured. Set browseRoot on the server.",
|
|
@@ -5871,8 +6545,6 @@ var StreamerServer = class {
|
|
|
5871
6545
|
});
|
|
5872
6546
|
return;
|
|
5873
6547
|
}
|
|
5874
|
-
const body = await readBody(req);
|
|
5875
|
-
const { path: relativePath, systemPrompt: clientPrompt } = body;
|
|
5876
6548
|
if (typeof relativePath !== "string") {
|
|
5877
6549
|
json(res, 400, { error: "Missing path field" });
|
|
5878
6550
|
return;
|
|
@@ -5893,21 +6565,27 @@ var StreamerServer = class {
|
|
|
5893
6565
|
].filter(Boolean);
|
|
5894
6566
|
try {
|
|
5895
6567
|
const session = await this.ptyManager.startFresh({
|
|
6568
|
+
provider,
|
|
5896
6569
|
projectPath: resolvedPath,
|
|
5897
6570
|
projectName: body.projectName,
|
|
5898
6571
|
systemPrompt: systemPromptParts.join("\n")
|
|
5899
6572
|
});
|
|
5900
6573
|
this.sessionStore.addManaged(session);
|
|
5901
6574
|
json(res, 202, { id: session.id, status: "pending" });
|
|
5902
|
-
|
|
6575
|
+
if (provider === CODEX_CLI_PROVIDER) {
|
|
6576
|
+
this.watchForCodexRollout(session.id, resolvedPath);
|
|
6577
|
+
} else {
|
|
6578
|
+
this.watchForJsonl(session.id, resolvedPath);
|
|
6579
|
+
}
|
|
5903
6580
|
this.broadcastOrUnicastSessionList(req);
|
|
5904
6581
|
} catch (err) {
|
|
5905
6582
|
const message = err instanceof Error ? err.message : "Failed to start session";
|
|
6583
|
+
const statusCode = typeof err.statusCode === "number" ? err.statusCode : 500;
|
|
5906
6584
|
this.log.error(`[start] failed to start session: ${message}`, {
|
|
5907
6585
|
event: "session.start_failed",
|
|
5908
6586
|
error: message
|
|
5909
6587
|
});
|
|
5910
|
-
json(res,
|
|
6588
|
+
json(res, statusCode, { error: message });
|
|
5911
6589
|
}
|
|
5912
6590
|
}
|
|
5913
6591
|
// ─── Project linking ─────────────────────────────────────────────
|
|
@@ -5960,9 +6638,9 @@ var StreamerServer = class {
|
|
|
5960
6638
|
// was passed to Claude via --session-id so the filename matches from the start.
|
|
5961
6639
|
watchForJsonl(sessionId, projectPath) {
|
|
5962
6640
|
const encoded = projectPath.replace(/[/\\:.]/g, "-");
|
|
5963
|
-
const projectsDir = (0,
|
|
6641
|
+
const projectsDir = (0, import_path13.join)((0, import_os6.homedir)(), ".claude", "projects", encoded);
|
|
5964
6642
|
const expectedFile = `${sessionId}.jsonl`;
|
|
5965
|
-
const filePath = (0,
|
|
6643
|
+
const filePath = (0, import_path13.join)(projectsDir, expectedFile);
|
|
5966
6644
|
const deadline = Date.now() + 12e4;
|
|
5967
6645
|
let watcher = null;
|
|
5968
6646
|
const cleanup = () => {
|
|
@@ -5980,12 +6658,12 @@ var StreamerServer = class {
|
|
|
5980
6658
|
cleanup();
|
|
5981
6659
|
return;
|
|
5982
6660
|
}
|
|
5983
|
-
let resolvedFilePath = (0,
|
|
5984
|
-
if (!resolvedFilePath && (0,
|
|
6661
|
+
let resolvedFilePath = (0, import_fs12.existsSync)(filePath) ? filePath : null;
|
|
6662
|
+
if (!resolvedFilePath && (0, import_fs12.existsSync)(projectsDir)) {
|
|
5985
6663
|
try {
|
|
5986
6664
|
const now = Date.now();
|
|
5987
|
-
const recent = (0,
|
|
5988
|
-
if (recent) resolvedFilePath = (0,
|
|
6665
|
+
const recent = (0, import_fs12.readdirSync)(projectsDir).filter((f) => f.endsWith(".jsonl")).map((f) => ({ f, mtime: (0, import_fs12.statSync)((0, import_path13.join)(projectsDir, f)).mtimeMs })).filter(({ mtime }) => now - mtime < 5e3).sort((a, b) => b.mtime - a.mtime)[0];
|
|
6666
|
+
if (recent) resolvedFilePath = (0, import_path13.join)(projectsDir, recent.f);
|
|
5989
6667
|
} catch {
|
|
5990
6668
|
}
|
|
5991
6669
|
}
|
|
@@ -5994,7 +6672,7 @@ var StreamerServer = class {
|
|
|
5994
6672
|
this.sessionFileMap.set(sessionId, resolvedFilePath);
|
|
5995
6673
|
this.fileWatcher.watch(resolvedFilePath);
|
|
5996
6674
|
try {
|
|
5997
|
-
const existing = (0,
|
|
6675
|
+
const existing = (0, import_fs12.readFileSync)(resolvedFilePath, "utf8").split("\n").filter(Boolean);
|
|
5998
6676
|
if (existing.length > 0) {
|
|
5999
6677
|
this.wsHub.broadcast({ type: "conversation_events", sessionId, lines: existing });
|
|
6000
6678
|
for (const line of existing) {
|
|
@@ -6020,11 +6698,126 @@ var StreamerServer = class {
|
|
|
6020
6698
|
if (this.sessionFileMap.has(sessionId)) return;
|
|
6021
6699
|
try {
|
|
6022
6700
|
require("fs").mkdirSync(projectsDir, { recursive: true });
|
|
6023
|
-
watcher = (0,
|
|
6701
|
+
watcher = (0, import_fs12.watch)(projectsDir, tryWire);
|
|
6024
6702
|
watcher.on("error", cleanup);
|
|
6025
6703
|
} catch {
|
|
6026
6704
|
}
|
|
6027
6705
|
}
|
|
6706
|
+
// Codex-equivalent of watchForJsonl(). Differs because Codex has no
|
|
6707
|
+
// filename-encoded session id (it assigns its own persisted id) and its
|
|
6708
|
+
// rollout files live under a date-nested directory
|
|
6709
|
+
// (~/.codex/sessions/<YYYY>/<MM>/<DD>/rollout-*.jsonl) that Codex creates
|
|
6710
|
+
// itself — it may not exist yet when this function is first called, so we
|
|
6711
|
+
// poll rather than fs.watch a not-yet-existent directory. Per Phase 0
|
|
6712
|
+
// findings, the rollout file appears within ~1s of process spawn (after
|
|
6713
|
+
// any directory-trust gate is cleared), well before any user input.
|
|
6714
|
+
watchForCodexRollout(sessionId, projectPath) {
|
|
6715
|
+
const deadline = Date.now() + 12e4;
|
|
6716
|
+
const now = /* @__PURE__ */ new Date();
|
|
6717
|
+
const dateDir = (0, import_path13.join)(
|
|
6718
|
+
String(now.getFullYear()),
|
|
6719
|
+
String(now.getMonth() + 1).padStart(2, "0"),
|
|
6720
|
+
String(now.getDate()).padStart(2, "0")
|
|
6721
|
+
);
|
|
6722
|
+
const sessionStartedAtMs = (this.sessionStore.getManaged(sessionId)?.startedAt?.getTime() ?? Date.now()) - 5e3;
|
|
6723
|
+
let intervalHandle = null;
|
|
6724
|
+
const cleanup = () => {
|
|
6725
|
+
if (intervalHandle) clearInterval(intervalHandle);
|
|
6726
|
+
intervalHandle = null;
|
|
6727
|
+
};
|
|
6728
|
+
const matchesProjectPath = (candidatePath) => {
|
|
6729
|
+
try {
|
|
6730
|
+
const firstLine = (0, import_fs12.readFileSync)(candidatePath, "utf8").split("\n", 1)[0];
|
|
6731
|
+
if (!firstLine) return null;
|
|
6732
|
+
const parsed = JSON.parse(firstLine);
|
|
6733
|
+
if (parsed?.type !== "session_meta") return null;
|
|
6734
|
+
const payload = parsed.payload ?? {};
|
|
6735
|
+
if (payload.cwd !== projectPath) return null;
|
|
6736
|
+
if (typeof payload.id !== "string") return null;
|
|
6737
|
+
const createdIso = payload.timestamp ?? parsed.timestamp;
|
|
6738
|
+
const createdAtMs = typeof createdIso === "string" ? Date.parse(createdIso) : Number.NaN;
|
|
6739
|
+
if (Number.isNaN(createdAtMs) || createdAtMs < sessionStartedAtMs) return null;
|
|
6740
|
+
return { id: payload.id, createdAtMs };
|
|
6741
|
+
} catch {
|
|
6742
|
+
return null;
|
|
6743
|
+
}
|
|
6744
|
+
};
|
|
6745
|
+
const tryWire = () => {
|
|
6746
|
+
if (!this.ptyManager.hasSession(sessionId)) {
|
|
6747
|
+
cleanup();
|
|
6748
|
+
return;
|
|
6749
|
+
}
|
|
6750
|
+
if (Date.now() > deadline) {
|
|
6751
|
+
cleanup();
|
|
6752
|
+
return;
|
|
6753
|
+
}
|
|
6754
|
+
const boundElsewhere = new Set(
|
|
6755
|
+
this.sessionStore.listManaged().filter((s) => s.id !== sessionId && s.boundConversationId != null).map((s) => s.boundConversationId)
|
|
6756
|
+
);
|
|
6757
|
+
for (const root of this.codexRoots) {
|
|
6758
|
+
const sessionsDir = (0, import_path13.join)(root, dateDir);
|
|
6759
|
+
if (!(0, import_fs12.existsSync)(sessionsDir)) continue;
|
|
6760
|
+
let candidateFiles;
|
|
6761
|
+
try {
|
|
6762
|
+
candidateFiles = (0, import_fs12.readdirSync)(sessionsDir).filter((f) => f.endsWith(".jsonl"));
|
|
6763
|
+
} catch {
|
|
6764
|
+
continue;
|
|
6765
|
+
}
|
|
6766
|
+
const nowMs = Date.now();
|
|
6767
|
+
const recentCandidates = candidateFiles.map((f) => ({ f, mtime: (0, import_fs12.statSync)((0, import_path13.join)(sessionsDir, f)).mtimeMs })).filter(({ mtime }) => nowMs - mtime < 1e4).sort((a, b) => b.mtime - a.mtime);
|
|
6768
|
+
for (const { f } of recentCandidates) {
|
|
6769
|
+
const candidatePath = (0, import_path13.join)(sessionsDir, f);
|
|
6770
|
+
const match = matchesProjectPath(candidatePath);
|
|
6771
|
+
if (!match) continue;
|
|
6772
|
+
if (boundElsewhere.has(match.id)) continue;
|
|
6773
|
+
const codexSessionId = match.id;
|
|
6774
|
+
cleanup();
|
|
6775
|
+
this.sessionStore.updateManaged(sessionId, { boundConversationId: codexSessionId });
|
|
6776
|
+
this.sessionFileMap.set(sessionId, candidatePath);
|
|
6777
|
+
this.fileWatcher.watch(candidatePath);
|
|
6778
|
+
try {
|
|
6779
|
+
const existing = (0, import_fs12.readFileSync)(candidatePath, "utf8").split("\n").filter(Boolean);
|
|
6780
|
+
if (existing.length > 0) {
|
|
6781
|
+
this.wsHub.broadcast({ type: "conversation_events", sessionId, lines: existing });
|
|
6782
|
+
for (const line of existing) {
|
|
6783
|
+
this.wsHub.broadcast({ type: "conversation_event", sessionId, line });
|
|
6784
|
+
}
|
|
6785
|
+
}
|
|
6786
|
+
} catch {
|
|
6787
|
+
}
|
|
6788
|
+
if (this.scannerReady) {
|
|
6789
|
+
this.scannerStale = true;
|
|
6790
|
+
} else {
|
|
6791
|
+
this.scanner = null;
|
|
6792
|
+
}
|
|
6793
|
+
this.linkSessionToProject(sessionId, projectPath, candidatePath);
|
|
6794
|
+
this.cache?.markAsStreamer(sessionId);
|
|
6795
|
+
const resp = this.sessionStore.get(sessionId, this.ptyAttachedIds());
|
|
6796
|
+
if (resp) {
|
|
6797
|
+
this.wsHub.broadcast({ type: "session_update", session: resp });
|
|
6798
|
+
}
|
|
6799
|
+
this.log.info(
|
|
6800
|
+
`[startFresh] bound Codex rollout for ${sessionId}`,
|
|
6801
|
+
{
|
|
6802
|
+
event: "session.codex_rollout_bound",
|
|
6803
|
+
sessionId,
|
|
6804
|
+
boundConversationId: codexSessionId,
|
|
6805
|
+
filePath: candidatePath
|
|
6806
|
+
},
|
|
6807
|
+
"pino"
|
|
6808
|
+
);
|
|
6809
|
+
return;
|
|
6810
|
+
}
|
|
6811
|
+
}
|
|
6812
|
+
};
|
|
6813
|
+
tryWire();
|
|
6814
|
+
if (!intervalHandle && Date.now() <= deadline) {
|
|
6815
|
+
const alreadyBound = this.sessionStore.getManaged(sessionId)?.boundConversationId != null;
|
|
6816
|
+
if (!alreadyBound) {
|
|
6817
|
+
intervalHandle = setInterval(tryWire, 250);
|
|
6818
|
+
}
|
|
6819
|
+
}
|
|
6820
|
+
}
|
|
6028
6821
|
async handleBrowse(url, res) {
|
|
6029
6822
|
if (!this.browseRoot) {
|
|
6030
6823
|
json(res, 403, {
|
|
@@ -6108,7 +6901,7 @@ var StreamerServer = class {
|
|
|
6108
6901
|
};
|
|
6109
6902
|
function classifyResumability(cwd) {
|
|
6110
6903
|
if (!cwd) return { resumable: true };
|
|
6111
|
-
if ((0,
|
|
6904
|
+
if ((0, import_fs12.existsSync)(cwd)) return { resumable: true };
|
|
6112
6905
|
const ranInWorktree = /\/\.worktrees\//.test(cwd) || /\/\.claude\/worktrees\//.test(cwd);
|
|
6113
6906
|
return {
|
|
6114
6907
|
resumable: false,
|
|
@@ -6240,7 +7033,10 @@ function readBody(req) {
|
|
|
6240
7033
|
}
|
|
6241
7034
|
// Annotate the CommonJS export names for ESM import in node:
|
|
6242
7035
|
0 && (module.exports = {
|
|
7036
|
+
CLAUDE_CODE_PROVIDER,
|
|
7037
|
+
CODEX_CLI_PROVIDER,
|
|
6243
7038
|
ConversationWatcher,
|
|
7039
|
+
LiveSessionManager,
|
|
6244
7040
|
PTYManager,
|
|
6245
7041
|
SessionStore,
|
|
6246
7042
|
StreamerServer,
|
|
@@ -6254,6 +7050,8 @@ function readBody(req) {
|
|
|
6254
7050
|
generateApiKey,
|
|
6255
7051
|
getDbConfig,
|
|
6256
7052
|
isDbEnabled,
|
|
7053
|
+
isProviderName,
|
|
7054
|
+
isProviderResumable,
|
|
6257
7055
|
loadOrCreateApiKey,
|
|
6258
7056
|
maskConnectionString,
|
|
6259
7057
|
readAgentConfig,
|