@threadbase-sh/streamer 1.23.1 → 1.24.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/cli.cjs +1175 -350
- package/dist/cli.cjs.map +1 -1
- package/dist/index.cjs +1157 -374
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +89 -28
- package/dist/index.d.ts +89 -28
- package/dist/index.js +1027 -249
- package/dist/index.js.map +1 -1
- package/package.json +1 -1
package/dist/index.js
CHANGED
|
@@ -480,10 +480,50 @@ async function createPool(config) {
|
|
|
480
480
|
return new Pool(poolConfig);
|
|
481
481
|
}
|
|
482
482
|
|
|
483
|
-
// src/
|
|
484
|
-
import {
|
|
485
|
-
|
|
486
|
-
|
|
483
|
+
// src/live-session-manager.ts
|
|
484
|
+
import { basename as basename3 } from "path";
|
|
485
|
+
|
|
486
|
+
// src/codex-pty-runner.ts
|
|
487
|
+
import { Terminal } from "@xterm/headless";
|
|
488
|
+
import { randomUUID } from "crypto";
|
|
489
|
+
import { existsSync as existsSync2 } from "fs";
|
|
490
|
+
import { basename } from "path";
|
|
491
|
+
|
|
492
|
+
// src/logger.ts
|
|
493
|
+
import pino from "pino";
|
|
494
|
+
var baseLogger = pino({
|
|
495
|
+
level: process.env.LOG_LEVEL ?? "info",
|
|
496
|
+
base: { service: "tb-streamer" },
|
|
497
|
+
timestamp: pino.stdTimeFunctions.isoTime,
|
|
498
|
+
redact: {
|
|
499
|
+
paths: ["req.headers.authorization", "req.headers.cookie", 'req.headers["x-api-key"]'],
|
|
500
|
+
censor: "[redacted]"
|
|
501
|
+
}
|
|
502
|
+
});
|
|
503
|
+
function emit(pinoChild, level, msg, fields, dest) {
|
|
504
|
+
if (dest === "pino" || dest === "both") {
|
|
505
|
+
if (fields && Object.keys(fields).length > 0) pinoChild[level](fields, msg);
|
|
506
|
+
else pinoChild[level](msg);
|
|
507
|
+
}
|
|
508
|
+
if (dest === "console" || dest === "both") {
|
|
509
|
+
const consoleMethod = level === "error" ? "error" : level === "warn" ? "warn" : "log";
|
|
510
|
+
console[consoleMethod](msg);
|
|
511
|
+
}
|
|
512
|
+
}
|
|
513
|
+
function build(pinoChild) {
|
|
514
|
+
return {
|
|
515
|
+
debug: (m, f, d = "both") => emit(pinoChild, "debug", m, f, d),
|
|
516
|
+
info: (m, f, d = "both") => emit(pinoChild, "info", m, f, d),
|
|
517
|
+
warn: (m, f, d = "both") => emit(pinoChild, "warn", m, f, d),
|
|
518
|
+
error: (m, f, d = "both") => emit(pinoChild, "error", m, f, d),
|
|
519
|
+
log: (lvl, m, f, d = "both") => emit(pinoChild, lvl, m, f, d),
|
|
520
|
+
pino: pinoChild
|
|
521
|
+
};
|
|
522
|
+
}
|
|
523
|
+
function getLogger(component) {
|
|
524
|
+
return build(component ? baseLogger.child({ component }) : baseLogger);
|
|
525
|
+
}
|
|
526
|
+
var logger = build(baseLogger);
|
|
487
527
|
|
|
488
528
|
// src/platform.ts
|
|
489
529
|
import { execFileSync } from "child_process";
|
|
@@ -549,188 +589,534 @@ function resolveClaudeExe() {
|
|
|
549
589
|
_claudeExe = "claude";
|
|
550
590
|
return _claudeExe;
|
|
551
591
|
}
|
|
592
|
+
var _codexExe;
|
|
593
|
+
function resolveCodexExe() {
|
|
594
|
+
if (_codexExe !== void 0) return _codexExe;
|
|
595
|
+
if (isWindows) {
|
|
596
|
+
try {
|
|
597
|
+
const found = execFileSync("where.exe", ["codex"], {
|
|
598
|
+
encoding: "utf-8",
|
|
599
|
+
windowsHide: true,
|
|
600
|
+
timeout: 3e3
|
|
601
|
+
}).trim().split("\n")[0].trim();
|
|
602
|
+
if (found) {
|
|
603
|
+
_codexExe = found;
|
|
604
|
+
return _codexExe;
|
|
605
|
+
}
|
|
606
|
+
} catch {
|
|
607
|
+
}
|
|
608
|
+
const candidates = [
|
|
609
|
+
join4(homedir2(), ".local", "bin", "codex.exe"),
|
|
610
|
+
join4(
|
|
611
|
+
process.env.LOCALAPPDATA ?? join4(homedir2(), "AppData", "Local"),
|
|
612
|
+
"Microsoft",
|
|
613
|
+
"WindowsApps",
|
|
614
|
+
"codex.exe"
|
|
615
|
+
)
|
|
616
|
+
];
|
|
617
|
+
for (const p of candidates) {
|
|
618
|
+
if (existsSync(p)) {
|
|
619
|
+
_codexExe = p;
|
|
620
|
+
return _codexExe;
|
|
621
|
+
}
|
|
622
|
+
}
|
|
623
|
+
} else {
|
|
624
|
+
try {
|
|
625
|
+
const found = execFileSync("/usr/bin/which", ["codex"], {
|
|
626
|
+
encoding: "utf-8",
|
|
627
|
+
timeout: 3e3
|
|
628
|
+
}).trim().split("\n")[0].trim();
|
|
629
|
+
if (found && existsSync(found)) {
|
|
630
|
+
_codexExe = found;
|
|
631
|
+
return _codexExe;
|
|
632
|
+
}
|
|
633
|
+
} catch {
|
|
634
|
+
}
|
|
635
|
+
const candidates = [
|
|
636
|
+
"/opt/homebrew/bin/codex",
|
|
637
|
+
"/usr/local/bin/codex",
|
|
638
|
+
join4(homedir2(), ".local", "bin", "codex")
|
|
639
|
+
];
|
|
640
|
+
for (const p of candidates) {
|
|
641
|
+
if (existsSync(p)) {
|
|
642
|
+
_codexExe = p;
|
|
643
|
+
return _codexExe;
|
|
644
|
+
}
|
|
645
|
+
}
|
|
646
|
+
}
|
|
647
|
+
_codexExe = "codex";
|
|
648
|
+
return _codexExe;
|
|
649
|
+
}
|
|
552
650
|
|
|
553
|
-
// src/
|
|
554
|
-
|
|
555
|
-
|
|
556
|
-
|
|
651
|
+
// src/providers.ts
|
|
652
|
+
var CLAUDE_CODE_PROVIDER = "claude-code";
|
|
653
|
+
var CODEX_CLI_PROVIDER = "codex-cli";
|
|
654
|
+
function isProviderName(value) {
|
|
655
|
+
return value === CLAUDE_CODE_PROVIDER || value === CODEX_CLI_PROVIDER;
|
|
557
656
|
}
|
|
558
|
-
|
|
559
|
-
|
|
560
|
-
const results = await Promise.all(
|
|
561
|
-
pids.map(async (pid) => {
|
|
562
|
-
try {
|
|
563
|
-
const [cwd, args, startedAt] = await Promise.all([
|
|
564
|
-
getProcessCwdUnix(pid),
|
|
565
|
-
getProcessArgsUnix(pid),
|
|
566
|
-
getProcessStartTimeUnix(pid)
|
|
567
|
-
]);
|
|
568
|
-
const conversationId = extractResumeId(args);
|
|
569
|
-
return {
|
|
570
|
-
pid,
|
|
571
|
-
projectPath: cwd,
|
|
572
|
-
projectName: basename(cwd),
|
|
573
|
-
branch: await readGitBranch(cwd),
|
|
574
|
-
conversationId,
|
|
575
|
-
startedAt
|
|
576
|
-
};
|
|
577
|
-
} catch {
|
|
578
|
-
return null;
|
|
579
|
-
}
|
|
580
|
-
})
|
|
581
|
-
);
|
|
582
|
-
return results.filter((r) => r !== null);
|
|
657
|
+
function isProviderResumable(_provider, availabilityResumable) {
|
|
658
|
+
return availabilityResumable;
|
|
583
659
|
}
|
|
584
|
-
|
|
585
|
-
|
|
586
|
-
|
|
587
|
-
|
|
660
|
+
|
|
661
|
+
// src/codex-pty-runner.ts
|
|
662
|
+
var OUTPUT_BUFFER_MAX = 65536;
|
|
663
|
+
var PTY_COLS = 120;
|
|
664
|
+
var PTY_ROWS = 40;
|
|
665
|
+
var SCREEN_SCROLLBACK = 1e3;
|
|
666
|
+
var CODEX_PROMPT_READY_TEXT = "Ready";
|
|
667
|
+
var CODEX_TRUST_GATE_REGEX = /trust the contents/i;
|
|
668
|
+
var SUBMIT_BYTES = "\r";
|
|
669
|
+
var CODEX_SUBMIT_DELAY_MS = 16;
|
|
670
|
+
function digestBytes(s) {
|
|
671
|
+
const escaped = s.replace(new RegExp(String.fromCharCode(27), "g"), "\\x1b").replace(/\r/g, "\\r").replace(/\n/g, "\\n").replace(/\t/g, "\\t");
|
|
672
|
+
if (escaped.length <= 200) return escaped;
|
|
673
|
+
return `${escaped.slice(0, 100)}\u2026[${escaped.length - 200}B omitted]\u2026${escaped.slice(-100)}`;
|
|
674
|
+
}
|
|
675
|
+
var pty = null;
|
|
676
|
+
async function loadPty() {
|
|
677
|
+
if (pty) return pty;
|
|
678
|
+
try {
|
|
679
|
+
pty = await import("node-pty");
|
|
680
|
+
return pty;
|
|
681
|
+
} catch (err) {
|
|
682
|
+
throw new Error(
|
|
683
|
+
`node-pty is required for PTY management but failed to load. Ensure it is installed: npm install node-pty
|
|
684
|
+
Original error: ${err}`
|
|
685
|
+
);
|
|
686
|
+
}
|
|
687
|
+
}
|
|
688
|
+
function createScreen() {
|
|
689
|
+
return new Terminal({
|
|
690
|
+
cols: PTY_COLS,
|
|
691
|
+
rows: PTY_ROWS,
|
|
692
|
+
scrollback: SCREEN_SCROLLBACK,
|
|
693
|
+
allowProposedApi: true
|
|
694
|
+
});
|
|
695
|
+
}
|
|
696
|
+
var CodexPtyRunner = class {
|
|
697
|
+
sessions = /* @__PURE__ */ new Map();
|
|
698
|
+
onOutput;
|
|
699
|
+
onStatusChange;
|
|
700
|
+
onReady;
|
|
701
|
+
// Accepted for shape-compatibility with PTYManagerOptions; Codex has no
|
|
702
|
+
// detected equivalent yet (Phase 0) — never invoked.
|
|
703
|
+
onPermissionChange;
|
|
704
|
+
onLiveQuestion;
|
|
705
|
+
onLiveQuestionGone;
|
|
706
|
+
log;
|
|
707
|
+
// Tracks sessions whose PTY has spawned but Codex hasn't yet reached its
|
|
708
|
+
// "Ready" status bar — i.e. onReady hasn't fired.
|
|
709
|
+
pendingReady = /* @__PURE__ */ new Set();
|
|
710
|
+
// Inputs received via sendInput() while the session was still pendingReady.
|
|
711
|
+
// Flushed in arrival order once Codex reaches Ready.
|
|
712
|
+
queuedInputs = /* @__PURE__ */ new Map();
|
|
713
|
+
// Per-session debounce so the directory-trust gate's \r is only written once.
|
|
714
|
+
trustGateAnswered = /* @__PURE__ */ new Set();
|
|
715
|
+
// In-flight start()/startFresh() calls keyed by sessionId. A second
|
|
716
|
+
// concurrent resume for the same session (double-tap, client retry) awaits
|
|
717
|
+
// the first call's promise instead of spawning a duplicate PTY (CRITICAL #3).
|
|
718
|
+
startPromises = /* @__PURE__ */ new Map();
|
|
719
|
+
constructor(options = {}) {
|
|
720
|
+
this.onOutput = options.onOutput;
|
|
721
|
+
this.onStatusChange = options.onStatusChange;
|
|
722
|
+
this.onReady = options.onReady;
|
|
723
|
+
this.onPermissionChange = options.onPermissionChange;
|
|
724
|
+
this.onLiveQuestion = options.onLiveQuestion;
|
|
725
|
+
this.onLiveQuestionGone = options.onLiveQuestionGone;
|
|
726
|
+
this.log = options.logger ?? getLogger("codex-pty");
|
|
727
|
+
}
|
|
728
|
+
// Resume an existing Codex session. sessionId is the Codex-persisted
|
|
729
|
+
// session_meta.payload.id (Phase 0, Section 8) — Codex has no fresh-session
|
|
730
|
+
// equivalent of --session-id, so start() always means "resume".
|
|
731
|
+
async start(sessionId, options) {
|
|
732
|
+
const existing = this.sessions.get(sessionId);
|
|
733
|
+
if (existing) return toPublicSession(existing);
|
|
734
|
+
const inFlight = this.startPromises.get(sessionId);
|
|
735
|
+
if (inFlight) return inFlight;
|
|
736
|
+
const promise = this.doStart(sessionId, options).finally(() => {
|
|
737
|
+
this.startPromises.delete(sessionId);
|
|
738
|
+
});
|
|
739
|
+
this.startPromises.set(sessionId, promise);
|
|
740
|
+
return promise;
|
|
741
|
+
}
|
|
742
|
+
async doStart(sessionId, options) {
|
|
743
|
+
const nodePty = await loadPty();
|
|
744
|
+
const projectName = options.projectName ?? basename(options.projectPath);
|
|
745
|
+
const proc = nodePty.spawn(
|
|
746
|
+
resolveCodexExe(),
|
|
747
|
+
["resume", sessionId, "--cd", options.projectPath, "--no-alt-screen"],
|
|
748
|
+
{
|
|
749
|
+
name: "xterm-256color",
|
|
750
|
+
cols: PTY_COLS,
|
|
751
|
+
rows: PTY_ROWS,
|
|
752
|
+
cwd: options.projectPath,
|
|
753
|
+
env: process.env
|
|
754
|
+
}
|
|
755
|
+
);
|
|
756
|
+
const session = {
|
|
757
|
+
id: sessionId,
|
|
758
|
+
provider: CODEX_CLI_PROVIDER,
|
|
759
|
+
projectPath: options.projectPath,
|
|
760
|
+
projectName,
|
|
761
|
+
branch: options.branch ?? "",
|
|
762
|
+
status: "running",
|
|
763
|
+
startedAt: /* @__PURE__ */ new Date(),
|
|
764
|
+
completedAt: null,
|
|
765
|
+
promptCount: 0,
|
|
766
|
+
lastOutput: "",
|
|
767
|
+
process: proc,
|
|
768
|
+
outputBuffer: Buffer.alloc(0),
|
|
769
|
+
screen: createScreen()
|
|
770
|
+
};
|
|
771
|
+
this.sessions.set(sessionId, session);
|
|
772
|
+
this.pendingReady.add(sessionId);
|
|
773
|
+
proc.onData((data) => {
|
|
774
|
+
this.handleOutput(sessionId, data);
|
|
775
|
+
});
|
|
776
|
+
proc.onExit(({ exitCode }) => {
|
|
777
|
+
this.pendingReady.delete(sessionId);
|
|
778
|
+
this.handleExit(sessionId, exitCode);
|
|
779
|
+
});
|
|
780
|
+
return toPublicSession(session);
|
|
781
|
+
}
|
|
782
|
+
// Start a brand-new Codex session. Codex has no --session-id equivalent for
|
|
783
|
+
// a fresh launch — it assigns its own id, discovered later (Task 3's
|
|
784
|
+
// binding logic). This runner generates a local placeholder id for the
|
|
785
|
+
// ManagedSession handle only.
|
|
786
|
+
async startFresh(options) {
|
|
787
|
+
const nodePty = await loadPty();
|
|
788
|
+
const sessionId = randomUUID();
|
|
789
|
+
const projectName = options.projectName ?? basename(options.projectPath);
|
|
790
|
+
const args = ["--cd", options.projectPath, "--no-alt-screen"];
|
|
791
|
+
if (options.systemPrompt) {
|
|
792
|
+
args.push(options.systemPrompt);
|
|
793
|
+
}
|
|
794
|
+
const proc = nodePty.spawn(resolveCodexExe(), args, {
|
|
795
|
+
name: "xterm-256color",
|
|
796
|
+
cols: PTY_COLS,
|
|
797
|
+
rows: PTY_ROWS,
|
|
798
|
+
cwd: options.projectPath,
|
|
799
|
+
env: process.env
|
|
800
|
+
});
|
|
801
|
+
const session = {
|
|
802
|
+
id: sessionId,
|
|
803
|
+
provider: CODEX_CLI_PROVIDER,
|
|
804
|
+
projectPath: options.projectPath,
|
|
805
|
+
projectName,
|
|
806
|
+
branch: "",
|
|
807
|
+
status: "running",
|
|
808
|
+
startedAt: /* @__PURE__ */ new Date(),
|
|
809
|
+
completedAt: null,
|
|
810
|
+
promptCount: 0,
|
|
811
|
+
lastOutput: "",
|
|
812
|
+
process: proc,
|
|
813
|
+
outputBuffer: Buffer.alloc(0),
|
|
814
|
+
screen: createScreen()
|
|
815
|
+
};
|
|
816
|
+
this.sessions.set(sessionId, session);
|
|
817
|
+
this.pendingReady.add(sessionId);
|
|
818
|
+
proc.onData((data) => {
|
|
819
|
+
this.handleOutput(sessionId, data);
|
|
820
|
+
});
|
|
821
|
+
proc.onExit(({ exitCode }) => {
|
|
822
|
+
this.pendingReady.delete(sessionId);
|
|
823
|
+
this.handleExit(sessionId, exitCode);
|
|
824
|
+
});
|
|
825
|
+
return toPublicSession(session);
|
|
826
|
+
}
|
|
827
|
+
// Write raw key bytes directly to the PTY, same as PTYManager.sendKeys.
|
|
828
|
+
sendKeys(sessionId, keys) {
|
|
829
|
+
const session = this.sessions.get(sessionId);
|
|
830
|
+
if (!session) throw new Error(`Session not found: ${sessionId}`);
|
|
831
|
+
if (session.status === "idle") {
|
|
832
|
+
throw new Error(`Session is idle (no active PTY): ${sessionId}`);
|
|
833
|
+
}
|
|
834
|
+
if (session.status === "waiting_input") {
|
|
835
|
+
session.status = "running";
|
|
836
|
+
this.onStatusChange?.(toPublicSession(session));
|
|
837
|
+
}
|
|
838
|
+
this.log.info(
|
|
839
|
+
`[codex.keys.write] ${sessionId.slice(0, 8)} bytes=${keys.length} digest=${digestBytes(keys)}`,
|
|
840
|
+
{ event: "codex.keys_write", sessionId, byteLen: keys.length }
|
|
841
|
+
);
|
|
842
|
+
session.process.write(keys);
|
|
843
|
+
session.lastActivityAt = /* @__PURE__ */ new Date();
|
|
844
|
+
}
|
|
845
|
+
sendInput(sessionId, input) {
|
|
846
|
+
const session = this.sessions.get(sessionId);
|
|
847
|
+
if (!session) throw new Error(`Session not found: ${sessionId}`);
|
|
848
|
+
if (session.status === "idle") {
|
|
849
|
+
throw new Error(`Session is idle (no active PTY): ${sessionId}`);
|
|
850
|
+
}
|
|
851
|
+
if (this.pendingReady.has(sessionId)) {
|
|
852
|
+
const queue = this.queuedInputs.get(sessionId) ?? [];
|
|
853
|
+
queue.push(input);
|
|
854
|
+
this.queuedInputs.set(sessionId, queue);
|
|
855
|
+
session.lastActivityAt = /* @__PURE__ */ new Date();
|
|
856
|
+
session.promptCount++;
|
|
857
|
+
this.log.warn(
|
|
858
|
+
`[codex.input.queued] ${sessionId.slice(0, 8)} promptCount=${session.promptCount} queueLen=${queue.length}`,
|
|
859
|
+
{
|
|
860
|
+
event: "codex.input_queued",
|
|
861
|
+
sessionId,
|
|
862
|
+
promptCount: session.promptCount,
|
|
863
|
+
queueLen: queue.length,
|
|
864
|
+
inputLen: input.length
|
|
865
|
+
}
|
|
866
|
+
);
|
|
867
|
+
return session.promptCount;
|
|
868
|
+
}
|
|
869
|
+
if (session.status === "waiting_input") {
|
|
870
|
+
session.status = "running";
|
|
871
|
+
this.onStatusChange?.(toPublicSession(session));
|
|
872
|
+
}
|
|
873
|
+
this.writeSubmit(sessionId, session, input, "direct", session.promptCount + 1);
|
|
874
|
+
session.lastActivityAt = /* @__PURE__ */ new Date();
|
|
875
|
+
session.promptCount++;
|
|
876
|
+
return session.promptCount;
|
|
877
|
+
}
|
|
878
|
+
// Write the input as plain bytes (no bracketed-paste wrap — Phase 0
|
|
879
|
+
// confirmed Codex accepts plain keystrokes), then submit \r after a short
|
|
880
|
+
// delay so Codex's TUI gets an event-loop tick to process the input first.
|
|
881
|
+
writeSubmit(sessionId, session, input, path, promptCount) {
|
|
882
|
+
this.log.info(
|
|
883
|
+
`[codex.input.write] ${sessionId.slice(0, 8)} promptCount=${promptCount} bytes=${input.length} digest=${digestBytes(input)}`,
|
|
884
|
+
{
|
|
885
|
+
event: "codex.input_write",
|
|
886
|
+
sessionId,
|
|
887
|
+
promptCount,
|
|
888
|
+
byteLen: input.length,
|
|
889
|
+
digest: digestBytes(input),
|
|
890
|
+
path,
|
|
891
|
+
phase: "input"
|
|
892
|
+
}
|
|
893
|
+
);
|
|
894
|
+
session.process.write(input);
|
|
895
|
+
setTimeout(() => {
|
|
896
|
+
const current = this.sessions.get(sessionId);
|
|
897
|
+
if (!current || current !== session) return;
|
|
898
|
+
this.log.info(
|
|
899
|
+
`[codex.input.submit] ${sessionId.slice(0, 8)} promptCount=${promptCount} digest=\\r`,
|
|
900
|
+
{
|
|
901
|
+
event: "codex.input_write",
|
|
902
|
+
sessionId,
|
|
903
|
+
promptCount,
|
|
904
|
+
byteLen: SUBMIT_BYTES.length,
|
|
905
|
+
digest: "\\r",
|
|
906
|
+
path,
|
|
907
|
+
phase: "submit"
|
|
908
|
+
}
|
|
909
|
+
);
|
|
910
|
+
current.process.write(SUBMIT_BYTES);
|
|
911
|
+
}, CODEX_SUBMIT_DELAY_MS);
|
|
912
|
+
}
|
|
913
|
+
// Drain any inputs sent while the session was still pendingReady, writing
|
|
914
|
+
// them in arrival order now that Codex is Ready.
|
|
915
|
+
flushQueuedInputs(sessionId) {
|
|
916
|
+
const queue = this.queuedInputs.get(sessionId);
|
|
917
|
+
if (!queue || queue.length === 0) return;
|
|
918
|
+
this.queuedInputs.delete(sessionId);
|
|
919
|
+
const session = this.sessions.get(sessionId);
|
|
920
|
+
if (!session) return;
|
|
921
|
+
this.log.info(
|
|
922
|
+
`[codex.flush] ${sessionId.slice(0, 8)} flushing ${queue.length} queued input(s)`,
|
|
923
|
+
{
|
|
924
|
+
event: "codex.flush_queued",
|
|
925
|
+
sessionId,
|
|
926
|
+
queueLen: queue.length
|
|
927
|
+
}
|
|
928
|
+
);
|
|
929
|
+
queue.forEach((input, i) => {
|
|
930
|
+
const writeAt = i * CODEX_SUBMIT_DELAY_MS * 2;
|
|
931
|
+
if (writeAt === 0) {
|
|
932
|
+
this.writeSubmit(sessionId, session, input, "flush", session.promptCount);
|
|
933
|
+
} else {
|
|
934
|
+
setTimeout(() => {
|
|
935
|
+
const current = this.sessions.get(sessionId);
|
|
936
|
+
if (!current || current !== session) return;
|
|
937
|
+
this.writeSubmit(sessionId, session, input, "flush", session.promptCount);
|
|
938
|
+
}, writeAt);
|
|
939
|
+
}
|
|
940
|
+
});
|
|
941
|
+
}
|
|
942
|
+
// SIGINT produces a clean exitCode=0 exit (Phase 0 — confirmed).
|
|
943
|
+
cancel(sessionId) {
|
|
944
|
+
const session = this.sessions.get(sessionId);
|
|
945
|
+
if (!session) throw new Error(`Session not found: ${sessionId}`);
|
|
946
|
+
session.process.kill("SIGINT");
|
|
947
|
+
}
|
|
948
|
+
killPid(pid) {
|
|
949
|
+
try {
|
|
950
|
+
process.kill(pid, "SIGTERM");
|
|
951
|
+
} catch {
|
|
952
|
+
}
|
|
953
|
+
}
|
|
954
|
+
// Kill the PTY and mark the session idle. Mirrors PTYManager.putOnHold.
|
|
955
|
+
putOnHold(sessionId) {
|
|
956
|
+
const session = this.sessions.get(sessionId);
|
|
957
|
+
if (!session) return;
|
|
958
|
+
this.pendingReady.delete(sessionId);
|
|
959
|
+
this.queuedInputs.delete(sessionId);
|
|
960
|
+
this.trustGateAnswered.delete(sessionId);
|
|
961
|
+
try {
|
|
962
|
+
session.process.kill("SIGINT");
|
|
963
|
+
} catch {
|
|
964
|
+
}
|
|
965
|
+
session.status = "idle";
|
|
966
|
+
session.completedAt = /* @__PURE__ */ new Date();
|
|
967
|
+
session.screen.dispose();
|
|
968
|
+
this.sessions.delete(sessionId);
|
|
969
|
+
this.onStatusChange?.(toPublicSession(session));
|
|
970
|
+
}
|
|
971
|
+
getOutput(sessionId) {
|
|
972
|
+
const session = this.sessions.get(sessionId);
|
|
973
|
+
if (!session) throw new Error(`Session not found: ${sessionId}`);
|
|
974
|
+
return session.outputBuffer.toString("utf-8");
|
|
975
|
+
}
|
|
976
|
+
// Render the last `maxLines` rows of the session's screen in true on-screen
|
|
977
|
+
// order — same flush-then-read technique as PTYManager.getOutputLines.
|
|
978
|
+
async getOutputLines(sessionId, maxLines) {
|
|
979
|
+
const session = this.sessions.get(sessionId);
|
|
980
|
+
if (!session) throw new Error(`Session not found: ${sessionId}`);
|
|
981
|
+
await new Promise((resolve2) => session.screen.write("", () => resolve2()));
|
|
982
|
+
const buf = session.screen.buffer.active;
|
|
983
|
+
const lines = [];
|
|
984
|
+
for (let y = 0; y < buf.length; y++) {
|
|
985
|
+
lines.push(buf.getLine(y)?.translateToString(true) ?? "");
|
|
986
|
+
}
|
|
987
|
+
while (lines.length > 0 && lines[lines.length - 1] === "") {
|
|
988
|
+
lines.pop();
|
|
989
|
+
}
|
|
990
|
+
return lines.slice(-maxLines);
|
|
991
|
+
}
|
|
992
|
+
getSession(sessionId) {
|
|
993
|
+
const session = this.sessions.get(sessionId);
|
|
994
|
+
return session ? toPublicSession(session) : null;
|
|
995
|
+
}
|
|
996
|
+
hasSession(sessionId) {
|
|
997
|
+
return this.sessions.has(sessionId);
|
|
998
|
+
}
|
|
999
|
+
listSessions() {
|
|
1000
|
+
return Array.from(this.sessions.values()).map(toPublicSession);
|
|
1001
|
+
}
|
|
1002
|
+
dispose() {
|
|
1003
|
+
for (const session of this.sessions.values()) {
|
|
588
1004
|
try {
|
|
589
|
-
|
|
590
|
-
if (!info) return null;
|
|
591
|
-
return {
|
|
592
|
-
pid,
|
|
593
|
-
projectPath: info.cwd,
|
|
594
|
-
projectName: basename(info.cwd),
|
|
595
|
-
branch: await readGitBranch(info.cwd),
|
|
596
|
-
conversationId: extractResumeId(info.args),
|
|
597
|
-
startedAt: info.startedAt
|
|
598
|
-
};
|
|
1005
|
+
session.process.kill();
|
|
599
1006
|
} catch {
|
|
600
|
-
return null;
|
|
601
|
-
}
|
|
602
|
-
})
|
|
603
|
-
);
|
|
604
|
-
return results.filter((r) => r !== null);
|
|
605
|
-
}
|
|
606
|
-
function run(cmd, args, opts = {}) {
|
|
607
|
-
return new Promise((resolve2, reject) => {
|
|
608
|
-
execFile(
|
|
609
|
-
cmd,
|
|
610
|
-
args,
|
|
611
|
-
{ windowsHide: isWindows, encoding: "utf-8", timeout: opts.timeout ?? 5e3, cwd: opts.cwd },
|
|
612
|
-
(err, stdout) => {
|
|
613
|
-
if (err) reject(err);
|
|
614
|
-
else resolve2(stdout);
|
|
615
1007
|
}
|
|
616
|
-
|
|
617
|
-
|
|
618
|
-
|
|
619
|
-
|
|
620
|
-
|
|
621
|
-
|
|
622
|
-
return output.trim().split("\n").filter(Boolean).map((s) => Number.parseInt(s, 10));
|
|
623
|
-
} catch {
|
|
624
|
-
return [];
|
|
625
|
-
}
|
|
626
|
-
}
|
|
627
|
-
async function getProcessCwdUnix(pid) {
|
|
628
|
-
const output = await run("lsof", ["-p", String(pid), "-a", "-d", "cwd", "-Fn"]);
|
|
629
|
-
const match = output.match(/n(.+)/);
|
|
630
|
-
return match?.[1] ?? "";
|
|
631
|
-
}
|
|
632
|
-
async function getProcessArgsUnix(pid) {
|
|
633
|
-
return (await run("ps", ["-p", String(pid), "-o", "args="])).trim();
|
|
634
|
-
}
|
|
635
|
-
async function getProcessStartTimeUnix(pid) {
|
|
636
|
-
const raw = (await run("ps", ["-p", String(pid), "-o", "lstart="])).trim();
|
|
637
|
-
const d = new Date(raw);
|
|
638
|
-
return Number.isNaN(d.getTime()) ? /* @__PURE__ */ new Date() : d;
|
|
639
|
-
}
|
|
640
|
-
async function getPidsWindows() {
|
|
641
|
-
try {
|
|
642
|
-
const output = await run("tasklist", ["/FI", "IMAGENAME eq claude.exe", "/FO", "CSV", "/NH"]);
|
|
643
|
-
return output.trim().split("\n").filter(Boolean).map((line) => {
|
|
644
|
-
const parts = line.split(",");
|
|
645
|
-
return Number.parseInt(parts[1]?.replace(/"/g, "") ?? "0", 10);
|
|
646
|
-
}).filter((pid) => pid > 0);
|
|
647
|
-
} catch {
|
|
648
|
-
return [];
|
|
649
|
-
}
|
|
650
|
-
}
|
|
651
|
-
async function getProcessInfoWindows(pid) {
|
|
652
|
-
try {
|
|
653
|
-
const output = await run("wmic", [
|
|
654
|
-
"process",
|
|
655
|
-
"where",
|
|
656
|
-
`ProcessId=${pid}`,
|
|
657
|
-
"get",
|
|
658
|
-
"CommandLine,CreationDate,ExecutablePath",
|
|
659
|
-
"/FORMAT:CSV"
|
|
660
|
-
]);
|
|
661
|
-
const lines = output.trim().split(/\r?\n/).filter((l) => l.trim().length > 0);
|
|
662
|
-
if (lines.length < 2) return null;
|
|
663
|
-
const parts = lines[1].split(",");
|
|
664
|
-
const args = parts[1] ?? "";
|
|
665
|
-
const creationDate = parts[2] ?? "";
|
|
666
|
-
const year = creationDate.slice(0, 4);
|
|
667
|
-
const month = creationDate.slice(4, 6);
|
|
668
|
-
const day = creationDate.slice(6, 8);
|
|
669
|
-
const hour = creationDate.slice(8, 10);
|
|
670
|
-
const min = creationDate.slice(10, 12);
|
|
671
|
-
const sec = creationDate.slice(12, 14);
|
|
672
|
-
const startedAt = /* @__PURE__ */ new Date(`${year}-${month}-${day}T${hour}:${min}:${sec}`);
|
|
673
|
-
if (Number.isNaN(startedAt.getTime())) return null;
|
|
674
|
-
const exePath = parts[3] ?? "";
|
|
675
|
-
const cwd = exePath ? dirname3(exePath) : "";
|
|
676
|
-
return { cwd, args, startedAt };
|
|
677
|
-
} catch {
|
|
678
|
-
return null;
|
|
1008
|
+
session.screen.dispose();
|
|
1009
|
+
}
|
|
1010
|
+
this.sessions.clear();
|
|
1011
|
+
this.pendingReady.clear();
|
|
1012
|
+
this.queuedInputs.clear();
|
|
1013
|
+
this.trustGateAnswered.clear();
|
|
679
1014
|
}
|
|
680
|
-
|
|
681
|
-
|
|
682
|
-
|
|
683
|
-
|
|
684
|
-
|
|
685
|
-
|
|
686
|
-
|
|
687
|
-
|
|
688
|
-
|
|
689
|
-
|
|
1015
|
+
handleOutput(sessionId, data) {
|
|
1016
|
+
const session = this.sessions.get(sessionId);
|
|
1017
|
+
if (!session) return;
|
|
1018
|
+
const chunk = Buffer.from(data, "utf-8");
|
|
1019
|
+
session.outputBuffer = Buffer.concat([session.outputBuffer, chunk]);
|
|
1020
|
+
if (session.outputBuffer.length > OUTPUT_BUFFER_MAX) {
|
|
1021
|
+
session.outputBuffer = session.outputBuffer.subarray(
|
|
1022
|
+
session.outputBuffer.length - OUTPUT_BUFFER_MAX
|
|
1023
|
+
);
|
|
1024
|
+
}
|
|
1025
|
+
session.screen.write(data);
|
|
1026
|
+
session.lastOutput = stripAnsi(data);
|
|
1027
|
+
this.onOutput?.(sessionId, data);
|
|
1028
|
+
this.detectReady(sessionId, session).catch((err) => {
|
|
1029
|
+
this.log.warn("[codex.ready_detect] failed", {
|
|
1030
|
+
event: "codex.ready_detect_failed",
|
|
1031
|
+
sessionId,
|
|
1032
|
+
err
|
|
1033
|
+
});
|
|
1034
|
+
});
|
|
690
1035
|
}
|
|
691
|
-
|
|
692
|
-
|
|
693
|
-
//
|
|
694
|
-
|
|
695
|
-
|
|
696
|
-
|
|
697
|
-
|
|
698
|
-
|
|
699
|
-
|
|
700
|
-
|
|
701
|
-
|
|
702
|
-
|
|
703
|
-
|
|
704
|
-
|
|
705
|
-
|
|
706
|
-
|
|
707
|
-
|
|
1036
|
+
// Renders the session's headless screen and checks for the directory-trust
|
|
1037
|
+
// gate (answered once, debounced) and the "Ready" status-bar text. Only
|
|
1038
|
+
// transitions to waiting_input / fires onReady when the rendered status
|
|
1039
|
+
// line literally contains "Ready" — `›` alone (visible during "Starting")
|
|
1040
|
+
// is NOT a valid readiness signal (Phase 0).
|
|
1041
|
+
async detectReady(sessionId, session) {
|
|
1042
|
+
if (session.status !== "running" || !this.pendingReady.has(sessionId)) return;
|
|
1043
|
+
const lines = await this.getOutputLines(sessionId, PTY_ROWS);
|
|
1044
|
+
const screenText = lines.join("\n");
|
|
1045
|
+
if (CODEX_TRUST_GATE_REGEX.test(screenText)) {
|
|
1046
|
+
if (!this.trustGateAnswered.has(sessionId)) {
|
|
1047
|
+
this.trustGateAnswered.add(sessionId);
|
|
1048
|
+
this.log.info(`[codex.trust_gate] ${sessionId.slice(0, 8)} auto-answering`, {
|
|
1049
|
+
event: "codex.trust_gate",
|
|
1050
|
+
sessionId
|
|
1051
|
+
});
|
|
1052
|
+
session.process.write("\r");
|
|
1053
|
+
}
|
|
1054
|
+
return;
|
|
1055
|
+
}
|
|
1056
|
+
const lastNonBlank2 = [...lines].reverse().find((l) => l.trim() !== "") ?? "";
|
|
1057
|
+
if (!lastNonBlank2.includes(CODEX_PROMPT_READY_TEXT)) return;
|
|
1058
|
+
this.markReady(sessionId, session);
|
|
708
1059
|
}
|
|
709
|
-
|
|
710
|
-
|
|
711
|
-
|
|
712
|
-
|
|
713
|
-
|
|
1060
|
+
markReady(sessionId, session) {
|
|
1061
|
+
session.lastActivityAt = /* @__PURE__ */ new Date();
|
|
1062
|
+
session.status = "waiting_input";
|
|
1063
|
+
this.log.info(`[codex.ready] ${sessionId.slice(0, 8)}`, {
|
|
1064
|
+
event: "codex.ready",
|
|
1065
|
+
sessionId
|
|
1066
|
+
});
|
|
1067
|
+
this.onStatusChange?.(toPublicSession(session));
|
|
1068
|
+
if (this.pendingReady.has(sessionId)) {
|
|
1069
|
+
this.pendingReady.delete(sessionId);
|
|
1070
|
+
this.flushQueuedInputs(sessionId);
|
|
1071
|
+
this.onReady?.(toPublicSession(session));
|
|
1072
|
+
}
|
|
714
1073
|
}
|
|
715
|
-
|
|
716
|
-
const
|
|
717
|
-
|
|
1074
|
+
handleExit(sessionId, exitCode) {
|
|
1075
|
+
const session = this.sessions.get(sessionId);
|
|
1076
|
+
if (!session) return;
|
|
1077
|
+
session.completedAt = /* @__PURE__ */ new Date();
|
|
1078
|
+
session.status = "idle";
|
|
1079
|
+
const elapsedMs = session.completedAt.getTime() - session.startedAt.getTime();
|
|
1080
|
+
if (exitCode !== 0 && elapsedMs < 2e3 && session.lastOutput === "") {
|
|
1081
|
+
if (!existsSync2(session.projectPath)) {
|
|
1082
|
+
session.failureReason = `Project directory not found: ${session.projectPath}`;
|
|
1083
|
+
} else {
|
|
1084
|
+
session.failureReason = `Codex process exited immediately (code ${exitCode}).`;
|
|
1085
|
+
}
|
|
1086
|
+
}
|
|
1087
|
+
this.onStatusChange?.(toPublicSession(session));
|
|
1088
|
+
session.screen.dispose();
|
|
1089
|
+
this.sessions.delete(sessionId);
|
|
1090
|
+
this.queuedInputs.delete(sessionId);
|
|
1091
|
+
this.trustGateAnswered.delete(sessionId);
|
|
718
1092
|
}
|
|
719
|
-
}
|
|
720
|
-
function
|
|
1093
|
+
};
|
|
1094
|
+
function toPublicSession(s) {
|
|
721
1095
|
return {
|
|
722
|
-
|
|
723
|
-
|
|
724
|
-
|
|
725
|
-
|
|
726
|
-
|
|
727
|
-
|
|
1096
|
+
id: s.id,
|
|
1097
|
+
provider: s.provider ?? CODEX_CLI_PROVIDER,
|
|
1098
|
+
projectPath: s.projectPath,
|
|
1099
|
+
projectName: s.projectName,
|
|
1100
|
+
branch: s.branch,
|
|
1101
|
+
status: s.status,
|
|
1102
|
+
startedAt: s.startedAt,
|
|
1103
|
+
completedAt: s.completedAt,
|
|
1104
|
+
promptCount: s.promptCount,
|
|
1105
|
+
lastOutput: s.lastOutput,
|
|
1106
|
+
...s.failureReason != null && { failureReason: s.failureReason },
|
|
1107
|
+
...s.lastActivityAt != null && { lastActivityAt: s.lastActivityAt },
|
|
1108
|
+
...s.filePath != null && { filePath: s.filePath }
|
|
728
1109
|
};
|
|
729
1110
|
}
|
|
730
|
-
function
|
|
731
|
-
return
|
|
1111
|
+
function stripAnsi(str) {
|
|
1112
|
+
return str.replace(/\x1b\[[0-9;]*[a-zA-Z]/g, "").replace(/\x1b\][^\x07]*\x07/g, "");
|
|
732
1113
|
}
|
|
733
|
-
|
|
1114
|
+
|
|
1115
|
+
// src/pty-manager.ts
|
|
1116
|
+
import { Terminal as Terminal2 } from "@xterm/headless";
|
|
1117
|
+
import { randomUUID as randomUUID2 } from "crypto";
|
|
1118
|
+
import { existsSync as existsSync3 } from "fs";
|
|
1119
|
+
import { basename as basename2 } from "path";
|
|
734
1120
|
|
|
735
1121
|
// src/services/questions/detectPermissionGate.ts
|
|
736
1122
|
var OSC_777_RE = /\x1b\]777;notify;Claude Code;[^\x07\x1b]*/;
|
|
@@ -889,28 +1275,28 @@ function detectShellPrompt(lines) {
|
|
|
889
1275
|
}
|
|
890
1276
|
|
|
891
1277
|
// src/pty-manager.ts
|
|
892
|
-
var
|
|
893
|
-
var
|
|
894
|
-
var
|
|
895
|
-
var
|
|
1278
|
+
var OUTPUT_BUFFER_MAX2 = 65536;
|
|
1279
|
+
var PTY_COLS2 = 120;
|
|
1280
|
+
var PTY_ROWS2 = 40;
|
|
1281
|
+
var SCREEN_SCROLLBACK2 = 1e3;
|
|
896
1282
|
var CLAUDE_PROMPT_MARKERS = ["\u256D", "\u276F"];
|
|
897
1283
|
var PROMPT_MARKER_FALLBACK_MS = 1e4;
|
|
898
1284
|
function buildPasteBytes(input) {
|
|
899
1285
|
return `\x1B[200~${input}\x1B[201~`;
|
|
900
1286
|
}
|
|
901
|
-
var
|
|
1287
|
+
var SUBMIT_BYTES2 = "\r";
|
|
902
1288
|
var SUBMIT_DELAY_MS = 16;
|
|
903
|
-
function
|
|
1289
|
+
function digestBytes2(s) {
|
|
904
1290
|
const escaped = s.replace(new RegExp(String.fromCharCode(27), "g"), "\\x1b").replace(/\r/g, "\\r").replace(/\n/g, "\\n").replace(/\t/g, "\\t");
|
|
905
1291
|
if (escaped.length <= 200) return escaped;
|
|
906
1292
|
return `${escaped.slice(0, 100)}\u2026[${escaped.length - 200}B omitted]\u2026${escaped.slice(-100)}`;
|
|
907
1293
|
}
|
|
908
|
-
var
|
|
909
|
-
async function
|
|
910
|
-
if (
|
|
1294
|
+
var pty2 = null;
|
|
1295
|
+
async function loadPty2() {
|
|
1296
|
+
if (pty2) return pty2;
|
|
911
1297
|
try {
|
|
912
|
-
|
|
913
|
-
return
|
|
1298
|
+
pty2 = await import("node-pty");
|
|
1299
|
+
return pty2;
|
|
914
1300
|
} catch (err) {
|
|
915
1301
|
throw new Error(
|
|
916
1302
|
`node-pty is required for PTY management but failed to load. Ensure it is installed: npm install node-pty
|
|
@@ -918,11 +1304,11 @@ Original error: ${err}`
|
|
|
918
1304
|
);
|
|
919
1305
|
}
|
|
920
1306
|
}
|
|
921
|
-
function
|
|
922
|
-
return new
|
|
923
|
-
cols:
|
|
924
|
-
rows:
|
|
925
|
-
scrollback:
|
|
1307
|
+
function createScreen2() {
|
|
1308
|
+
return new Terminal2({
|
|
1309
|
+
cols: PTY_COLS2,
|
|
1310
|
+
rows: PTY_ROWS2,
|
|
1311
|
+
scrollback: SCREEN_SCROLLBACK2,
|
|
926
1312
|
allowProposedApi: true
|
|
927
1313
|
});
|
|
928
1314
|
}
|
|
@@ -931,6 +1317,11 @@ function buildSpawnEnv() {
|
|
|
931
1317
|
if (env.CLAUDE_API_KEY) {
|
|
932
1318
|
env.ANTHROPIC_API_KEY = env.CLAUDE_API_KEY;
|
|
933
1319
|
}
|
|
1320
|
+
for (const key of Object.keys(env)) {
|
|
1321
|
+
if (key === "CLAUDECODE" || key.startsWith("CLAUDE_CODE_")) {
|
|
1322
|
+
delete env[key];
|
|
1323
|
+
}
|
|
1324
|
+
}
|
|
934
1325
|
return env;
|
|
935
1326
|
}
|
|
936
1327
|
var PTYManager = class {
|
|
@@ -971,6 +1362,10 @@ var PTYManager = class {
|
|
|
971
1362
|
// to a given input or fell silent. Reset on dispose().
|
|
972
1363
|
chunkIndex = /* @__PURE__ */ new Map();
|
|
973
1364
|
lastChunkAt = /* @__PURE__ */ new Map();
|
|
1365
|
+
// In-flight start()/startFresh() calls keyed by sessionId. A second
|
|
1366
|
+
// concurrent resume for the same session (double-tap, client retry) awaits
|
|
1367
|
+
// the first call's promise instead of spawning a duplicate PTY (CRITICAL #3).
|
|
1368
|
+
startPromises = /* @__PURE__ */ new Map();
|
|
974
1369
|
constructor(options = {}) {
|
|
975
1370
|
this.onOutput = options.onOutput;
|
|
976
1371
|
this.onStatusChange = options.onStatusChange;
|
|
@@ -993,7 +1388,18 @@ var PTYManager = class {
|
|
|
993
1388
|
// custom-API-key — are cleared by the seeded ~/.claude.json in
|
|
994
1389
|
// docker/entrypoint.sh.) startFresh() uses the same flag for the same reason.
|
|
995
1390
|
async start(sessionId, options) {
|
|
996
|
-
const
|
|
1391
|
+
const existing = this.sessions.get(sessionId);
|
|
1392
|
+
if (existing) return toPublicSession2(existing);
|
|
1393
|
+
const inFlight = this.startPromises.get(sessionId);
|
|
1394
|
+
if (inFlight) return inFlight;
|
|
1395
|
+
const promise = this.doStart(sessionId, options).finally(() => {
|
|
1396
|
+
this.startPromises.delete(sessionId);
|
|
1397
|
+
});
|
|
1398
|
+
this.startPromises.set(sessionId, promise);
|
|
1399
|
+
return promise;
|
|
1400
|
+
}
|
|
1401
|
+
async doStart(sessionId, options) {
|
|
1402
|
+
const nodePty = await loadPty2();
|
|
997
1403
|
const projectName = options.projectName ?? basename2(options.projectPath);
|
|
998
1404
|
const proc = nodePty.spawn(
|
|
999
1405
|
resolveClaudeExe(),
|
|
@@ -1015,6 +1421,7 @@ var PTYManager = class {
|
|
|
1015
1421
|
);
|
|
1016
1422
|
const session = {
|
|
1017
1423
|
id: sessionId,
|
|
1424
|
+
provider: CLAUDE_CODE_PROVIDER,
|
|
1018
1425
|
projectPath: options.projectPath,
|
|
1019
1426
|
projectName,
|
|
1020
1427
|
branch: options.branch ?? "",
|
|
@@ -1025,7 +1432,7 @@ var PTYManager = class {
|
|
|
1025
1432
|
lastOutput: "",
|
|
1026
1433
|
process: proc,
|
|
1027
1434
|
outputBuffer: Buffer.alloc(0),
|
|
1028
|
-
screen:
|
|
1435
|
+
screen: createScreen2()
|
|
1029
1436
|
};
|
|
1030
1437
|
this.sessions.set(sessionId, session);
|
|
1031
1438
|
this.pendingReady.add(sessionId);
|
|
@@ -1036,14 +1443,14 @@ var PTYManager = class {
|
|
|
1036
1443
|
this.pendingReady.delete(sessionId);
|
|
1037
1444
|
this.handleExit(sessionId, exitCode);
|
|
1038
1445
|
});
|
|
1039
|
-
return
|
|
1446
|
+
return toPublicSession2(session);
|
|
1040
1447
|
}
|
|
1041
1448
|
// Start a brand-new Claude session. A stable UUID is generated here and passed
|
|
1042
1449
|
// to Claude via --session-id so the JSONL filename matches from the start.
|
|
1043
1450
|
// onReady fires once Claude reaches its first prompt (waiting_input).
|
|
1044
1451
|
async startFresh(options) {
|
|
1045
|
-
const nodePty = await
|
|
1046
|
-
const sessionId =
|
|
1452
|
+
const nodePty = await loadPty2();
|
|
1453
|
+
const sessionId = randomUUID2();
|
|
1047
1454
|
const projectName = options.projectName ?? basename2(options.projectPath);
|
|
1048
1455
|
const args = [
|
|
1049
1456
|
"--permission-mode",
|
|
@@ -1065,6 +1472,7 @@ var PTYManager = class {
|
|
|
1065
1472
|
});
|
|
1066
1473
|
const session = {
|
|
1067
1474
|
id: sessionId,
|
|
1475
|
+
provider: CLAUDE_CODE_PROVIDER,
|
|
1068
1476
|
projectPath: options.projectPath,
|
|
1069
1477
|
projectName,
|
|
1070
1478
|
branch: "",
|
|
@@ -1075,7 +1483,7 @@ var PTYManager = class {
|
|
|
1075
1483
|
lastOutput: "",
|
|
1076
1484
|
process: proc,
|
|
1077
1485
|
outputBuffer: Buffer.alloc(0),
|
|
1078
|
-
screen:
|
|
1486
|
+
screen: createScreen2()
|
|
1079
1487
|
};
|
|
1080
1488
|
this.sessions.set(sessionId, session);
|
|
1081
1489
|
this.pendingReady.add(sessionId);
|
|
@@ -1086,7 +1494,7 @@ var PTYManager = class {
|
|
|
1086
1494
|
this.pendingReady.delete(sessionId);
|
|
1087
1495
|
this.handleExit(sessionId, exitCode);
|
|
1088
1496
|
});
|
|
1089
|
-
return
|
|
1497
|
+
return toPublicSession2(session);
|
|
1090
1498
|
}
|
|
1091
1499
|
// Write raw key bytes directly to the PTY without bracketed-paste wrapping.
|
|
1092
1500
|
// Use for control sequences (arrow keys, Enter) that must not be quoted.
|
|
@@ -1098,10 +1506,10 @@ var PTYManager = class {
|
|
|
1098
1506
|
}
|
|
1099
1507
|
if (session.status === "waiting_input") {
|
|
1100
1508
|
session.status = "running";
|
|
1101
|
-
this.onStatusChange?.(
|
|
1509
|
+
this.onStatusChange?.(toPublicSession2(session));
|
|
1102
1510
|
}
|
|
1103
1511
|
this.log.info(
|
|
1104
|
-
`[pty.keys.write] ${sessionId.slice(0, 8)} bytes=${keys.length} digest=${
|
|
1512
|
+
`[pty.keys.write] ${sessionId.slice(0, 8)} bytes=${keys.length} digest=${digestBytes2(keys)}`,
|
|
1105
1513
|
{ event: "pty.keys_write", sessionId, byteLen: keys.length }
|
|
1106
1514
|
);
|
|
1107
1515
|
session.process.write(keys);
|
|
@@ -1133,7 +1541,7 @@ var PTYManager = class {
|
|
|
1133
1541
|
}
|
|
1134
1542
|
if (session.status === "waiting_input") {
|
|
1135
1543
|
session.status = "running";
|
|
1136
|
-
this.onStatusChange?.(
|
|
1544
|
+
this.onStatusChange?.(toPublicSession2(session));
|
|
1137
1545
|
}
|
|
1138
1546
|
this.writeSubmit(sessionId, session, input, "direct", session.promptCount + 1);
|
|
1139
1547
|
session.lastActivityAt = /* @__PURE__ */ new Date();
|
|
@@ -1146,13 +1554,13 @@ var PTYManager = class {
|
|
|
1146
1554
|
writeSubmit(sessionId, session, input, path, promptCount) {
|
|
1147
1555
|
const pasteBytes = buildPasteBytes(input);
|
|
1148
1556
|
this.log.info(
|
|
1149
|
-
`[pty.input.write] ${sessionId.slice(0, 8)} promptCount=${promptCount} bytes=${pasteBytes.length} digest=${
|
|
1557
|
+
`[pty.input.write] ${sessionId.slice(0, 8)} promptCount=${promptCount} bytes=${pasteBytes.length} digest=${digestBytes2(pasteBytes)}`,
|
|
1150
1558
|
{
|
|
1151
1559
|
event: "pty.input_write",
|
|
1152
1560
|
sessionId,
|
|
1153
1561
|
promptCount,
|
|
1154
1562
|
byteLen: pasteBytes.length,
|
|
1155
|
-
digest:
|
|
1563
|
+
digest: digestBytes2(pasteBytes),
|
|
1156
1564
|
path,
|
|
1157
1565
|
phase: "paste"
|
|
1158
1566
|
}
|
|
@@ -1167,13 +1575,13 @@ var PTYManager = class {
|
|
|
1167
1575
|
event: "pty.input_write",
|
|
1168
1576
|
sessionId,
|
|
1169
1577
|
promptCount,
|
|
1170
|
-
byteLen:
|
|
1578
|
+
byteLen: SUBMIT_BYTES2.length,
|
|
1171
1579
|
digest: "\\r",
|
|
1172
1580
|
path,
|
|
1173
1581
|
phase: "submit"
|
|
1174
1582
|
}
|
|
1175
1583
|
);
|
|
1176
|
-
current.process.write(
|
|
1584
|
+
current.process.write(SUBMIT_BYTES2);
|
|
1177
1585
|
}, SUBMIT_DELAY_MS);
|
|
1178
1586
|
}
|
|
1179
1587
|
// Drain any inputs that were sent while the session was still pendingReady,
|
|
@@ -1231,7 +1639,7 @@ var PTYManager = class {
|
|
|
1231
1639
|
session.completedAt = /* @__PURE__ */ new Date();
|
|
1232
1640
|
session.screen.dispose();
|
|
1233
1641
|
this.sessions.delete(sessionId);
|
|
1234
|
-
this.onStatusChange?.(
|
|
1642
|
+
this.onStatusChange?.(toPublicSession2(session));
|
|
1235
1643
|
}
|
|
1236
1644
|
getOutput(sessionId) {
|
|
1237
1645
|
const session = this.sessions.get(sessionId);
|
|
@@ -1262,13 +1670,13 @@ var PTYManager = class {
|
|
|
1262
1670
|
}
|
|
1263
1671
|
getSession(sessionId) {
|
|
1264
1672
|
const session = this.sessions.get(sessionId);
|
|
1265
|
-
return session ?
|
|
1673
|
+
return session ? toPublicSession2(session) : null;
|
|
1266
1674
|
}
|
|
1267
1675
|
hasSession(sessionId) {
|
|
1268
1676
|
return this.sessions.has(sessionId);
|
|
1269
1677
|
}
|
|
1270
1678
|
listSessions() {
|
|
1271
|
-
return Array.from(this.sessions.values()).map(
|
|
1679
|
+
return Array.from(this.sessions.values()).map(toPublicSession2);
|
|
1272
1680
|
}
|
|
1273
1681
|
dispose() {
|
|
1274
1682
|
for (const session of this.sessions.values()) {
|
|
@@ -1300,7 +1708,7 @@ var PTYManager = class {
|
|
|
1300
1708
|
this.lastChunkAt.set(sessionId, now);
|
|
1301
1709
|
const gapMs = last == null ? 0 : now - last;
|
|
1302
1710
|
this.log.info(
|
|
1303
|
-
`[pty.chunk] ${sessionId.slice(0, 8)} #${idx} +${chunk.length}B gap=${gapMs}ms status=${session.status} digest=${
|
|
1711
|
+
`[pty.chunk] ${sessionId.slice(0, 8)} #${idx} +${chunk.length}B gap=${gapMs}ms status=${session.status} digest=${digestBytes2(data)}`,
|
|
1304
1712
|
{
|
|
1305
1713
|
event: "pty.chunk",
|
|
1306
1714
|
sessionId,
|
|
@@ -1309,17 +1717,17 @@ var PTYManager = class {
|
|
|
1309
1717
|
gapMs,
|
|
1310
1718
|
status: session.status,
|
|
1311
1719
|
pendingReady: this.pendingReady.has(sessionId),
|
|
1312
|
-
digest:
|
|
1720
|
+
digest: digestBytes2(data)
|
|
1313
1721
|
}
|
|
1314
1722
|
);
|
|
1315
1723
|
session.outputBuffer = Buffer.concat([session.outputBuffer, chunk]);
|
|
1316
|
-
if (session.outputBuffer.length >
|
|
1724
|
+
if (session.outputBuffer.length > OUTPUT_BUFFER_MAX2) {
|
|
1317
1725
|
session.outputBuffer = session.outputBuffer.subarray(
|
|
1318
|
-
session.outputBuffer.length -
|
|
1726
|
+
session.outputBuffer.length - OUTPUT_BUFFER_MAX2
|
|
1319
1727
|
);
|
|
1320
1728
|
}
|
|
1321
1729
|
session.screen.write(data);
|
|
1322
|
-
const stripped =
|
|
1730
|
+
const stripped = stripAnsi2(data);
|
|
1323
1731
|
session.lastOutput = stripped;
|
|
1324
1732
|
const matchedMarker = CLAUDE_PROMPT_MARKERS.find((m) => stripped.includes(m));
|
|
1325
1733
|
if (session.status === "running" && matchedMarker) {
|
|
@@ -1422,11 +1830,11 @@ var PTYManager = class {
|
|
|
1422
1830
|
reason,
|
|
1423
1831
|
elapsedMs
|
|
1424
1832
|
});
|
|
1425
|
-
this.onStatusChange?.(
|
|
1833
|
+
this.onStatusChange?.(toPublicSession2(session));
|
|
1426
1834
|
if (this.pendingReady.has(sessionId)) {
|
|
1427
1835
|
this.pendingReady.delete(sessionId);
|
|
1428
1836
|
this.flushQueuedInputs(sessionId);
|
|
1429
|
-
this.onReady?.(
|
|
1837
|
+
this.onReady?.(toPublicSession2(session));
|
|
1430
1838
|
}
|
|
1431
1839
|
}
|
|
1432
1840
|
handleExit(sessionId, exitCode) {
|
|
@@ -1436,13 +1844,13 @@ var PTYManager = class {
|
|
|
1436
1844
|
session.status = "idle";
|
|
1437
1845
|
const elapsedMs = session.completedAt.getTime() - session.startedAt.getTime();
|
|
1438
1846
|
if (exitCode !== 0 && elapsedMs < 2e3 && session.lastOutput === "") {
|
|
1439
|
-
if (!
|
|
1847
|
+
if (!existsSync3(session.projectPath)) {
|
|
1440
1848
|
session.failureReason = `Project directory not found: ${session.projectPath}`;
|
|
1441
1849
|
} else {
|
|
1442
1850
|
session.failureReason = `Process exited immediately (code ${exitCode}). Check that the Claude binary is installed and accessible.`;
|
|
1443
1851
|
}
|
|
1444
1852
|
}
|
|
1445
|
-
this.onStatusChange?.(
|
|
1853
|
+
this.onStatusChange?.(toPublicSession2(session));
|
|
1446
1854
|
session.screen.dispose();
|
|
1447
1855
|
this.sessions.delete(sessionId);
|
|
1448
1856
|
this.queuedInputs.delete(sessionId);
|
|
@@ -1452,9 +1860,10 @@ var PTYManager = class {
|
|
|
1452
1860
|
this.shellPromptOpen.delete(sessionId);
|
|
1453
1861
|
}
|
|
1454
1862
|
};
|
|
1455
|
-
function
|
|
1863
|
+
function toPublicSession2(s) {
|
|
1456
1864
|
return {
|
|
1457
1865
|
id: s.id,
|
|
1866
|
+
provider: s.provider ?? CLAUDE_CODE_PROVIDER,
|
|
1458
1867
|
projectPath: s.projectPath,
|
|
1459
1868
|
projectName: s.projectName,
|
|
1460
1869
|
branch: s.branch,
|
|
@@ -1468,10 +1877,249 @@ function toPublicSession(s) {
|
|
|
1468
1877
|
...s.filePath != null && { filePath: s.filePath }
|
|
1469
1878
|
};
|
|
1470
1879
|
}
|
|
1471
|
-
function
|
|
1880
|
+
function stripAnsi2(str) {
|
|
1472
1881
|
return str.replace(/\x1b\[[0-9;]*[a-zA-Z]/g, "").replace(/\x1b\][^\x07]*\x07/g, "");
|
|
1473
1882
|
}
|
|
1474
1883
|
|
|
1884
|
+
// src/live-session-manager.ts
|
|
1885
|
+
var LiveSessionManager = class {
|
|
1886
|
+
runners;
|
|
1887
|
+
constructor(options = {}) {
|
|
1888
|
+
this.runners = /* @__PURE__ */ new Map([
|
|
1889
|
+
[CLAUDE_CODE_PROVIDER, new PTYManager(options)],
|
|
1890
|
+
[CODEX_CLI_PROVIDER, new CodexPtyRunner(options)]
|
|
1891
|
+
]);
|
|
1892
|
+
}
|
|
1893
|
+
async start(sessionId, options) {
|
|
1894
|
+
const provider = options.provider ?? CLAUDE_CODE_PROVIDER;
|
|
1895
|
+
const runner = this.assertSupportedProvider(provider, options.projectPath);
|
|
1896
|
+
return runner.start(sessionId, options);
|
|
1897
|
+
}
|
|
1898
|
+
async startFresh(options) {
|
|
1899
|
+
const provider = options.provider ?? CLAUDE_CODE_PROVIDER;
|
|
1900
|
+
const runner = this.assertSupportedProvider(provider, options.projectPath);
|
|
1901
|
+
return runner.startFresh(options);
|
|
1902
|
+
}
|
|
1903
|
+
sendInput(sessionId, input) {
|
|
1904
|
+
return this.runnerFor(sessionId).sendInput(sessionId, input);
|
|
1905
|
+
}
|
|
1906
|
+
sendKeys(sessionId, keys) {
|
|
1907
|
+
this.runnerFor(sessionId).sendKeys(sessionId, keys);
|
|
1908
|
+
}
|
|
1909
|
+
cancel(sessionId) {
|
|
1910
|
+
this.runnerFor(sessionId).cancel(sessionId);
|
|
1911
|
+
}
|
|
1912
|
+
killPid(pid) {
|
|
1913
|
+
for (const runner of this.runners.values()) {
|
|
1914
|
+
runner.killPid(pid);
|
|
1915
|
+
}
|
|
1916
|
+
}
|
|
1917
|
+
// putOnHold tolerates an unknown sessionId (PTYManager.putOnHold is a no-op
|
|
1918
|
+
// when the session isn't in its map), so — unlike the other session-keyed
|
|
1919
|
+
// methods — route to the owning runner when found, otherwise broadcast to
|
|
1920
|
+
// every runner rather than throwing; this matches the pre-extraction
|
|
1921
|
+
// behavior of delegating straight through with no existence check.
|
|
1922
|
+
putOnHold(sessionId) {
|
|
1923
|
+
for (const runner of this.runners.values()) {
|
|
1924
|
+
if (runner.hasSession(sessionId) || runner.getSession(sessionId)) {
|
|
1925
|
+
runner.putOnHold(sessionId);
|
|
1926
|
+
return;
|
|
1927
|
+
}
|
|
1928
|
+
}
|
|
1929
|
+
for (const runner of this.runners.values()) {
|
|
1930
|
+
runner.putOnHold(sessionId);
|
|
1931
|
+
}
|
|
1932
|
+
}
|
|
1933
|
+
getOutput(sessionId) {
|
|
1934
|
+
return this.runnerFor(sessionId).getOutput(sessionId);
|
|
1935
|
+
}
|
|
1936
|
+
getOutputLines(sessionId, maxLines) {
|
|
1937
|
+
return this.runnerFor(sessionId).getOutputLines(sessionId, maxLines);
|
|
1938
|
+
}
|
|
1939
|
+
getSession(sessionId) {
|
|
1940
|
+
for (const runner of this.runners.values()) {
|
|
1941
|
+
const session = runner.getSession(sessionId);
|
|
1942
|
+
if (session) return session;
|
|
1943
|
+
}
|
|
1944
|
+
return null;
|
|
1945
|
+
}
|
|
1946
|
+
hasSession(sessionId) {
|
|
1947
|
+
for (const runner of this.runners.values()) {
|
|
1948
|
+
if (runner.hasSession(sessionId)) return true;
|
|
1949
|
+
}
|
|
1950
|
+
return false;
|
|
1951
|
+
}
|
|
1952
|
+
listSessions() {
|
|
1953
|
+
return Array.from(this.runners.values()).flatMap((runner) => runner.listSessions());
|
|
1954
|
+
}
|
|
1955
|
+
dispose() {
|
|
1956
|
+
for (const runner of this.runners.values()) {
|
|
1957
|
+
runner.dispose();
|
|
1958
|
+
}
|
|
1959
|
+
}
|
|
1960
|
+
// Look up which runner owns a session. Only one runner exists today, so
|
|
1961
|
+
// this is a linear scan across hasSession()/getSession() rather than a
|
|
1962
|
+
// separate session→provider index — see task-1-brief.md.
|
|
1963
|
+
runnerFor(sessionId) {
|
|
1964
|
+
for (const runner of this.runners.values()) {
|
|
1965
|
+
if (runner.hasSession(sessionId) || runner.getSession(sessionId)) return runner;
|
|
1966
|
+
}
|
|
1967
|
+
throw new Error(`Session not found: ${sessionId}`);
|
|
1968
|
+
}
|
|
1969
|
+
assertSupportedProvider(provider, projectPath) {
|
|
1970
|
+
const runner = this.runners.get(provider);
|
|
1971
|
+
if (runner) return runner;
|
|
1972
|
+
const err = new Error(
|
|
1973
|
+
`Live ${provider} sessions are not implemented yet for ${basename3(projectPath)}`
|
|
1974
|
+
);
|
|
1975
|
+
err.statusCode = 501;
|
|
1976
|
+
throw err;
|
|
1977
|
+
}
|
|
1978
|
+
};
|
|
1979
|
+
|
|
1980
|
+
// src/process-discovery.ts
|
|
1981
|
+
import { execFile } from "child_process";
|
|
1982
|
+
import { platform as platform2 } from "os";
|
|
1983
|
+
import { basename as basename4, dirname as dirname3 } from "path";
|
|
1984
|
+
async function discoverClaudeProcesses() {
|
|
1985
|
+
if (platform2() === "win32") return discoverWindows();
|
|
1986
|
+
return discoverUnix();
|
|
1987
|
+
}
|
|
1988
|
+
async function discoverUnix() {
|
|
1989
|
+
const pids = await getPidsUnix();
|
|
1990
|
+
const results = await Promise.all(
|
|
1991
|
+
pids.map(async (pid) => {
|
|
1992
|
+
try {
|
|
1993
|
+
const [cwd, args, startedAt] = await Promise.all([
|
|
1994
|
+
getProcessCwdUnix(pid),
|
|
1995
|
+
getProcessArgsUnix(pid),
|
|
1996
|
+
getProcessStartTimeUnix(pid)
|
|
1997
|
+
]);
|
|
1998
|
+
const conversationId = extractResumeId(args);
|
|
1999
|
+
return {
|
|
2000
|
+
pid,
|
|
2001
|
+
projectPath: cwd,
|
|
2002
|
+
projectName: basename4(cwd),
|
|
2003
|
+
branch: await readGitBranch(cwd),
|
|
2004
|
+
conversationId,
|
|
2005
|
+
startedAt
|
|
2006
|
+
};
|
|
2007
|
+
} catch {
|
|
2008
|
+
return null;
|
|
2009
|
+
}
|
|
2010
|
+
})
|
|
2011
|
+
);
|
|
2012
|
+
return results.filter((r) => r !== null);
|
|
2013
|
+
}
|
|
2014
|
+
async function discoverWindows() {
|
|
2015
|
+
const pids = await getPidsWindows();
|
|
2016
|
+
const results = await Promise.all(
|
|
2017
|
+
pids.map(async (pid) => {
|
|
2018
|
+
try {
|
|
2019
|
+
const info = await getProcessInfoWindows(pid);
|
|
2020
|
+
if (!info) return null;
|
|
2021
|
+
return {
|
|
2022
|
+
pid,
|
|
2023
|
+
projectPath: info.cwd,
|
|
2024
|
+
projectName: basename4(info.cwd),
|
|
2025
|
+
branch: await readGitBranch(info.cwd),
|
|
2026
|
+
conversationId: extractResumeId(info.args),
|
|
2027
|
+
startedAt: info.startedAt
|
|
2028
|
+
};
|
|
2029
|
+
} catch {
|
|
2030
|
+
return null;
|
|
2031
|
+
}
|
|
2032
|
+
})
|
|
2033
|
+
);
|
|
2034
|
+
return results.filter((r) => r !== null);
|
|
2035
|
+
}
|
|
2036
|
+
function run(cmd, args, opts = {}) {
|
|
2037
|
+
return new Promise((resolve2, reject) => {
|
|
2038
|
+
execFile(
|
|
2039
|
+
cmd,
|
|
2040
|
+
args,
|
|
2041
|
+
{ windowsHide: isWindows, encoding: "utf-8", timeout: opts.timeout ?? 5e3, cwd: opts.cwd },
|
|
2042
|
+
(err, stdout) => {
|
|
2043
|
+
if (err) reject(err);
|
|
2044
|
+
else resolve2(stdout);
|
|
2045
|
+
}
|
|
2046
|
+
);
|
|
2047
|
+
});
|
|
2048
|
+
}
|
|
2049
|
+
async function getPidsUnix() {
|
|
2050
|
+
try {
|
|
2051
|
+
const output = await run("pgrep", ["-x", "claude"]);
|
|
2052
|
+
return output.trim().split("\n").filter(Boolean).map((s) => Number.parseInt(s, 10));
|
|
2053
|
+
} catch {
|
|
2054
|
+
return [];
|
|
2055
|
+
}
|
|
2056
|
+
}
|
|
2057
|
+
async function getProcessCwdUnix(pid) {
|
|
2058
|
+
const output = await run("lsof", ["-p", String(pid), "-a", "-d", "cwd", "-Fn"]);
|
|
2059
|
+
const match = output.match(/n(.+)/);
|
|
2060
|
+
return match?.[1] ?? "";
|
|
2061
|
+
}
|
|
2062
|
+
async function getProcessArgsUnix(pid) {
|
|
2063
|
+
return (await run("ps", ["-p", String(pid), "-o", "args="])).trim();
|
|
2064
|
+
}
|
|
2065
|
+
async function getProcessStartTimeUnix(pid) {
|
|
2066
|
+
const raw = (await run("ps", ["-p", String(pid), "-o", "lstart="])).trim();
|
|
2067
|
+
const d = new Date(raw);
|
|
2068
|
+
return Number.isNaN(d.getTime()) ? /* @__PURE__ */ new Date() : d;
|
|
2069
|
+
}
|
|
2070
|
+
async function getPidsWindows() {
|
|
2071
|
+
try {
|
|
2072
|
+
const output = await run("tasklist", ["/FI", "IMAGENAME eq claude.exe", "/FO", "CSV", "/NH"]);
|
|
2073
|
+
return output.trim().split("\n").filter(Boolean).map((line) => {
|
|
2074
|
+
const parts = line.split(",");
|
|
2075
|
+
return Number.parseInt(parts[1]?.replace(/"/g, "") ?? "0", 10);
|
|
2076
|
+
}).filter((pid) => pid > 0);
|
|
2077
|
+
} catch {
|
|
2078
|
+
return [];
|
|
2079
|
+
}
|
|
2080
|
+
}
|
|
2081
|
+
async function getProcessInfoWindows(pid) {
|
|
2082
|
+
try {
|
|
2083
|
+
const output = await run("wmic", [
|
|
2084
|
+
"process",
|
|
2085
|
+
"where",
|
|
2086
|
+
`ProcessId=${pid}`,
|
|
2087
|
+
"get",
|
|
2088
|
+
"CommandLine,CreationDate,ExecutablePath",
|
|
2089
|
+
"/FORMAT:CSV"
|
|
2090
|
+
]);
|
|
2091
|
+
const lines = output.trim().split(/\r?\n/).filter((l) => l.trim().length > 0);
|
|
2092
|
+
if (lines.length < 2) return null;
|
|
2093
|
+
const parts = lines[1].split(",");
|
|
2094
|
+
const args = parts[1] ?? "";
|
|
2095
|
+
const creationDate = parts[2] ?? "";
|
|
2096
|
+
const year = creationDate.slice(0, 4);
|
|
2097
|
+
const month = creationDate.slice(4, 6);
|
|
2098
|
+
const day = creationDate.slice(6, 8);
|
|
2099
|
+
const hour = creationDate.slice(8, 10);
|
|
2100
|
+
const min = creationDate.slice(10, 12);
|
|
2101
|
+
const sec = creationDate.slice(12, 14);
|
|
2102
|
+
const startedAt = /* @__PURE__ */ new Date(`${year}-${month}-${day}T${hour}:${min}:${sec}`);
|
|
2103
|
+
if (Number.isNaN(startedAt.getTime())) return null;
|
|
2104
|
+
const exePath = parts[3] ?? "";
|
|
2105
|
+
const cwd = exePath ? dirname3(exePath) : "";
|
|
2106
|
+
return { cwd, args, startedAt };
|
|
2107
|
+
} catch {
|
|
2108
|
+
return null;
|
|
2109
|
+
}
|
|
2110
|
+
}
|
|
2111
|
+
function extractResumeId(args) {
|
|
2112
|
+
const match = args.match(/--resume\s+(\S+)/);
|
|
2113
|
+
return match?.[1] ?? null;
|
|
2114
|
+
}
|
|
2115
|
+
async function readGitBranch(dir) {
|
|
2116
|
+
try {
|
|
2117
|
+
return (await run("git", ["rev-parse", "--abbrev-ref", "HEAD"], { cwd: dir, timeout: 3e3 })).trim();
|
|
2118
|
+
} catch {
|
|
2119
|
+
return "";
|
|
2120
|
+
}
|
|
2121
|
+
}
|
|
2122
|
+
|
|
1475
2123
|
// src/server.ts
|
|
1476
2124
|
import { createNodeWebSocket } from "@hono/node-ws";
|
|
1477
2125
|
import { Connection, Client as TemporalClient } from "@temporalio/client";
|
|
@@ -1486,7 +2134,7 @@ import {
|
|
|
1486
2134
|
import { EventEmitter } from "events";
|
|
1487
2135
|
import {
|
|
1488
2136
|
createReadStream,
|
|
1489
|
-
existsSync as
|
|
2137
|
+
existsSync as existsSync7,
|
|
1490
2138
|
watch as fsWatch,
|
|
1491
2139
|
readdirSync as readdirSync4,
|
|
1492
2140
|
readFileSync as readFileSync6,
|
|
@@ -1678,7 +2326,7 @@ async function handleSendAgentInput(sessionId, body, deps) {
|
|
|
1678
2326
|
}
|
|
1679
2327
|
|
|
1680
2328
|
// src/agent/handle-start-agent-session.ts
|
|
1681
|
-
import { existsSync as
|
|
2329
|
+
import { existsSync as existsSync4 } from "fs";
|
|
1682
2330
|
import { join as join5 } from "path";
|
|
1683
2331
|
function validateBody(body) {
|
|
1684
2332
|
if (body === null || body === void 0 || typeof body !== "object") {
|
|
@@ -1710,7 +2358,7 @@ async function handleStartAgentSession(body, deps) {
|
|
|
1710
2358
|
let conversationId = parsed.conversationId;
|
|
1711
2359
|
if (conversationId) {
|
|
1712
2360
|
const jsonlPath = join5(deps.conversationsDir, `${conversationId}.jsonl`);
|
|
1713
|
-
if (!
|
|
2361
|
+
if (!existsSync4(jsonlPath)) {
|
|
1714
2362
|
return {
|
|
1715
2363
|
status: 404,
|
|
1716
2364
|
body: agentErrorResponse(
|
|
@@ -2300,7 +2948,7 @@ async function createDirectory(parentAbsolutePath, name) {
|
|
|
2300
2948
|
|
|
2301
2949
|
// src/conversation-cache.ts
|
|
2302
2950
|
import Database from "better-sqlite3";
|
|
2303
|
-
import { closeSync as closeSync2, existsSync as
|
|
2951
|
+
import { closeSync as closeSync2, existsSync as existsSync5, mkdirSync as mkdirSync2, openSync as openSync2, readSync as readSync2, statSync as statSync2 } from "fs";
|
|
2304
2952
|
import { dirname as dirname6 } from "path";
|
|
2305
2953
|
|
|
2306
2954
|
// src/db/sqlite-migrate.ts
|
|
@@ -2344,14 +2992,6 @@ function runSqliteMigrations(db, migrationsDir) {
|
|
|
2344
2992
|
return { applied, skipped };
|
|
2345
2993
|
}
|
|
2346
2994
|
|
|
2347
|
-
// src/providers.ts
|
|
2348
|
-
var CLAUDE_CODE_PROVIDER = "claude-code";
|
|
2349
|
-
var CODEX_CLI_PROVIDER = "codex-cli";
|
|
2350
|
-
function isProviderResumable(provider, availabilityResumable) {
|
|
2351
|
-
if (provider === CODEX_CLI_PROVIDER) return false;
|
|
2352
|
-
return availabilityResumable;
|
|
2353
|
-
}
|
|
2354
|
-
|
|
2355
2995
|
// src/services/conversations/isAgentConversation.ts
|
|
2356
2996
|
import { closeSync, openSync, readSync, statSync } from "fs";
|
|
2357
2997
|
var DEFAULT_AGENT_ENTRYPOINTS = /* @__PURE__ */ new Set(["sdk-cli", "claude-vscode"]);
|
|
@@ -3072,7 +3712,7 @@ var ConversationCache = class _ConversationCache {
|
|
|
3072
3712
|
* `handleGetConversation` can still serve the cached tail even when the
|
|
3073
3713
|
* JSONL has been deleted.
|
|
3074
3714
|
*/
|
|
3075
|
-
pruneGhostFiles(exists =
|
|
3715
|
+
pruneGhostFiles(exists = existsSync5) {
|
|
3076
3716
|
const rows = this.stmts.allFilePaths.all();
|
|
3077
3717
|
const ghosts = [];
|
|
3078
3718
|
const prune = this.db.transaction((ids) => {
|
|
@@ -3152,7 +3792,7 @@ var ConversationsRepository = class {
|
|
|
3152
3792
|
};
|
|
3153
3793
|
|
|
3154
3794
|
// src/db/repositories/projects.repository.ts
|
|
3155
|
-
import { randomUUID as
|
|
3795
|
+
import { randomUUID as randomUUID3 } from "crypto";
|
|
3156
3796
|
|
|
3157
3797
|
// src/utils/canonicalizeProjectPath.ts
|
|
3158
3798
|
function canonicalizeProjectPath(projectPath) {
|
|
@@ -3245,7 +3885,7 @@ var ProjectsRepository = class {
|
|
|
3245
3885
|
});
|
|
3246
3886
|
return rowToProject(this.getById.get(existing.id));
|
|
3247
3887
|
}
|
|
3248
|
-
const id =
|
|
3888
|
+
const id = randomUUID3();
|
|
3249
3889
|
this.insert.run({
|
|
3250
3890
|
id,
|
|
3251
3891
|
path,
|
|
@@ -3542,14 +4182,14 @@ var ConversationWatcher = class {
|
|
|
3542
4182
|
};
|
|
3543
4183
|
|
|
3544
4184
|
// src/services/conversations/pruneAgentConversations.ts
|
|
3545
|
-
import { existsSync as
|
|
4185
|
+
import { existsSync as existsSync6 } from "fs";
|
|
3546
4186
|
function pruneAgentConversations(cache) {
|
|
3547
4187
|
const db = cache.getDatabase();
|
|
3548
4188
|
const rows = db.prepare("SELECT id, file_path FROM conversation_meta").all();
|
|
3549
4189
|
let pruned = 0;
|
|
3550
4190
|
let missing = 0;
|
|
3551
4191
|
for (const row of rows) {
|
|
3552
|
-
if (!
|
|
4192
|
+
if (!existsSync6(row.file_path)) {
|
|
3553
4193
|
missing += 1;
|
|
3554
4194
|
continue;
|
|
3555
4195
|
}
|
|
@@ -3832,6 +4472,7 @@ function managedToResponse(s, ptyAttached) {
|
|
|
3832
4472
|
return {
|
|
3833
4473
|
id: s.id,
|
|
3834
4474
|
conversationId: s.id,
|
|
4475
|
+
provider: s.provider ?? CLAUDE_CODE_PROVIDER,
|
|
3835
4476
|
status: s.status,
|
|
3836
4477
|
projectPath: s.projectPath,
|
|
3837
4478
|
projectName: s.projectName,
|
|
@@ -3857,13 +4498,15 @@ function managedToResponse(s, ptyAttached) {
|
|
|
3857
4498
|
...s.failureReason != null && { failureReason: s.failureReason },
|
|
3858
4499
|
...s.resumedFromConversationId != null && {
|
|
3859
4500
|
resumedFromConversationId: s.resumedFromConversationId
|
|
3860
|
-
}
|
|
4501
|
+
},
|
|
4502
|
+
...s.boundConversationId != null && { boundConversationId: s.boundConversationId }
|
|
3861
4503
|
};
|
|
3862
4504
|
}
|
|
3863
4505
|
function discoveredToResponse(d, conversationId) {
|
|
3864
4506
|
return {
|
|
3865
4507
|
id: conversationId,
|
|
3866
4508
|
conversationId,
|
|
4509
|
+
provider: CLAUDE_CODE_PROVIDER,
|
|
3867
4510
|
status: "idle",
|
|
3868
4511
|
projectPath: d.projectPath,
|
|
3869
4512
|
projectName: d.projectName,
|
|
@@ -4305,7 +4948,7 @@ var StreamerServer = class {
|
|
|
4305
4948
|
});
|
|
4306
4949
|
}
|
|
4307
4950
|
});
|
|
4308
|
-
this.ptyManager = new
|
|
4951
|
+
this.ptyManager = new LiveSessionManager({
|
|
4309
4952
|
logger: getLogger("pty"),
|
|
4310
4953
|
onOutput: (sessionId, data) => {
|
|
4311
4954
|
this.wsHub.broadcast({ type: "terminal_output", sessionId, data });
|
|
@@ -4642,10 +5285,11 @@ var StreamerServer = class {
|
|
|
4642
5285
|
}
|
|
4643
5286
|
} catch (err) {
|
|
4644
5287
|
const message = err instanceof Error ? err.message : String(err);
|
|
4645
|
-
|
|
4646
|
-
|
|
4647
|
-
|
|
4648
|
-
|
|
5288
|
+
const abiMismatch = message.includes("NODE_MODULE_VERSION") || message.includes("was compiled against a different Node.js version");
|
|
5289
|
+
this.log.error(
|
|
5290
|
+
`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})`,
|
|
5291
|
+
{ error: message, abiMismatch, event: "cache.open_failed" }
|
|
5292
|
+
);
|
|
4649
5293
|
}
|
|
4650
5294
|
const warmupScanner = new ConversationScanner();
|
|
4651
5295
|
this.allScanners.add(warmupScanner);
|
|
@@ -5133,16 +5777,16 @@ var StreamerServer = class {
|
|
|
5133
5777
|
}
|
|
5134
5778
|
findJsonlPath(uuid) {
|
|
5135
5779
|
const projectsDir = join12(homedir5(), ".claude", "projects");
|
|
5136
|
-
if (!
|
|
5780
|
+
if (!existsSync7(projectsDir)) return null;
|
|
5137
5781
|
const filename = `${uuid}.jsonl`;
|
|
5138
5782
|
for (const dir of readdirSync4(projectsDir)) {
|
|
5139
5783
|
const fp = join12(projectsDir, dir, filename);
|
|
5140
|
-
if (
|
|
5784
|
+
if (existsSync7(fp)) return fp;
|
|
5141
5785
|
const projectDir = join12(projectsDir, dir);
|
|
5142
5786
|
try {
|
|
5143
5787
|
for (const sub of readdirSync4(projectDir)) {
|
|
5144
5788
|
const subagentPath = join12(projectDir, sub, "subagents", filename);
|
|
5145
|
-
if (
|
|
5789
|
+
if (existsSync7(subagentPath)) return subagentPath;
|
|
5146
5790
|
}
|
|
5147
5791
|
} catch {
|
|
5148
5792
|
}
|
|
@@ -5451,7 +6095,7 @@ var StreamerServer = class {
|
|
|
5451
6095
|
handleGetSession(sessionId, res) {
|
|
5452
6096
|
const session = this.sessionStore.get(sessionId, this.ptyAttachedIds());
|
|
5453
6097
|
if (session) {
|
|
5454
|
-
if (!
|
|
6098
|
+
if (!existsSync7(session.projectPath)) {
|
|
5455
6099
|
session.failureReason = `Project directory not found: ${session.projectPath}`;
|
|
5456
6100
|
}
|
|
5457
6101
|
json(res, 200, session);
|
|
@@ -5491,7 +6135,10 @@ var StreamerServer = class {
|
|
|
5491
6135
|
json(res, 400, { error: "Could not determine project path" });
|
|
5492
6136
|
return;
|
|
5493
6137
|
}
|
|
6138
|
+
const cachedConvMeta = this.cache?.getMetaById(sessionId);
|
|
6139
|
+
const provider = conv?.provider ?? cachedConvMeta?.provider ?? CLAUDE_CODE_PROVIDER;
|
|
5494
6140
|
const session = await this.ptyManager.start(sessionId, {
|
|
6141
|
+
provider,
|
|
5495
6142
|
projectPath,
|
|
5496
6143
|
projectName: body.projectName,
|
|
5497
6144
|
branch: body.branch
|
|
@@ -5850,6 +6497,13 @@ var StreamerServer = class {
|
|
|
5850
6497
|
}
|
|
5851
6498
|
return;
|
|
5852
6499
|
}
|
|
6500
|
+
const body = await readBody(req);
|
|
6501
|
+
const { path: relativePath, provider: requestedProvider, systemPrompt: clientPrompt } = body;
|
|
6502
|
+
if (requestedProvider !== void 0 && !isProviderName(requestedProvider)) {
|
|
6503
|
+
json(res, 400, { error: "Invalid provider" });
|
|
6504
|
+
return;
|
|
6505
|
+
}
|
|
6506
|
+
const provider = requestedProvider ?? CLAUDE_CODE_PROVIDER;
|
|
5853
6507
|
if (!this.browseRoot) {
|
|
5854
6508
|
json(res, 403, {
|
|
5855
6509
|
error: "File browsing not configured. Set browseRoot on the server.",
|
|
@@ -5857,8 +6511,6 @@ var StreamerServer = class {
|
|
|
5857
6511
|
});
|
|
5858
6512
|
return;
|
|
5859
6513
|
}
|
|
5860
|
-
const body = await readBody(req);
|
|
5861
|
-
const { path: relativePath, systemPrompt: clientPrompt } = body;
|
|
5862
6514
|
if (typeof relativePath !== "string") {
|
|
5863
6515
|
json(res, 400, { error: "Missing path field" });
|
|
5864
6516
|
return;
|
|
@@ -5879,21 +6531,27 @@ var StreamerServer = class {
|
|
|
5879
6531
|
].filter(Boolean);
|
|
5880
6532
|
try {
|
|
5881
6533
|
const session = await this.ptyManager.startFresh({
|
|
6534
|
+
provider,
|
|
5882
6535
|
projectPath: resolvedPath,
|
|
5883
6536
|
projectName: body.projectName,
|
|
5884
6537
|
systemPrompt: systemPromptParts.join("\n")
|
|
5885
6538
|
});
|
|
5886
6539
|
this.sessionStore.addManaged(session);
|
|
5887
6540
|
json(res, 202, { id: session.id, status: "pending" });
|
|
5888
|
-
|
|
6541
|
+
if (provider === CODEX_CLI_PROVIDER) {
|
|
6542
|
+
this.watchForCodexRollout(session.id, resolvedPath);
|
|
6543
|
+
} else {
|
|
6544
|
+
this.watchForJsonl(session.id, resolvedPath);
|
|
6545
|
+
}
|
|
5889
6546
|
this.broadcastOrUnicastSessionList(req);
|
|
5890
6547
|
} catch (err) {
|
|
5891
6548
|
const message = err instanceof Error ? err.message : "Failed to start session";
|
|
6549
|
+
const statusCode = typeof err.statusCode === "number" ? err.statusCode : 500;
|
|
5892
6550
|
this.log.error(`[start] failed to start session: ${message}`, {
|
|
5893
6551
|
event: "session.start_failed",
|
|
5894
6552
|
error: message
|
|
5895
6553
|
});
|
|
5896
|
-
json(res,
|
|
6554
|
+
json(res, statusCode, { error: message });
|
|
5897
6555
|
}
|
|
5898
6556
|
}
|
|
5899
6557
|
// ─── Project linking ─────────────────────────────────────────────
|
|
@@ -5966,8 +6624,8 @@ var StreamerServer = class {
|
|
|
5966
6624
|
cleanup();
|
|
5967
6625
|
return;
|
|
5968
6626
|
}
|
|
5969
|
-
let resolvedFilePath =
|
|
5970
|
-
if (!resolvedFilePath &&
|
|
6627
|
+
let resolvedFilePath = existsSync7(filePath) ? filePath : null;
|
|
6628
|
+
if (!resolvedFilePath && existsSync7(projectsDir)) {
|
|
5971
6629
|
try {
|
|
5972
6630
|
const now = Date.now();
|
|
5973
6631
|
const recent = readdirSync4(projectsDir).filter((f) => f.endsWith(".jsonl")).map((f) => ({ f, mtime: statSync5(join12(projectsDir, f)).mtimeMs })).filter(({ mtime }) => now - mtime < 5e3).sort((a, b) => b.mtime - a.mtime)[0];
|
|
@@ -6011,6 +6669,121 @@ var StreamerServer = class {
|
|
|
6011
6669
|
} catch {
|
|
6012
6670
|
}
|
|
6013
6671
|
}
|
|
6672
|
+
// Codex-equivalent of watchForJsonl(). Differs because Codex has no
|
|
6673
|
+
// filename-encoded session id (it assigns its own persisted id) and its
|
|
6674
|
+
// rollout files live under a date-nested directory
|
|
6675
|
+
// (~/.codex/sessions/<YYYY>/<MM>/<DD>/rollout-*.jsonl) that Codex creates
|
|
6676
|
+
// itself — it may not exist yet when this function is first called, so we
|
|
6677
|
+
// poll rather than fs.watch a not-yet-existent directory. Per Phase 0
|
|
6678
|
+
// findings, the rollout file appears within ~1s of process spawn (after
|
|
6679
|
+
// any directory-trust gate is cleared), well before any user input.
|
|
6680
|
+
watchForCodexRollout(sessionId, projectPath) {
|
|
6681
|
+
const deadline = Date.now() + 12e4;
|
|
6682
|
+
const now = /* @__PURE__ */ new Date();
|
|
6683
|
+
const dateDir = join12(
|
|
6684
|
+
String(now.getFullYear()),
|
|
6685
|
+
String(now.getMonth() + 1).padStart(2, "0"),
|
|
6686
|
+
String(now.getDate()).padStart(2, "0")
|
|
6687
|
+
);
|
|
6688
|
+
const sessionStartedAtMs = (this.sessionStore.getManaged(sessionId)?.startedAt?.getTime() ?? Date.now()) - 5e3;
|
|
6689
|
+
let intervalHandle = null;
|
|
6690
|
+
const cleanup = () => {
|
|
6691
|
+
if (intervalHandle) clearInterval(intervalHandle);
|
|
6692
|
+
intervalHandle = null;
|
|
6693
|
+
};
|
|
6694
|
+
const matchesProjectPath = (candidatePath) => {
|
|
6695
|
+
try {
|
|
6696
|
+
const firstLine = readFileSync6(candidatePath, "utf8").split("\n", 1)[0];
|
|
6697
|
+
if (!firstLine) return null;
|
|
6698
|
+
const parsed = JSON.parse(firstLine);
|
|
6699
|
+
if (parsed?.type !== "session_meta") return null;
|
|
6700
|
+
const payload = parsed.payload ?? {};
|
|
6701
|
+
if (payload.cwd !== projectPath) return null;
|
|
6702
|
+
if (typeof payload.id !== "string") return null;
|
|
6703
|
+
const createdIso = payload.timestamp ?? parsed.timestamp;
|
|
6704
|
+
const createdAtMs = typeof createdIso === "string" ? Date.parse(createdIso) : Number.NaN;
|
|
6705
|
+
if (Number.isNaN(createdAtMs) || createdAtMs < sessionStartedAtMs) return null;
|
|
6706
|
+
return { id: payload.id, createdAtMs };
|
|
6707
|
+
} catch {
|
|
6708
|
+
return null;
|
|
6709
|
+
}
|
|
6710
|
+
};
|
|
6711
|
+
const tryWire = () => {
|
|
6712
|
+
if (!this.ptyManager.hasSession(sessionId)) {
|
|
6713
|
+
cleanup();
|
|
6714
|
+
return;
|
|
6715
|
+
}
|
|
6716
|
+
if (Date.now() > deadline) {
|
|
6717
|
+
cleanup();
|
|
6718
|
+
return;
|
|
6719
|
+
}
|
|
6720
|
+
const boundElsewhere = new Set(
|
|
6721
|
+
this.sessionStore.listManaged().filter((s) => s.id !== sessionId && s.boundConversationId != null).map((s) => s.boundConversationId)
|
|
6722
|
+
);
|
|
6723
|
+
for (const root of this.codexRoots) {
|
|
6724
|
+
const sessionsDir = join12(root, dateDir);
|
|
6725
|
+
if (!existsSync7(sessionsDir)) continue;
|
|
6726
|
+
let candidateFiles;
|
|
6727
|
+
try {
|
|
6728
|
+
candidateFiles = readdirSync4(sessionsDir).filter((f) => f.endsWith(".jsonl"));
|
|
6729
|
+
} catch {
|
|
6730
|
+
continue;
|
|
6731
|
+
}
|
|
6732
|
+
const nowMs = Date.now();
|
|
6733
|
+
const recentCandidates = candidateFiles.map((f) => ({ f, mtime: statSync5(join12(sessionsDir, f)).mtimeMs })).filter(({ mtime }) => nowMs - mtime < 1e4).sort((a, b) => b.mtime - a.mtime);
|
|
6734
|
+
for (const { f } of recentCandidates) {
|
|
6735
|
+
const candidatePath = join12(sessionsDir, f);
|
|
6736
|
+
const match = matchesProjectPath(candidatePath);
|
|
6737
|
+
if (!match) continue;
|
|
6738
|
+
if (boundElsewhere.has(match.id)) continue;
|
|
6739
|
+
const codexSessionId = match.id;
|
|
6740
|
+
cleanup();
|
|
6741
|
+
this.sessionStore.updateManaged(sessionId, { boundConversationId: codexSessionId });
|
|
6742
|
+
this.sessionFileMap.set(sessionId, candidatePath);
|
|
6743
|
+
this.fileWatcher.watch(candidatePath);
|
|
6744
|
+
try {
|
|
6745
|
+
const existing = readFileSync6(candidatePath, "utf8").split("\n").filter(Boolean);
|
|
6746
|
+
if (existing.length > 0) {
|
|
6747
|
+
this.wsHub.broadcast({ type: "conversation_events", sessionId, lines: existing });
|
|
6748
|
+
for (const line of existing) {
|
|
6749
|
+
this.wsHub.broadcast({ type: "conversation_event", sessionId, line });
|
|
6750
|
+
}
|
|
6751
|
+
}
|
|
6752
|
+
} catch {
|
|
6753
|
+
}
|
|
6754
|
+
if (this.scannerReady) {
|
|
6755
|
+
this.scannerStale = true;
|
|
6756
|
+
} else {
|
|
6757
|
+
this.scanner = null;
|
|
6758
|
+
}
|
|
6759
|
+
this.linkSessionToProject(sessionId, projectPath, candidatePath);
|
|
6760
|
+
this.cache?.markAsStreamer(sessionId);
|
|
6761
|
+
const resp = this.sessionStore.get(sessionId, this.ptyAttachedIds());
|
|
6762
|
+
if (resp) {
|
|
6763
|
+
this.wsHub.broadcast({ type: "session_update", session: resp });
|
|
6764
|
+
}
|
|
6765
|
+
this.log.info(
|
|
6766
|
+
`[startFresh] bound Codex rollout for ${sessionId}`,
|
|
6767
|
+
{
|
|
6768
|
+
event: "session.codex_rollout_bound",
|
|
6769
|
+
sessionId,
|
|
6770
|
+
boundConversationId: codexSessionId,
|
|
6771
|
+
filePath: candidatePath
|
|
6772
|
+
},
|
|
6773
|
+
"pino"
|
|
6774
|
+
);
|
|
6775
|
+
return;
|
|
6776
|
+
}
|
|
6777
|
+
}
|
|
6778
|
+
};
|
|
6779
|
+
tryWire();
|
|
6780
|
+
if (!intervalHandle && Date.now() <= deadline) {
|
|
6781
|
+
const alreadyBound = this.sessionStore.getManaged(sessionId)?.boundConversationId != null;
|
|
6782
|
+
if (!alreadyBound) {
|
|
6783
|
+
intervalHandle = setInterval(tryWire, 250);
|
|
6784
|
+
}
|
|
6785
|
+
}
|
|
6786
|
+
}
|
|
6014
6787
|
async handleBrowse(url, res) {
|
|
6015
6788
|
if (!this.browseRoot) {
|
|
6016
6789
|
json(res, 403, {
|
|
@@ -6094,7 +6867,7 @@ var StreamerServer = class {
|
|
|
6094
6867
|
};
|
|
6095
6868
|
function classifyResumability(cwd) {
|
|
6096
6869
|
if (!cwd) return { resumable: true };
|
|
6097
|
-
if (
|
|
6870
|
+
if (existsSync7(cwd)) return { resumable: true };
|
|
6098
6871
|
const ranInWorktree = /\/\.worktrees\//.test(cwd) || /\/\.claude\/worktrees\//.test(cwd);
|
|
6099
6872
|
return {
|
|
6100
6873
|
resumable: false,
|
|
@@ -6225,7 +6998,10 @@ function readBody(req) {
|
|
|
6225
6998
|
});
|
|
6226
6999
|
}
|
|
6227
7000
|
export {
|
|
7001
|
+
CLAUDE_CODE_PROVIDER,
|
|
7002
|
+
CODEX_CLI_PROVIDER,
|
|
6228
7003
|
ConversationWatcher,
|
|
7004
|
+
LiveSessionManager,
|
|
6229
7005
|
PTYManager,
|
|
6230
7006
|
SessionStore,
|
|
6231
7007
|
StreamerServer,
|
|
@@ -6239,6 +7015,8 @@ export {
|
|
|
6239
7015
|
generateApiKey,
|
|
6240
7016
|
getDbConfig,
|
|
6241
7017
|
isDbEnabled,
|
|
7018
|
+
isProviderName,
|
|
7019
|
+
isProviderResumable,
|
|
6242
7020
|
loadOrCreateApiKey,
|
|
6243
7021
|
maskConnectionString,
|
|
6244
7022
|
readAgentConfig,
|