@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.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
1007
|
}
|
|
602
|
-
|
|
603
|
-
|
|
604
|
-
|
|
605
|
-
|
|
606
|
-
|
|
607
|
-
|
|
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
|
-
}
|
|
616
|
-
);
|
|
617
|
-
});
|
|
618
|
-
}
|
|
619
|
-
async function getPidsUnix() {
|
|
620
|
-
try {
|
|
621
|
-
const output = await run("pgrep", ["-x", "claude"]);
|
|
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
|
}
|
|
@@ -971,6 +1357,10 @@ var PTYManager = class {
|
|
|
971
1357
|
// to a given input or fell silent. Reset on dispose().
|
|
972
1358
|
chunkIndex = /* @__PURE__ */ new Map();
|
|
973
1359
|
lastChunkAt = /* @__PURE__ */ new Map();
|
|
1360
|
+
// In-flight start()/startFresh() calls keyed by sessionId. A second
|
|
1361
|
+
// concurrent resume for the same session (double-tap, client retry) awaits
|
|
1362
|
+
// the first call's promise instead of spawning a duplicate PTY (CRITICAL #3).
|
|
1363
|
+
startPromises = /* @__PURE__ */ new Map();
|
|
974
1364
|
constructor(options = {}) {
|
|
975
1365
|
this.onOutput = options.onOutput;
|
|
976
1366
|
this.onStatusChange = options.onStatusChange;
|
|
@@ -993,7 +1383,18 @@ var PTYManager = class {
|
|
|
993
1383
|
// custom-API-key — are cleared by the seeded ~/.claude.json in
|
|
994
1384
|
// docker/entrypoint.sh.) startFresh() uses the same flag for the same reason.
|
|
995
1385
|
async start(sessionId, options) {
|
|
996
|
-
const
|
|
1386
|
+
const existing = this.sessions.get(sessionId);
|
|
1387
|
+
if (existing) return toPublicSession2(existing);
|
|
1388
|
+
const inFlight = this.startPromises.get(sessionId);
|
|
1389
|
+
if (inFlight) return inFlight;
|
|
1390
|
+
const promise = this.doStart(sessionId, options).finally(() => {
|
|
1391
|
+
this.startPromises.delete(sessionId);
|
|
1392
|
+
});
|
|
1393
|
+
this.startPromises.set(sessionId, promise);
|
|
1394
|
+
return promise;
|
|
1395
|
+
}
|
|
1396
|
+
async doStart(sessionId, options) {
|
|
1397
|
+
const nodePty = await loadPty2();
|
|
997
1398
|
const projectName = options.projectName ?? basename2(options.projectPath);
|
|
998
1399
|
const proc = nodePty.spawn(
|
|
999
1400
|
resolveClaudeExe(),
|
|
@@ -1015,6 +1416,7 @@ var PTYManager = class {
|
|
|
1015
1416
|
);
|
|
1016
1417
|
const session = {
|
|
1017
1418
|
id: sessionId,
|
|
1419
|
+
provider: CLAUDE_CODE_PROVIDER,
|
|
1018
1420
|
projectPath: options.projectPath,
|
|
1019
1421
|
projectName,
|
|
1020
1422
|
branch: options.branch ?? "",
|
|
@@ -1025,7 +1427,7 @@ var PTYManager = class {
|
|
|
1025
1427
|
lastOutput: "",
|
|
1026
1428
|
process: proc,
|
|
1027
1429
|
outputBuffer: Buffer.alloc(0),
|
|
1028
|
-
screen:
|
|
1430
|
+
screen: createScreen2()
|
|
1029
1431
|
};
|
|
1030
1432
|
this.sessions.set(sessionId, session);
|
|
1031
1433
|
this.pendingReady.add(sessionId);
|
|
@@ -1036,14 +1438,14 @@ var PTYManager = class {
|
|
|
1036
1438
|
this.pendingReady.delete(sessionId);
|
|
1037
1439
|
this.handleExit(sessionId, exitCode);
|
|
1038
1440
|
});
|
|
1039
|
-
return
|
|
1441
|
+
return toPublicSession2(session);
|
|
1040
1442
|
}
|
|
1041
1443
|
// Start a brand-new Claude session. A stable UUID is generated here and passed
|
|
1042
1444
|
// to Claude via --session-id so the JSONL filename matches from the start.
|
|
1043
1445
|
// onReady fires once Claude reaches its first prompt (waiting_input).
|
|
1044
1446
|
async startFresh(options) {
|
|
1045
|
-
const nodePty = await
|
|
1046
|
-
const sessionId =
|
|
1447
|
+
const nodePty = await loadPty2();
|
|
1448
|
+
const sessionId = randomUUID2();
|
|
1047
1449
|
const projectName = options.projectName ?? basename2(options.projectPath);
|
|
1048
1450
|
const args = [
|
|
1049
1451
|
"--permission-mode",
|
|
@@ -1065,6 +1467,7 @@ var PTYManager = class {
|
|
|
1065
1467
|
});
|
|
1066
1468
|
const session = {
|
|
1067
1469
|
id: sessionId,
|
|
1470
|
+
provider: CLAUDE_CODE_PROVIDER,
|
|
1068
1471
|
projectPath: options.projectPath,
|
|
1069
1472
|
projectName,
|
|
1070
1473
|
branch: "",
|
|
@@ -1075,7 +1478,7 @@ var PTYManager = class {
|
|
|
1075
1478
|
lastOutput: "",
|
|
1076
1479
|
process: proc,
|
|
1077
1480
|
outputBuffer: Buffer.alloc(0),
|
|
1078
|
-
screen:
|
|
1481
|
+
screen: createScreen2()
|
|
1079
1482
|
};
|
|
1080
1483
|
this.sessions.set(sessionId, session);
|
|
1081
1484
|
this.pendingReady.add(sessionId);
|
|
@@ -1086,7 +1489,7 @@ var PTYManager = class {
|
|
|
1086
1489
|
this.pendingReady.delete(sessionId);
|
|
1087
1490
|
this.handleExit(sessionId, exitCode);
|
|
1088
1491
|
});
|
|
1089
|
-
return
|
|
1492
|
+
return toPublicSession2(session);
|
|
1090
1493
|
}
|
|
1091
1494
|
// Write raw key bytes directly to the PTY without bracketed-paste wrapping.
|
|
1092
1495
|
// Use for control sequences (arrow keys, Enter) that must not be quoted.
|
|
@@ -1098,10 +1501,10 @@ var PTYManager = class {
|
|
|
1098
1501
|
}
|
|
1099
1502
|
if (session.status === "waiting_input") {
|
|
1100
1503
|
session.status = "running";
|
|
1101
|
-
this.onStatusChange?.(
|
|
1504
|
+
this.onStatusChange?.(toPublicSession2(session));
|
|
1102
1505
|
}
|
|
1103
1506
|
this.log.info(
|
|
1104
|
-
`[pty.keys.write] ${sessionId.slice(0, 8)} bytes=${keys.length} digest=${
|
|
1507
|
+
`[pty.keys.write] ${sessionId.slice(0, 8)} bytes=${keys.length} digest=${digestBytes2(keys)}`,
|
|
1105
1508
|
{ event: "pty.keys_write", sessionId, byteLen: keys.length }
|
|
1106
1509
|
);
|
|
1107
1510
|
session.process.write(keys);
|
|
@@ -1133,7 +1536,7 @@ var PTYManager = class {
|
|
|
1133
1536
|
}
|
|
1134
1537
|
if (session.status === "waiting_input") {
|
|
1135
1538
|
session.status = "running";
|
|
1136
|
-
this.onStatusChange?.(
|
|
1539
|
+
this.onStatusChange?.(toPublicSession2(session));
|
|
1137
1540
|
}
|
|
1138
1541
|
this.writeSubmit(sessionId, session, input, "direct", session.promptCount + 1);
|
|
1139
1542
|
session.lastActivityAt = /* @__PURE__ */ new Date();
|
|
@@ -1146,13 +1549,13 @@ var PTYManager = class {
|
|
|
1146
1549
|
writeSubmit(sessionId, session, input, path, promptCount) {
|
|
1147
1550
|
const pasteBytes = buildPasteBytes(input);
|
|
1148
1551
|
this.log.info(
|
|
1149
|
-
`[pty.input.write] ${sessionId.slice(0, 8)} promptCount=${promptCount} bytes=${pasteBytes.length} digest=${
|
|
1552
|
+
`[pty.input.write] ${sessionId.slice(0, 8)} promptCount=${promptCount} bytes=${pasteBytes.length} digest=${digestBytes2(pasteBytes)}`,
|
|
1150
1553
|
{
|
|
1151
1554
|
event: "pty.input_write",
|
|
1152
1555
|
sessionId,
|
|
1153
1556
|
promptCount,
|
|
1154
1557
|
byteLen: pasteBytes.length,
|
|
1155
|
-
digest:
|
|
1558
|
+
digest: digestBytes2(pasteBytes),
|
|
1156
1559
|
path,
|
|
1157
1560
|
phase: "paste"
|
|
1158
1561
|
}
|
|
@@ -1167,13 +1570,13 @@ var PTYManager = class {
|
|
|
1167
1570
|
event: "pty.input_write",
|
|
1168
1571
|
sessionId,
|
|
1169
1572
|
promptCount,
|
|
1170
|
-
byteLen:
|
|
1573
|
+
byteLen: SUBMIT_BYTES2.length,
|
|
1171
1574
|
digest: "\\r",
|
|
1172
1575
|
path,
|
|
1173
1576
|
phase: "submit"
|
|
1174
1577
|
}
|
|
1175
1578
|
);
|
|
1176
|
-
current.process.write(
|
|
1579
|
+
current.process.write(SUBMIT_BYTES2);
|
|
1177
1580
|
}, SUBMIT_DELAY_MS);
|
|
1178
1581
|
}
|
|
1179
1582
|
// Drain any inputs that were sent while the session was still pendingReady,
|
|
@@ -1231,7 +1634,7 @@ var PTYManager = class {
|
|
|
1231
1634
|
session.completedAt = /* @__PURE__ */ new Date();
|
|
1232
1635
|
session.screen.dispose();
|
|
1233
1636
|
this.sessions.delete(sessionId);
|
|
1234
|
-
this.onStatusChange?.(
|
|
1637
|
+
this.onStatusChange?.(toPublicSession2(session));
|
|
1235
1638
|
}
|
|
1236
1639
|
getOutput(sessionId) {
|
|
1237
1640
|
const session = this.sessions.get(sessionId);
|
|
@@ -1262,13 +1665,13 @@ var PTYManager = class {
|
|
|
1262
1665
|
}
|
|
1263
1666
|
getSession(sessionId) {
|
|
1264
1667
|
const session = this.sessions.get(sessionId);
|
|
1265
|
-
return session ?
|
|
1668
|
+
return session ? toPublicSession2(session) : null;
|
|
1266
1669
|
}
|
|
1267
1670
|
hasSession(sessionId) {
|
|
1268
1671
|
return this.sessions.has(sessionId);
|
|
1269
1672
|
}
|
|
1270
1673
|
listSessions() {
|
|
1271
|
-
return Array.from(this.sessions.values()).map(
|
|
1674
|
+
return Array.from(this.sessions.values()).map(toPublicSession2);
|
|
1272
1675
|
}
|
|
1273
1676
|
dispose() {
|
|
1274
1677
|
for (const session of this.sessions.values()) {
|
|
@@ -1300,7 +1703,7 @@ var PTYManager = class {
|
|
|
1300
1703
|
this.lastChunkAt.set(sessionId, now);
|
|
1301
1704
|
const gapMs = last == null ? 0 : now - last;
|
|
1302
1705
|
this.log.info(
|
|
1303
|
-
`[pty.chunk] ${sessionId.slice(0, 8)} #${idx} +${chunk.length}B gap=${gapMs}ms status=${session.status} digest=${
|
|
1706
|
+
`[pty.chunk] ${sessionId.slice(0, 8)} #${idx} +${chunk.length}B gap=${gapMs}ms status=${session.status} digest=${digestBytes2(data)}`,
|
|
1304
1707
|
{
|
|
1305
1708
|
event: "pty.chunk",
|
|
1306
1709
|
sessionId,
|
|
@@ -1309,17 +1712,17 @@ var PTYManager = class {
|
|
|
1309
1712
|
gapMs,
|
|
1310
1713
|
status: session.status,
|
|
1311
1714
|
pendingReady: this.pendingReady.has(sessionId),
|
|
1312
|
-
digest:
|
|
1715
|
+
digest: digestBytes2(data)
|
|
1313
1716
|
}
|
|
1314
1717
|
);
|
|
1315
1718
|
session.outputBuffer = Buffer.concat([session.outputBuffer, chunk]);
|
|
1316
|
-
if (session.outputBuffer.length >
|
|
1719
|
+
if (session.outputBuffer.length > OUTPUT_BUFFER_MAX2) {
|
|
1317
1720
|
session.outputBuffer = session.outputBuffer.subarray(
|
|
1318
|
-
session.outputBuffer.length -
|
|
1721
|
+
session.outputBuffer.length - OUTPUT_BUFFER_MAX2
|
|
1319
1722
|
);
|
|
1320
1723
|
}
|
|
1321
1724
|
session.screen.write(data);
|
|
1322
|
-
const stripped =
|
|
1725
|
+
const stripped = stripAnsi2(data);
|
|
1323
1726
|
session.lastOutput = stripped;
|
|
1324
1727
|
const matchedMarker = CLAUDE_PROMPT_MARKERS.find((m) => stripped.includes(m));
|
|
1325
1728
|
if (session.status === "running" && matchedMarker) {
|
|
@@ -1422,11 +1825,11 @@ var PTYManager = class {
|
|
|
1422
1825
|
reason,
|
|
1423
1826
|
elapsedMs
|
|
1424
1827
|
});
|
|
1425
|
-
this.onStatusChange?.(
|
|
1828
|
+
this.onStatusChange?.(toPublicSession2(session));
|
|
1426
1829
|
if (this.pendingReady.has(sessionId)) {
|
|
1427
1830
|
this.pendingReady.delete(sessionId);
|
|
1428
1831
|
this.flushQueuedInputs(sessionId);
|
|
1429
|
-
this.onReady?.(
|
|
1832
|
+
this.onReady?.(toPublicSession2(session));
|
|
1430
1833
|
}
|
|
1431
1834
|
}
|
|
1432
1835
|
handleExit(sessionId, exitCode) {
|
|
@@ -1436,13 +1839,13 @@ var PTYManager = class {
|
|
|
1436
1839
|
session.status = "idle";
|
|
1437
1840
|
const elapsedMs = session.completedAt.getTime() - session.startedAt.getTime();
|
|
1438
1841
|
if (exitCode !== 0 && elapsedMs < 2e3 && session.lastOutput === "") {
|
|
1439
|
-
if (!
|
|
1842
|
+
if (!existsSync3(session.projectPath)) {
|
|
1440
1843
|
session.failureReason = `Project directory not found: ${session.projectPath}`;
|
|
1441
1844
|
} else {
|
|
1442
1845
|
session.failureReason = `Process exited immediately (code ${exitCode}). Check that the Claude binary is installed and accessible.`;
|
|
1443
1846
|
}
|
|
1444
1847
|
}
|
|
1445
|
-
this.onStatusChange?.(
|
|
1848
|
+
this.onStatusChange?.(toPublicSession2(session));
|
|
1446
1849
|
session.screen.dispose();
|
|
1447
1850
|
this.sessions.delete(sessionId);
|
|
1448
1851
|
this.queuedInputs.delete(sessionId);
|
|
@@ -1452,9 +1855,10 @@ var PTYManager = class {
|
|
|
1452
1855
|
this.shellPromptOpen.delete(sessionId);
|
|
1453
1856
|
}
|
|
1454
1857
|
};
|
|
1455
|
-
function
|
|
1858
|
+
function toPublicSession2(s) {
|
|
1456
1859
|
return {
|
|
1457
1860
|
id: s.id,
|
|
1861
|
+
provider: s.provider ?? CLAUDE_CODE_PROVIDER,
|
|
1458
1862
|
projectPath: s.projectPath,
|
|
1459
1863
|
projectName: s.projectName,
|
|
1460
1864
|
branch: s.branch,
|
|
@@ -1468,10 +1872,249 @@ function toPublicSession(s) {
|
|
|
1468
1872
|
...s.filePath != null && { filePath: s.filePath }
|
|
1469
1873
|
};
|
|
1470
1874
|
}
|
|
1471
|
-
function
|
|
1875
|
+
function stripAnsi2(str) {
|
|
1472
1876
|
return str.replace(/\x1b\[[0-9;]*[a-zA-Z]/g, "").replace(/\x1b\][^\x07]*\x07/g, "");
|
|
1473
1877
|
}
|
|
1474
1878
|
|
|
1879
|
+
// src/live-session-manager.ts
|
|
1880
|
+
var LiveSessionManager = class {
|
|
1881
|
+
runners;
|
|
1882
|
+
constructor(options = {}) {
|
|
1883
|
+
this.runners = /* @__PURE__ */ new Map([
|
|
1884
|
+
[CLAUDE_CODE_PROVIDER, new PTYManager(options)],
|
|
1885
|
+
[CODEX_CLI_PROVIDER, new CodexPtyRunner(options)]
|
|
1886
|
+
]);
|
|
1887
|
+
}
|
|
1888
|
+
async start(sessionId, options) {
|
|
1889
|
+
const provider = options.provider ?? CLAUDE_CODE_PROVIDER;
|
|
1890
|
+
const runner = this.assertSupportedProvider(provider, options.projectPath);
|
|
1891
|
+
return runner.start(sessionId, options);
|
|
1892
|
+
}
|
|
1893
|
+
async startFresh(options) {
|
|
1894
|
+
const provider = options.provider ?? CLAUDE_CODE_PROVIDER;
|
|
1895
|
+
const runner = this.assertSupportedProvider(provider, options.projectPath);
|
|
1896
|
+
return runner.startFresh(options);
|
|
1897
|
+
}
|
|
1898
|
+
sendInput(sessionId, input) {
|
|
1899
|
+
return this.runnerFor(sessionId).sendInput(sessionId, input);
|
|
1900
|
+
}
|
|
1901
|
+
sendKeys(sessionId, keys) {
|
|
1902
|
+
this.runnerFor(sessionId).sendKeys(sessionId, keys);
|
|
1903
|
+
}
|
|
1904
|
+
cancel(sessionId) {
|
|
1905
|
+
this.runnerFor(sessionId).cancel(sessionId);
|
|
1906
|
+
}
|
|
1907
|
+
killPid(pid) {
|
|
1908
|
+
for (const runner of this.runners.values()) {
|
|
1909
|
+
runner.killPid(pid);
|
|
1910
|
+
}
|
|
1911
|
+
}
|
|
1912
|
+
// putOnHold tolerates an unknown sessionId (PTYManager.putOnHold is a no-op
|
|
1913
|
+
// when the session isn't in its map), so — unlike the other session-keyed
|
|
1914
|
+
// methods — route to the owning runner when found, otherwise broadcast to
|
|
1915
|
+
// every runner rather than throwing; this matches the pre-extraction
|
|
1916
|
+
// behavior of delegating straight through with no existence check.
|
|
1917
|
+
putOnHold(sessionId) {
|
|
1918
|
+
for (const runner of this.runners.values()) {
|
|
1919
|
+
if (runner.hasSession(sessionId) || runner.getSession(sessionId)) {
|
|
1920
|
+
runner.putOnHold(sessionId);
|
|
1921
|
+
return;
|
|
1922
|
+
}
|
|
1923
|
+
}
|
|
1924
|
+
for (const runner of this.runners.values()) {
|
|
1925
|
+
runner.putOnHold(sessionId);
|
|
1926
|
+
}
|
|
1927
|
+
}
|
|
1928
|
+
getOutput(sessionId) {
|
|
1929
|
+
return this.runnerFor(sessionId).getOutput(sessionId);
|
|
1930
|
+
}
|
|
1931
|
+
getOutputLines(sessionId, maxLines) {
|
|
1932
|
+
return this.runnerFor(sessionId).getOutputLines(sessionId, maxLines);
|
|
1933
|
+
}
|
|
1934
|
+
getSession(sessionId) {
|
|
1935
|
+
for (const runner of this.runners.values()) {
|
|
1936
|
+
const session = runner.getSession(sessionId);
|
|
1937
|
+
if (session) return session;
|
|
1938
|
+
}
|
|
1939
|
+
return null;
|
|
1940
|
+
}
|
|
1941
|
+
hasSession(sessionId) {
|
|
1942
|
+
for (const runner of this.runners.values()) {
|
|
1943
|
+
if (runner.hasSession(sessionId)) return true;
|
|
1944
|
+
}
|
|
1945
|
+
return false;
|
|
1946
|
+
}
|
|
1947
|
+
listSessions() {
|
|
1948
|
+
return Array.from(this.runners.values()).flatMap((runner) => runner.listSessions());
|
|
1949
|
+
}
|
|
1950
|
+
dispose() {
|
|
1951
|
+
for (const runner of this.runners.values()) {
|
|
1952
|
+
runner.dispose();
|
|
1953
|
+
}
|
|
1954
|
+
}
|
|
1955
|
+
// Look up which runner owns a session. Only one runner exists today, so
|
|
1956
|
+
// this is a linear scan across hasSession()/getSession() rather than a
|
|
1957
|
+
// separate session→provider index — see task-1-brief.md.
|
|
1958
|
+
runnerFor(sessionId) {
|
|
1959
|
+
for (const runner of this.runners.values()) {
|
|
1960
|
+
if (runner.hasSession(sessionId) || runner.getSession(sessionId)) return runner;
|
|
1961
|
+
}
|
|
1962
|
+
throw new Error(`Session not found: ${sessionId}`);
|
|
1963
|
+
}
|
|
1964
|
+
assertSupportedProvider(provider, projectPath) {
|
|
1965
|
+
const runner = this.runners.get(provider);
|
|
1966
|
+
if (runner) return runner;
|
|
1967
|
+
const err = new Error(
|
|
1968
|
+
`Live ${provider} sessions are not implemented yet for ${basename3(projectPath)}`
|
|
1969
|
+
);
|
|
1970
|
+
err.statusCode = 501;
|
|
1971
|
+
throw err;
|
|
1972
|
+
}
|
|
1973
|
+
};
|
|
1974
|
+
|
|
1975
|
+
// src/process-discovery.ts
|
|
1976
|
+
import { execFile } from "child_process";
|
|
1977
|
+
import { platform as platform2 } from "os";
|
|
1978
|
+
import { basename as basename4, dirname as dirname3 } from "path";
|
|
1979
|
+
async function discoverClaudeProcesses() {
|
|
1980
|
+
if (platform2() === "win32") return discoverWindows();
|
|
1981
|
+
return discoverUnix();
|
|
1982
|
+
}
|
|
1983
|
+
async function discoverUnix() {
|
|
1984
|
+
const pids = await getPidsUnix();
|
|
1985
|
+
const results = await Promise.all(
|
|
1986
|
+
pids.map(async (pid) => {
|
|
1987
|
+
try {
|
|
1988
|
+
const [cwd, args, startedAt] = await Promise.all([
|
|
1989
|
+
getProcessCwdUnix(pid),
|
|
1990
|
+
getProcessArgsUnix(pid),
|
|
1991
|
+
getProcessStartTimeUnix(pid)
|
|
1992
|
+
]);
|
|
1993
|
+
const conversationId = extractResumeId(args);
|
|
1994
|
+
return {
|
|
1995
|
+
pid,
|
|
1996
|
+
projectPath: cwd,
|
|
1997
|
+
projectName: basename4(cwd),
|
|
1998
|
+
branch: await readGitBranch(cwd),
|
|
1999
|
+
conversationId,
|
|
2000
|
+
startedAt
|
|
2001
|
+
};
|
|
2002
|
+
} catch {
|
|
2003
|
+
return null;
|
|
2004
|
+
}
|
|
2005
|
+
})
|
|
2006
|
+
);
|
|
2007
|
+
return results.filter((r) => r !== null);
|
|
2008
|
+
}
|
|
2009
|
+
async function discoverWindows() {
|
|
2010
|
+
const pids = await getPidsWindows();
|
|
2011
|
+
const results = await Promise.all(
|
|
2012
|
+
pids.map(async (pid) => {
|
|
2013
|
+
try {
|
|
2014
|
+
const info = await getProcessInfoWindows(pid);
|
|
2015
|
+
if (!info) return null;
|
|
2016
|
+
return {
|
|
2017
|
+
pid,
|
|
2018
|
+
projectPath: info.cwd,
|
|
2019
|
+
projectName: basename4(info.cwd),
|
|
2020
|
+
branch: await readGitBranch(info.cwd),
|
|
2021
|
+
conversationId: extractResumeId(info.args),
|
|
2022
|
+
startedAt: info.startedAt
|
|
2023
|
+
};
|
|
2024
|
+
} catch {
|
|
2025
|
+
return null;
|
|
2026
|
+
}
|
|
2027
|
+
})
|
|
2028
|
+
);
|
|
2029
|
+
return results.filter((r) => r !== null);
|
|
2030
|
+
}
|
|
2031
|
+
function run(cmd, args, opts = {}) {
|
|
2032
|
+
return new Promise((resolve2, reject) => {
|
|
2033
|
+
execFile(
|
|
2034
|
+
cmd,
|
|
2035
|
+
args,
|
|
2036
|
+
{ windowsHide: isWindows, encoding: "utf-8", timeout: opts.timeout ?? 5e3, cwd: opts.cwd },
|
|
2037
|
+
(err, stdout) => {
|
|
2038
|
+
if (err) reject(err);
|
|
2039
|
+
else resolve2(stdout);
|
|
2040
|
+
}
|
|
2041
|
+
);
|
|
2042
|
+
});
|
|
2043
|
+
}
|
|
2044
|
+
async function getPidsUnix() {
|
|
2045
|
+
try {
|
|
2046
|
+
const output = await run("pgrep", ["-x", "claude"]);
|
|
2047
|
+
return output.trim().split("\n").filter(Boolean).map((s) => Number.parseInt(s, 10));
|
|
2048
|
+
} catch {
|
|
2049
|
+
return [];
|
|
2050
|
+
}
|
|
2051
|
+
}
|
|
2052
|
+
async function getProcessCwdUnix(pid) {
|
|
2053
|
+
const output = await run("lsof", ["-p", String(pid), "-a", "-d", "cwd", "-Fn"]);
|
|
2054
|
+
const match = output.match(/n(.+)/);
|
|
2055
|
+
return match?.[1] ?? "";
|
|
2056
|
+
}
|
|
2057
|
+
async function getProcessArgsUnix(pid) {
|
|
2058
|
+
return (await run("ps", ["-p", String(pid), "-o", "args="])).trim();
|
|
2059
|
+
}
|
|
2060
|
+
async function getProcessStartTimeUnix(pid) {
|
|
2061
|
+
const raw = (await run("ps", ["-p", String(pid), "-o", "lstart="])).trim();
|
|
2062
|
+
const d = new Date(raw);
|
|
2063
|
+
return Number.isNaN(d.getTime()) ? /* @__PURE__ */ new Date() : d;
|
|
2064
|
+
}
|
|
2065
|
+
async function getPidsWindows() {
|
|
2066
|
+
try {
|
|
2067
|
+
const output = await run("tasklist", ["/FI", "IMAGENAME eq claude.exe", "/FO", "CSV", "/NH"]);
|
|
2068
|
+
return output.trim().split("\n").filter(Boolean).map((line) => {
|
|
2069
|
+
const parts = line.split(",");
|
|
2070
|
+
return Number.parseInt(parts[1]?.replace(/"/g, "") ?? "0", 10);
|
|
2071
|
+
}).filter((pid) => pid > 0);
|
|
2072
|
+
} catch {
|
|
2073
|
+
return [];
|
|
2074
|
+
}
|
|
2075
|
+
}
|
|
2076
|
+
async function getProcessInfoWindows(pid) {
|
|
2077
|
+
try {
|
|
2078
|
+
const output = await run("wmic", [
|
|
2079
|
+
"process",
|
|
2080
|
+
"where",
|
|
2081
|
+
`ProcessId=${pid}`,
|
|
2082
|
+
"get",
|
|
2083
|
+
"CommandLine,CreationDate,ExecutablePath",
|
|
2084
|
+
"/FORMAT:CSV"
|
|
2085
|
+
]);
|
|
2086
|
+
const lines = output.trim().split(/\r?\n/).filter((l) => l.trim().length > 0);
|
|
2087
|
+
if (lines.length < 2) return null;
|
|
2088
|
+
const parts = lines[1].split(",");
|
|
2089
|
+
const args = parts[1] ?? "";
|
|
2090
|
+
const creationDate = parts[2] ?? "";
|
|
2091
|
+
const year = creationDate.slice(0, 4);
|
|
2092
|
+
const month = creationDate.slice(4, 6);
|
|
2093
|
+
const day = creationDate.slice(6, 8);
|
|
2094
|
+
const hour = creationDate.slice(8, 10);
|
|
2095
|
+
const min = creationDate.slice(10, 12);
|
|
2096
|
+
const sec = creationDate.slice(12, 14);
|
|
2097
|
+
const startedAt = /* @__PURE__ */ new Date(`${year}-${month}-${day}T${hour}:${min}:${sec}`);
|
|
2098
|
+
if (Number.isNaN(startedAt.getTime())) return null;
|
|
2099
|
+
const exePath = parts[3] ?? "";
|
|
2100
|
+
const cwd = exePath ? dirname3(exePath) : "";
|
|
2101
|
+
return { cwd, args, startedAt };
|
|
2102
|
+
} catch {
|
|
2103
|
+
return null;
|
|
2104
|
+
}
|
|
2105
|
+
}
|
|
2106
|
+
function extractResumeId(args) {
|
|
2107
|
+
const match = args.match(/--resume\s+(\S+)/);
|
|
2108
|
+
return match?.[1] ?? null;
|
|
2109
|
+
}
|
|
2110
|
+
async function readGitBranch(dir) {
|
|
2111
|
+
try {
|
|
2112
|
+
return (await run("git", ["rev-parse", "--abbrev-ref", "HEAD"], { cwd: dir, timeout: 3e3 })).trim();
|
|
2113
|
+
} catch {
|
|
2114
|
+
return "";
|
|
2115
|
+
}
|
|
2116
|
+
}
|
|
2117
|
+
|
|
1475
2118
|
// src/server.ts
|
|
1476
2119
|
import { createNodeWebSocket } from "@hono/node-ws";
|
|
1477
2120
|
import { Connection, Client as TemporalClient } from "@temporalio/client";
|
|
@@ -1486,7 +2129,7 @@ import {
|
|
|
1486
2129
|
import { EventEmitter } from "events";
|
|
1487
2130
|
import {
|
|
1488
2131
|
createReadStream,
|
|
1489
|
-
existsSync as
|
|
2132
|
+
existsSync as existsSync7,
|
|
1490
2133
|
watch as fsWatch,
|
|
1491
2134
|
readdirSync as readdirSync4,
|
|
1492
2135
|
readFileSync as readFileSync6,
|
|
@@ -1678,7 +2321,7 @@ async function handleSendAgentInput(sessionId, body, deps) {
|
|
|
1678
2321
|
}
|
|
1679
2322
|
|
|
1680
2323
|
// src/agent/handle-start-agent-session.ts
|
|
1681
|
-
import { existsSync as
|
|
2324
|
+
import { existsSync as existsSync4 } from "fs";
|
|
1682
2325
|
import { join as join5 } from "path";
|
|
1683
2326
|
function validateBody(body) {
|
|
1684
2327
|
if (body === null || body === void 0 || typeof body !== "object") {
|
|
@@ -1710,7 +2353,7 @@ async function handleStartAgentSession(body, deps) {
|
|
|
1710
2353
|
let conversationId = parsed.conversationId;
|
|
1711
2354
|
if (conversationId) {
|
|
1712
2355
|
const jsonlPath = join5(deps.conversationsDir, `${conversationId}.jsonl`);
|
|
1713
|
-
if (!
|
|
2356
|
+
if (!existsSync4(jsonlPath)) {
|
|
1714
2357
|
return {
|
|
1715
2358
|
status: 404,
|
|
1716
2359
|
body: agentErrorResponse(
|
|
@@ -2300,7 +2943,7 @@ async function createDirectory(parentAbsolutePath, name) {
|
|
|
2300
2943
|
|
|
2301
2944
|
// src/conversation-cache.ts
|
|
2302
2945
|
import Database from "better-sqlite3";
|
|
2303
|
-
import { closeSync as closeSync2, existsSync as
|
|
2946
|
+
import { closeSync as closeSync2, existsSync as existsSync5, mkdirSync as mkdirSync2, openSync as openSync2, readSync as readSync2, statSync as statSync2 } from "fs";
|
|
2304
2947
|
import { dirname as dirname6 } from "path";
|
|
2305
2948
|
|
|
2306
2949
|
// src/db/sqlite-migrate.ts
|
|
@@ -2344,14 +2987,6 @@ function runSqliteMigrations(db, migrationsDir) {
|
|
|
2344
2987
|
return { applied, skipped };
|
|
2345
2988
|
}
|
|
2346
2989
|
|
|
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
2990
|
// src/services/conversations/isAgentConversation.ts
|
|
2356
2991
|
import { closeSync, openSync, readSync, statSync } from "fs";
|
|
2357
2992
|
var DEFAULT_AGENT_ENTRYPOINTS = /* @__PURE__ */ new Set(["sdk-cli", "claude-vscode"]);
|
|
@@ -3072,7 +3707,7 @@ var ConversationCache = class _ConversationCache {
|
|
|
3072
3707
|
* `handleGetConversation` can still serve the cached tail even when the
|
|
3073
3708
|
* JSONL has been deleted.
|
|
3074
3709
|
*/
|
|
3075
|
-
pruneGhostFiles(exists =
|
|
3710
|
+
pruneGhostFiles(exists = existsSync5) {
|
|
3076
3711
|
const rows = this.stmts.allFilePaths.all();
|
|
3077
3712
|
const ghosts = [];
|
|
3078
3713
|
const prune = this.db.transaction((ids) => {
|
|
@@ -3152,7 +3787,7 @@ var ConversationsRepository = class {
|
|
|
3152
3787
|
};
|
|
3153
3788
|
|
|
3154
3789
|
// src/db/repositories/projects.repository.ts
|
|
3155
|
-
import { randomUUID as
|
|
3790
|
+
import { randomUUID as randomUUID3 } from "crypto";
|
|
3156
3791
|
|
|
3157
3792
|
// src/utils/canonicalizeProjectPath.ts
|
|
3158
3793
|
function canonicalizeProjectPath(projectPath) {
|
|
@@ -3245,7 +3880,7 @@ var ProjectsRepository = class {
|
|
|
3245
3880
|
});
|
|
3246
3881
|
return rowToProject(this.getById.get(existing.id));
|
|
3247
3882
|
}
|
|
3248
|
-
const id =
|
|
3883
|
+
const id = randomUUID3();
|
|
3249
3884
|
this.insert.run({
|
|
3250
3885
|
id,
|
|
3251
3886
|
path,
|
|
@@ -3542,14 +4177,14 @@ var ConversationWatcher = class {
|
|
|
3542
4177
|
};
|
|
3543
4178
|
|
|
3544
4179
|
// src/services/conversations/pruneAgentConversations.ts
|
|
3545
|
-
import { existsSync as
|
|
4180
|
+
import { existsSync as existsSync6 } from "fs";
|
|
3546
4181
|
function pruneAgentConversations(cache) {
|
|
3547
4182
|
const db = cache.getDatabase();
|
|
3548
4183
|
const rows = db.prepare("SELECT id, file_path FROM conversation_meta").all();
|
|
3549
4184
|
let pruned = 0;
|
|
3550
4185
|
let missing = 0;
|
|
3551
4186
|
for (const row of rows) {
|
|
3552
|
-
if (!
|
|
4187
|
+
if (!existsSync6(row.file_path)) {
|
|
3553
4188
|
missing += 1;
|
|
3554
4189
|
continue;
|
|
3555
4190
|
}
|
|
@@ -3832,6 +4467,7 @@ function managedToResponse(s, ptyAttached) {
|
|
|
3832
4467
|
return {
|
|
3833
4468
|
id: s.id,
|
|
3834
4469
|
conversationId: s.id,
|
|
4470
|
+
provider: s.provider ?? CLAUDE_CODE_PROVIDER,
|
|
3835
4471
|
status: s.status,
|
|
3836
4472
|
projectPath: s.projectPath,
|
|
3837
4473
|
projectName: s.projectName,
|
|
@@ -3857,13 +4493,15 @@ function managedToResponse(s, ptyAttached) {
|
|
|
3857
4493
|
...s.failureReason != null && { failureReason: s.failureReason },
|
|
3858
4494
|
...s.resumedFromConversationId != null && {
|
|
3859
4495
|
resumedFromConversationId: s.resumedFromConversationId
|
|
3860
|
-
}
|
|
4496
|
+
},
|
|
4497
|
+
...s.boundConversationId != null && { boundConversationId: s.boundConversationId }
|
|
3861
4498
|
};
|
|
3862
4499
|
}
|
|
3863
4500
|
function discoveredToResponse(d, conversationId) {
|
|
3864
4501
|
return {
|
|
3865
4502
|
id: conversationId,
|
|
3866
4503
|
conversationId,
|
|
4504
|
+
provider: CLAUDE_CODE_PROVIDER,
|
|
3867
4505
|
status: "idle",
|
|
3868
4506
|
projectPath: d.projectPath,
|
|
3869
4507
|
projectName: d.projectName,
|
|
@@ -4305,7 +4943,7 @@ var StreamerServer = class {
|
|
|
4305
4943
|
});
|
|
4306
4944
|
}
|
|
4307
4945
|
});
|
|
4308
|
-
this.ptyManager = new
|
|
4946
|
+
this.ptyManager = new LiveSessionManager({
|
|
4309
4947
|
logger: getLogger("pty"),
|
|
4310
4948
|
onOutput: (sessionId, data) => {
|
|
4311
4949
|
this.wsHub.broadcast({ type: "terminal_output", sessionId, data });
|
|
@@ -4642,10 +5280,11 @@ var StreamerServer = class {
|
|
|
4642
5280
|
}
|
|
4643
5281
|
} catch (err) {
|
|
4644
5282
|
const message = err instanceof Error ? err.message : String(err);
|
|
4645
|
-
|
|
4646
|
-
|
|
4647
|
-
|
|
4648
|
-
|
|
5283
|
+
const abiMismatch = message.includes("NODE_MODULE_VERSION") || message.includes("was compiled against a different Node.js version");
|
|
5284
|
+
this.log.error(
|
|
5285
|
+
`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})`,
|
|
5286
|
+
{ error: message, abiMismatch, event: "cache.open_failed" }
|
|
5287
|
+
);
|
|
4649
5288
|
}
|
|
4650
5289
|
const warmupScanner = new ConversationScanner();
|
|
4651
5290
|
this.allScanners.add(warmupScanner);
|
|
@@ -5133,16 +5772,16 @@ var StreamerServer = class {
|
|
|
5133
5772
|
}
|
|
5134
5773
|
findJsonlPath(uuid) {
|
|
5135
5774
|
const projectsDir = join12(homedir5(), ".claude", "projects");
|
|
5136
|
-
if (!
|
|
5775
|
+
if (!existsSync7(projectsDir)) return null;
|
|
5137
5776
|
const filename = `${uuid}.jsonl`;
|
|
5138
5777
|
for (const dir of readdirSync4(projectsDir)) {
|
|
5139
5778
|
const fp = join12(projectsDir, dir, filename);
|
|
5140
|
-
if (
|
|
5779
|
+
if (existsSync7(fp)) return fp;
|
|
5141
5780
|
const projectDir = join12(projectsDir, dir);
|
|
5142
5781
|
try {
|
|
5143
5782
|
for (const sub of readdirSync4(projectDir)) {
|
|
5144
5783
|
const subagentPath = join12(projectDir, sub, "subagents", filename);
|
|
5145
|
-
if (
|
|
5784
|
+
if (existsSync7(subagentPath)) return subagentPath;
|
|
5146
5785
|
}
|
|
5147
5786
|
} catch {
|
|
5148
5787
|
}
|
|
@@ -5451,7 +6090,7 @@ var StreamerServer = class {
|
|
|
5451
6090
|
handleGetSession(sessionId, res) {
|
|
5452
6091
|
const session = this.sessionStore.get(sessionId, this.ptyAttachedIds());
|
|
5453
6092
|
if (session) {
|
|
5454
|
-
if (!
|
|
6093
|
+
if (!existsSync7(session.projectPath)) {
|
|
5455
6094
|
session.failureReason = `Project directory not found: ${session.projectPath}`;
|
|
5456
6095
|
}
|
|
5457
6096
|
json(res, 200, session);
|
|
@@ -5491,7 +6130,10 @@ var StreamerServer = class {
|
|
|
5491
6130
|
json(res, 400, { error: "Could not determine project path" });
|
|
5492
6131
|
return;
|
|
5493
6132
|
}
|
|
6133
|
+
const cachedConvMeta = this.cache?.getMetaById(sessionId);
|
|
6134
|
+
const provider = conv?.provider ?? cachedConvMeta?.provider ?? CLAUDE_CODE_PROVIDER;
|
|
5494
6135
|
const session = await this.ptyManager.start(sessionId, {
|
|
6136
|
+
provider,
|
|
5495
6137
|
projectPath,
|
|
5496
6138
|
projectName: body.projectName,
|
|
5497
6139
|
branch: body.branch
|
|
@@ -5850,6 +6492,13 @@ var StreamerServer = class {
|
|
|
5850
6492
|
}
|
|
5851
6493
|
return;
|
|
5852
6494
|
}
|
|
6495
|
+
const body = await readBody(req);
|
|
6496
|
+
const { path: relativePath, provider: requestedProvider, systemPrompt: clientPrompt } = body;
|
|
6497
|
+
if (requestedProvider !== void 0 && !isProviderName(requestedProvider)) {
|
|
6498
|
+
json(res, 400, { error: "Invalid provider" });
|
|
6499
|
+
return;
|
|
6500
|
+
}
|
|
6501
|
+
const provider = requestedProvider ?? CLAUDE_CODE_PROVIDER;
|
|
5853
6502
|
if (!this.browseRoot) {
|
|
5854
6503
|
json(res, 403, {
|
|
5855
6504
|
error: "File browsing not configured. Set browseRoot on the server.",
|
|
@@ -5857,8 +6506,6 @@ var StreamerServer = class {
|
|
|
5857
6506
|
});
|
|
5858
6507
|
return;
|
|
5859
6508
|
}
|
|
5860
|
-
const body = await readBody(req);
|
|
5861
|
-
const { path: relativePath, systemPrompt: clientPrompt } = body;
|
|
5862
6509
|
if (typeof relativePath !== "string") {
|
|
5863
6510
|
json(res, 400, { error: "Missing path field" });
|
|
5864
6511
|
return;
|
|
@@ -5879,21 +6526,27 @@ var StreamerServer = class {
|
|
|
5879
6526
|
].filter(Boolean);
|
|
5880
6527
|
try {
|
|
5881
6528
|
const session = await this.ptyManager.startFresh({
|
|
6529
|
+
provider,
|
|
5882
6530
|
projectPath: resolvedPath,
|
|
5883
6531
|
projectName: body.projectName,
|
|
5884
6532
|
systemPrompt: systemPromptParts.join("\n")
|
|
5885
6533
|
});
|
|
5886
6534
|
this.sessionStore.addManaged(session);
|
|
5887
6535
|
json(res, 202, { id: session.id, status: "pending" });
|
|
5888
|
-
|
|
6536
|
+
if (provider === CODEX_CLI_PROVIDER) {
|
|
6537
|
+
this.watchForCodexRollout(session.id, resolvedPath);
|
|
6538
|
+
} else {
|
|
6539
|
+
this.watchForJsonl(session.id, resolvedPath);
|
|
6540
|
+
}
|
|
5889
6541
|
this.broadcastOrUnicastSessionList(req);
|
|
5890
6542
|
} catch (err) {
|
|
5891
6543
|
const message = err instanceof Error ? err.message : "Failed to start session";
|
|
6544
|
+
const statusCode = typeof err.statusCode === "number" ? err.statusCode : 500;
|
|
5892
6545
|
this.log.error(`[start] failed to start session: ${message}`, {
|
|
5893
6546
|
event: "session.start_failed",
|
|
5894
6547
|
error: message
|
|
5895
6548
|
});
|
|
5896
|
-
json(res,
|
|
6549
|
+
json(res, statusCode, { error: message });
|
|
5897
6550
|
}
|
|
5898
6551
|
}
|
|
5899
6552
|
// ─── Project linking ─────────────────────────────────────────────
|
|
@@ -5966,8 +6619,8 @@ var StreamerServer = class {
|
|
|
5966
6619
|
cleanup();
|
|
5967
6620
|
return;
|
|
5968
6621
|
}
|
|
5969
|
-
let resolvedFilePath =
|
|
5970
|
-
if (!resolvedFilePath &&
|
|
6622
|
+
let resolvedFilePath = existsSync7(filePath) ? filePath : null;
|
|
6623
|
+
if (!resolvedFilePath && existsSync7(projectsDir)) {
|
|
5971
6624
|
try {
|
|
5972
6625
|
const now = Date.now();
|
|
5973
6626
|
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 +6664,121 @@ var StreamerServer = class {
|
|
|
6011
6664
|
} catch {
|
|
6012
6665
|
}
|
|
6013
6666
|
}
|
|
6667
|
+
// Codex-equivalent of watchForJsonl(). Differs because Codex has no
|
|
6668
|
+
// filename-encoded session id (it assigns its own persisted id) and its
|
|
6669
|
+
// rollout files live under a date-nested directory
|
|
6670
|
+
// (~/.codex/sessions/<YYYY>/<MM>/<DD>/rollout-*.jsonl) that Codex creates
|
|
6671
|
+
// itself — it may not exist yet when this function is first called, so we
|
|
6672
|
+
// poll rather than fs.watch a not-yet-existent directory. Per Phase 0
|
|
6673
|
+
// findings, the rollout file appears within ~1s of process spawn (after
|
|
6674
|
+
// any directory-trust gate is cleared), well before any user input.
|
|
6675
|
+
watchForCodexRollout(sessionId, projectPath) {
|
|
6676
|
+
const deadline = Date.now() + 12e4;
|
|
6677
|
+
const now = /* @__PURE__ */ new Date();
|
|
6678
|
+
const dateDir = join12(
|
|
6679
|
+
String(now.getFullYear()),
|
|
6680
|
+
String(now.getMonth() + 1).padStart(2, "0"),
|
|
6681
|
+
String(now.getDate()).padStart(2, "0")
|
|
6682
|
+
);
|
|
6683
|
+
const sessionStartedAtMs = (this.sessionStore.getManaged(sessionId)?.startedAt?.getTime() ?? Date.now()) - 5e3;
|
|
6684
|
+
let intervalHandle = null;
|
|
6685
|
+
const cleanup = () => {
|
|
6686
|
+
if (intervalHandle) clearInterval(intervalHandle);
|
|
6687
|
+
intervalHandle = null;
|
|
6688
|
+
};
|
|
6689
|
+
const matchesProjectPath = (candidatePath) => {
|
|
6690
|
+
try {
|
|
6691
|
+
const firstLine = readFileSync6(candidatePath, "utf8").split("\n", 1)[0];
|
|
6692
|
+
if (!firstLine) return null;
|
|
6693
|
+
const parsed = JSON.parse(firstLine);
|
|
6694
|
+
if (parsed?.type !== "session_meta") return null;
|
|
6695
|
+
const payload = parsed.payload ?? {};
|
|
6696
|
+
if (payload.cwd !== projectPath) return null;
|
|
6697
|
+
if (typeof payload.id !== "string") return null;
|
|
6698
|
+
const createdIso = payload.timestamp ?? parsed.timestamp;
|
|
6699
|
+
const createdAtMs = typeof createdIso === "string" ? Date.parse(createdIso) : Number.NaN;
|
|
6700
|
+
if (Number.isNaN(createdAtMs) || createdAtMs < sessionStartedAtMs) return null;
|
|
6701
|
+
return { id: payload.id, createdAtMs };
|
|
6702
|
+
} catch {
|
|
6703
|
+
return null;
|
|
6704
|
+
}
|
|
6705
|
+
};
|
|
6706
|
+
const tryWire = () => {
|
|
6707
|
+
if (!this.ptyManager.hasSession(sessionId)) {
|
|
6708
|
+
cleanup();
|
|
6709
|
+
return;
|
|
6710
|
+
}
|
|
6711
|
+
if (Date.now() > deadline) {
|
|
6712
|
+
cleanup();
|
|
6713
|
+
return;
|
|
6714
|
+
}
|
|
6715
|
+
const boundElsewhere = new Set(
|
|
6716
|
+
this.sessionStore.listManaged().filter((s) => s.id !== sessionId && s.boundConversationId != null).map((s) => s.boundConversationId)
|
|
6717
|
+
);
|
|
6718
|
+
for (const root of this.codexRoots) {
|
|
6719
|
+
const sessionsDir = join12(root, dateDir);
|
|
6720
|
+
if (!existsSync7(sessionsDir)) continue;
|
|
6721
|
+
let candidateFiles;
|
|
6722
|
+
try {
|
|
6723
|
+
candidateFiles = readdirSync4(sessionsDir).filter((f) => f.endsWith(".jsonl"));
|
|
6724
|
+
} catch {
|
|
6725
|
+
continue;
|
|
6726
|
+
}
|
|
6727
|
+
const nowMs = Date.now();
|
|
6728
|
+
const recentCandidates = candidateFiles.map((f) => ({ f, mtime: statSync5(join12(sessionsDir, f)).mtimeMs })).filter(({ mtime }) => nowMs - mtime < 1e4).sort((a, b) => b.mtime - a.mtime);
|
|
6729
|
+
for (const { f } of recentCandidates) {
|
|
6730
|
+
const candidatePath = join12(sessionsDir, f);
|
|
6731
|
+
const match = matchesProjectPath(candidatePath);
|
|
6732
|
+
if (!match) continue;
|
|
6733
|
+
if (boundElsewhere.has(match.id)) continue;
|
|
6734
|
+
const codexSessionId = match.id;
|
|
6735
|
+
cleanup();
|
|
6736
|
+
this.sessionStore.updateManaged(sessionId, { boundConversationId: codexSessionId });
|
|
6737
|
+
this.sessionFileMap.set(sessionId, candidatePath);
|
|
6738
|
+
this.fileWatcher.watch(candidatePath);
|
|
6739
|
+
try {
|
|
6740
|
+
const existing = readFileSync6(candidatePath, "utf8").split("\n").filter(Boolean);
|
|
6741
|
+
if (existing.length > 0) {
|
|
6742
|
+
this.wsHub.broadcast({ type: "conversation_events", sessionId, lines: existing });
|
|
6743
|
+
for (const line of existing) {
|
|
6744
|
+
this.wsHub.broadcast({ type: "conversation_event", sessionId, line });
|
|
6745
|
+
}
|
|
6746
|
+
}
|
|
6747
|
+
} catch {
|
|
6748
|
+
}
|
|
6749
|
+
if (this.scannerReady) {
|
|
6750
|
+
this.scannerStale = true;
|
|
6751
|
+
} else {
|
|
6752
|
+
this.scanner = null;
|
|
6753
|
+
}
|
|
6754
|
+
this.linkSessionToProject(sessionId, projectPath, candidatePath);
|
|
6755
|
+
this.cache?.markAsStreamer(sessionId);
|
|
6756
|
+
const resp = this.sessionStore.get(sessionId, this.ptyAttachedIds());
|
|
6757
|
+
if (resp) {
|
|
6758
|
+
this.wsHub.broadcast({ type: "session_update", session: resp });
|
|
6759
|
+
}
|
|
6760
|
+
this.log.info(
|
|
6761
|
+
`[startFresh] bound Codex rollout for ${sessionId}`,
|
|
6762
|
+
{
|
|
6763
|
+
event: "session.codex_rollout_bound",
|
|
6764
|
+
sessionId,
|
|
6765
|
+
boundConversationId: codexSessionId,
|
|
6766
|
+
filePath: candidatePath
|
|
6767
|
+
},
|
|
6768
|
+
"pino"
|
|
6769
|
+
);
|
|
6770
|
+
return;
|
|
6771
|
+
}
|
|
6772
|
+
}
|
|
6773
|
+
};
|
|
6774
|
+
tryWire();
|
|
6775
|
+
if (!intervalHandle && Date.now() <= deadline) {
|
|
6776
|
+
const alreadyBound = this.sessionStore.getManaged(sessionId)?.boundConversationId != null;
|
|
6777
|
+
if (!alreadyBound) {
|
|
6778
|
+
intervalHandle = setInterval(tryWire, 250);
|
|
6779
|
+
}
|
|
6780
|
+
}
|
|
6781
|
+
}
|
|
6014
6782
|
async handleBrowse(url, res) {
|
|
6015
6783
|
if (!this.browseRoot) {
|
|
6016
6784
|
json(res, 403, {
|
|
@@ -6094,7 +6862,7 @@ var StreamerServer = class {
|
|
|
6094
6862
|
};
|
|
6095
6863
|
function classifyResumability(cwd) {
|
|
6096
6864
|
if (!cwd) return { resumable: true };
|
|
6097
|
-
if (
|
|
6865
|
+
if (existsSync7(cwd)) return { resumable: true };
|
|
6098
6866
|
const ranInWorktree = /\/\.worktrees\//.test(cwd) || /\/\.claude\/worktrees\//.test(cwd);
|
|
6099
6867
|
return {
|
|
6100
6868
|
resumable: false,
|
|
@@ -6225,7 +6993,10 @@ function readBody(req) {
|
|
|
6225
6993
|
});
|
|
6226
6994
|
}
|
|
6227
6995
|
export {
|
|
6996
|
+
CLAUDE_CODE_PROVIDER,
|
|
6997
|
+
CODEX_CLI_PROVIDER,
|
|
6228
6998
|
ConversationWatcher,
|
|
6999
|
+
LiveSessionManager,
|
|
6229
7000
|
PTYManager,
|
|
6230
7001
|
SessionStore,
|
|
6231
7002
|
StreamerServer,
|
|
@@ -6239,6 +7010,8 @@ export {
|
|
|
6239
7010
|
generateApiKey,
|
|
6240
7011
|
getDbConfig,
|
|
6241
7012
|
isDbEnabled,
|
|
7013
|
+
isProviderName,
|
|
7014
|
+
isProviderResumable,
|
|
6242
7015
|
loadOrCreateApiKey,
|
|
6243
7016
|
maskConnectionString,
|
|
6244
7017
|
readAgentConfig,
|