@threadbase-sh/streamer 1.23.1 → 1.24.1
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 +1175 -350
- package/dist/cli.cjs.map +1 -1
- package/dist/index.cjs +1157 -374
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +89 -28
- package/dist/index.d.ts +89 -28
- package/dist/index.js +1027 -249
- package/dist/index.js.map +1 -1
- package/package.json +1 -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
|
}
|
|
@@ -978,6 +1369,11 @@ function buildSpawnEnv() {
|
|
|
978
1369
|
if (env.CLAUDE_API_KEY) {
|
|
979
1370
|
env.ANTHROPIC_API_KEY = env.CLAUDE_API_KEY;
|
|
980
1371
|
}
|
|
1372
|
+
for (const key of Object.keys(env)) {
|
|
1373
|
+
if (key === "CLAUDECODE" || key.startsWith("CLAUDE_CODE_")) {
|
|
1374
|
+
delete env[key];
|
|
1375
|
+
}
|
|
1376
|
+
}
|
|
981
1377
|
return env;
|
|
982
1378
|
}
|
|
983
1379
|
var PTYManager = class {
|
|
@@ -1018,6 +1414,10 @@ var PTYManager = class {
|
|
|
1018
1414
|
// to a given input or fell silent. Reset on dispose().
|
|
1019
1415
|
chunkIndex = /* @__PURE__ */ new Map();
|
|
1020
1416
|
lastChunkAt = /* @__PURE__ */ new Map();
|
|
1417
|
+
// In-flight start()/startFresh() calls keyed by sessionId. A second
|
|
1418
|
+
// concurrent resume for the same session (double-tap, client retry) awaits
|
|
1419
|
+
// the first call's promise instead of spawning a duplicate PTY (CRITICAL #3).
|
|
1420
|
+
startPromises = /* @__PURE__ */ new Map();
|
|
1021
1421
|
constructor(options = {}) {
|
|
1022
1422
|
this.onOutput = options.onOutput;
|
|
1023
1423
|
this.onStatusChange = options.onStatusChange;
|
|
@@ -1040,7 +1440,18 @@ var PTYManager = class {
|
|
|
1040
1440
|
// custom-API-key — are cleared by the seeded ~/.claude.json in
|
|
1041
1441
|
// docker/entrypoint.sh.) startFresh() uses the same flag for the same reason.
|
|
1042
1442
|
async start(sessionId, options) {
|
|
1043
|
-
const
|
|
1443
|
+
const existing = this.sessions.get(sessionId);
|
|
1444
|
+
if (existing) return toPublicSession2(existing);
|
|
1445
|
+
const inFlight = this.startPromises.get(sessionId);
|
|
1446
|
+
if (inFlight) return inFlight;
|
|
1447
|
+
const promise = this.doStart(sessionId, options).finally(() => {
|
|
1448
|
+
this.startPromises.delete(sessionId);
|
|
1449
|
+
});
|
|
1450
|
+
this.startPromises.set(sessionId, promise);
|
|
1451
|
+
return promise;
|
|
1452
|
+
}
|
|
1453
|
+
async doStart(sessionId, options) {
|
|
1454
|
+
const nodePty = await loadPty2();
|
|
1044
1455
|
const projectName = options.projectName ?? (0, import_path5.basename)(options.projectPath);
|
|
1045
1456
|
const proc = nodePty.spawn(
|
|
1046
1457
|
resolveClaudeExe(),
|
|
@@ -1062,6 +1473,7 @@ var PTYManager = class {
|
|
|
1062
1473
|
);
|
|
1063
1474
|
const session = {
|
|
1064
1475
|
id: sessionId,
|
|
1476
|
+
provider: CLAUDE_CODE_PROVIDER,
|
|
1065
1477
|
projectPath: options.projectPath,
|
|
1066
1478
|
projectName,
|
|
1067
1479
|
branch: options.branch ?? "",
|
|
@@ -1072,7 +1484,7 @@ var PTYManager = class {
|
|
|
1072
1484
|
lastOutput: "",
|
|
1073
1485
|
process: proc,
|
|
1074
1486
|
outputBuffer: Buffer.alloc(0),
|
|
1075
|
-
screen:
|
|
1487
|
+
screen: createScreen2()
|
|
1076
1488
|
};
|
|
1077
1489
|
this.sessions.set(sessionId, session);
|
|
1078
1490
|
this.pendingReady.add(sessionId);
|
|
@@ -1083,14 +1495,14 @@ var PTYManager = class {
|
|
|
1083
1495
|
this.pendingReady.delete(sessionId);
|
|
1084
1496
|
this.handleExit(sessionId, exitCode);
|
|
1085
1497
|
});
|
|
1086
|
-
return
|
|
1498
|
+
return toPublicSession2(session);
|
|
1087
1499
|
}
|
|
1088
1500
|
// Start a brand-new Claude session. A stable UUID is generated here and passed
|
|
1089
1501
|
// to Claude via --session-id so the JSONL filename matches from the start.
|
|
1090
1502
|
// onReady fires once Claude reaches its first prompt (waiting_input).
|
|
1091
1503
|
async startFresh(options) {
|
|
1092
|
-
const nodePty = await
|
|
1093
|
-
const sessionId = (0,
|
|
1504
|
+
const nodePty = await loadPty2();
|
|
1505
|
+
const sessionId = (0, import_crypto3.randomUUID)();
|
|
1094
1506
|
const projectName = options.projectName ?? (0, import_path5.basename)(options.projectPath);
|
|
1095
1507
|
const args = [
|
|
1096
1508
|
"--permission-mode",
|
|
@@ -1112,6 +1524,7 @@ var PTYManager = class {
|
|
|
1112
1524
|
});
|
|
1113
1525
|
const session = {
|
|
1114
1526
|
id: sessionId,
|
|
1527
|
+
provider: CLAUDE_CODE_PROVIDER,
|
|
1115
1528
|
projectPath: options.projectPath,
|
|
1116
1529
|
projectName,
|
|
1117
1530
|
branch: "",
|
|
@@ -1122,7 +1535,7 @@ var PTYManager = class {
|
|
|
1122
1535
|
lastOutput: "",
|
|
1123
1536
|
process: proc,
|
|
1124
1537
|
outputBuffer: Buffer.alloc(0),
|
|
1125
|
-
screen:
|
|
1538
|
+
screen: createScreen2()
|
|
1126
1539
|
};
|
|
1127
1540
|
this.sessions.set(sessionId, session);
|
|
1128
1541
|
this.pendingReady.add(sessionId);
|
|
@@ -1133,7 +1546,7 @@ var PTYManager = class {
|
|
|
1133
1546
|
this.pendingReady.delete(sessionId);
|
|
1134
1547
|
this.handleExit(sessionId, exitCode);
|
|
1135
1548
|
});
|
|
1136
|
-
return
|
|
1549
|
+
return toPublicSession2(session);
|
|
1137
1550
|
}
|
|
1138
1551
|
// Write raw key bytes directly to the PTY without bracketed-paste wrapping.
|
|
1139
1552
|
// Use for control sequences (arrow keys, Enter) that must not be quoted.
|
|
@@ -1145,10 +1558,10 @@ var PTYManager = class {
|
|
|
1145
1558
|
}
|
|
1146
1559
|
if (session.status === "waiting_input") {
|
|
1147
1560
|
session.status = "running";
|
|
1148
|
-
this.onStatusChange?.(
|
|
1561
|
+
this.onStatusChange?.(toPublicSession2(session));
|
|
1149
1562
|
}
|
|
1150
1563
|
this.log.info(
|
|
1151
|
-
`[pty.keys.write] ${sessionId.slice(0, 8)} bytes=${keys.length} digest=${
|
|
1564
|
+
`[pty.keys.write] ${sessionId.slice(0, 8)} bytes=${keys.length} digest=${digestBytes2(keys)}`,
|
|
1152
1565
|
{ event: "pty.keys_write", sessionId, byteLen: keys.length }
|
|
1153
1566
|
);
|
|
1154
1567
|
session.process.write(keys);
|
|
@@ -1180,7 +1593,7 @@ var PTYManager = class {
|
|
|
1180
1593
|
}
|
|
1181
1594
|
if (session.status === "waiting_input") {
|
|
1182
1595
|
session.status = "running";
|
|
1183
|
-
this.onStatusChange?.(
|
|
1596
|
+
this.onStatusChange?.(toPublicSession2(session));
|
|
1184
1597
|
}
|
|
1185
1598
|
this.writeSubmit(sessionId, session, input, "direct", session.promptCount + 1);
|
|
1186
1599
|
session.lastActivityAt = /* @__PURE__ */ new Date();
|
|
@@ -1193,13 +1606,13 @@ var PTYManager = class {
|
|
|
1193
1606
|
writeSubmit(sessionId, session, input, path, promptCount) {
|
|
1194
1607
|
const pasteBytes = buildPasteBytes(input);
|
|
1195
1608
|
this.log.info(
|
|
1196
|
-
`[pty.input.write] ${sessionId.slice(0, 8)} promptCount=${promptCount} bytes=${pasteBytes.length} digest=${
|
|
1609
|
+
`[pty.input.write] ${sessionId.slice(0, 8)} promptCount=${promptCount} bytes=${pasteBytes.length} digest=${digestBytes2(pasteBytes)}`,
|
|
1197
1610
|
{
|
|
1198
1611
|
event: "pty.input_write",
|
|
1199
1612
|
sessionId,
|
|
1200
1613
|
promptCount,
|
|
1201
1614
|
byteLen: pasteBytes.length,
|
|
1202
|
-
digest:
|
|
1615
|
+
digest: digestBytes2(pasteBytes),
|
|
1203
1616
|
path,
|
|
1204
1617
|
phase: "paste"
|
|
1205
1618
|
}
|
|
@@ -1214,13 +1627,13 @@ var PTYManager = class {
|
|
|
1214
1627
|
event: "pty.input_write",
|
|
1215
1628
|
sessionId,
|
|
1216
1629
|
promptCount,
|
|
1217
|
-
byteLen:
|
|
1630
|
+
byteLen: SUBMIT_BYTES2.length,
|
|
1218
1631
|
digest: "\\r",
|
|
1219
1632
|
path,
|
|
1220
1633
|
phase: "submit"
|
|
1221
1634
|
}
|
|
1222
1635
|
);
|
|
1223
|
-
current.process.write(
|
|
1636
|
+
current.process.write(SUBMIT_BYTES2);
|
|
1224
1637
|
}, SUBMIT_DELAY_MS);
|
|
1225
1638
|
}
|
|
1226
1639
|
// Drain any inputs that were sent while the session was still pendingReady,
|
|
@@ -1278,7 +1691,7 @@ var PTYManager = class {
|
|
|
1278
1691
|
session.completedAt = /* @__PURE__ */ new Date();
|
|
1279
1692
|
session.screen.dispose();
|
|
1280
1693
|
this.sessions.delete(sessionId);
|
|
1281
|
-
this.onStatusChange?.(
|
|
1694
|
+
this.onStatusChange?.(toPublicSession2(session));
|
|
1282
1695
|
}
|
|
1283
1696
|
getOutput(sessionId) {
|
|
1284
1697
|
const session = this.sessions.get(sessionId);
|
|
@@ -1309,13 +1722,13 @@ var PTYManager = class {
|
|
|
1309
1722
|
}
|
|
1310
1723
|
getSession(sessionId) {
|
|
1311
1724
|
const session = this.sessions.get(sessionId);
|
|
1312
|
-
return session ?
|
|
1725
|
+
return session ? toPublicSession2(session) : null;
|
|
1313
1726
|
}
|
|
1314
1727
|
hasSession(sessionId) {
|
|
1315
1728
|
return this.sessions.has(sessionId);
|
|
1316
1729
|
}
|
|
1317
1730
|
listSessions() {
|
|
1318
|
-
return Array.from(this.sessions.values()).map(
|
|
1731
|
+
return Array.from(this.sessions.values()).map(toPublicSession2);
|
|
1319
1732
|
}
|
|
1320
1733
|
dispose() {
|
|
1321
1734
|
for (const session of this.sessions.values()) {
|
|
@@ -1347,7 +1760,7 @@ var PTYManager = class {
|
|
|
1347
1760
|
this.lastChunkAt.set(sessionId, now);
|
|
1348
1761
|
const gapMs = last == null ? 0 : now - last;
|
|
1349
1762
|
this.log.info(
|
|
1350
|
-
`[pty.chunk] ${sessionId.slice(0, 8)} #${idx} +${chunk.length}B gap=${gapMs}ms status=${session.status} digest=${
|
|
1763
|
+
`[pty.chunk] ${sessionId.slice(0, 8)} #${idx} +${chunk.length}B gap=${gapMs}ms status=${session.status} digest=${digestBytes2(data)}`,
|
|
1351
1764
|
{
|
|
1352
1765
|
event: "pty.chunk",
|
|
1353
1766
|
sessionId,
|
|
@@ -1356,17 +1769,17 @@ var PTYManager = class {
|
|
|
1356
1769
|
gapMs,
|
|
1357
1770
|
status: session.status,
|
|
1358
1771
|
pendingReady: this.pendingReady.has(sessionId),
|
|
1359
|
-
digest:
|
|
1772
|
+
digest: digestBytes2(data)
|
|
1360
1773
|
}
|
|
1361
1774
|
);
|
|
1362
1775
|
session.outputBuffer = Buffer.concat([session.outputBuffer, chunk]);
|
|
1363
|
-
if (session.outputBuffer.length >
|
|
1776
|
+
if (session.outputBuffer.length > OUTPUT_BUFFER_MAX2) {
|
|
1364
1777
|
session.outputBuffer = session.outputBuffer.subarray(
|
|
1365
|
-
session.outputBuffer.length -
|
|
1778
|
+
session.outputBuffer.length - OUTPUT_BUFFER_MAX2
|
|
1366
1779
|
);
|
|
1367
1780
|
}
|
|
1368
1781
|
session.screen.write(data);
|
|
1369
|
-
const stripped =
|
|
1782
|
+
const stripped = stripAnsi2(data);
|
|
1370
1783
|
session.lastOutput = stripped;
|
|
1371
1784
|
const matchedMarker = CLAUDE_PROMPT_MARKERS.find((m) => stripped.includes(m));
|
|
1372
1785
|
if (session.status === "running" && matchedMarker) {
|
|
@@ -1436,87 +1849,327 @@ var PTYManager = class {
|
|
|
1436
1849
|
this.onLiveQuestion?.(sessionId, detected.questions);
|
|
1437
1850
|
}
|
|
1438
1851
|
}
|
|
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);
|
|
1852
|
+
} else if (this.lastScreenQuestionKey.has(sessionId) && hasPromptMarker) {
|
|
1853
|
+
this.lastScreenQuestionKey.delete(sessionId);
|
|
1854
|
+
this.onLiveQuestionGone?.(sessionId);
|
|
1855
|
+
}
|
|
1856
|
+
if (!oscPermission && !askFooterOnScreen && !this.permissionOpen.has(sessionId)) {
|
|
1857
|
+
const shell = detectShellPrompt(lines);
|
|
1858
|
+
if (shell) {
|
|
1859
|
+
const key = `${shell.prompt}\0${shell.options.map((o) => o.label).join("\0")}`;
|
|
1860
|
+
if (this.shellPromptOpen.get(sessionId) !== key) {
|
|
1861
|
+
this.shellPromptOpen.set(sessionId, key);
|
|
1862
|
+
this.onPermissionChange?.(sessionId, {
|
|
1863
|
+
prompt: shell.prompt,
|
|
1864
|
+
options: shell.options
|
|
1865
|
+
});
|
|
1866
|
+
}
|
|
1867
|
+
} else if (this.shellPromptOpen.has(sessionId) && hasPromptMarker) {
|
|
1868
|
+
this.shellPromptOpen.delete(sessionId);
|
|
1869
|
+
this.onPermissionChange?.(sessionId, null);
|
|
1870
|
+
}
|
|
1871
|
+
}
|
|
1872
|
+
}
|
|
1873
|
+
// Transition a session from "running" to "waiting_input", clear pendingReady,
|
|
1874
|
+
// and flush any queued input. Idempotent: callers can invoke at any chunk.
|
|
1875
|
+
markReady(sessionId, session, reason) {
|
|
1876
|
+
session.lastActivityAt = /* @__PURE__ */ new Date();
|
|
1877
|
+
session.status = "waiting_input";
|
|
1878
|
+
const elapsedMs = Date.now() - (this.firstChunkAt.get(sessionId) ?? Date.now());
|
|
1879
|
+
this.log.info(`[pty.ready] ${sessionId.slice(0, 8)} ${reason} (elapsed=${elapsedMs}ms)`, {
|
|
1880
|
+
event: "pty.ready",
|
|
1881
|
+
sessionId,
|
|
1882
|
+
reason,
|
|
1883
|
+
elapsedMs
|
|
1884
|
+
});
|
|
1885
|
+
this.onStatusChange?.(toPublicSession2(session));
|
|
1886
|
+
if (this.pendingReady.has(sessionId)) {
|
|
1887
|
+
this.pendingReady.delete(sessionId);
|
|
1888
|
+
this.flushQueuedInputs(sessionId);
|
|
1889
|
+
this.onReady?.(toPublicSession2(session));
|
|
1890
|
+
}
|
|
1891
|
+
}
|
|
1892
|
+
handleExit(sessionId, exitCode) {
|
|
1893
|
+
const session = this.sessions.get(sessionId);
|
|
1894
|
+
if (!session) return;
|
|
1895
|
+
session.completedAt = /* @__PURE__ */ new Date();
|
|
1896
|
+
session.status = "idle";
|
|
1897
|
+
const elapsedMs = session.completedAt.getTime() - session.startedAt.getTime();
|
|
1898
|
+
if (exitCode !== 0 && elapsedMs < 2e3 && session.lastOutput === "") {
|
|
1899
|
+
if (!(0, import_fs5.existsSync)(session.projectPath)) {
|
|
1900
|
+
session.failureReason = `Project directory not found: ${session.projectPath}`;
|
|
1901
|
+
} else {
|
|
1902
|
+
session.failureReason = `Process exited immediately (code ${exitCode}). Check that the Claude binary is installed and accessible.`;
|
|
1903
|
+
}
|
|
1904
|
+
}
|
|
1905
|
+
this.onStatusChange?.(toPublicSession2(session));
|
|
1906
|
+
session.screen.dispose();
|
|
1907
|
+
this.sessions.delete(sessionId);
|
|
1908
|
+
this.queuedInputs.delete(sessionId);
|
|
1909
|
+
this.firstChunkAt.delete(sessionId);
|
|
1910
|
+
this.permissionOpen.delete(sessionId);
|
|
1911
|
+
this.lastScreenQuestionKey.delete(sessionId);
|
|
1912
|
+
this.shellPromptOpen.delete(sessionId);
|
|
1913
|
+
}
|
|
1914
|
+
};
|
|
1915
|
+
function toPublicSession2(s) {
|
|
1916
|
+
return {
|
|
1917
|
+
id: s.id,
|
|
1918
|
+
provider: s.provider ?? CLAUDE_CODE_PROVIDER,
|
|
1919
|
+
projectPath: s.projectPath,
|
|
1920
|
+
projectName: s.projectName,
|
|
1921
|
+
branch: s.branch,
|
|
1922
|
+
status: s.status,
|
|
1923
|
+
startedAt: s.startedAt,
|
|
1924
|
+
completedAt: s.completedAt,
|
|
1925
|
+
promptCount: s.promptCount,
|
|
1926
|
+
lastOutput: s.lastOutput,
|
|
1927
|
+
...s.failureReason != null && { failureReason: s.failureReason },
|
|
1928
|
+
...s.lastActivityAt != null && { lastActivityAt: s.lastActivityAt },
|
|
1929
|
+
...s.filePath != null && { filePath: s.filePath }
|
|
1930
|
+
};
|
|
1931
|
+
}
|
|
1932
|
+
function stripAnsi2(str) {
|
|
1933
|
+
return str.replace(/\x1b\[[0-9;]*[a-zA-Z]/g, "").replace(/\x1b\][^\x07]*\x07/g, "");
|
|
1934
|
+
}
|
|
1935
|
+
|
|
1936
|
+
// src/live-session-manager.ts
|
|
1937
|
+
var LiveSessionManager = class {
|
|
1938
|
+
runners;
|
|
1939
|
+
constructor(options = {}) {
|
|
1940
|
+
this.runners = /* @__PURE__ */ new Map([
|
|
1941
|
+
[CLAUDE_CODE_PROVIDER, new PTYManager(options)],
|
|
1942
|
+
[CODEX_CLI_PROVIDER, new CodexPtyRunner(options)]
|
|
1943
|
+
]);
|
|
1944
|
+
}
|
|
1945
|
+
async start(sessionId, options) {
|
|
1946
|
+
const provider = options.provider ?? CLAUDE_CODE_PROVIDER;
|
|
1947
|
+
const runner = this.assertSupportedProvider(provider, options.projectPath);
|
|
1948
|
+
return runner.start(sessionId, options);
|
|
1949
|
+
}
|
|
1950
|
+
async startFresh(options) {
|
|
1951
|
+
const provider = options.provider ?? CLAUDE_CODE_PROVIDER;
|
|
1952
|
+
const runner = this.assertSupportedProvider(provider, options.projectPath);
|
|
1953
|
+
return runner.startFresh(options);
|
|
1954
|
+
}
|
|
1955
|
+
sendInput(sessionId, input) {
|
|
1956
|
+
return this.runnerFor(sessionId).sendInput(sessionId, input);
|
|
1957
|
+
}
|
|
1958
|
+
sendKeys(sessionId, keys) {
|
|
1959
|
+
this.runnerFor(sessionId).sendKeys(sessionId, keys);
|
|
1960
|
+
}
|
|
1961
|
+
cancel(sessionId) {
|
|
1962
|
+
this.runnerFor(sessionId).cancel(sessionId);
|
|
1963
|
+
}
|
|
1964
|
+
killPid(pid) {
|
|
1965
|
+
for (const runner of this.runners.values()) {
|
|
1966
|
+
runner.killPid(pid);
|
|
1967
|
+
}
|
|
1968
|
+
}
|
|
1969
|
+
// putOnHold tolerates an unknown sessionId (PTYManager.putOnHold is a no-op
|
|
1970
|
+
// when the session isn't in its map), so — unlike the other session-keyed
|
|
1971
|
+
// methods — route to the owning runner when found, otherwise broadcast to
|
|
1972
|
+
// every runner rather than throwing; this matches the pre-extraction
|
|
1973
|
+
// behavior of delegating straight through with no existence check.
|
|
1974
|
+
putOnHold(sessionId) {
|
|
1975
|
+
for (const runner of this.runners.values()) {
|
|
1976
|
+
if (runner.hasSession(sessionId) || runner.getSession(sessionId)) {
|
|
1977
|
+
runner.putOnHold(sessionId);
|
|
1978
|
+
return;
|
|
1979
|
+
}
|
|
1980
|
+
}
|
|
1981
|
+
for (const runner of this.runners.values()) {
|
|
1982
|
+
runner.putOnHold(sessionId);
|
|
1983
|
+
}
|
|
1984
|
+
}
|
|
1985
|
+
getOutput(sessionId) {
|
|
1986
|
+
return this.runnerFor(sessionId).getOutput(sessionId);
|
|
1987
|
+
}
|
|
1988
|
+
getOutputLines(sessionId, maxLines) {
|
|
1989
|
+
return this.runnerFor(sessionId).getOutputLines(sessionId, maxLines);
|
|
1990
|
+
}
|
|
1991
|
+
getSession(sessionId) {
|
|
1992
|
+
for (const runner of this.runners.values()) {
|
|
1993
|
+
const session = runner.getSession(sessionId);
|
|
1994
|
+
if (session) return session;
|
|
1995
|
+
}
|
|
1996
|
+
return null;
|
|
1997
|
+
}
|
|
1998
|
+
hasSession(sessionId) {
|
|
1999
|
+
for (const runner of this.runners.values()) {
|
|
2000
|
+
if (runner.hasSession(sessionId)) return true;
|
|
2001
|
+
}
|
|
2002
|
+
return false;
|
|
2003
|
+
}
|
|
2004
|
+
listSessions() {
|
|
2005
|
+
return Array.from(this.runners.values()).flatMap((runner) => runner.listSessions());
|
|
2006
|
+
}
|
|
2007
|
+
dispose() {
|
|
2008
|
+
for (const runner of this.runners.values()) {
|
|
2009
|
+
runner.dispose();
|
|
2010
|
+
}
|
|
2011
|
+
}
|
|
2012
|
+
// Look up which runner owns a session. Only one runner exists today, so
|
|
2013
|
+
// this is a linear scan across hasSession()/getSession() rather than a
|
|
2014
|
+
// separate session→provider index — see task-1-brief.md.
|
|
2015
|
+
runnerFor(sessionId) {
|
|
2016
|
+
for (const runner of this.runners.values()) {
|
|
2017
|
+
if (runner.hasSession(sessionId) || runner.getSession(sessionId)) return runner;
|
|
2018
|
+
}
|
|
2019
|
+
throw new Error(`Session not found: ${sessionId}`);
|
|
2020
|
+
}
|
|
2021
|
+
assertSupportedProvider(provider, projectPath) {
|
|
2022
|
+
const runner = this.runners.get(provider);
|
|
2023
|
+
if (runner) return runner;
|
|
2024
|
+
const err = new Error(
|
|
2025
|
+
`Live ${provider} sessions are not implemented yet for ${(0, import_path6.basename)(projectPath)}`
|
|
2026
|
+
);
|
|
2027
|
+
err.statusCode = 501;
|
|
2028
|
+
throw err;
|
|
2029
|
+
}
|
|
2030
|
+
};
|
|
2031
|
+
|
|
2032
|
+
// src/process-discovery.ts
|
|
2033
|
+
var import_child_process2 = require("child_process");
|
|
2034
|
+
var import_os3 = require("os");
|
|
2035
|
+
var import_path7 = require("path");
|
|
2036
|
+
async function discoverClaudeProcesses() {
|
|
2037
|
+
if ((0, import_os3.platform)() === "win32") return discoverWindows();
|
|
2038
|
+
return discoverUnix();
|
|
2039
|
+
}
|
|
2040
|
+
async function discoverUnix() {
|
|
2041
|
+
const pids = await getPidsUnix();
|
|
2042
|
+
const results = await Promise.all(
|
|
2043
|
+
pids.map(async (pid) => {
|
|
2044
|
+
try {
|
|
2045
|
+
const [cwd, args, startedAt] = await Promise.all([
|
|
2046
|
+
getProcessCwdUnix(pid),
|
|
2047
|
+
getProcessArgsUnix(pid),
|
|
2048
|
+
getProcessStartTimeUnix(pid)
|
|
2049
|
+
]);
|
|
2050
|
+
const conversationId = extractResumeId(args);
|
|
2051
|
+
return {
|
|
2052
|
+
pid,
|
|
2053
|
+
projectPath: cwd,
|
|
2054
|
+
projectName: (0, import_path7.basename)(cwd),
|
|
2055
|
+
branch: await readGitBranch(cwd),
|
|
2056
|
+
conversationId,
|
|
2057
|
+
startedAt
|
|
2058
|
+
};
|
|
2059
|
+
} catch {
|
|
2060
|
+
return null;
|
|
2061
|
+
}
|
|
2062
|
+
})
|
|
2063
|
+
);
|
|
2064
|
+
return results.filter((r) => r !== null);
|
|
2065
|
+
}
|
|
2066
|
+
async function discoverWindows() {
|
|
2067
|
+
const pids = await getPidsWindows();
|
|
2068
|
+
const results = await Promise.all(
|
|
2069
|
+
pids.map(async (pid) => {
|
|
2070
|
+
try {
|
|
2071
|
+
const info = await getProcessInfoWindows(pid);
|
|
2072
|
+
if (!info) return null;
|
|
2073
|
+
return {
|
|
2074
|
+
pid,
|
|
2075
|
+
projectPath: info.cwd,
|
|
2076
|
+
projectName: (0, import_path7.basename)(info.cwd),
|
|
2077
|
+
branch: await readGitBranch(info.cwd),
|
|
2078
|
+
conversationId: extractResumeId(info.args),
|
|
2079
|
+
startedAt: info.startedAt
|
|
2080
|
+
};
|
|
2081
|
+
} catch {
|
|
2082
|
+
return null;
|
|
2083
|
+
}
|
|
2084
|
+
})
|
|
2085
|
+
);
|
|
2086
|
+
return results.filter((r) => r !== null);
|
|
2087
|
+
}
|
|
2088
|
+
function run(cmd, args, opts = {}) {
|
|
2089
|
+
return new Promise((resolve2, reject) => {
|
|
2090
|
+
(0, import_child_process2.execFile)(
|
|
2091
|
+
cmd,
|
|
2092
|
+
args,
|
|
2093
|
+
{ windowsHide: isWindows, encoding: "utf-8", timeout: opts.timeout ?? 5e3, cwd: opts.cwd },
|
|
2094
|
+
(err, stdout) => {
|
|
2095
|
+
if (err) reject(err);
|
|
2096
|
+
else resolve2(stdout);
|
|
1457
2097
|
}
|
|
1458
|
-
|
|
2098
|
+
);
|
|
2099
|
+
});
|
|
2100
|
+
}
|
|
2101
|
+
async function getPidsUnix() {
|
|
2102
|
+
try {
|
|
2103
|
+
const output = await run("pgrep", ["-x", "claude"]);
|
|
2104
|
+
return output.trim().split("\n").filter(Boolean).map((s) => Number.parseInt(s, 10));
|
|
2105
|
+
} catch {
|
|
2106
|
+
return [];
|
|
1459
2107
|
}
|
|
1460
|
-
|
|
1461
|
-
|
|
1462
|
-
|
|
1463
|
-
|
|
1464
|
-
|
|
1465
|
-
|
|
1466
|
-
|
|
1467
|
-
|
|
1468
|
-
|
|
1469
|
-
|
|
1470
|
-
|
|
1471
|
-
|
|
1472
|
-
|
|
1473
|
-
|
|
1474
|
-
|
|
1475
|
-
|
|
1476
|
-
|
|
1477
|
-
|
|
2108
|
+
}
|
|
2109
|
+
async function getProcessCwdUnix(pid) {
|
|
2110
|
+
const output = await run("lsof", ["-p", String(pid), "-a", "-d", "cwd", "-Fn"]);
|
|
2111
|
+
const match = output.match(/n(.+)/);
|
|
2112
|
+
return match?.[1] ?? "";
|
|
2113
|
+
}
|
|
2114
|
+
async function getProcessArgsUnix(pid) {
|
|
2115
|
+
return (await run("ps", ["-p", String(pid), "-o", "args="])).trim();
|
|
2116
|
+
}
|
|
2117
|
+
async function getProcessStartTimeUnix(pid) {
|
|
2118
|
+
const raw = (await run("ps", ["-p", String(pid), "-o", "lstart="])).trim();
|
|
2119
|
+
const d = new Date(raw);
|
|
2120
|
+
return Number.isNaN(d.getTime()) ? /* @__PURE__ */ new Date() : d;
|
|
2121
|
+
}
|
|
2122
|
+
async function getPidsWindows() {
|
|
2123
|
+
try {
|
|
2124
|
+
const output = await run("tasklist", ["/FI", "IMAGENAME eq claude.exe", "/FO", "CSV", "/NH"]);
|
|
2125
|
+
return output.trim().split("\n").filter(Boolean).map((line) => {
|
|
2126
|
+
const parts = line.split(",");
|
|
2127
|
+
return Number.parseInt(parts[1]?.replace(/"/g, "") ?? "0", 10);
|
|
2128
|
+
}).filter((pid) => pid > 0);
|
|
2129
|
+
} catch {
|
|
2130
|
+
return [];
|
|
1478
2131
|
}
|
|
1479
|
-
|
|
1480
|
-
|
|
1481
|
-
|
|
1482
|
-
|
|
1483
|
-
|
|
1484
|
-
|
|
1485
|
-
|
|
1486
|
-
|
|
1487
|
-
|
|
1488
|
-
|
|
1489
|
-
|
|
1490
|
-
|
|
1491
|
-
|
|
1492
|
-
|
|
1493
|
-
|
|
1494
|
-
|
|
1495
|
-
|
|
1496
|
-
|
|
1497
|
-
|
|
1498
|
-
|
|
1499
|
-
|
|
2132
|
+
}
|
|
2133
|
+
async function getProcessInfoWindows(pid) {
|
|
2134
|
+
try {
|
|
2135
|
+
const output = await run("wmic", [
|
|
2136
|
+
"process",
|
|
2137
|
+
"where",
|
|
2138
|
+
`ProcessId=${pid}`,
|
|
2139
|
+
"get",
|
|
2140
|
+
"CommandLine,CreationDate,ExecutablePath",
|
|
2141
|
+
"/FORMAT:CSV"
|
|
2142
|
+
]);
|
|
2143
|
+
const lines = output.trim().split(/\r?\n/).filter((l) => l.trim().length > 0);
|
|
2144
|
+
if (lines.length < 2) return null;
|
|
2145
|
+
const parts = lines[1].split(",");
|
|
2146
|
+
const args = parts[1] ?? "";
|
|
2147
|
+
const creationDate = parts[2] ?? "";
|
|
2148
|
+
const year = creationDate.slice(0, 4);
|
|
2149
|
+
const month = creationDate.slice(4, 6);
|
|
2150
|
+
const day = creationDate.slice(6, 8);
|
|
2151
|
+
const hour = creationDate.slice(8, 10);
|
|
2152
|
+
const min = creationDate.slice(10, 12);
|
|
2153
|
+
const sec = creationDate.slice(12, 14);
|
|
2154
|
+
const startedAt = /* @__PURE__ */ new Date(`${year}-${month}-${day}T${hour}:${min}:${sec}`);
|
|
2155
|
+
if (Number.isNaN(startedAt.getTime())) return null;
|
|
2156
|
+
const exePath = parts[3] ?? "";
|
|
2157
|
+
const cwd = exePath ? (0, import_path7.dirname)(exePath) : "";
|
|
2158
|
+
return { cwd, args, startedAt };
|
|
2159
|
+
} catch {
|
|
2160
|
+
return null;
|
|
1500
2161
|
}
|
|
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
2162
|
}
|
|
1518
|
-
function
|
|
1519
|
-
|
|
2163
|
+
function extractResumeId(args) {
|
|
2164
|
+
const match = args.match(/--resume\s+(\S+)/);
|
|
2165
|
+
return match?.[1] ?? null;
|
|
2166
|
+
}
|
|
2167
|
+
async function readGitBranch(dir) {
|
|
2168
|
+
try {
|
|
2169
|
+
return (await run("git", ["rev-parse", "--abbrev-ref", "HEAD"], { cwd: dir, timeout: 3e3 })).trim();
|
|
2170
|
+
} catch {
|
|
2171
|
+
return "";
|
|
2172
|
+
}
|
|
1520
2173
|
}
|
|
1521
2174
|
|
|
1522
2175
|
// src/server.ts
|
|
@@ -1524,15 +2177,15 @@ var import_node_ws = require("@hono/node-ws");
|
|
|
1524
2177
|
var import_client = require("@temporalio/client");
|
|
1525
2178
|
var import_scanner2 = require("@threadbase-sh/scanner");
|
|
1526
2179
|
var import_events = require("events");
|
|
1527
|
-
var
|
|
2180
|
+
var import_fs12 = require("fs");
|
|
1528
2181
|
var import_promises5 = require("fs/promises");
|
|
1529
2182
|
var import_http = require("http");
|
|
1530
2183
|
var import_os6 = require("os");
|
|
1531
|
-
var
|
|
2184
|
+
var import_path13 = require("path");
|
|
1532
2185
|
var import_readline = require("readline");
|
|
1533
2186
|
|
|
1534
2187
|
// node_modules/nanoid/index.js
|
|
1535
|
-
var
|
|
2188
|
+
var import_crypto4 = __toESM(require("crypto"), 1);
|
|
1536
2189
|
|
|
1537
2190
|
// node_modules/nanoid/url-alphabet/index.js
|
|
1538
2191
|
var urlAlphabet = "useandom-26T198340PX75pxJACKVERYMINDBUSHWOLF_GQZbfghjklqvwyzrict";
|
|
@@ -1546,10 +2199,10 @@ var fillPool = (bytes) => {
|
|
|
1546
2199
|
try {
|
|
1547
2200
|
if (!pool || pool.length < bytes) {
|
|
1548
2201
|
pool = Buffer.allocUnsafe(bytes * POOL_SIZE_MULTIPLIER);
|
|
1549
|
-
|
|
2202
|
+
import_crypto4.default.randomFillSync(pool);
|
|
1550
2203
|
poolOffset = 0;
|
|
1551
2204
|
} else if (poolOffset + bytes > pool.length) {
|
|
1552
|
-
|
|
2205
|
+
import_crypto4.default.randomFillSync(pool);
|
|
1553
2206
|
poolOffset = 0;
|
|
1554
2207
|
}
|
|
1555
2208
|
} catch (e) {
|
|
@@ -2282,7 +2935,7 @@ var createHonoApp = (deps, upgradeWebSocket) => {
|
|
|
2282
2935
|
|
|
2283
2936
|
// src/browse.ts
|
|
2284
2937
|
var import_promises2 = require("fs/promises");
|
|
2285
|
-
var
|
|
2938
|
+
var import_path8 = require("path");
|
|
2286
2939
|
var BrowsePathNotFoundError = class extends Error {
|
|
2287
2940
|
constructor(message) {
|
|
2288
2941
|
super(message);
|
|
@@ -2290,15 +2943,15 @@ var BrowsePathNotFoundError = class extends Error {
|
|
|
2290
2943
|
}
|
|
2291
2944
|
};
|
|
2292
2945
|
async function resolveBrowsePath(browseRoot, relativePath) {
|
|
2293
|
-
const normalizedRoot = (0,
|
|
2946
|
+
const normalizedRoot = (0, import_path8.resolve)(browseRoot);
|
|
2294
2947
|
let sanitized;
|
|
2295
2948
|
if (process.platform !== "win32" && relativePath.startsWith("/") && relativePath.length > 1 && relativePath.includes("/", 1)) {
|
|
2296
2949
|
sanitized = relativePath;
|
|
2297
2950
|
} else {
|
|
2298
2951
|
sanitized = relativePath.replace(/^[/\\]+/, "");
|
|
2299
2952
|
}
|
|
2300
|
-
const target = sanitized ? (0,
|
|
2301
|
-
const rootPrefix = normalizedRoot.endsWith(
|
|
2953
|
+
const target = sanitized ? (0, import_path8.resolve)(normalizedRoot, sanitized) : normalizedRoot;
|
|
2954
|
+
const rootPrefix = normalizedRoot.endsWith(import_path8.sep) ? normalizedRoot : `${normalizedRoot}${import_path8.sep}`;
|
|
2302
2955
|
if (!target.startsWith(rootPrefix) && target !== normalizedRoot) {
|
|
2303
2956
|
throw new Error("Path outside browse root");
|
|
2304
2957
|
}
|
|
@@ -2320,7 +2973,7 @@ async function createDirectory(parentAbsolutePath, name) {
|
|
|
2320
2973
|
if (name.includes("/") || name.includes("\\") || name === ".." || name === ".") {
|
|
2321
2974
|
throw new Error("Invalid directory name");
|
|
2322
2975
|
}
|
|
2323
|
-
const target = (0,
|
|
2976
|
+
const target = (0, import_path8.join)(parentAbsolutePath, name);
|
|
2324
2977
|
try {
|
|
2325
2978
|
const s = await (0, import_promises2.stat)(target);
|
|
2326
2979
|
if (s.isDirectory()) throw new Error("Directory already exists");
|
|
@@ -2333,17 +2986,17 @@ async function createDirectory(parentAbsolutePath, name) {
|
|
|
2333
2986
|
|
|
2334
2987
|
// src/conversation-cache.ts
|
|
2335
2988
|
var import_better_sqlite3 = __toESM(require("better-sqlite3"), 1);
|
|
2336
|
-
var
|
|
2337
|
-
var
|
|
2989
|
+
var import_fs8 = require("fs");
|
|
2990
|
+
var import_path10 = require("path");
|
|
2338
2991
|
|
|
2339
2992
|
// src/db/sqlite-migrate.ts
|
|
2340
|
-
var
|
|
2341
|
-
var
|
|
2993
|
+
var import_fs6 = require("fs");
|
|
2994
|
+
var import_path9 = require("path");
|
|
2342
2995
|
var import_url2 = require("url");
|
|
2343
2996
|
var import_meta2 = {};
|
|
2344
2997
|
function getMigrationsDir2() {
|
|
2345
2998
|
if (typeof import_meta2 !== "undefined" && import_meta2.url) {
|
|
2346
|
-
return (0,
|
|
2999
|
+
return (0, import_path9.dirname)((0, import_url2.fileURLToPath)(import_meta2.url));
|
|
2347
3000
|
}
|
|
2348
3001
|
return __dirname;
|
|
2349
3002
|
}
|
|
@@ -2355,8 +3008,8 @@ CREATE TABLE IF NOT EXISTS schema_migrations (
|
|
|
2355
3008
|
`;
|
|
2356
3009
|
function runSqliteMigrations(db, migrationsDir) {
|
|
2357
3010
|
db.exec(SCHEMA_MIGRATIONS_SQL);
|
|
2358
|
-
const dir = migrationsDir ?? (0,
|
|
2359
|
-
const files = (0,
|
|
3011
|
+
const dir = migrationsDir ?? (0, import_path9.join)(getMigrationsDir2(), "migrations");
|
|
3012
|
+
const files = (0, import_fs6.readdirSync)(dir).filter((f) => f.endsWith(".sql")).sort();
|
|
2360
3013
|
const appliedRows = db.prepare("SELECT id FROM schema_migrations").all();
|
|
2361
3014
|
const appliedSet = new Set(appliedRows.map((r) => r.id));
|
|
2362
3015
|
const recordApplied = db.prepare("INSERT INTO schema_migrations (id, applied_at) VALUES (?, ?)");
|
|
@@ -2367,7 +3020,7 @@ function runSqliteMigrations(db, migrationsDir) {
|
|
|
2367
3020
|
skipped.push(file);
|
|
2368
3021
|
continue;
|
|
2369
3022
|
}
|
|
2370
|
-
const sql = (0,
|
|
3023
|
+
const sql = (0, import_fs6.readFileSync)((0, import_path9.join)(dir, file), "utf-8");
|
|
2371
3024
|
const tx = db.transaction(() => {
|
|
2372
3025
|
db.exec(sql);
|
|
2373
3026
|
recordApplied.run(file, (/* @__PURE__ */ new Date()).toISOString());
|
|
@@ -2378,16 +3031,8 @@ function runSqliteMigrations(db, migrationsDir) {
|
|
|
2378
3031
|
return { applied, skipped };
|
|
2379
3032
|
}
|
|
2380
3033
|
|
|
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
3034
|
// src/services/conversations/isAgentConversation.ts
|
|
2390
|
-
var
|
|
3035
|
+
var import_fs7 = require("fs");
|
|
2391
3036
|
var DEFAULT_AGENT_ENTRYPOINTS = /* @__PURE__ */ new Set(["sdk-cli", "claude-vscode"]);
|
|
2392
3037
|
var CHUNK_BYTES = 64 * 1024;
|
|
2393
3038
|
var ENTRYPOINT_PROBE = `"entrypoint":`;
|
|
@@ -2409,12 +3054,12 @@ function isAgentFile(filePath, entrypoints = DEFAULT_AGENT_ENTRYPOINTS) {
|
|
|
2409
3054
|
if (cached2 !== void 0) return cached2;
|
|
2410
3055
|
let fd;
|
|
2411
3056
|
try {
|
|
2412
|
-
fd = (0,
|
|
3057
|
+
fd = (0, import_fs7.openSync)(filePath, "r");
|
|
2413
3058
|
} catch {
|
|
2414
3059
|
return false;
|
|
2415
3060
|
}
|
|
2416
3061
|
try {
|
|
2417
|
-
const fileSize = (0,
|
|
3062
|
+
const fileSize = (0, import_fs7.statSync)(filePath).size;
|
|
2418
3063
|
if (fileSize === 0) {
|
|
2419
3064
|
fileDecisionCache.set(key, false);
|
|
2420
3065
|
return false;
|
|
@@ -2425,7 +3070,7 @@ function isAgentFile(filePath, entrypoints = DEFAULT_AGENT_ENTRYPOINTS) {
|
|
|
2425
3070
|
let carry = "";
|
|
2426
3071
|
while (offset < fileSize) {
|
|
2427
3072
|
const toRead = Math.min(CHUNK_BYTES, fileSize - offset);
|
|
2428
|
-
const got = (0,
|
|
3073
|
+
const got = (0, import_fs7.readSync)(fd, buf, 0, toRead, offset);
|
|
2429
3074
|
if (got <= 0) break;
|
|
2430
3075
|
const chunk = carry + buf.toString("utf8", 0, got);
|
|
2431
3076
|
for (const marker of markers) {
|
|
@@ -2446,7 +3091,7 @@ function isAgentFile(filePath, entrypoints = DEFAULT_AGENT_ENTRYPOINTS) {
|
|
|
2446
3091
|
} catch {
|
|
2447
3092
|
return false;
|
|
2448
3093
|
} finally {
|
|
2449
|
-
(0,
|
|
3094
|
+
(0, import_fs7.closeSync)(fd);
|
|
2450
3095
|
}
|
|
2451
3096
|
}
|
|
2452
3097
|
function parseAgentEntrypointsEnv(raw) {
|
|
@@ -2658,7 +3303,7 @@ var ConversationCache = class _ConversationCache {
|
|
|
2658
3303
|
return this.agentEntrypoints;
|
|
2659
3304
|
}
|
|
2660
3305
|
static open(dbPath, tailSize = 10, migrationsDir, options) {
|
|
2661
|
-
(0,
|
|
3306
|
+
(0, import_fs8.mkdirSync)((0, import_path10.dirname)(dbPath), { recursive: true });
|
|
2662
3307
|
const db = new import_better_sqlite3.default(dbPath);
|
|
2663
3308
|
db.pragma("journal_mode = WAL");
|
|
2664
3309
|
db.pragma("foreign_keys = ON");
|
|
@@ -2841,7 +3486,7 @@ var ConversationCache = class _ConversationCache {
|
|
|
2841
3486
|
let mtimeMs = null;
|
|
2842
3487
|
let fileSize = null;
|
|
2843
3488
|
try {
|
|
2844
|
-
const s = (0,
|
|
3489
|
+
const s = (0, import_fs8.statSync)(m.filePath);
|
|
2845
3490
|
mtimeMs = s.mtimeMs;
|
|
2846
3491
|
fileSize = s.size;
|
|
2847
3492
|
} catch {
|
|
@@ -2894,8 +3539,8 @@ var ConversationCache = class _ConversationCache {
|
|
|
2894
3539
|
let fileSize;
|
|
2895
3540
|
let fd;
|
|
2896
3541
|
try {
|
|
2897
|
-
fileSize = (0,
|
|
2898
|
-
fd = (0,
|
|
3542
|
+
fileSize = (0, import_fs8.statSync)(filePath).size;
|
|
3543
|
+
fd = (0, import_fs8.openSync)(filePath, "r");
|
|
2899
3544
|
} catch {
|
|
2900
3545
|
return false;
|
|
2901
3546
|
}
|
|
@@ -2908,7 +3553,7 @@ var ConversationCache = class _ConversationCache {
|
|
|
2908
3553
|
while (pos > 0 && lines.length < this.tailSize * 4) {
|
|
2909
3554
|
const toRead = Math.min(CHUNK, pos);
|
|
2910
3555
|
pos -= toRead;
|
|
2911
|
-
(0,
|
|
3556
|
+
(0, import_fs8.readSync)(fd, buf, 0, toRead, pos);
|
|
2912
3557
|
const chunk = buf.subarray(0, toRead).toString("utf8");
|
|
2913
3558
|
const combined = chunk + partial;
|
|
2914
3559
|
const parts = combined.split("\n");
|
|
@@ -2919,7 +3564,7 @@ var ConversationCache = class _ConversationCache {
|
|
|
2919
3564
|
}
|
|
2920
3565
|
if (partial) lines.push(partial);
|
|
2921
3566
|
} finally {
|
|
2922
|
-
(0,
|
|
3567
|
+
(0, import_fs8.closeSync)(fd);
|
|
2923
3568
|
}
|
|
2924
3569
|
const msgs = [];
|
|
2925
3570
|
for (let i = 0; i < lines.length && msgs.length < this.tailSize; i++) {
|
|
@@ -3106,7 +3751,7 @@ var ConversationCache = class _ConversationCache {
|
|
|
3106
3751
|
* `handleGetConversation` can still serve the cached tail even when the
|
|
3107
3752
|
* JSONL has been deleted.
|
|
3108
3753
|
*/
|
|
3109
|
-
pruneGhostFiles(exists =
|
|
3754
|
+
pruneGhostFiles(exists = import_fs8.existsSync) {
|
|
3110
3755
|
const rows = this.stmts.allFilePaths.all();
|
|
3111
3756
|
const ghosts = [];
|
|
3112
3757
|
const prune = this.db.transaction((ids) => {
|
|
@@ -3186,7 +3831,7 @@ var ConversationsRepository = class {
|
|
|
3186
3831
|
};
|
|
3187
3832
|
|
|
3188
3833
|
// src/db/repositories/projects.repository.ts
|
|
3189
|
-
var
|
|
3834
|
+
var import_crypto5 = require("crypto");
|
|
3190
3835
|
|
|
3191
3836
|
// src/utils/canonicalizeProjectPath.ts
|
|
3192
3837
|
function canonicalizeProjectPath(projectPath) {
|
|
@@ -3279,7 +3924,7 @@ var ProjectsRepository = class {
|
|
|
3279
3924
|
});
|
|
3280
3925
|
return rowToProject(this.getById.get(existing.id));
|
|
3281
3926
|
}
|
|
3282
|
-
const id = (0,
|
|
3927
|
+
const id = (0, import_crypto5.randomUUID)();
|
|
3283
3928
|
this.insert.run({
|
|
3284
3929
|
id,
|
|
3285
3930
|
path,
|
|
@@ -3335,23 +3980,23 @@ async function recordUpload(pool2, instanceId, row) {
|
|
|
3335
3980
|
}
|
|
3336
3981
|
|
|
3337
3982
|
// src/handlers/handleListProjects.ts
|
|
3338
|
-
var
|
|
3983
|
+
var import_fs9 = require("fs");
|
|
3339
3984
|
var import_os5 = require("os");
|
|
3340
|
-
var
|
|
3985
|
+
var import_path11 = require("path");
|
|
3341
3986
|
function decodeProjectPath(dirName) {
|
|
3342
3987
|
return dirName.replace(/-/g, "/");
|
|
3343
3988
|
}
|
|
3344
3989
|
function handleListProjects(url, res) {
|
|
3345
3990
|
const limit = Math.max(1, parseInt(url.searchParams.get("limit") ?? "50", 10) || 50);
|
|
3346
3991
|
const offset = Math.max(0, parseInt(url.searchParams.get("offset") ?? "0", 10) || 0);
|
|
3347
|
-
const projectsDir = (0,
|
|
3992
|
+
const projectsDir = (0, import_path11.join)((0, import_os5.homedir)(), ".claude", "projects");
|
|
3348
3993
|
let entries;
|
|
3349
3994
|
try {
|
|
3350
|
-
entries = (0,
|
|
3351
|
-
const fullPath = (0,
|
|
3995
|
+
entries = (0, import_fs9.readdirSync)(projectsDir).map((dirName) => {
|
|
3996
|
+
const fullPath = (0, import_path11.join)(projectsDir, dirName);
|
|
3352
3997
|
let mtime = 0;
|
|
3353
3998
|
try {
|
|
3354
|
-
mtime = (0,
|
|
3999
|
+
mtime = (0, import_fs9.statSync)(fullPath).mtimeMs;
|
|
3355
4000
|
} catch {
|
|
3356
4001
|
}
|
|
3357
4002
|
const path = decodeProjectPath(dirName);
|
|
@@ -3370,7 +4015,7 @@ function handleListProjects(url, res) {
|
|
|
3370
4015
|
}
|
|
3371
4016
|
|
|
3372
4017
|
// src/pair-store.ts
|
|
3373
|
-
var
|
|
4018
|
+
var import_crypto6 = require("crypto");
|
|
3374
4019
|
var DEFAULT_TTL_SECONDS = 180;
|
|
3375
4020
|
var SWEEP_INTERVAL_MS = 6e4;
|
|
3376
4021
|
var PairTokenStore = class {
|
|
@@ -3385,7 +4030,7 @@ var PairTokenStore = class {
|
|
|
3385
4030
|
}
|
|
3386
4031
|
}
|
|
3387
4032
|
mint() {
|
|
3388
|
-
const token = `pt_${(0,
|
|
4033
|
+
const token = `pt_${(0, import_crypto6.randomBytes)(16).toString("hex")}`;
|
|
3389
4034
|
const expiresAt = Date.now() + this.ttlMs;
|
|
3390
4035
|
this.current = { token, expiresAt, used: false };
|
|
3391
4036
|
return {
|
|
@@ -3446,7 +4091,7 @@ function seal(plaintext, recipientPublicKeyBase64) {
|
|
|
3446
4091
|
|
|
3447
4092
|
// src/services/conversations/conversationWatcher.ts
|
|
3448
4093
|
var import_chokidar = __toESM(require("chokidar"), 1);
|
|
3449
|
-
var
|
|
4094
|
+
var import_fs10 = require("fs");
|
|
3450
4095
|
var import_promises3 = require("fs/promises");
|
|
3451
4096
|
var ConversationWatcher = class {
|
|
3452
4097
|
files = /* @__PURE__ */ new Map();
|
|
@@ -3467,7 +4112,7 @@ var ConversationWatcher = class {
|
|
|
3467
4112
|
if (this.files.has(filePath)) return;
|
|
3468
4113
|
let offset;
|
|
3469
4114
|
try {
|
|
3470
|
-
offset = (0,
|
|
4115
|
+
offset = (0, import_fs10.statSync)(filePath).size;
|
|
3471
4116
|
} catch {
|
|
3472
4117
|
offset = 0;
|
|
3473
4118
|
}
|
|
@@ -3576,14 +4221,14 @@ var ConversationWatcher = class {
|
|
|
3576
4221
|
};
|
|
3577
4222
|
|
|
3578
4223
|
// src/services/conversations/pruneAgentConversations.ts
|
|
3579
|
-
var
|
|
4224
|
+
var import_fs11 = require("fs");
|
|
3580
4225
|
function pruneAgentConversations(cache) {
|
|
3581
4226
|
const db = cache.getDatabase();
|
|
3582
4227
|
const rows = db.prepare("SELECT id, file_path FROM conversation_meta").all();
|
|
3583
4228
|
let pruned = 0;
|
|
3584
4229
|
let missing = 0;
|
|
3585
4230
|
for (const row of rows) {
|
|
3586
|
-
if (!(0,
|
|
4231
|
+
if (!(0, import_fs11.existsSync)(row.file_path)) {
|
|
3587
4232
|
missing += 1;
|
|
3588
4233
|
continue;
|
|
3589
4234
|
}
|
|
@@ -3866,6 +4511,7 @@ function managedToResponse(s, ptyAttached) {
|
|
|
3866
4511
|
return {
|
|
3867
4512
|
id: s.id,
|
|
3868
4513
|
conversationId: s.id,
|
|
4514
|
+
provider: s.provider ?? CLAUDE_CODE_PROVIDER,
|
|
3869
4515
|
status: s.status,
|
|
3870
4516
|
projectPath: s.projectPath,
|
|
3871
4517
|
projectName: s.projectName,
|
|
@@ -3891,13 +4537,15 @@ function managedToResponse(s, ptyAttached) {
|
|
|
3891
4537
|
...s.failureReason != null && { failureReason: s.failureReason },
|
|
3892
4538
|
...s.resumedFromConversationId != null && {
|
|
3893
4539
|
resumedFromConversationId: s.resumedFromConversationId
|
|
3894
|
-
}
|
|
4540
|
+
},
|
|
4541
|
+
...s.boundConversationId != null && { boundConversationId: s.boundConversationId }
|
|
3895
4542
|
};
|
|
3896
4543
|
}
|
|
3897
4544
|
function discoveredToResponse(d, conversationId) {
|
|
3898
4545
|
return {
|
|
3899
4546
|
id: conversationId,
|
|
3900
4547
|
conversationId,
|
|
4548
|
+
provider: CLAUDE_CODE_PROVIDER,
|
|
3901
4549
|
status: "idle",
|
|
3902
4550
|
projectPath: d.projectPath,
|
|
3903
4551
|
projectName: d.projectName,
|
|
@@ -3913,10 +4561,10 @@ function discoveredToResponse(d, conversationId) {
|
|
|
3913
4561
|
}
|
|
3914
4562
|
|
|
3915
4563
|
// src/uploads.ts
|
|
3916
|
-
var
|
|
4564
|
+
var import_crypto7 = require("crypto");
|
|
3917
4565
|
var import_promises4 = require("fs/promises");
|
|
3918
4566
|
var import_heic_convert = __toESM(require("heic-convert"), 1);
|
|
3919
|
-
var
|
|
4567
|
+
var import_path12 = require("path");
|
|
3920
4568
|
var UPLOAD_DIR_NAME = ".threadbase-uploads";
|
|
3921
4569
|
var MAX_BYTES = 25 * 1024 * 1024;
|
|
3922
4570
|
var HEIC_MIMES = /* @__PURE__ */ new Set(["image/heic", "image/heif"]);
|
|
@@ -3947,11 +4595,11 @@ async function saveUploadFile(input) {
|
|
|
3947
4595
|
mimeType = "image/jpeg";
|
|
3948
4596
|
originalName = originalName.replace(/\.(heic|heif)$/i, ".jpg");
|
|
3949
4597
|
}
|
|
3950
|
-
const id = `up_${(0,
|
|
4598
|
+
const id = `up_${(0, import_crypto7.randomBytes)(8).toString("hex")}`;
|
|
3951
4599
|
const safeName = sanitizeFilename(originalName) || `file${MIME_TO_EXT[mimeType] ?? ""}`;
|
|
3952
|
-
const dir = (0,
|
|
4600
|
+
const dir = (0, import_path12.join)(input.projectPath, UPLOAD_DIR_NAME, input.sessionId);
|
|
3953
4601
|
await (0, import_promises4.mkdir)(dir, { recursive: true });
|
|
3954
|
-
const filePath = (0,
|
|
4602
|
+
const filePath = (0, import_path12.join)(dir, `${Date.now()}-${id}-${safeName}`);
|
|
3955
4603
|
await (0, import_promises4.writeFile)(filePath, buffer);
|
|
3956
4604
|
return {
|
|
3957
4605
|
id,
|
|
@@ -4252,10 +4900,10 @@ var StreamerServer = class {
|
|
|
4252
4900
|
this.verbose = config.verbose ?? false;
|
|
4253
4901
|
this.disableDb = config.disableDb ?? false;
|
|
4254
4902
|
this.scanProfiles = config.scanProfiles;
|
|
4255
|
-
this.codexRoots = config.codexRoots ?? [(0,
|
|
4903
|
+
this.codexRoots = config.codexRoots ?? [(0, import_path13.join)((0, import_os6.homedir)(), ".codex", "sessions")];
|
|
4256
4904
|
this.ptyGracePeriodMs = config.ptyGracePeriodMs ?? DEFAULT_PTY_GRACE_PERIOD_MS;
|
|
4257
4905
|
this.defaultSystemPrompt = config.defaultSystemPrompt ?? DEFAULT_SYSTEM_PROMPT;
|
|
4258
|
-
this.cacheDir = config.cacheDir ?? loadCacheDir() ?? (0,
|
|
4906
|
+
this.cacheDir = config.cacheDir ?? loadCacheDir() ?? (0, import_path13.join)((0, import_os6.homedir)(), ".threadbase", "cache");
|
|
4259
4907
|
this.tailSize = config.tailSize ?? loadTailSize() ?? 10;
|
|
4260
4908
|
this.directoryDebounceMs = parseDirScanDebounceEnv(process.env.THREADBASE_DIR_SCAN_DEBOUNCE_MS) ?? config.directoryScanDebounceMs ?? 1e3;
|
|
4261
4909
|
this.markScannerStaleDebounced = debounce(() => {
|
|
@@ -4339,7 +4987,7 @@ var StreamerServer = class {
|
|
|
4339
4987
|
});
|
|
4340
4988
|
}
|
|
4341
4989
|
});
|
|
4342
|
-
this.ptyManager = new
|
|
4990
|
+
this.ptyManager = new LiveSessionManager({
|
|
4343
4991
|
logger: getLogger("pty"),
|
|
4344
4992
|
onOutput: (sessionId, data) => {
|
|
4345
4993
|
this.wsHub.broadcast({ type: "terminal_output", sessionId, data });
|
|
@@ -4420,7 +5068,7 @@ var StreamerServer = class {
|
|
|
4420
5068
|
temporalClient,
|
|
4421
5069
|
taskQueue: agentConfig.temporal.taskQueue
|
|
4422
5070
|
});
|
|
4423
|
-
const conversationsBaseDir = agentConfig.conversationsDir || (0,
|
|
5071
|
+
const conversationsBaseDir = agentConfig.conversationsDir || (0, import_path13.join)((0, import_path13.dirname)(this.cacheDir), "conversations");
|
|
4424
5072
|
conversationWriter = createConversationWriter({
|
|
4425
5073
|
baseDir: conversationsBaseDir
|
|
4426
5074
|
});
|
|
@@ -4642,7 +5290,7 @@ var StreamerServer = class {
|
|
|
4642
5290
|
});
|
|
4643
5291
|
try {
|
|
4644
5292
|
this.cache = ConversationCache.open(
|
|
4645
|
-
(0,
|
|
5293
|
+
(0, import_path13.join)(this.cacheDir, "cache.db"),
|
|
4646
5294
|
this.tailSize,
|
|
4647
5295
|
void 0,
|
|
4648
5296
|
{
|
|
@@ -4668,18 +5316,19 @@ var StreamerServer = class {
|
|
|
4668
5316
|
if (this.scanProfiles && this.scanProfiles.length > 0) {
|
|
4669
5317
|
for (const profile of this.scanProfiles) {
|
|
4670
5318
|
if (profile.enabled) {
|
|
4671
|
-
this.fileWatcher.watchDirectory((0,
|
|
5319
|
+
this.fileWatcher.watchDirectory((0, import_path13.join)(profile.configDir, "projects"));
|
|
4672
5320
|
}
|
|
4673
5321
|
}
|
|
4674
5322
|
} else {
|
|
4675
|
-
this.fileWatcher.watchDirectory((0,
|
|
5323
|
+
this.fileWatcher.watchDirectory((0, import_path13.join)((0, import_os6.homedir)(), ".claude", "projects"));
|
|
4676
5324
|
}
|
|
4677
5325
|
} catch (err) {
|
|
4678
5326
|
const message = err instanceof Error ? err.message : String(err);
|
|
4679
|
-
|
|
4680
|
-
|
|
4681
|
-
|
|
4682
|
-
|
|
5327
|
+
const abiMismatch = message.includes("NODE_MODULE_VERSION") || message.includes("was compiled against a different Node.js version");
|
|
5328
|
+
this.log.error(
|
|
5329
|
+
`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})`,
|
|
5330
|
+
{ error: message, abiMismatch, event: "cache.open_failed" }
|
|
5331
|
+
);
|
|
4683
5332
|
}
|
|
4684
5333
|
const warmupScanner = new import_scanner2.ConversationScanner();
|
|
4685
5334
|
this.allScanners.add(warmupScanner);
|
|
@@ -5166,17 +5815,17 @@ var StreamerServer = class {
|
|
|
5166
5815
|
return this.getScanner();
|
|
5167
5816
|
}
|
|
5168
5817
|
findJsonlPath(uuid) {
|
|
5169
|
-
const projectsDir = (0,
|
|
5170
|
-
if (!(0,
|
|
5818
|
+
const projectsDir = (0, import_path13.join)((0, import_os6.homedir)(), ".claude", "projects");
|
|
5819
|
+
if (!(0, import_fs12.existsSync)(projectsDir)) return null;
|
|
5171
5820
|
const filename = `${uuid}.jsonl`;
|
|
5172
|
-
for (const dir of (0,
|
|
5173
|
-
const fp = (0,
|
|
5174
|
-
if ((0,
|
|
5175
|
-
const projectDir = (0,
|
|
5821
|
+
for (const dir of (0, import_fs12.readdirSync)(projectsDir)) {
|
|
5822
|
+
const fp = (0, import_path13.join)(projectsDir, dir, filename);
|
|
5823
|
+
if ((0, import_fs12.existsSync)(fp)) return fp;
|
|
5824
|
+
const projectDir = (0, import_path13.join)(projectsDir, dir);
|
|
5176
5825
|
try {
|
|
5177
|
-
for (const sub of (0,
|
|
5178
|
-
const subagentPath = (0,
|
|
5179
|
-
if ((0,
|
|
5826
|
+
for (const sub of (0, import_fs12.readdirSync)(projectDir)) {
|
|
5827
|
+
const subagentPath = (0, import_path13.join)(projectDir, sub, "subagents", filename);
|
|
5828
|
+
if ((0, import_fs12.existsSync)(subagentPath)) return subagentPath;
|
|
5180
5829
|
}
|
|
5181
5830
|
} catch {
|
|
5182
5831
|
}
|
|
@@ -5185,7 +5834,7 @@ var StreamerServer = class {
|
|
|
5185
5834
|
}
|
|
5186
5835
|
async readCwdFromJsonl(filePath) {
|
|
5187
5836
|
return new Promise((resolve2) => {
|
|
5188
|
-
const rl = (0, import_readline.createInterface)({ input: (0,
|
|
5837
|
+
const rl = (0, import_readline.createInterface)({ input: (0, import_fs12.createReadStream)(filePath), crlfDelay: Infinity });
|
|
5189
5838
|
let found = false;
|
|
5190
5839
|
rl.on("line", (line) => {
|
|
5191
5840
|
if (found) return;
|
|
@@ -5242,7 +5891,7 @@ var StreamerServer = class {
|
|
|
5242
5891
|
if (!conv.filePath) return false;
|
|
5243
5892
|
let mtimeMs = null;
|
|
5244
5893
|
try {
|
|
5245
|
-
mtimeMs = (0,
|
|
5894
|
+
mtimeMs = (0, import_fs12.statSync)(conv.filePath).mtimeMs;
|
|
5246
5895
|
} catch {
|
|
5247
5896
|
return false;
|
|
5248
5897
|
}
|
|
@@ -5485,7 +6134,7 @@ var StreamerServer = class {
|
|
|
5485
6134
|
handleGetSession(sessionId, res) {
|
|
5486
6135
|
const session = this.sessionStore.get(sessionId, this.ptyAttachedIds());
|
|
5487
6136
|
if (session) {
|
|
5488
|
-
if (!(0,
|
|
6137
|
+
if (!(0, import_fs12.existsSync)(session.projectPath)) {
|
|
5489
6138
|
session.failureReason = `Project directory not found: ${session.projectPath}`;
|
|
5490
6139
|
}
|
|
5491
6140
|
json(res, 200, session);
|
|
@@ -5525,7 +6174,10 @@ var StreamerServer = class {
|
|
|
5525
6174
|
json(res, 400, { error: "Could not determine project path" });
|
|
5526
6175
|
return;
|
|
5527
6176
|
}
|
|
6177
|
+
const cachedConvMeta = this.cache?.getMetaById(sessionId);
|
|
6178
|
+
const provider = conv?.provider ?? cachedConvMeta?.provider ?? CLAUDE_CODE_PROVIDER;
|
|
5528
6179
|
const session = await this.ptyManager.start(sessionId, {
|
|
6180
|
+
provider,
|
|
5529
6181
|
projectPath,
|
|
5530
6182
|
projectName: body.projectName,
|
|
5531
6183
|
branch: body.branch
|
|
@@ -5875,7 +6527,7 @@ var StreamerServer = class {
|
|
|
5875
6527
|
sessionStore: this.sessionStore,
|
|
5876
6528
|
// biome-ignore lint/style/noNonNullAssertion: agentClient is set when agentConfig.enabled is true
|
|
5877
6529
|
agentClient: this.agentClient,
|
|
5878
|
-
conversationsDir: this.cacheDir ? (0,
|
|
6530
|
+
conversationsDir: this.cacheDir ? (0, import_path13.join)((0, import_path13.dirname)(this.cacheDir), "conversations") : "",
|
|
5879
6531
|
agentConfig: this.agentConfig
|
|
5880
6532
|
});
|
|
5881
6533
|
json(res, result.status, result.body);
|
|
@@ -5884,6 +6536,13 @@ var StreamerServer = class {
|
|
|
5884
6536
|
}
|
|
5885
6537
|
return;
|
|
5886
6538
|
}
|
|
6539
|
+
const body = await readBody(req);
|
|
6540
|
+
const { path: relativePath, provider: requestedProvider, systemPrompt: clientPrompt } = body;
|
|
6541
|
+
if (requestedProvider !== void 0 && !isProviderName(requestedProvider)) {
|
|
6542
|
+
json(res, 400, { error: "Invalid provider" });
|
|
6543
|
+
return;
|
|
6544
|
+
}
|
|
6545
|
+
const provider = requestedProvider ?? CLAUDE_CODE_PROVIDER;
|
|
5887
6546
|
if (!this.browseRoot) {
|
|
5888
6547
|
json(res, 403, {
|
|
5889
6548
|
error: "File browsing not configured. Set browseRoot on the server.",
|
|
@@ -5891,8 +6550,6 @@ var StreamerServer = class {
|
|
|
5891
6550
|
});
|
|
5892
6551
|
return;
|
|
5893
6552
|
}
|
|
5894
|
-
const body = await readBody(req);
|
|
5895
|
-
const { path: relativePath, systemPrompt: clientPrompt } = body;
|
|
5896
6553
|
if (typeof relativePath !== "string") {
|
|
5897
6554
|
json(res, 400, { error: "Missing path field" });
|
|
5898
6555
|
return;
|
|
@@ -5913,21 +6570,27 @@ var StreamerServer = class {
|
|
|
5913
6570
|
].filter(Boolean);
|
|
5914
6571
|
try {
|
|
5915
6572
|
const session = await this.ptyManager.startFresh({
|
|
6573
|
+
provider,
|
|
5916
6574
|
projectPath: resolvedPath,
|
|
5917
6575
|
projectName: body.projectName,
|
|
5918
6576
|
systemPrompt: systemPromptParts.join("\n")
|
|
5919
6577
|
});
|
|
5920
6578
|
this.sessionStore.addManaged(session);
|
|
5921
6579
|
json(res, 202, { id: session.id, status: "pending" });
|
|
5922
|
-
|
|
6580
|
+
if (provider === CODEX_CLI_PROVIDER) {
|
|
6581
|
+
this.watchForCodexRollout(session.id, resolvedPath);
|
|
6582
|
+
} else {
|
|
6583
|
+
this.watchForJsonl(session.id, resolvedPath);
|
|
6584
|
+
}
|
|
5923
6585
|
this.broadcastOrUnicastSessionList(req);
|
|
5924
6586
|
} catch (err) {
|
|
5925
6587
|
const message = err instanceof Error ? err.message : "Failed to start session";
|
|
6588
|
+
const statusCode = typeof err.statusCode === "number" ? err.statusCode : 500;
|
|
5926
6589
|
this.log.error(`[start] failed to start session: ${message}`, {
|
|
5927
6590
|
event: "session.start_failed",
|
|
5928
6591
|
error: message
|
|
5929
6592
|
});
|
|
5930
|
-
json(res,
|
|
6593
|
+
json(res, statusCode, { error: message });
|
|
5931
6594
|
}
|
|
5932
6595
|
}
|
|
5933
6596
|
// ─── Project linking ─────────────────────────────────────────────
|
|
@@ -5980,9 +6643,9 @@ var StreamerServer = class {
|
|
|
5980
6643
|
// was passed to Claude via --session-id so the filename matches from the start.
|
|
5981
6644
|
watchForJsonl(sessionId, projectPath) {
|
|
5982
6645
|
const encoded = projectPath.replace(/[/\\:.]/g, "-");
|
|
5983
|
-
const projectsDir = (0,
|
|
6646
|
+
const projectsDir = (0, import_path13.join)((0, import_os6.homedir)(), ".claude", "projects", encoded);
|
|
5984
6647
|
const expectedFile = `${sessionId}.jsonl`;
|
|
5985
|
-
const filePath = (0,
|
|
6648
|
+
const filePath = (0, import_path13.join)(projectsDir, expectedFile);
|
|
5986
6649
|
const deadline = Date.now() + 12e4;
|
|
5987
6650
|
let watcher = null;
|
|
5988
6651
|
const cleanup = () => {
|
|
@@ -6000,12 +6663,12 @@ var StreamerServer = class {
|
|
|
6000
6663
|
cleanup();
|
|
6001
6664
|
return;
|
|
6002
6665
|
}
|
|
6003
|
-
let resolvedFilePath = (0,
|
|
6004
|
-
if (!resolvedFilePath && (0,
|
|
6666
|
+
let resolvedFilePath = (0, import_fs12.existsSync)(filePath) ? filePath : null;
|
|
6667
|
+
if (!resolvedFilePath && (0, import_fs12.existsSync)(projectsDir)) {
|
|
6005
6668
|
try {
|
|
6006
6669
|
const now = Date.now();
|
|
6007
|
-
const recent = (0,
|
|
6008
|
-
if (recent) resolvedFilePath = (0,
|
|
6670
|
+
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];
|
|
6671
|
+
if (recent) resolvedFilePath = (0, import_path13.join)(projectsDir, recent.f);
|
|
6009
6672
|
} catch {
|
|
6010
6673
|
}
|
|
6011
6674
|
}
|
|
@@ -6014,7 +6677,7 @@ var StreamerServer = class {
|
|
|
6014
6677
|
this.sessionFileMap.set(sessionId, resolvedFilePath);
|
|
6015
6678
|
this.fileWatcher.watch(resolvedFilePath);
|
|
6016
6679
|
try {
|
|
6017
|
-
const existing = (0,
|
|
6680
|
+
const existing = (0, import_fs12.readFileSync)(resolvedFilePath, "utf8").split("\n").filter(Boolean);
|
|
6018
6681
|
if (existing.length > 0) {
|
|
6019
6682
|
this.wsHub.broadcast({ type: "conversation_events", sessionId, lines: existing });
|
|
6020
6683
|
for (const line of existing) {
|
|
@@ -6040,11 +6703,126 @@ var StreamerServer = class {
|
|
|
6040
6703
|
if (this.sessionFileMap.has(sessionId)) return;
|
|
6041
6704
|
try {
|
|
6042
6705
|
require("fs").mkdirSync(projectsDir, { recursive: true });
|
|
6043
|
-
watcher = (0,
|
|
6706
|
+
watcher = (0, import_fs12.watch)(projectsDir, tryWire);
|
|
6044
6707
|
watcher.on("error", cleanup);
|
|
6045
6708
|
} catch {
|
|
6046
6709
|
}
|
|
6047
6710
|
}
|
|
6711
|
+
// Codex-equivalent of watchForJsonl(). Differs because Codex has no
|
|
6712
|
+
// filename-encoded session id (it assigns its own persisted id) and its
|
|
6713
|
+
// rollout files live under a date-nested directory
|
|
6714
|
+
// (~/.codex/sessions/<YYYY>/<MM>/<DD>/rollout-*.jsonl) that Codex creates
|
|
6715
|
+
// itself — it may not exist yet when this function is first called, so we
|
|
6716
|
+
// poll rather than fs.watch a not-yet-existent directory. Per Phase 0
|
|
6717
|
+
// findings, the rollout file appears within ~1s of process spawn (after
|
|
6718
|
+
// any directory-trust gate is cleared), well before any user input.
|
|
6719
|
+
watchForCodexRollout(sessionId, projectPath) {
|
|
6720
|
+
const deadline = Date.now() + 12e4;
|
|
6721
|
+
const now = /* @__PURE__ */ new Date();
|
|
6722
|
+
const dateDir = (0, import_path13.join)(
|
|
6723
|
+
String(now.getFullYear()),
|
|
6724
|
+
String(now.getMonth() + 1).padStart(2, "0"),
|
|
6725
|
+
String(now.getDate()).padStart(2, "0")
|
|
6726
|
+
);
|
|
6727
|
+
const sessionStartedAtMs = (this.sessionStore.getManaged(sessionId)?.startedAt?.getTime() ?? Date.now()) - 5e3;
|
|
6728
|
+
let intervalHandle = null;
|
|
6729
|
+
const cleanup = () => {
|
|
6730
|
+
if (intervalHandle) clearInterval(intervalHandle);
|
|
6731
|
+
intervalHandle = null;
|
|
6732
|
+
};
|
|
6733
|
+
const matchesProjectPath = (candidatePath) => {
|
|
6734
|
+
try {
|
|
6735
|
+
const firstLine = (0, import_fs12.readFileSync)(candidatePath, "utf8").split("\n", 1)[0];
|
|
6736
|
+
if (!firstLine) return null;
|
|
6737
|
+
const parsed = JSON.parse(firstLine);
|
|
6738
|
+
if (parsed?.type !== "session_meta") return null;
|
|
6739
|
+
const payload = parsed.payload ?? {};
|
|
6740
|
+
if (payload.cwd !== projectPath) return null;
|
|
6741
|
+
if (typeof payload.id !== "string") return null;
|
|
6742
|
+
const createdIso = payload.timestamp ?? parsed.timestamp;
|
|
6743
|
+
const createdAtMs = typeof createdIso === "string" ? Date.parse(createdIso) : Number.NaN;
|
|
6744
|
+
if (Number.isNaN(createdAtMs) || createdAtMs < sessionStartedAtMs) return null;
|
|
6745
|
+
return { id: payload.id, createdAtMs };
|
|
6746
|
+
} catch {
|
|
6747
|
+
return null;
|
|
6748
|
+
}
|
|
6749
|
+
};
|
|
6750
|
+
const tryWire = () => {
|
|
6751
|
+
if (!this.ptyManager.hasSession(sessionId)) {
|
|
6752
|
+
cleanup();
|
|
6753
|
+
return;
|
|
6754
|
+
}
|
|
6755
|
+
if (Date.now() > deadline) {
|
|
6756
|
+
cleanup();
|
|
6757
|
+
return;
|
|
6758
|
+
}
|
|
6759
|
+
const boundElsewhere = new Set(
|
|
6760
|
+
this.sessionStore.listManaged().filter((s) => s.id !== sessionId && s.boundConversationId != null).map((s) => s.boundConversationId)
|
|
6761
|
+
);
|
|
6762
|
+
for (const root of this.codexRoots) {
|
|
6763
|
+
const sessionsDir = (0, import_path13.join)(root, dateDir);
|
|
6764
|
+
if (!(0, import_fs12.existsSync)(sessionsDir)) continue;
|
|
6765
|
+
let candidateFiles;
|
|
6766
|
+
try {
|
|
6767
|
+
candidateFiles = (0, import_fs12.readdirSync)(sessionsDir).filter((f) => f.endsWith(".jsonl"));
|
|
6768
|
+
} catch {
|
|
6769
|
+
continue;
|
|
6770
|
+
}
|
|
6771
|
+
const nowMs = Date.now();
|
|
6772
|
+
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);
|
|
6773
|
+
for (const { f } of recentCandidates) {
|
|
6774
|
+
const candidatePath = (0, import_path13.join)(sessionsDir, f);
|
|
6775
|
+
const match = matchesProjectPath(candidatePath);
|
|
6776
|
+
if (!match) continue;
|
|
6777
|
+
if (boundElsewhere.has(match.id)) continue;
|
|
6778
|
+
const codexSessionId = match.id;
|
|
6779
|
+
cleanup();
|
|
6780
|
+
this.sessionStore.updateManaged(sessionId, { boundConversationId: codexSessionId });
|
|
6781
|
+
this.sessionFileMap.set(sessionId, candidatePath);
|
|
6782
|
+
this.fileWatcher.watch(candidatePath);
|
|
6783
|
+
try {
|
|
6784
|
+
const existing = (0, import_fs12.readFileSync)(candidatePath, "utf8").split("\n").filter(Boolean);
|
|
6785
|
+
if (existing.length > 0) {
|
|
6786
|
+
this.wsHub.broadcast({ type: "conversation_events", sessionId, lines: existing });
|
|
6787
|
+
for (const line of existing) {
|
|
6788
|
+
this.wsHub.broadcast({ type: "conversation_event", sessionId, line });
|
|
6789
|
+
}
|
|
6790
|
+
}
|
|
6791
|
+
} catch {
|
|
6792
|
+
}
|
|
6793
|
+
if (this.scannerReady) {
|
|
6794
|
+
this.scannerStale = true;
|
|
6795
|
+
} else {
|
|
6796
|
+
this.scanner = null;
|
|
6797
|
+
}
|
|
6798
|
+
this.linkSessionToProject(sessionId, projectPath, candidatePath);
|
|
6799
|
+
this.cache?.markAsStreamer(sessionId);
|
|
6800
|
+
const resp = this.sessionStore.get(sessionId, this.ptyAttachedIds());
|
|
6801
|
+
if (resp) {
|
|
6802
|
+
this.wsHub.broadcast({ type: "session_update", session: resp });
|
|
6803
|
+
}
|
|
6804
|
+
this.log.info(
|
|
6805
|
+
`[startFresh] bound Codex rollout for ${sessionId}`,
|
|
6806
|
+
{
|
|
6807
|
+
event: "session.codex_rollout_bound",
|
|
6808
|
+
sessionId,
|
|
6809
|
+
boundConversationId: codexSessionId,
|
|
6810
|
+
filePath: candidatePath
|
|
6811
|
+
},
|
|
6812
|
+
"pino"
|
|
6813
|
+
);
|
|
6814
|
+
return;
|
|
6815
|
+
}
|
|
6816
|
+
}
|
|
6817
|
+
};
|
|
6818
|
+
tryWire();
|
|
6819
|
+
if (!intervalHandle && Date.now() <= deadline) {
|
|
6820
|
+
const alreadyBound = this.sessionStore.getManaged(sessionId)?.boundConversationId != null;
|
|
6821
|
+
if (!alreadyBound) {
|
|
6822
|
+
intervalHandle = setInterval(tryWire, 250);
|
|
6823
|
+
}
|
|
6824
|
+
}
|
|
6825
|
+
}
|
|
6048
6826
|
async handleBrowse(url, res) {
|
|
6049
6827
|
if (!this.browseRoot) {
|
|
6050
6828
|
json(res, 403, {
|
|
@@ -6128,7 +6906,7 @@ var StreamerServer = class {
|
|
|
6128
6906
|
};
|
|
6129
6907
|
function classifyResumability(cwd) {
|
|
6130
6908
|
if (!cwd) return { resumable: true };
|
|
6131
|
-
if ((0,
|
|
6909
|
+
if ((0, import_fs12.existsSync)(cwd)) return { resumable: true };
|
|
6132
6910
|
const ranInWorktree = /\/\.worktrees\//.test(cwd) || /\/\.claude\/worktrees\//.test(cwd);
|
|
6133
6911
|
return {
|
|
6134
6912
|
resumable: false,
|
|
@@ -6260,7 +7038,10 @@ function readBody(req) {
|
|
|
6260
7038
|
}
|
|
6261
7039
|
// Annotate the CommonJS export names for ESM import in node:
|
|
6262
7040
|
0 && (module.exports = {
|
|
7041
|
+
CLAUDE_CODE_PROVIDER,
|
|
7042
|
+
CODEX_CLI_PROVIDER,
|
|
6263
7043
|
ConversationWatcher,
|
|
7044
|
+
LiveSessionManager,
|
|
6264
7045
|
PTYManager,
|
|
6265
7046
|
SessionStore,
|
|
6266
7047
|
StreamerServer,
|
|
@@ -6274,6 +7055,8 @@ function readBody(req) {
|
|
|
6274
7055
|
generateApiKey,
|
|
6275
7056
|
getDbConfig,
|
|
6276
7057
|
isDbEnabled,
|
|
7058
|
+
isProviderName,
|
|
7059
|
+
isProviderResumable,
|
|
6277
7060
|
loadOrCreateApiKey,
|
|
6278
7061
|
maskConnectionString,
|
|
6279
7062
|
readAgentConfig,
|