@threadbase-sh/streamer 1.23.1 → 1.24.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/cli.cjs +1170 -350
- package/dist/cli.cjs.map +1 -1
- package/dist/index.cjs +1152 -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 +1022 -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
|
}
|
|
@@ -1018,6 +1409,10 @@ var PTYManager = class {
|
|
|
1018
1409
|
// to a given input or fell silent. Reset on dispose().
|
|
1019
1410
|
chunkIndex = /* @__PURE__ */ new Map();
|
|
1020
1411
|
lastChunkAt = /* @__PURE__ */ new Map();
|
|
1412
|
+
// In-flight start()/startFresh() calls keyed by sessionId. A second
|
|
1413
|
+
// concurrent resume for the same session (double-tap, client retry) awaits
|
|
1414
|
+
// the first call's promise instead of spawning a duplicate PTY (CRITICAL #3).
|
|
1415
|
+
startPromises = /* @__PURE__ */ new Map();
|
|
1021
1416
|
constructor(options = {}) {
|
|
1022
1417
|
this.onOutput = options.onOutput;
|
|
1023
1418
|
this.onStatusChange = options.onStatusChange;
|
|
@@ -1040,7 +1435,18 @@ var PTYManager = class {
|
|
|
1040
1435
|
// custom-API-key — are cleared by the seeded ~/.claude.json in
|
|
1041
1436
|
// docker/entrypoint.sh.) startFresh() uses the same flag for the same reason.
|
|
1042
1437
|
async start(sessionId, options) {
|
|
1043
|
-
const
|
|
1438
|
+
const existing = this.sessions.get(sessionId);
|
|
1439
|
+
if (existing) return toPublicSession2(existing);
|
|
1440
|
+
const inFlight = this.startPromises.get(sessionId);
|
|
1441
|
+
if (inFlight) return inFlight;
|
|
1442
|
+
const promise = this.doStart(sessionId, options).finally(() => {
|
|
1443
|
+
this.startPromises.delete(sessionId);
|
|
1444
|
+
});
|
|
1445
|
+
this.startPromises.set(sessionId, promise);
|
|
1446
|
+
return promise;
|
|
1447
|
+
}
|
|
1448
|
+
async doStart(sessionId, options) {
|
|
1449
|
+
const nodePty = await loadPty2();
|
|
1044
1450
|
const projectName = options.projectName ?? (0, import_path5.basename)(options.projectPath);
|
|
1045
1451
|
const proc = nodePty.spawn(
|
|
1046
1452
|
resolveClaudeExe(),
|
|
@@ -1062,6 +1468,7 @@ var PTYManager = class {
|
|
|
1062
1468
|
);
|
|
1063
1469
|
const session = {
|
|
1064
1470
|
id: sessionId,
|
|
1471
|
+
provider: CLAUDE_CODE_PROVIDER,
|
|
1065
1472
|
projectPath: options.projectPath,
|
|
1066
1473
|
projectName,
|
|
1067
1474
|
branch: options.branch ?? "",
|
|
@@ -1072,7 +1479,7 @@ var PTYManager = class {
|
|
|
1072
1479
|
lastOutput: "",
|
|
1073
1480
|
process: proc,
|
|
1074
1481
|
outputBuffer: Buffer.alloc(0),
|
|
1075
|
-
screen:
|
|
1482
|
+
screen: createScreen2()
|
|
1076
1483
|
};
|
|
1077
1484
|
this.sessions.set(sessionId, session);
|
|
1078
1485
|
this.pendingReady.add(sessionId);
|
|
@@ -1083,14 +1490,14 @@ var PTYManager = class {
|
|
|
1083
1490
|
this.pendingReady.delete(sessionId);
|
|
1084
1491
|
this.handleExit(sessionId, exitCode);
|
|
1085
1492
|
});
|
|
1086
|
-
return
|
|
1493
|
+
return toPublicSession2(session);
|
|
1087
1494
|
}
|
|
1088
1495
|
// Start a brand-new Claude session. A stable UUID is generated here and passed
|
|
1089
1496
|
// to Claude via --session-id so the JSONL filename matches from the start.
|
|
1090
1497
|
// onReady fires once Claude reaches its first prompt (waiting_input).
|
|
1091
1498
|
async startFresh(options) {
|
|
1092
|
-
const nodePty = await
|
|
1093
|
-
const sessionId = (0,
|
|
1499
|
+
const nodePty = await loadPty2();
|
|
1500
|
+
const sessionId = (0, import_crypto3.randomUUID)();
|
|
1094
1501
|
const projectName = options.projectName ?? (0, import_path5.basename)(options.projectPath);
|
|
1095
1502
|
const args = [
|
|
1096
1503
|
"--permission-mode",
|
|
@@ -1112,6 +1519,7 @@ var PTYManager = class {
|
|
|
1112
1519
|
});
|
|
1113
1520
|
const session = {
|
|
1114
1521
|
id: sessionId,
|
|
1522
|
+
provider: CLAUDE_CODE_PROVIDER,
|
|
1115
1523
|
projectPath: options.projectPath,
|
|
1116
1524
|
projectName,
|
|
1117
1525
|
branch: "",
|
|
@@ -1122,7 +1530,7 @@ var PTYManager = class {
|
|
|
1122
1530
|
lastOutput: "",
|
|
1123
1531
|
process: proc,
|
|
1124
1532
|
outputBuffer: Buffer.alloc(0),
|
|
1125
|
-
screen:
|
|
1533
|
+
screen: createScreen2()
|
|
1126
1534
|
};
|
|
1127
1535
|
this.sessions.set(sessionId, session);
|
|
1128
1536
|
this.pendingReady.add(sessionId);
|
|
@@ -1133,7 +1541,7 @@ var PTYManager = class {
|
|
|
1133
1541
|
this.pendingReady.delete(sessionId);
|
|
1134
1542
|
this.handleExit(sessionId, exitCode);
|
|
1135
1543
|
});
|
|
1136
|
-
return
|
|
1544
|
+
return toPublicSession2(session);
|
|
1137
1545
|
}
|
|
1138
1546
|
// Write raw key bytes directly to the PTY without bracketed-paste wrapping.
|
|
1139
1547
|
// Use for control sequences (arrow keys, Enter) that must not be quoted.
|
|
@@ -1145,10 +1553,10 @@ var PTYManager = class {
|
|
|
1145
1553
|
}
|
|
1146
1554
|
if (session.status === "waiting_input") {
|
|
1147
1555
|
session.status = "running";
|
|
1148
|
-
this.onStatusChange?.(
|
|
1556
|
+
this.onStatusChange?.(toPublicSession2(session));
|
|
1149
1557
|
}
|
|
1150
1558
|
this.log.info(
|
|
1151
|
-
`[pty.keys.write] ${sessionId.slice(0, 8)} bytes=${keys.length} digest=${
|
|
1559
|
+
`[pty.keys.write] ${sessionId.slice(0, 8)} bytes=${keys.length} digest=${digestBytes2(keys)}`,
|
|
1152
1560
|
{ event: "pty.keys_write", sessionId, byteLen: keys.length }
|
|
1153
1561
|
);
|
|
1154
1562
|
session.process.write(keys);
|
|
@@ -1180,7 +1588,7 @@ var PTYManager = class {
|
|
|
1180
1588
|
}
|
|
1181
1589
|
if (session.status === "waiting_input") {
|
|
1182
1590
|
session.status = "running";
|
|
1183
|
-
this.onStatusChange?.(
|
|
1591
|
+
this.onStatusChange?.(toPublicSession2(session));
|
|
1184
1592
|
}
|
|
1185
1593
|
this.writeSubmit(sessionId, session, input, "direct", session.promptCount + 1);
|
|
1186
1594
|
session.lastActivityAt = /* @__PURE__ */ new Date();
|
|
@@ -1193,13 +1601,13 @@ var PTYManager = class {
|
|
|
1193
1601
|
writeSubmit(sessionId, session, input, path, promptCount) {
|
|
1194
1602
|
const pasteBytes = buildPasteBytes(input);
|
|
1195
1603
|
this.log.info(
|
|
1196
|
-
`[pty.input.write] ${sessionId.slice(0, 8)} promptCount=${promptCount} bytes=${pasteBytes.length} digest=${
|
|
1604
|
+
`[pty.input.write] ${sessionId.slice(0, 8)} promptCount=${promptCount} bytes=${pasteBytes.length} digest=${digestBytes2(pasteBytes)}`,
|
|
1197
1605
|
{
|
|
1198
1606
|
event: "pty.input_write",
|
|
1199
1607
|
sessionId,
|
|
1200
1608
|
promptCount,
|
|
1201
1609
|
byteLen: pasteBytes.length,
|
|
1202
|
-
digest:
|
|
1610
|
+
digest: digestBytes2(pasteBytes),
|
|
1203
1611
|
path,
|
|
1204
1612
|
phase: "paste"
|
|
1205
1613
|
}
|
|
@@ -1214,13 +1622,13 @@ var PTYManager = class {
|
|
|
1214
1622
|
event: "pty.input_write",
|
|
1215
1623
|
sessionId,
|
|
1216
1624
|
promptCount,
|
|
1217
|
-
byteLen:
|
|
1625
|
+
byteLen: SUBMIT_BYTES2.length,
|
|
1218
1626
|
digest: "\\r",
|
|
1219
1627
|
path,
|
|
1220
1628
|
phase: "submit"
|
|
1221
1629
|
}
|
|
1222
1630
|
);
|
|
1223
|
-
current.process.write(
|
|
1631
|
+
current.process.write(SUBMIT_BYTES2);
|
|
1224
1632
|
}, SUBMIT_DELAY_MS);
|
|
1225
1633
|
}
|
|
1226
1634
|
// Drain any inputs that were sent while the session was still pendingReady,
|
|
@@ -1278,7 +1686,7 @@ var PTYManager = class {
|
|
|
1278
1686
|
session.completedAt = /* @__PURE__ */ new Date();
|
|
1279
1687
|
session.screen.dispose();
|
|
1280
1688
|
this.sessions.delete(sessionId);
|
|
1281
|
-
this.onStatusChange?.(
|
|
1689
|
+
this.onStatusChange?.(toPublicSession2(session));
|
|
1282
1690
|
}
|
|
1283
1691
|
getOutput(sessionId) {
|
|
1284
1692
|
const session = this.sessions.get(sessionId);
|
|
@@ -1309,13 +1717,13 @@ var PTYManager = class {
|
|
|
1309
1717
|
}
|
|
1310
1718
|
getSession(sessionId) {
|
|
1311
1719
|
const session = this.sessions.get(sessionId);
|
|
1312
|
-
return session ?
|
|
1720
|
+
return session ? toPublicSession2(session) : null;
|
|
1313
1721
|
}
|
|
1314
1722
|
hasSession(sessionId) {
|
|
1315
1723
|
return this.sessions.has(sessionId);
|
|
1316
1724
|
}
|
|
1317
1725
|
listSessions() {
|
|
1318
|
-
return Array.from(this.sessions.values()).map(
|
|
1726
|
+
return Array.from(this.sessions.values()).map(toPublicSession2);
|
|
1319
1727
|
}
|
|
1320
1728
|
dispose() {
|
|
1321
1729
|
for (const session of this.sessions.values()) {
|
|
@@ -1347,7 +1755,7 @@ var PTYManager = class {
|
|
|
1347
1755
|
this.lastChunkAt.set(sessionId, now);
|
|
1348
1756
|
const gapMs = last == null ? 0 : now - last;
|
|
1349
1757
|
this.log.info(
|
|
1350
|
-
`[pty.chunk] ${sessionId.slice(0, 8)} #${idx} +${chunk.length}B gap=${gapMs}ms status=${session.status} digest=${
|
|
1758
|
+
`[pty.chunk] ${sessionId.slice(0, 8)} #${idx} +${chunk.length}B gap=${gapMs}ms status=${session.status} digest=${digestBytes2(data)}`,
|
|
1351
1759
|
{
|
|
1352
1760
|
event: "pty.chunk",
|
|
1353
1761
|
sessionId,
|
|
@@ -1356,17 +1764,17 @@ var PTYManager = class {
|
|
|
1356
1764
|
gapMs,
|
|
1357
1765
|
status: session.status,
|
|
1358
1766
|
pendingReady: this.pendingReady.has(sessionId),
|
|
1359
|
-
digest:
|
|
1767
|
+
digest: digestBytes2(data)
|
|
1360
1768
|
}
|
|
1361
1769
|
);
|
|
1362
1770
|
session.outputBuffer = Buffer.concat([session.outputBuffer, chunk]);
|
|
1363
|
-
if (session.outputBuffer.length >
|
|
1771
|
+
if (session.outputBuffer.length > OUTPUT_BUFFER_MAX2) {
|
|
1364
1772
|
session.outputBuffer = session.outputBuffer.subarray(
|
|
1365
|
-
session.outputBuffer.length -
|
|
1773
|
+
session.outputBuffer.length - OUTPUT_BUFFER_MAX2
|
|
1366
1774
|
);
|
|
1367
1775
|
}
|
|
1368
1776
|
session.screen.write(data);
|
|
1369
|
-
const stripped =
|
|
1777
|
+
const stripped = stripAnsi2(data);
|
|
1370
1778
|
session.lastOutput = stripped;
|
|
1371
1779
|
const matchedMarker = CLAUDE_PROMPT_MARKERS.find((m) => stripped.includes(m));
|
|
1372
1780
|
if (session.status === "running" && matchedMarker) {
|
|
@@ -1436,87 +1844,327 @@ var PTYManager = class {
|
|
|
1436
1844
|
this.onLiveQuestion?.(sessionId, detected.questions);
|
|
1437
1845
|
}
|
|
1438
1846
|
}
|
|
1439
|
-
} else if (this.lastScreenQuestionKey.has(sessionId) && hasPromptMarker) {
|
|
1440
|
-
this.lastScreenQuestionKey.delete(sessionId);
|
|
1441
|
-
this.onLiveQuestionGone?.(sessionId);
|
|
1442
|
-
}
|
|
1443
|
-
if (!oscPermission && !askFooterOnScreen && !this.permissionOpen.has(sessionId)) {
|
|
1444
|
-
const shell = detectShellPrompt(lines);
|
|
1445
|
-
if (shell) {
|
|
1446
|
-
const key = `${shell.prompt}\0${shell.options.map((o) => o.label).join("\0")}`;
|
|
1447
|
-
if (this.shellPromptOpen.get(sessionId) !== key) {
|
|
1448
|
-
this.shellPromptOpen.set(sessionId, key);
|
|
1449
|
-
this.onPermissionChange?.(sessionId, {
|
|
1450
|
-
prompt: shell.prompt,
|
|
1451
|
-
options: shell.options
|
|
1452
|
-
});
|
|
1453
|
-
}
|
|
1454
|
-
} else if (this.shellPromptOpen.has(sessionId) && hasPromptMarker) {
|
|
1455
|
-
this.shellPromptOpen.delete(sessionId);
|
|
1456
|
-
this.onPermissionChange?.(sessionId, null);
|
|
1847
|
+
} else if (this.lastScreenQuestionKey.has(sessionId) && hasPromptMarker) {
|
|
1848
|
+
this.lastScreenQuestionKey.delete(sessionId);
|
|
1849
|
+
this.onLiveQuestionGone?.(sessionId);
|
|
1850
|
+
}
|
|
1851
|
+
if (!oscPermission && !askFooterOnScreen && !this.permissionOpen.has(sessionId)) {
|
|
1852
|
+
const shell = detectShellPrompt(lines);
|
|
1853
|
+
if (shell) {
|
|
1854
|
+
const key = `${shell.prompt}\0${shell.options.map((o) => o.label).join("\0")}`;
|
|
1855
|
+
if (this.shellPromptOpen.get(sessionId) !== key) {
|
|
1856
|
+
this.shellPromptOpen.set(sessionId, key);
|
|
1857
|
+
this.onPermissionChange?.(sessionId, {
|
|
1858
|
+
prompt: shell.prompt,
|
|
1859
|
+
options: shell.options
|
|
1860
|
+
});
|
|
1861
|
+
}
|
|
1862
|
+
} else if (this.shellPromptOpen.has(sessionId) && hasPromptMarker) {
|
|
1863
|
+
this.shellPromptOpen.delete(sessionId);
|
|
1864
|
+
this.onPermissionChange?.(sessionId, null);
|
|
1865
|
+
}
|
|
1866
|
+
}
|
|
1867
|
+
}
|
|
1868
|
+
// Transition a session from "running" to "waiting_input", clear pendingReady,
|
|
1869
|
+
// and flush any queued input. Idempotent: callers can invoke at any chunk.
|
|
1870
|
+
markReady(sessionId, session, reason) {
|
|
1871
|
+
session.lastActivityAt = /* @__PURE__ */ new Date();
|
|
1872
|
+
session.status = "waiting_input";
|
|
1873
|
+
const elapsedMs = Date.now() - (this.firstChunkAt.get(sessionId) ?? Date.now());
|
|
1874
|
+
this.log.info(`[pty.ready] ${sessionId.slice(0, 8)} ${reason} (elapsed=${elapsedMs}ms)`, {
|
|
1875
|
+
event: "pty.ready",
|
|
1876
|
+
sessionId,
|
|
1877
|
+
reason,
|
|
1878
|
+
elapsedMs
|
|
1879
|
+
});
|
|
1880
|
+
this.onStatusChange?.(toPublicSession2(session));
|
|
1881
|
+
if (this.pendingReady.has(sessionId)) {
|
|
1882
|
+
this.pendingReady.delete(sessionId);
|
|
1883
|
+
this.flushQueuedInputs(sessionId);
|
|
1884
|
+
this.onReady?.(toPublicSession2(session));
|
|
1885
|
+
}
|
|
1886
|
+
}
|
|
1887
|
+
handleExit(sessionId, exitCode) {
|
|
1888
|
+
const session = this.sessions.get(sessionId);
|
|
1889
|
+
if (!session) return;
|
|
1890
|
+
session.completedAt = /* @__PURE__ */ new Date();
|
|
1891
|
+
session.status = "idle";
|
|
1892
|
+
const elapsedMs = session.completedAt.getTime() - session.startedAt.getTime();
|
|
1893
|
+
if (exitCode !== 0 && elapsedMs < 2e3 && session.lastOutput === "") {
|
|
1894
|
+
if (!(0, import_fs5.existsSync)(session.projectPath)) {
|
|
1895
|
+
session.failureReason = `Project directory not found: ${session.projectPath}`;
|
|
1896
|
+
} else {
|
|
1897
|
+
session.failureReason = `Process exited immediately (code ${exitCode}). Check that the Claude binary is installed and accessible.`;
|
|
1898
|
+
}
|
|
1899
|
+
}
|
|
1900
|
+
this.onStatusChange?.(toPublicSession2(session));
|
|
1901
|
+
session.screen.dispose();
|
|
1902
|
+
this.sessions.delete(sessionId);
|
|
1903
|
+
this.queuedInputs.delete(sessionId);
|
|
1904
|
+
this.firstChunkAt.delete(sessionId);
|
|
1905
|
+
this.permissionOpen.delete(sessionId);
|
|
1906
|
+
this.lastScreenQuestionKey.delete(sessionId);
|
|
1907
|
+
this.shellPromptOpen.delete(sessionId);
|
|
1908
|
+
}
|
|
1909
|
+
};
|
|
1910
|
+
function toPublicSession2(s) {
|
|
1911
|
+
return {
|
|
1912
|
+
id: s.id,
|
|
1913
|
+
provider: s.provider ?? CLAUDE_CODE_PROVIDER,
|
|
1914
|
+
projectPath: s.projectPath,
|
|
1915
|
+
projectName: s.projectName,
|
|
1916
|
+
branch: s.branch,
|
|
1917
|
+
status: s.status,
|
|
1918
|
+
startedAt: s.startedAt,
|
|
1919
|
+
completedAt: s.completedAt,
|
|
1920
|
+
promptCount: s.promptCount,
|
|
1921
|
+
lastOutput: s.lastOutput,
|
|
1922
|
+
...s.failureReason != null && { failureReason: s.failureReason },
|
|
1923
|
+
...s.lastActivityAt != null && { lastActivityAt: s.lastActivityAt },
|
|
1924
|
+
...s.filePath != null && { filePath: s.filePath }
|
|
1925
|
+
};
|
|
1926
|
+
}
|
|
1927
|
+
function stripAnsi2(str) {
|
|
1928
|
+
return str.replace(/\x1b\[[0-9;]*[a-zA-Z]/g, "").replace(/\x1b\][^\x07]*\x07/g, "");
|
|
1929
|
+
}
|
|
1930
|
+
|
|
1931
|
+
// src/live-session-manager.ts
|
|
1932
|
+
var LiveSessionManager = class {
|
|
1933
|
+
runners;
|
|
1934
|
+
constructor(options = {}) {
|
|
1935
|
+
this.runners = /* @__PURE__ */ new Map([
|
|
1936
|
+
[CLAUDE_CODE_PROVIDER, new PTYManager(options)],
|
|
1937
|
+
[CODEX_CLI_PROVIDER, new CodexPtyRunner(options)]
|
|
1938
|
+
]);
|
|
1939
|
+
}
|
|
1940
|
+
async start(sessionId, options) {
|
|
1941
|
+
const provider = options.provider ?? CLAUDE_CODE_PROVIDER;
|
|
1942
|
+
const runner = this.assertSupportedProvider(provider, options.projectPath);
|
|
1943
|
+
return runner.start(sessionId, options);
|
|
1944
|
+
}
|
|
1945
|
+
async startFresh(options) {
|
|
1946
|
+
const provider = options.provider ?? CLAUDE_CODE_PROVIDER;
|
|
1947
|
+
const runner = this.assertSupportedProvider(provider, options.projectPath);
|
|
1948
|
+
return runner.startFresh(options);
|
|
1949
|
+
}
|
|
1950
|
+
sendInput(sessionId, input) {
|
|
1951
|
+
return this.runnerFor(sessionId).sendInput(sessionId, input);
|
|
1952
|
+
}
|
|
1953
|
+
sendKeys(sessionId, keys) {
|
|
1954
|
+
this.runnerFor(sessionId).sendKeys(sessionId, keys);
|
|
1955
|
+
}
|
|
1956
|
+
cancel(sessionId) {
|
|
1957
|
+
this.runnerFor(sessionId).cancel(sessionId);
|
|
1958
|
+
}
|
|
1959
|
+
killPid(pid) {
|
|
1960
|
+
for (const runner of this.runners.values()) {
|
|
1961
|
+
runner.killPid(pid);
|
|
1962
|
+
}
|
|
1963
|
+
}
|
|
1964
|
+
// putOnHold tolerates an unknown sessionId (PTYManager.putOnHold is a no-op
|
|
1965
|
+
// when the session isn't in its map), so — unlike the other session-keyed
|
|
1966
|
+
// methods — route to the owning runner when found, otherwise broadcast to
|
|
1967
|
+
// every runner rather than throwing; this matches the pre-extraction
|
|
1968
|
+
// behavior of delegating straight through with no existence check.
|
|
1969
|
+
putOnHold(sessionId) {
|
|
1970
|
+
for (const runner of this.runners.values()) {
|
|
1971
|
+
if (runner.hasSession(sessionId) || runner.getSession(sessionId)) {
|
|
1972
|
+
runner.putOnHold(sessionId);
|
|
1973
|
+
return;
|
|
1974
|
+
}
|
|
1975
|
+
}
|
|
1976
|
+
for (const runner of this.runners.values()) {
|
|
1977
|
+
runner.putOnHold(sessionId);
|
|
1978
|
+
}
|
|
1979
|
+
}
|
|
1980
|
+
getOutput(sessionId) {
|
|
1981
|
+
return this.runnerFor(sessionId).getOutput(sessionId);
|
|
1982
|
+
}
|
|
1983
|
+
getOutputLines(sessionId, maxLines) {
|
|
1984
|
+
return this.runnerFor(sessionId).getOutputLines(sessionId, maxLines);
|
|
1985
|
+
}
|
|
1986
|
+
getSession(sessionId) {
|
|
1987
|
+
for (const runner of this.runners.values()) {
|
|
1988
|
+
const session = runner.getSession(sessionId);
|
|
1989
|
+
if (session) return session;
|
|
1990
|
+
}
|
|
1991
|
+
return null;
|
|
1992
|
+
}
|
|
1993
|
+
hasSession(sessionId) {
|
|
1994
|
+
for (const runner of this.runners.values()) {
|
|
1995
|
+
if (runner.hasSession(sessionId)) return true;
|
|
1996
|
+
}
|
|
1997
|
+
return false;
|
|
1998
|
+
}
|
|
1999
|
+
listSessions() {
|
|
2000
|
+
return Array.from(this.runners.values()).flatMap((runner) => runner.listSessions());
|
|
2001
|
+
}
|
|
2002
|
+
dispose() {
|
|
2003
|
+
for (const runner of this.runners.values()) {
|
|
2004
|
+
runner.dispose();
|
|
2005
|
+
}
|
|
2006
|
+
}
|
|
2007
|
+
// Look up which runner owns a session. Only one runner exists today, so
|
|
2008
|
+
// this is a linear scan across hasSession()/getSession() rather than a
|
|
2009
|
+
// separate session→provider index — see task-1-brief.md.
|
|
2010
|
+
runnerFor(sessionId) {
|
|
2011
|
+
for (const runner of this.runners.values()) {
|
|
2012
|
+
if (runner.hasSession(sessionId) || runner.getSession(sessionId)) return runner;
|
|
2013
|
+
}
|
|
2014
|
+
throw new Error(`Session not found: ${sessionId}`);
|
|
2015
|
+
}
|
|
2016
|
+
assertSupportedProvider(provider, projectPath) {
|
|
2017
|
+
const runner = this.runners.get(provider);
|
|
2018
|
+
if (runner) return runner;
|
|
2019
|
+
const err = new Error(
|
|
2020
|
+
`Live ${provider} sessions are not implemented yet for ${(0, import_path6.basename)(projectPath)}`
|
|
2021
|
+
);
|
|
2022
|
+
err.statusCode = 501;
|
|
2023
|
+
throw err;
|
|
2024
|
+
}
|
|
2025
|
+
};
|
|
2026
|
+
|
|
2027
|
+
// src/process-discovery.ts
|
|
2028
|
+
var import_child_process2 = require("child_process");
|
|
2029
|
+
var import_os3 = require("os");
|
|
2030
|
+
var import_path7 = require("path");
|
|
2031
|
+
async function discoverClaudeProcesses() {
|
|
2032
|
+
if ((0, import_os3.platform)() === "win32") return discoverWindows();
|
|
2033
|
+
return discoverUnix();
|
|
2034
|
+
}
|
|
2035
|
+
async function discoverUnix() {
|
|
2036
|
+
const pids = await getPidsUnix();
|
|
2037
|
+
const results = await Promise.all(
|
|
2038
|
+
pids.map(async (pid) => {
|
|
2039
|
+
try {
|
|
2040
|
+
const [cwd, args, startedAt] = await Promise.all([
|
|
2041
|
+
getProcessCwdUnix(pid),
|
|
2042
|
+
getProcessArgsUnix(pid),
|
|
2043
|
+
getProcessStartTimeUnix(pid)
|
|
2044
|
+
]);
|
|
2045
|
+
const conversationId = extractResumeId(args);
|
|
2046
|
+
return {
|
|
2047
|
+
pid,
|
|
2048
|
+
projectPath: cwd,
|
|
2049
|
+
projectName: (0, import_path7.basename)(cwd),
|
|
2050
|
+
branch: await readGitBranch(cwd),
|
|
2051
|
+
conversationId,
|
|
2052
|
+
startedAt
|
|
2053
|
+
};
|
|
2054
|
+
} catch {
|
|
2055
|
+
return null;
|
|
2056
|
+
}
|
|
2057
|
+
})
|
|
2058
|
+
);
|
|
2059
|
+
return results.filter((r) => r !== null);
|
|
2060
|
+
}
|
|
2061
|
+
async function discoverWindows() {
|
|
2062
|
+
const pids = await getPidsWindows();
|
|
2063
|
+
const results = await Promise.all(
|
|
2064
|
+
pids.map(async (pid) => {
|
|
2065
|
+
try {
|
|
2066
|
+
const info = await getProcessInfoWindows(pid);
|
|
2067
|
+
if (!info) return null;
|
|
2068
|
+
return {
|
|
2069
|
+
pid,
|
|
2070
|
+
projectPath: info.cwd,
|
|
2071
|
+
projectName: (0, import_path7.basename)(info.cwd),
|
|
2072
|
+
branch: await readGitBranch(info.cwd),
|
|
2073
|
+
conversationId: extractResumeId(info.args),
|
|
2074
|
+
startedAt: info.startedAt
|
|
2075
|
+
};
|
|
2076
|
+
} catch {
|
|
2077
|
+
return null;
|
|
2078
|
+
}
|
|
2079
|
+
})
|
|
2080
|
+
);
|
|
2081
|
+
return results.filter((r) => r !== null);
|
|
2082
|
+
}
|
|
2083
|
+
function run(cmd, args, opts = {}) {
|
|
2084
|
+
return new Promise((resolve2, reject) => {
|
|
2085
|
+
(0, import_child_process2.execFile)(
|
|
2086
|
+
cmd,
|
|
2087
|
+
args,
|
|
2088
|
+
{ windowsHide: isWindows, encoding: "utf-8", timeout: opts.timeout ?? 5e3, cwd: opts.cwd },
|
|
2089
|
+
(err, stdout) => {
|
|
2090
|
+
if (err) reject(err);
|
|
2091
|
+
else resolve2(stdout);
|
|
1457
2092
|
}
|
|
1458
|
-
|
|
2093
|
+
);
|
|
2094
|
+
});
|
|
2095
|
+
}
|
|
2096
|
+
async function getPidsUnix() {
|
|
2097
|
+
try {
|
|
2098
|
+
const output = await run("pgrep", ["-x", "claude"]);
|
|
2099
|
+
return output.trim().split("\n").filter(Boolean).map((s) => Number.parseInt(s, 10));
|
|
2100
|
+
} catch {
|
|
2101
|
+
return [];
|
|
1459
2102
|
}
|
|
1460
|
-
|
|
1461
|
-
|
|
1462
|
-
|
|
1463
|
-
|
|
1464
|
-
|
|
1465
|
-
|
|
1466
|
-
|
|
1467
|
-
|
|
1468
|
-
|
|
1469
|
-
|
|
1470
|
-
|
|
1471
|
-
|
|
1472
|
-
|
|
1473
|
-
|
|
1474
|
-
|
|
1475
|
-
|
|
1476
|
-
|
|
1477
|
-
|
|
2103
|
+
}
|
|
2104
|
+
async function getProcessCwdUnix(pid) {
|
|
2105
|
+
const output = await run("lsof", ["-p", String(pid), "-a", "-d", "cwd", "-Fn"]);
|
|
2106
|
+
const match = output.match(/n(.+)/);
|
|
2107
|
+
return match?.[1] ?? "";
|
|
2108
|
+
}
|
|
2109
|
+
async function getProcessArgsUnix(pid) {
|
|
2110
|
+
return (await run("ps", ["-p", String(pid), "-o", "args="])).trim();
|
|
2111
|
+
}
|
|
2112
|
+
async function getProcessStartTimeUnix(pid) {
|
|
2113
|
+
const raw = (await run("ps", ["-p", String(pid), "-o", "lstart="])).trim();
|
|
2114
|
+
const d = new Date(raw);
|
|
2115
|
+
return Number.isNaN(d.getTime()) ? /* @__PURE__ */ new Date() : d;
|
|
2116
|
+
}
|
|
2117
|
+
async function getPidsWindows() {
|
|
2118
|
+
try {
|
|
2119
|
+
const output = await run("tasklist", ["/FI", "IMAGENAME eq claude.exe", "/FO", "CSV", "/NH"]);
|
|
2120
|
+
return output.trim().split("\n").filter(Boolean).map((line) => {
|
|
2121
|
+
const parts = line.split(",");
|
|
2122
|
+
return Number.parseInt(parts[1]?.replace(/"/g, "") ?? "0", 10);
|
|
2123
|
+
}).filter((pid) => pid > 0);
|
|
2124
|
+
} catch {
|
|
2125
|
+
return [];
|
|
1478
2126
|
}
|
|
1479
|
-
|
|
1480
|
-
|
|
1481
|
-
|
|
1482
|
-
|
|
1483
|
-
|
|
1484
|
-
|
|
1485
|
-
|
|
1486
|
-
|
|
1487
|
-
|
|
1488
|
-
|
|
1489
|
-
|
|
1490
|
-
|
|
1491
|
-
|
|
1492
|
-
|
|
1493
|
-
|
|
1494
|
-
|
|
1495
|
-
|
|
1496
|
-
|
|
1497
|
-
|
|
1498
|
-
|
|
1499
|
-
|
|
2127
|
+
}
|
|
2128
|
+
async function getProcessInfoWindows(pid) {
|
|
2129
|
+
try {
|
|
2130
|
+
const output = await run("wmic", [
|
|
2131
|
+
"process",
|
|
2132
|
+
"where",
|
|
2133
|
+
`ProcessId=${pid}`,
|
|
2134
|
+
"get",
|
|
2135
|
+
"CommandLine,CreationDate,ExecutablePath",
|
|
2136
|
+
"/FORMAT:CSV"
|
|
2137
|
+
]);
|
|
2138
|
+
const lines = output.trim().split(/\r?\n/).filter((l) => l.trim().length > 0);
|
|
2139
|
+
if (lines.length < 2) return null;
|
|
2140
|
+
const parts = lines[1].split(",");
|
|
2141
|
+
const args = parts[1] ?? "";
|
|
2142
|
+
const creationDate = parts[2] ?? "";
|
|
2143
|
+
const year = creationDate.slice(0, 4);
|
|
2144
|
+
const month = creationDate.slice(4, 6);
|
|
2145
|
+
const day = creationDate.slice(6, 8);
|
|
2146
|
+
const hour = creationDate.slice(8, 10);
|
|
2147
|
+
const min = creationDate.slice(10, 12);
|
|
2148
|
+
const sec = creationDate.slice(12, 14);
|
|
2149
|
+
const startedAt = /* @__PURE__ */ new Date(`${year}-${month}-${day}T${hour}:${min}:${sec}`);
|
|
2150
|
+
if (Number.isNaN(startedAt.getTime())) return null;
|
|
2151
|
+
const exePath = parts[3] ?? "";
|
|
2152
|
+
const cwd = exePath ? (0, import_path7.dirname)(exePath) : "";
|
|
2153
|
+
return { cwd, args, startedAt };
|
|
2154
|
+
} catch {
|
|
2155
|
+
return null;
|
|
1500
2156
|
}
|
|
1501
|
-
};
|
|
1502
|
-
function toPublicSession(s) {
|
|
1503
|
-
return {
|
|
1504
|
-
id: s.id,
|
|
1505
|
-
projectPath: s.projectPath,
|
|
1506
|
-
projectName: s.projectName,
|
|
1507
|
-
branch: s.branch,
|
|
1508
|
-
status: s.status,
|
|
1509
|
-
startedAt: s.startedAt,
|
|
1510
|
-
completedAt: s.completedAt,
|
|
1511
|
-
promptCount: s.promptCount,
|
|
1512
|
-
lastOutput: s.lastOutput,
|
|
1513
|
-
...s.failureReason != null && { failureReason: s.failureReason },
|
|
1514
|
-
...s.lastActivityAt != null && { lastActivityAt: s.lastActivityAt },
|
|
1515
|
-
...s.filePath != null && { filePath: s.filePath }
|
|
1516
|
-
};
|
|
1517
2157
|
}
|
|
1518
|
-
function
|
|
1519
|
-
|
|
2158
|
+
function extractResumeId(args) {
|
|
2159
|
+
const match = args.match(/--resume\s+(\S+)/);
|
|
2160
|
+
return match?.[1] ?? null;
|
|
2161
|
+
}
|
|
2162
|
+
async function readGitBranch(dir) {
|
|
2163
|
+
try {
|
|
2164
|
+
return (await run("git", ["rev-parse", "--abbrev-ref", "HEAD"], { cwd: dir, timeout: 3e3 })).trim();
|
|
2165
|
+
} catch {
|
|
2166
|
+
return "";
|
|
2167
|
+
}
|
|
1520
2168
|
}
|
|
1521
2169
|
|
|
1522
2170
|
// src/server.ts
|
|
@@ -1524,15 +2172,15 @@ var import_node_ws = require("@hono/node-ws");
|
|
|
1524
2172
|
var import_client = require("@temporalio/client");
|
|
1525
2173
|
var import_scanner2 = require("@threadbase-sh/scanner");
|
|
1526
2174
|
var import_events = require("events");
|
|
1527
|
-
var
|
|
2175
|
+
var import_fs12 = require("fs");
|
|
1528
2176
|
var import_promises5 = require("fs/promises");
|
|
1529
2177
|
var import_http = require("http");
|
|
1530
2178
|
var import_os6 = require("os");
|
|
1531
|
-
var
|
|
2179
|
+
var import_path13 = require("path");
|
|
1532
2180
|
var import_readline = require("readline");
|
|
1533
2181
|
|
|
1534
2182
|
// node_modules/nanoid/index.js
|
|
1535
|
-
var
|
|
2183
|
+
var import_crypto4 = __toESM(require("crypto"), 1);
|
|
1536
2184
|
|
|
1537
2185
|
// node_modules/nanoid/url-alphabet/index.js
|
|
1538
2186
|
var urlAlphabet = "useandom-26T198340PX75pxJACKVERYMINDBUSHWOLF_GQZbfghjklqvwyzrict";
|
|
@@ -1546,10 +2194,10 @@ var fillPool = (bytes) => {
|
|
|
1546
2194
|
try {
|
|
1547
2195
|
if (!pool || pool.length < bytes) {
|
|
1548
2196
|
pool = Buffer.allocUnsafe(bytes * POOL_SIZE_MULTIPLIER);
|
|
1549
|
-
|
|
2197
|
+
import_crypto4.default.randomFillSync(pool);
|
|
1550
2198
|
poolOffset = 0;
|
|
1551
2199
|
} else if (poolOffset + bytes > pool.length) {
|
|
1552
|
-
|
|
2200
|
+
import_crypto4.default.randomFillSync(pool);
|
|
1553
2201
|
poolOffset = 0;
|
|
1554
2202
|
}
|
|
1555
2203
|
} catch (e) {
|
|
@@ -2282,7 +2930,7 @@ var createHonoApp = (deps, upgradeWebSocket) => {
|
|
|
2282
2930
|
|
|
2283
2931
|
// src/browse.ts
|
|
2284
2932
|
var import_promises2 = require("fs/promises");
|
|
2285
|
-
var
|
|
2933
|
+
var import_path8 = require("path");
|
|
2286
2934
|
var BrowsePathNotFoundError = class extends Error {
|
|
2287
2935
|
constructor(message) {
|
|
2288
2936
|
super(message);
|
|
@@ -2290,15 +2938,15 @@ var BrowsePathNotFoundError = class extends Error {
|
|
|
2290
2938
|
}
|
|
2291
2939
|
};
|
|
2292
2940
|
async function resolveBrowsePath(browseRoot, relativePath) {
|
|
2293
|
-
const normalizedRoot = (0,
|
|
2941
|
+
const normalizedRoot = (0, import_path8.resolve)(browseRoot);
|
|
2294
2942
|
let sanitized;
|
|
2295
2943
|
if (process.platform !== "win32" && relativePath.startsWith("/") && relativePath.length > 1 && relativePath.includes("/", 1)) {
|
|
2296
2944
|
sanitized = relativePath;
|
|
2297
2945
|
} else {
|
|
2298
2946
|
sanitized = relativePath.replace(/^[/\\]+/, "");
|
|
2299
2947
|
}
|
|
2300
|
-
const target = sanitized ? (0,
|
|
2301
|
-
const rootPrefix = normalizedRoot.endsWith(
|
|
2948
|
+
const target = sanitized ? (0, import_path8.resolve)(normalizedRoot, sanitized) : normalizedRoot;
|
|
2949
|
+
const rootPrefix = normalizedRoot.endsWith(import_path8.sep) ? normalizedRoot : `${normalizedRoot}${import_path8.sep}`;
|
|
2302
2950
|
if (!target.startsWith(rootPrefix) && target !== normalizedRoot) {
|
|
2303
2951
|
throw new Error("Path outside browse root");
|
|
2304
2952
|
}
|
|
@@ -2320,7 +2968,7 @@ async function createDirectory(parentAbsolutePath, name) {
|
|
|
2320
2968
|
if (name.includes("/") || name.includes("\\") || name === ".." || name === ".") {
|
|
2321
2969
|
throw new Error("Invalid directory name");
|
|
2322
2970
|
}
|
|
2323
|
-
const target = (0,
|
|
2971
|
+
const target = (0, import_path8.join)(parentAbsolutePath, name);
|
|
2324
2972
|
try {
|
|
2325
2973
|
const s = await (0, import_promises2.stat)(target);
|
|
2326
2974
|
if (s.isDirectory()) throw new Error("Directory already exists");
|
|
@@ -2333,17 +2981,17 @@ async function createDirectory(parentAbsolutePath, name) {
|
|
|
2333
2981
|
|
|
2334
2982
|
// src/conversation-cache.ts
|
|
2335
2983
|
var import_better_sqlite3 = __toESM(require("better-sqlite3"), 1);
|
|
2336
|
-
var
|
|
2337
|
-
var
|
|
2984
|
+
var import_fs8 = require("fs");
|
|
2985
|
+
var import_path10 = require("path");
|
|
2338
2986
|
|
|
2339
2987
|
// src/db/sqlite-migrate.ts
|
|
2340
|
-
var
|
|
2341
|
-
var
|
|
2988
|
+
var import_fs6 = require("fs");
|
|
2989
|
+
var import_path9 = require("path");
|
|
2342
2990
|
var import_url2 = require("url");
|
|
2343
2991
|
var import_meta2 = {};
|
|
2344
2992
|
function getMigrationsDir2() {
|
|
2345
2993
|
if (typeof import_meta2 !== "undefined" && import_meta2.url) {
|
|
2346
|
-
return (0,
|
|
2994
|
+
return (0, import_path9.dirname)((0, import_url2.fileURLToPath)(import_meta2.url));
|
|
2347
2995
|
}
|
|
2348
2996
|
return __dirname;
|
|
2349
2997
|
}
|
|
@@ -2355,8 +3003,8 @@ CREATE TABLE IF NOT EXISTS schema_migrations (
|
|
|
2355
3003
|
`;
|
|
2356
3004
|
function runSqliteMigrations(db, migrationsDir) {
|
|
2357
3005
|
db.exec(SCHEMA_MIGRATIONS_SQL);
|
|
2358
|
-
const dir = migrationsDir ?? (0,
|
|
2359
|
-
const files = (0,
|
|
3006
|
+
const dir = migrationsDir ?? (0, import_path9.join)(getMigrationsDir2(), "migrations");
|
|
3007
|
+
const files = (0, import_fs6.readdirSync)(dir).filter((f) => f.endsWith(".sql")).sort();
|
|
2360
3008
|
const appliedRows = db.prepare("SELECT id FROM schema_migrations").all();
|
|
2361
3009
|
const appliedSet = new Set(appliedRows.map((r) => r.id));
|
|
2362
3010
|
const recordApplied = db.prepare("INSERT INTO schema_migrations (id, applied_at) VALUES (?, ?)");
|
|
@@ -2367,7 +3015,7 @@ function runSqliteMigrations(db, migrationsDir) {
|
|
|
2367
3015
|
skipped.push(file);
|
|
2368
3016
|
continue;
|
|
2369
3017
|
}
|
|
2370
|
-
const sql = (0,
|
|
3018
|
+
const sql = (0, import_fs6.readFileSync)((0, import_path9.join)(dir, file), "utf-8");
|
|
2371
3019
|
const tx = db.transaction(() => {
|
|
2372
3020
|
db.exec(sql);
|
|
2373
3021
|
recordApplied.run(file, (/* @__PURE__ */ new Date()).toISOString());
|
|
@@ -2378,16 +3026,8 @@ function runSqliteMigrations(db, migrationsDir) {
|
|
|
2378
3026
|
return { applied, skipped };
|
|
2379
3027
|
}
|
|
2380
3028
|
|
|
2381
|
-
// src/providers.ts
|
|
2382
|
-
var CLAUDE_CODE_PROVIDER = "claude-code";
|
|
2383
|
-
var CODEX_CLI_PROVIDER = "codex-cli";
|
|
2384
|
-
function isProviderResumable(provider, availabilityResumable) {
|
|
2385
|
-
if (provider === CODEX_CLI_PROVIDER) return false;
|
|
2386
|
-
return availabilityResumable;
|
|
2387
|
-
}
|
|
2388
|
-
|
|
2389
3029
|
// src/services/conversations/isAgentConversation.ts
|
|
2390
|
-
var
|
|
3030
|
+
var import_fs7 = require("fs");
|
|
2391
3031
|
var DEFAULT_AGENT_ENTRYPOINTS = /* @__PURE__ */ new Set(["sdk-cli", "claude-vscode"]);
|
|
2392
3032
|
var CHUNK_BYTES = 64 * 1024;
|
|
2393
3033
|
var ENTRYPOINT_PROBE = `"entrypoint":`;
|
|
@@ -2409,12 +3049,12 @@ function isAgentFile(filePath, entrypoints = DEFAULT_AGENT_ENTRYPOINTS) {
|
|
|
2409
3049
|
if (cached2 !== void 0) return cached2;
|
|
2410
3050
|
let fd;
|
|
2411
3051
|
try {
|
|
2412
|
-
fd = (0,
|
|
3052
|
+
fd = (0, import_fs7.openSync)(filePath, "r");
|
|
2413
3053
|
} catch {
|
|
2414
3054
|
return false;
|
|
2415
3055
|
}
|
|
2416
3056
|
try {
|
|
2417
|
-
const fileSize = (0,
|
|
3057
|
+
const fileSize = (0, import_fs7.statSync)(filePath).size;
|
|
2418
3058
|
if (fileSize === 0) {
|
|
2419
3059
|
fileDecisionCache.set(key, false);
|
|
2420
3060
|
return false;
|
|
@@ -2425,7 +3065,7 @@ function isAgentFile(filePath, entrypoints = DEFAULT_AGENT_ENTRYPOINTS) {
|
|
|
2425
3065
|
let carry = "";
|
|
2426
3066
|
while (offset < fileSize) {
|
|
2427
3067
|
const toRead = Math.min(CHUNK_BYTES, fileSize - offset);
|
|
2428
|
-
const got = (0,
|
|
3068
|
+
const got = (0, import_fs7.readSync)(fd, buf, 0, toRead, offset);
|
|
2429
3069
|
if (got <= 0) break;
|
|
2430
3070
|
const chunk = carry + buf.toString("utf8", 0, got);
|
|
2431
3071
|
for (const marker of markers) {
|
|
@@ -2446,7 +3086,7 @@ function isAgentFile(filePath, entrypoints = DEFAULT_AGENT_ENTRYPOINTS) {
|
|
|
2446
3086
|
} catch {
|
|
2447
3087
|
return false;
|
|
2448
3088
|
} finally {
|
|
2449
|
-
(0,
|
|
3089
|
+
(0, import_fs7.closeSync)(fd);
|
|
2450
3090
|
}
|
|
2451
3091
|
}
|
|
2452
3092
|
function parseAgentEntrypointsEnv(raw) {
|
|
@@ -2658,7 +3298,7 @@ var ConversationCache = class _ConversationCache {
|
|
|
2658
3298
|
return this.agentEntrypoints;
|
|
2659
3299
|
}
|
|
2660
3300
|
static open(dbPath, tailSize = 10, migrationsDir, options) {
|
|
2661
|
-
(0,
|
|
3301
|
+
(0, import_fs8.mkdirSync)((0, import_path10.dirname)(dbPath), { recursive: true });
|
|
2662
3302
|
const db = new import_better_sqlite3.default(dbPath);
|
|
2663
3303
|
db.pragma("journal_mode = WAL");
|
|
2664
3304
|
db.pragma("foreign_keys = ON");
|
|
@@ -2841,7 +3481,7 @@ var ConversationCache = class _ConversationCache {
|
|
|
2841
3481
|
let mtimeMs = null;
|
|
2842
3482
|
let fileSize = null;
|
|
2843
3483
|
try {
|
|
2844
|
-
const s = (0,
|
|
3484
|
+
const s = (0, import_fs8.statSync)(m.filePath);
|
|
2845
3485
|
mtimeMs = s.mtimeMs;
|
|
2846
3486
|
fileSize = s.size;
|
|
2847
3487
|
} catch {
|
|
@@ -2894,8 +3534,8 @@ var ConversationCache = class _ConversationCache {
|
|
|
2894
3534
|
let fileSize;
|
|
2895
3535
|
let fd;
|
|
2896
3536
|
try {
|
|
2897
|
-
fileSize = (0,
|
|
2898
|
-
fd = (0,
|
|
3537
|
+
fileSize = (0, import_fs8.statSync)(filePath).size;
|
|
3538
|
+
fd = (0, import_fs8.openSync)(filePath, "r");
|
|
2899
3539
|
} catch {
|
|
2900
3540
|
return false;
|
|
2901
3541
|
}
|
|
@@ -2908,7 +3548,7 @@ var ConversationCache = class _ConversationCache {
|
|
|
2908
3548
|
while (pos > 0 && lines.length < this.tailSize * 4) {
|
|
2909
3549
|
const toRead = Math.min(CHUNK, pos);
|
|
2910
3550
|
pos -= toRead;
|
|
2911
|
-
(0,
|
|
3551
|
+
(0, import_fs8.readSync)(fd, buf, 0, toRead, pos);
|
|
2912
3552
|
const chunk = buf.subarray(0, toRead).toString("utf8");
|
|
2913
3553
|
const combined = chunk + partial;
|
|
2914
3554
|
const parts = combined.split("\n");
|
|
@@ -2919,7 +3559,7 @@ var ConversationCache = class _ConversationCache {
|
|
|
2919
3559
|
}
|
|
2920
3560
|
if (partial) lines.push(partial);
|
|
2921
3561
|
} finally {
|
|
2922
|
-
(0,
|
|
3562
|
+
(0, import_fs8.closeSync)(fd);
|
|
2923
3563
|
}
|
|
2924
3564
|
const msgs = [];
|
|
2925
3565
|
for (let i = 0; i < lines.length && msgs.length < this.tailSize; i++) {
|
|
@@ -3106,7 +3746,7 @@ var ConversationCache = class _ConversationCache {
|
|
|
3106
3746
|
* `handleGetConversation` can still serve the cached tail even when the
|
|
3107
3747
|
* JSONL has been deleted.
|
|
3108
3748
|
*/
|
|
3109
|
-
pruneGhostFiles(exists =
|
|
3749
|
+
pruneGhostFiles(exists = import_fs8.existsSync) {
|
|
3110
3750
|
const rows = this.stmts.allFilePaths.all();
|
|
3111
3751
|
const ghosts = [];
|
|
3112
3752
|
const prune = this.db.transaction((ids) => {
|
|
@@ -3186,7 +3826,7 @@ var ConversationsRepository = class {
|
|
|
3186
3826
|
};
|
|
3187
3827
|
|
|
3188
3828
|
// src/db/repositories/projects.repository.ts
|
|
3189
|
-
var
|
|
3829
|
+
var import_crypto5 = require("crypto");
|
|
3190
3830
|
|
|
3191
3831
|
// src/utils/canonicalizeProjectPath.ts
|
|
3192
3832
|
function canonicalizeProjectPath(projectPath) {
|
|
@@ -3279,7 +3919,7 @@ var ProjectsRepository = class {
|
|
|
3279
3919
|
});
|
|
3280
3920
|
return rowToProject(this.getById.get(existing.id));
|
|
3281
3921
|
}
|
|
3282
|
-
const id = (0,
|
|
3922
|
+
const id = (0, import_crypto5.randomUUID)();
|
|
3283
3923
|
this.insert.run({
|
|
3284
3924
|
id,
|
|
3285
3925
|
path,
|
|
@@ -3335,23 +3975,23 @@ async function recordUpload(pool2, instanceId, row) {
|
|
|
3335
3975
|
}
|
|
3336
3976
|
|
|
3337
3977
|
// src/handlers/handleListProjects.ts
|
|
3338
|
-
var
|
|
3978
|
+
var import_fs9 = require("fs");
|
|
3339
3979
|
var import_os5 = require("os");
|
|
3340
|
-
var
|
|
3980
|
+
var import_path11 = require("path");
|
|
3341
3981
|
function decodeProjectPath(dirName) {
|
|
3342
3982
|
return dirName.replace(/-/g, "/");
|
|
3343
3983
|
}
|
|
3344
3984
|
function handleListProjects(url, res) {
|
|
3345
3985
|
const limit = Math.max(1, parseInt(url.searchParams.get("limit") ?? "50", 10) || 50);
|
|
3346
3986
|
const offset = Math.max(0, parseInt(url.searchParams.get("offset") ?? "0", 10) || 0);
|
|
3347
|
-
const projectsDir = (0,
|
|
3987
|
+
const projectsDir = (0, import_path11.join)((0, import_os5.homedir)(), ".claude", "projects");
|
|
3348
3988
|
let entries;
|
|
3349
3989
|
try {
|
|
3350
|
-
entries = (0,
|
|
3351
|
-
const fullPath = (0,
|
|
3990
|
+
entries = (0, import_fs9.readdirSync)(projectsDir).map((dirName) => {
|
|
3991
|
+
const fullPath = (0, import_path11.join)(projectsDir, dirName);
|
|
3352
3992
|
let mtime = 0;
|
|
3353
3993
|
try {
|
|
3354
|
-
mtime = (0,
|
|
3994
|
+
mtime = (0, import_fs9.statSync)(fullPath).mtimeMs;
|
|
3355
3995
|
} catch {
|
|
3356
3996
|
}
|
|
3357
3997
|
const path = decodeProjectPath(dirName);
|
|
@@ -3370,7 +4010,7 @@ function handleListProjects(url, res) {
|
|
|
3370
4010
|
}
|
|
3371
4011
|
|
|
3372
4012
|
// src/pair-store.ts
|
|
3373
|
-
var
|
|
4013
|
+
var import_crypto6 = require("crypto");
|
|
3374
4014
|
var DEFAULT_TTL_SECONDS = 180;
|
|
3375
4015
|
var SWEEP_INTERVAL_MS = 6e4;
|
|
3376
4016
|
var PairTokenStore = class {
|
|
@@ -3385,7 +4025,7 @@ var PairTokenStore = class {
|
|
|
3385
4025
|
}
|
|
3386
4026
|
}
|
|
3387
4027
|
mint() {
|
|
3388
|
-
const token = `pt_${(0,
|
|
4028
|
+
const token = `pt_${(0, import_crypto6.randomBytes)(16).toString("hex")}`;
|
|
3389
4029
|
const expiresAt = Date.now() + this.ttlMs;
|
|
3390
4030
|
this.current = { token, expiresAt, used: false };
|
|
3391
4031
|
return {
|
|
@@ -3446,7 +4086,7 @@ function seal(plaintext, recipientPublicKeyBase64) {
|
|
|
3446
4086
|
|
|
3447
4087
|
// src/services/conversations/conversationWatcher.ts
|
|
3448
4088
|
var import_chokidar = __toESM(require("chokidar"), 1);
|
|
3449
|
-
var
|
|
4089
|
+
var import_fs10 = require("fs");
|
|
3450
4090
|
var import_promises3 = require("fs/promises");
|
|
3451
4091
|
var ConversationWatcher = class {
|
|
3452
4092
|
files = /* @__PURE__ */ new Map();
|
|
@@ -3467,7 +4107,7 @@ var ConversationWatcher = class {
|
|
|
3467
4107
|
if (this.files.has(filePath)) return;
|
|
3468
4108
|
let offset;
|
|
3469
4109
|
try {
|
|
3470
|
-
offset = (0,
|
|
4110
|
+
offset = (0, import_fs10.statSync)(filePath).size;
|
|
3471
4111
|
} catch {
|
|
3472
4112
|
offset = 0;
|
|
3473
4113
|
}
|
|
@@ -3576,14 +4216,14 @@ var ConversationWatcher = class {
|
|
|
3576
4216
|
};
|
|
3577
4217
|
|
|
3578
4218
|
// src/services/conversations/pruneAgentConversations.ts
|
|
3579
|
-
var
|
|
4219
|
+
var import_fs11 = require("fs");
|
|
3580
4220
|
function pruneAgentConversations(cache) {
|
|
3581
4221
|
const db = cache.getDatabase();
|
|
3582
4222
|
const rows = db.prepare("SELECT id, file_path FROM conversation_meta").all();
|
|
3583
4223
|
let pruned = 0;
|
|
3584
4224
|
let missing = 0;
|
|
3585
4225
|
for (const row of rows) {
|
|
3586
|
-
if (!(0,
|
|
4226
|
+
if (!(0, import_fs11.existsSync)(row.file_path)) {
|
|
3587
4227
|
missing += 1;
|
|
3588
4228
|
continue;
|
|
3589
4229
|
}
|
|
@@ -3866,6 +4506,7 @@ function managedToResponse(s, ptyAttached) {
|
|
|
3866
4506
|
return {
|
|
3867
4507
|
id: s.id,
|
|
3868
4508
|
conversationId: s.id,
|
|
4509
|
+
provider: s.provider ?? CLAUDE_CODE_PROVIDER,
|
|
3869
4510
|
status: s.status,
|
|
3870
4511
|
projectPath: s.projectPath,
|
|
3871
4512
|
projectName: s.projectName,
|
|
@@ -3891,13 +4532,15 @@ function managedToResponse(s, ptyAttached) {
|
|
|
3891
4532
|
...s.failureReason != null && { failureReason: s.failureReason },
|
|
3892
4533
|
...s.resumedFromConversationId != null && {
|
|
3893
4534
|
resumedFromConversationId: s.resumedFromConversationId
|
|
3894
|
-
}
|
|
4535
|
+
},
|
|
4536
|
+
...s.boundConversationId != null && { boundConversationId: s.boundConversationId }
|
|
3895
4537
|
};
|
|
3896
4538
|
}
|
|
3897
4539
|
function discoveredToResponse(d, conversationId) {
|
|
3898
4540
|
return {
|
|
3899
4541
|
id: conversationId,
|
|
3900
4542
|
conversationId,
|
|
4543
|
+
provider: CLAUDE_CODE_PROVIDER,
|
|
3901
4544
|
status: "idle",
|
|
3902
4545
|
projectPath: d.projectPath,
|
|
3903
4546
|
projectName: d.projectName,
|
|
@@ -3913,10 +4556,10 @@ function discoveredToResponse(d, conversationId) {
|
|
|
3913
4556
|
}
|
|
3914
4557
|
|
|
3915
4558
|
// src/uploads.ts
|
|
3916
|
-
var
|
|
4559
|
+
var import_crypto7 = require("crypto");
|
|
3917
4560
|
var import_promises4 = require("fs/promises");
|
|
3918
4561
|
var import_heic_convert = __toESM(require("heic-convert"), 1);
|
|
3919
|
-
var
|
|
4562
|
+
var import_path12 = require("path");
|
|
3920
4563
|
var UPLOAD_DIR_NAME = ".threadbase-uploads";
|
|
3921
4564
|
var MAX_BYTES = 25 * 1024 * 1024;
|
|
3922
4565
|
var HEIC_MIMES = /* @__PURE__ */ new Set(["image/heic", "image/heif"]);
|
|
@@ -3947,11 +4590,11 @@ async function saveUploadFile(input) {
|
|
|
3947
4590
|
mimeType = "image/jpeg";
|
|
3948
4591
|
originalName = originalName.replace(/\.(heic|heif)$/i, ".jpg");
|
|
3949
4592
|
}
|
|
3950
|
-
const id = `up_${(0,
|
|
4593
|
+
const id = `up_${(0, import_crypto7.randomBytes)(8).toString("hex")}`;
|
|
3951
4594
|
const safeName = sanitizeFilename(originalName) || `file${MIME_TO_EXT[mimeType] ?? ""}`;
|
|
3952
|
-
const dir = (0,
|
|
4595
|
+
const dir = (0, import_path12.join)(input.projectPath, UPLOAD_DIR_NAME, input.sessionId);
|
|
3953
4596
|
await (0, import_promises4.mkdir)(dir, { recursive: true });
|
|
3954
|
-
const filePath = (0,
|
|
4597
|
+
const filePath = (0, import_path12.join)(dir, `${Date.now()}-${id}-${safeName}`);
|
|
3955
4598
|
await (0, import_promises4.writeFile)(filePath, buffer);
|
|
3956
4599
|
return {
|
|
3957
4600
|
id,
|
|
@@ -4252,10 +4895,10 @@ var StreamerServer = class {
|
|
|
4252
4895
|
this.verbose = config.verbose ?? false;
|
|
4253
4896
|
this.disableDb = config.disableDb ?? false;
|
|
4254
4897
|
this.scanProfiles = config.scanProfiles;
|
|
4255
|
-
this.codexRoots = config.codexRoots ?? [(0,
|
|
4898
|
+
this.codexRoots = config.codexRoots ?? [(0, import_path13.join)((0, import_os6.homedir)(), ".codex", "sessions")];
|
|
4256
4899
|
this.ptyGracePeriodMs = config.ptyGracePeriodMs ?? DEFAULT_PTY_GRACE_PERIOD_MS;
|
|
4257
4900
|
this.defaultSystemPrompt = config.defaultSystemPrompt ?? DEFAULT_SYSTEM_PROMPT;
|
|
4258
|
-
this.cacheDir = config.cacheDir ?? loadCacheDir() ?? (0,
|
|
4901
|
+
this.cacheDir = config.cacheDir ?? loadCacheDir() ?? (0, import_path13.join)((0, import_os6.homedir)(), ".threadbase", "cache");
|
|
4259
4902
|
this.tailSize = config.tailSize ?? loadTailSize() ?? 10;
|
|
4260
4903
|
this.directoryDebounceMs = parseDirScanDebounceEnv(process.env.THREADBASE_DIR_SCAN_DEBOUNCE_MS) ?? config.directoryScanDebounceMs ?? 1e3;
|
|
4261
4904
|
this.markScannerStaleDebounced = debounce(() => {
|
|
@@ -4339,7 +4982,7 @@ var StreamerServer = class {
|
|
|
4339
4982
|
});
|
|
4340
4983
|
}
|
|
4341
4984
|
});
|
|
4342
|
-
this.ptyManager = new
|
|
4985
|
+
this.ptyManager = new LiveSessionManager({
|
|
4343
4986
|
logger: getLogger("pty"),
|
|
4344
4987
|
onOutput: (sessionId, data) => {
|
|
4345
4988
|
this.wsHub.broadcast({ type: "terminal_output", sessionId, data });
|
|
@@ -4420,7 +5063,7 @@ var StreamerServer = class {
|
|
|
4420
5063
|
temporalClient,
|
|
4421
5064
|
taskQueue: agentConfig.temporal.taskQueue
|
|
4422
5065
|
});
|
|
4423
|
-
const conversationsBaseDir = agentConfig.conversationsDir || (0,
|
|
5066
|
+
const conversationsBaseDir = agentConfig.conversationsDir || (0, import_path13.join)((0, import_path13.dirname)(this.cacheDir), "conversations");
|
|
4424
5067
|
conversationWriter = createConversationWriter({
|
|
4425
5068
|
baseDir: conversationsBaseDir
|
|
4426
5069
|
});
|
|
@@ -4642,7 +5285,7 @@ var StreamerServer = class {
|
|
|
4642
5285
|
});
|
|
4643
5286
|
try {
|
|
4644
5287
|
this.cache = ConversationCache.open(
|
|
4645
|
-
(0,
|
|
5288
|
+
(0, import_path13.join)(this.cacheDir, "cache.db"),
|
|
4646
5289
|
this.tailSize,
|
|
4647
5290
|
void 0,
|
|
4648
5291
|
{
|
|
@@ -4668,18 +5311,19 @@ var StreamerServer = class {
|
|
|
4668
5311
|
if (this.scanProfiles && this.scanProfiles.length > 0) {
|
|
4669
5312
|
for (const profile of this.scanProfiles) {
|
|
4670
5313
|
if (profile.enabled) {
|
|
4671
|
-
this.fileWatcher.watchDirectory((0,
|
|
5314
|
+
this.fileWatcher.watchDirectory((0, import_path13.join)(profile.configDir, "projects"));
|
|
4672
5315
|
}
|
|
4673
5316
|
}
|
|
4674
5317
|
} else {
|
|
4675
|
-
this.fileWatcher.watchDirectory((0,
|
|
5318
|
+
this.fileWatcher.watchDirectory((0, import_path13.join)((0, import_os6.homedir)(), ".claude", "projects"));
|
|
4676
5319
|
}
|
|
4677
5320
|
} catch (err) {
|
|
4678
5321
|
const message = err instanceof Error ? err.message : String(err);
|
|
4679
|
-
|
|
4680
|
-
|
|
4681
|
-
|
|
4682
|
-
|
|
5322
|
+
const abiMismatch = message.includes("NODE_MODULE_VERSION") || message.includes("was compiled against a different Node.js version");
|
|
5323
|
+
this.log.error(
|
|
5324
|
+
`ConversationCache failed to open \u2014 running WITHOUT cache; /api/conversations, /api/conversations/count and /project-chats will 500.` + (abiMismatch ? ` Fix: npm rebuild better-sqlite3` : "") + ` (${message})`,
|
|
5325
|
+
{ error: message, abiMismatch, event: "cache.open_failed" }
|
|
5326
|
+
);
|
|
4683
5327
|
}
|
|
4684
5328
|
const warmupScanner = new import_scanner2.ConversationScanner();
|
|
4685
5329
|
this.allScanners.add(warmupScanner);
|
|
@@ -5166,17 +5810,17 @@ var StreamerServer = class {
|
|
|
5166
5810
|
return this.getScanner();
|
|
5167
5811
|
}
|
|
5168
5812
|
findJsonlPath(uuid) {
|
|
5169
|
-
const projectsDir = (0,
|
|
5170
|
-
if (!(0,
|
|
5813
|
+
const projectsDir = (0, import_path13.join)((0, import_os6.homedir)(), ".claude", "projects");
|
|
5814
|
+
if (!(0, import_fs12.existsSync)(projectsDir)) return null;
|
|
5171
5815
|
const filename = `${uuid}.jsonl`;
|
|
5172
|
-
for (const dir of (0,
|
|
5173
|
-
const fp = (0,
|
|
5174
|
-
if ((0,
|
|
5175
|
-
const projectDir = (0,
|
|
5816
|
+
for (const dir of (0, import_fs12.readdirSync)(projectsDir)) {
|
|
5817
|
+
const fp = (0, import_path13.join)(projectsDir, dir, filename);
|
|
5818
|
+
if ((0, import_fs12.existsSync)(fp)) return fp;
|
|
5819
|
+
const projectDir = (0, import_path13.join)(projectsDir, dir);
|
|
5176
5820
|
try {
|
|
5177
|
-
for (const sub of (0,
|
|
5178
|
-
const subagentPath = (0,
|
|
5179
|
-
if ((0,
|
|
5821
|
+
for (const sub of (0, import_fs12.readdirSync)(projectDir)) {
|
|
5822
|
+
const subagentPath = (0, import_path13.join)(projectDir, sub, "subagents", filename);
|
|
5823
|
+
if ((0, import_fs12.existsSync)(subagentPath)) return subagentPath;
|
|
5180
5824
|
}
|
|
5181
5825
|
} catch {
|
|
5182
5826
|
}
|
|
@@ -5185,7 +5829,7 @@ var StreamerServer = class {
|
|
|
5185
5829
|
}
|
|
5186
5830
|
async readCwdFromJsonl(filePath) {
|
|
5187
5831
|
return new Promise((resolve2) => {
|
|
5188
|
-
const rl = (0, import_readline.createInterface)({ input: (0,
|
|
5832
|
+
const rl = (0, import_readline.createInterface)({ input: (0, import_fs12.createReadStream)(filePath), crlfDelay: Infinity });
|
|
5189
5833
|
let found = false;
|
|
5190
5834
|
rl.on("line", (line) => {
|
|
5191
5835
|
if (found) return;
|
|
@@ -5242,7 +5886,7 @@ var StreamerServer = class {
|
|
|
5242
5886
|
if (!conv.filePath) return false;
|
|
5243
5887
|
let mtimeMs = null;
|
|
5244
5888
|
try {
|
|
5245
|
-
mtimeMs = (0,
|
|
5889
|
+
mtimeMs = (0, import_fs12.statSync)(conv.filePath).mtimeMs;
|
|
5246
5890
|
} catch {
|
|
5247
5891
|
return false;
|
|
5248
5892
|
}
|
|
@@ -5485,7 +6129,7 @@ var StreamerServer = class {
|
|
|
5485
6129
|
handleGetSession(sessionId, res) {
|
|
5486
6130
|
const session = this.sessionStore.get(sessionId, this.ptyAttachedIds());
|
|
5487
6131
|
if (session) {
|
|
5488
|
-
if (!(0,
|
|
6132
|
+
if (!(0, import_fs12.existsSync)(session.projectPath)) {
|
|
5489
6133
|
session.failureReason = `Project directory not found: ${session.projectPath}`;
|
|
5490
6134
|
}
|
|
5491
6135
|
json(res, 200, session);
|
|
@@ -5525,7 +6169,10 @@ var StreamerServer = class {
|
|
|
5525
6169
|
json(res, 400, { error: "Could not determine project path" });
|
|
5526
6170
|
return;
|
|
5527
6171
|
}
|
|
6172
|
+
const cachedConvMeta = this.cache?.getMetaById(sessionId);
|
|
6173
|
+
const provider = conv?.provider ?? cachedConvMeta?.provider ?? CLAUDE_CODE_PROVIDER;
|
|
5528
6174
|
const session = await this.ptyManager.start(sessionId, {
|
|
6175
|
+
provider,
|
|
5529
6176
|
projectPath,
|
|
5530
6177
|
projectName: body.projectName,
|
|
5531
6178
|
branch: body.branch
|
|
@@ -5875,7 +6522,7 @@ var StreamerServer = class {
|
|
|
5875
6522
|
sessionStore: this.sessionStore,
|
|
5876
6523
|
// biome-ignore lint/style/noNonNullAssertion: agentClient is set when agentConfig.enabled is true
|
|
5877
6524
|
agentClient: this.agentClient,
|
|
5878
|
-
conversationsDir: this.cacheDir ? (0,
|
|
6525
|
+
conversationsDir: this.cacheDir ? (0, import_path13.join)((0, import_path13.dirname)(this.cacheDir), "conversations") : "",
|
|
5879
6526
|
agentConfig: this.agentConfig
|
|
5880
6527
|
});
|
|
5881
6528
|
json(res, result.status, result.body);
|
|
@@ -5884,6 +6531,13 @@ var StreamerServer = class {
|
|
|
5884
6531
|
}
|
|
5885
6532
|
return;
|
|
5886
6533
|
}
|
|
6534
|
+
const body = await readBody(req);
|
|
6535
|
+
const { path: relativePath, provider: requestedProvider, systemPrompt: clientPrompt } = body;
|
|
6536
|
+
if (requestedProvider !== void 0 && !isProviderName(requestedProvider)) {
|
|
6537
|
+
json(res, 400, { error: "Invalid provider" });
|
|
6538
|
+
return;
|
|
6539
|
+
}
|
|
6540
|
+
const provider = requestedProvider ?? CLAUDE_CODE_PROVIDER;
|
|
5887
6541
|
if (!this.browseRoot) {
|
|
5888
6542
|
json(res, 403, {
|
|
5889
6543
|
error: "File browsing not configured. Set browseRoot on the server.",
|
|
@@ -5891,8 +6545,6 @@ var StreamerServer = class {
|
|
|
5891
6545
|
});
|
|
5892
6546
|
return;
|
|
5893
6547
|
}
|
|
5894
|
-
const body = await readBody(req);
|
|
5895
|
-
const { path: relativePath, systemPrompt: clientPrompt } = body;
|
|
5896
6548
|
if (typeof relativePath !== "string") {
|
|
5897
6549
|
json(res, 400, { error: "Missing path field" });
|
|
5898
6550
|
return;
|
|
@@ -5913,21 +6565,27 @@ var StreamerServer = class {
|
|
|
5913
6565
|
].filter(Boolean);
|
|
5914
6566
|
try {
|
|
5915
6567
|
const session = await this.ptyManager.startFresh({
|
|
6568
|
+
provider,
|
|
5916
6569
|
projectPath: resolvedPath,
|
|
5917
6570
|
projectName: body.projectName,
|
|
5918
6571
|
systemPrompt: systemPromptParts.join("\n")
|
|
5919
6572
|
});
|
|
5920
6573
|
this.sessionStore.addManaged(session);
|
|
5921
6574
|
json(res, 202, { id: session.id, status: "pending" });
|
|
5922
|
-
|
|
6575
|
+
if (provider === CODEX_CLI_PROVIDER) {
|
|
6576
|
+
this.watchForCodexRollout(session.id, resolvedPath);
|
|
6577
|
+
} else {
|
|
6578
|
+
this.watchForJsonl(session.id, resolvedPath);
|
|
6579
|
+
}
|
|
5923
6580
|
this.broadcastOrUnicastSessionList(req);
|
|
5924
6581
|
} catch (err) {
|
|
5925
6582
|
const message = err instanceof Error ? err.message : "Failed to start session";
|
|
6583
|
+
const statusCode = typeof err.statusCode === "number" ? err.statusCode : 500;
|
|
5926
6584
|
this.log.error(`[start] failed to start session: ${message}`, {
|
|
5927
6585
|
event: "session.start_failed",
|
|
5928
6586
|
error: message
|
|
5929
6587
|
});
|
|
5930
|
-
json(res,
|
|
6588
|
+
json(res, statusCode, { error: message });
|
|
5931
6589
|
}
|
|
5932
6590
|
}
|
|
5933
6591
|
// ─── Project linking ─────────────────────────────────────────────
|
|
@@ -5980,9 +6638,9 @@ var StreamerServer = class {
|
|
|
5980
6638
|
// was passed to Claude via --session-id so the filename matches from the start.
|
|
5981
6639
|
watchForJsonl(sessionId, projectPath) {
|
|
5982
6640
|
const encoded = projectPath.replace(/[/\\:.]/g, "-");
|
|
5983
|
-
const projectsDir = (0,
|
|
6641
|
+
const projectsDir = (0, import_path13.join)((0, import_os6.homedir)(), ".claude", "projects", encoded);
|
|
5984
6642
|
const expectedFile = `${sessionId}.jsonl`;
|
|
5985
|
-
const filePath = (0,
|
|
6643
|
+
const filePath = (0, import_path13.join)(projectsDir, expectedFile);
|
|
5986
6644
|
const deadline = Date.now() + 12e4;
|
|
5987
6645
|
let watcher = null;
|
|
5988
6646
|
const cleanup = () => {
|
|
@@ -6000,12 +6658,12 @@ var StreamerServer = class {
|
|
|
6000
6658
|
cleanup();
|
|
6001
6659
|
return;
|
|
6002
6660
|
}
|
|
6003
|
-
let resolvedFilePath = (0,
|
|
6004
|
-
if (!resolvedFilePath && (0,
|
|
6661
|
+
let resolvedFilePath = (0, import_fs12.existsSync)(filePath) ? filePath : null;
|
|
6662
|
+
if (!resolvedFilePath && (0, import_fs12.existsSync)(projectsDir)) {
|
|
6005
6663
|
try {
|
|
6006
6664
|
const now = Date.now();
|
|
6007
|
-
const recent = (0,
|
|
6008
|
-
if (recent) resolvedFilePath = (0,
|
|
6665
|
+
const recent = (0, import_fs12.readdirSync)(projectsDir).filter((f) => f.endsWith(".jsonl")).map((f) => ({ f, mtime: (0, import_fs12.statSync)((0, import_path13.join)(projectsDir, f)).mtimeMs })).filter(({ mtime }) => now - mtime < 5e3).sort((a, b) => b.mtime - a.mtime)[0];
|
|
6666
|
+
if (recent) resolvedFilePath = (0, import_path13.join)(projectsDir, recent.f);
|
|
6009
6667
|
} catch {
|
|
6010
6668
|
}
|
|
6011
6669
|
}
|
|
@@ -6014,7 +6672,7 @@ var StreamerServer = class {
|
|
|
6014
6672
|
this.sessionFileMap.set(sessionId, resolvedFilePath);
|
|
6015
6673
|
this.fileWatcher.watch(resolvedFilePath);
|
|
6016
6674
|
try {
|
|
6017
|
-
const existing = (0,
|
|
6675
|
+
const existing = (0, import_fs12.readFileSync)(resolvedFilePath, "utf8").split("\n").filter(Boolean);
|
|
6018
6676
|
if (existing.length > 0) {
|
|
6019
6677
|
this.wsHub.broadcast({ type: "conversation_events", sessionId, lines: existing });
|
|
6020
6678
|
for (const line of existing) {
|
|
@@ -6040,11 +6698,126 @@ var StreamerServer = class {
|
|
|
6040
6698
|
if (this.sessionFileMap.has(sessionId)) return;
|
|
6041
6699
|
try {
|
|
6042
6700
|
require("fs").mkdirSync(projectsDir, { recursive: true });
|
|
6043
|
-
watcher = (0,
|
|
6701
|
+
watcher = (0, import_fs12.watch)(projectsDir, tryWire);
|
|
6044
6702
|
watcher.on("error", cleanup);
|
|
6045
6703
|
} catch {
|
|
6046
6704
|
}
|
|
6047
6705
|
}
|
|
6706
|
+
// Codex-equivalent of watchForJsonl(). Differs because Codex has no
|
|
6707
|
+
// filename-encoded session id (it assigns its own persisted id) and its
|
|
6708
|
+
// rollout files live under a date-nested directory
|
|
6709
|
+
// (~/.codex/sessions/<YYYY>/<MM>/<DD>/rollout-*.jsonl) that Codex creates
|
|
6710
|
+
// itself — it may not exist yet when this function is first called, so we
|
|
6711
|
+
// poll rather than fs.watch a not-yet-existent directory. Per Phase 0
|
|
6712
|
+
// findings, the rollout file appears within ~1s of process spawn (after
|
|
6713
|
+
// any directory-trust gate is cleared), well before any user input.
|
|
6714
|
+
watchForCodexRollout(sessionId, projectPath) {
|
|
6715
|
+
const deadline = Date.now() + 12e4;
|
|
6716
|
+
const now = /* @__PURE__ */ new Date();
|
|
6717
|
+
const dateDir = (0, import_path13.join)(
|
|
6718
|
+
String(now.getFullYear()),
|
|
6719
|
+
String(now.getMonth() + 1).padStart(2, "0"),
|
|
6720
|
+
String(now.getDate()).padStart(2, "0")
|
|
6721
|
+
);
|
|
6722
|
+
const sessionStartedAtMs = (this.sessionStore.getManaged(sessionId)?.startedAt?.getTime() ?? Date.now()) - 5e3;
|
|
6723
|
+
let intervalHandle = null;
|
|
6724
|
+
const cleanup = () => {
|
|
6725
|
+
if (intervalHandle) clearInterval(intervalHandle);
|
|
6726
|
+
intervalHandle = null;
|
|
6727
|
+
};
|
|
6728
|
+
const matchesProjectPath = (candidatePath) => {
|
|
6729
|
+
try {
|
|
6730
|
+
const firstLine = (0, import_fs12.readFileSync)(candidatePath, "utf8").split("\n", 1)[0];
|
|
6731
|
+
if (!firstLine) return null;
|
|
6732
|
+
const parsed = JSON.parse(firstLine);
|
|
6733
|
+
if (parsed?.type !== "session_meta") return null;
|
|
6734
|
+
const payload = parsed.payload ?? {};
|
|
6735
|
+
if (payload.cwd !== projectPath) return null;
|
|
6736
|
+
if (typeof payload.id !== "string") return null;
|
|
6737
|
+
const createdIso = payload.timestamp ?? parsed.timestamp;
|
|
6738
|
+
const createdAtMs = typeof createdIso === "string" ? Date.parse(createdIso) : Number.NaN;
|
|
6739
|
+
if (Number.isNaN(createdAtMs) || createdAtMs < sessionStartedAtMs) return null;
|
|
6740
|
+
return { id: payload.id, createdAtMs };
|
|
6741
|
+
} catch {
|
|
6742
|
+
return null;
|
|
6743
|
+
}
|
|
6744
|
+
};
|
|
6745
|
+
const tryWire = () => {
|
|
6746
|
+
if (!this.ptyManager.hasSession(sessionId)) {
|
|
6747
|
+
cleanup();
|
|
6748
|
+
return;
|
|
6749
|
+
}
|
|
6750
|
+
if (Date.now() > deadline) {
|
|
6751
|
+
cleanup();
|
|
6752
|
+
return;
|
|
6753
|
+
}
|
|
6754
|
+
const boundElsewhere = new Set(
|
|
6755
|
+
this.sessionStore.listManaged().filter((s) => s.id !== sessionId && s.boundConversationId != null).map((s) => s.boundConversationId)
|
|
6756
|
+
);
|
|
6757
|
+
for (const root of this.codexRoots) {
|
|
6758
|
+
const sessionsDir = (0, import_path13.join)(root, dateDir);
|
|
6759
|
+
if (!(0, import_fs12.existsSync)(sessionsDir)) continue;
|
|
6760
|
+
let candidateFiles;
|
|
6761
|
+
try {
|
|
6762
|
+
candidateFiles = (0, import_fs12.readdirSync)(sessionsDir).filter((f) => f.endsWith(".jsonl"));
|
|
6763
|
+
} catch {
|
|
6764
|
+
continue;
|
|
6765
|
+
}
|
|
6766
|
+
const nowMs = Date.now();
|
|
6767
|
+
const recentCandidates = candidateFiles.map((f) => ({ f, mtime: (0, import_fs12.statSync)((0, import_path13.join)(sessionsDir, f)).mtimeMs })).filter(({ mtime }) => nowMs - mtime < 1e4).sort((a, b) => b.mtime - a.mtime);
|
|
6768
|
+
for (const { f } of recentCandidates) {
|
|
6769
|
+
const candidatePath = (0, import_path13.join)(sessionsDir, f);
|
|
6770
|
+
const match = matchesProjectPath(candidatePath);
|
|
6771
|
+
if (!match) continue;
|
|
6772
|
+
if (boundElsewhere.has(match.id)) continue;
|
|
6773
|
+
const codexSessionId = match.id;
|
|
6774
|
+
cleanup();
|
|
6775
|
+
this.sessionStore.updateManaged(sessionId, { boundConversationId: codexSessionId });
|
|
6776
|
+
this.sessionFileMap.set(sessionId, candidatePath);
|
|
6777
|
+
this.fileWatcher.watch(candidatePath);
|
|
6778
|
+
try {
|
|
6779
|
+
const existing = (0, import_fs12.readFileSync)(candidatePath, "utf8").split("\n").filter(Boolean);
|
|
6780
|
+
if (existing.length > 0) {
|
|
6781
|
+
this.wsHub.broadcast({ type: "conversation_events", sessionId, lines: existing });
|
|
6782
|
+
for (const line of existing) {
|
|
6783
|
+
this.wsHub.broadcast({ type: "conversation_event", sessionId, line });
|
|
6784
|
+
}
|
|
6785
|
+
}
|
|
6786
|
+
} catch {
|
|
6787
|
+
}
|
|
6788
|
+
if (this.scannerReady) {
|
|
6789
|
+
this.scannerStale = true;
|
|
6790
|
+
} else {
|
|
6791
|
+
this.scanner = null;
|
|
6792
|
+
}
|
|
6793
|
+
this.linkSessionToProject(sessionId, projectPath, candidatePath);
|
|
6794
|
+
this.cache?.markAsStreamer(sessionId);
|
|
6795
|
+
const resp = this.sessionStore.get(sessionId, this.ptyAttachedIds());
|
|
6796
|
+
if (resp) {
|
|
6797
|
+
this.wsHub.broadcast({ type: "session_update", session: resp });
|
|
6798
|
+
}
|
|
6799
|
+
this.log.info(
|
|
6800
|
+
`[startFresh] bound Codex rollout for ${sessionId}`,
|
|
6801
|
+
{
|
|
6802
|
+
event: "session.codex_rollout_bound",
|
|
6803
|
+
sessionId,
|
|
6804
|
+
boundConversationId: codexSessionId,
|
|
6805
|
+
filePath: candidatePath
|
|
6806
|
+
},
|
|
6807
|
+
"pino"
|
|
6808
|
+
);
|
|
6809
|
+
return;
|
|
6810
|
+
}
|
|
6811
|
+
}
|
|
6812
|
+
};
|
|
6813
|
+
tryWire();
|
|
6814
|
+
if (!intervalHandle && Date.now() <= deadline) {
|
|
6815
|
+
const alreadyBound = this.sessionStore.getManaged(sessionId)?.boundConversationId != null;
|
|
6816
|
+
if (!alreadyBound) {
|
|
6817
|
+
intervalHandle = setInterval(tryWire, 250);
|
|
6818
|
+
}
|
|
6819
|
+
}
|
|
6820
|
+
}
|
|
6048
6821
|
async handleBrowse(url, res) {
|
|
6049
6822
|
if (!this.browseRoot) {
|
|
6050
6823
|
json(res, 403, {
|
|
@@ -6128,7 +6901,7 @@ var StreamerServer = class {
|
|
|
6128
6901
|
};
|
|
6129
6902
|
function classifyResumability(cwd) {
|
|
6130
6903
|
if (!cwd) return { resumable: true };
|
|
6131
|
-
if ((0,
|
|
6904
|
+
if ((0, import_fs12.existsSync)(cwd)) return { resumable: true };
|
|
6132
6905
|
const ranInWorktree = /\/\.worktrees\//.test(cwd) || /\/\.claude\/worktrees\//.test(cwd);
|
|
6133
6906
|
return {
|
|
6134
6907
|
resumable: false,
|
|
@@ -6260,7 +7033,10 @@ function readBody(req) {
|
|
|
6260
7033
|
}
|
|
6261
7034
|
// Annotate the CommonJS export names for ESM import in node:
|
|
6262
7035
|
0 && (module.exports = {
|
|
7036
|
+
CLAUDE_CODE_PROVIDER,
|
|
7037
|
+
CODEX_CLI_PROVIDER,
|
|
6263
7038
|
ConversationWatcher,
|
|
7039
|
+
LiveSessionManager,
|
|
6264
7040
|
PTYManager,
|
|
6265
7041
|
SessionStore,
|
|
6266
7042
|
StreamerServer,
|
|
@@ -6274,6 +7050,8 @@ function readBody(req) {
|
|
|
6274
7050
|
generateApiKey,
|
|
6275
7051
|
getDbConfig,
|
|
6276
7052
|
isDbEnabled,
|
|
7053
|
+
isProviderName,
|
|
7054
|
+
isProviderResumable,
|
|
6277
7055
|
loadOrCreateApiKey,
|
|
6278
7056
|
maskConnectionString,
|
|
6279
7057
|
readAgentConfig,
|