cli-remote-agent 1.0.7 → 1.0.10

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.js CHANGED
@@ -472,6 +472,60 @@ var init_pty_helper_fix = __esm({
472
472
  }
473
473
  });
474
474
 
475
+ // src/single-instance.ts
476
+ function isRunningAgent(pid) {
477
+ try {
478
+ process.kill(pid, 0);
479
+ } catch {
480
+ return false;
481
+ }
482
+ if (process.platform === "win32") return true;
483
+ try {
484
+ const out = (0, import_child_process.execFileSync)("ps", ["-p", String(pid), "-o", "command="], { encoding: "utf8" });
485
+ return /crc-agent|cli-remote-agent|agent[\/\\](dist|src)[\/\\](cli|index)/.test(out);
486
+ } catch {
487
+ return true;
488
+ }
489
+ }
490
+ function acquireSingleInstanceLock() {
491
+ if (process.env.CRC_ALLOW_MULTIPLE === "1") return { ok: true };
492
+ try {
493
+ if (import_fs5.default.existsSync(LOCK_FILE)) {
494
+ const prev = parseInt(import_fs5.default.readFileSync(LOCK_FILE, "utf8").trim(), 10);
495
+ if (prev && prev !== process.pid && isRunningAgent(prev)) {
496
+ return { ok: false, pid: prev };
497
+ }
498
+ }
499
+ import_fs5.default.mkdirSync(CONFIG_DIR, { recursive: true });
500
+ import_fs5.default.writeFileSync(LOCK_FILE, String(process.pid));
501
+ const release = () => {
502
+ try {
503
+ if (parseInt(import_fs5.default.readFileSync(LOCK_FILE, "utf8").trim(), 10) === process.pid) {
504
+ import_fs5.default.unlinkSync(LOCK_FILE);
505
+ }
506
+ } catch {
507
+ }
508
+ };
509
+ process.on("exit", release);
510
+ return { ok: true };
511
+ } catch (err) {
512
+ logger.warn({ error: err?.message }, "Could not manage single-instance lock");
513
+ return { ok: true };
514
+ }
515
+ }
516
+ var import_fs5, import_path4, import_child_process, LOCK_FILE;
517
+ var init_single_instance = __esm({
518
+ "src/single-instance.ts"() {
519
+ "use strict";
520
+ import_fs5 = __toESM(require("fs"));
521
+ import_path4 = __toESM(require("path"));
522
+ import_child_process = require("child_process");
523
+ init_config();
524
+ init_logger();
525
+ LOCK_FILE = import_path4.default.join(CONFIG_DIR, "agent.lock");
526
+ }
527
+ });
528
+
475
529
  // src/claude-live.ts
476
530
  function isAlive(pid) {
477
531
  try {
@@ -484,18 +538,19 @@ function isAlive(pid) {
484
538
  function getLiveClaudeSessions() {
485
539
  let files;
486
540
  try {
487
- files = import_fs5.default.readdirSync(LIVE_SESSIONS_DIR).filter((f) => f.endsWith(".json"));
541
+ files = import_fs6.default.readdirSync(LIVE_SESSIONS_DIR).filter((f) => f.endsWith(".json"));
488
542
  } catch {
489
543
  return [];
490
544
  }
491
545
  const out = [];
492
546
  for (const f of files) {
493
547
  try {
494
- const obj = JSON.parse(import_fs5.default.readFileSync(import_path4.default.join(LIVE_SESSIONS_DIR, f), "utf-8"));
548
+ const obj = JSON.parse(import_fs6.default.readFileSync(import_path5.default.join(LIVE_SESSIONS_DIR, f), "utf-8"));
495
549
  if (typeof obj?.pid !== "number" || !isAlive(obj.pid)) continue;
496
550
  out.push({
497
551
  pid: obj.pid,
498
552
  cwd: typeof obj.cwd === "string" ? obj.cwd : "",
553
+ sessionId: typeof obj.sessionId === "string" ? obj.sessionId : void 0,
499
554
  name: typeof obj.name === "string" ? obj.name : void 0,
500
555
  status: typeof obj.status === "string" ? obj.status : void 0,
501
556
  updatedAt: typeof obj.updatedAt === "number" ? obj.updatedAt : void 0
@@ -526,31 +581,40 @@ function isDescendantOf(pid, ancestor, parents) {
526
581
  }
527
582
  return false;
528
583
  }
529
- var import_fs5, import_path4, import_os2, import_child_process, import_util, execFileAsync, LIVE_SESSIONS_DIR;
584
+ async function findClaudeSessionForPtyPid(ptyPid) {
585
+ const live = getLiveClaudeSessions().filter((s) => s.sessionId);
586
+ if (live.length === 0) return null;
587
+ const parents = await getPidParents();
588
+ for (let i = live.length - 1; i >= 0; i--) {
589
+ if (isDescendantOf(live[i].pid, ptyPid, parents)) return live[i].sessionId;
590
+ }
591
+ return null;
592
+ }
593
+ var import_fs6, import_path5, import_os2, import_child_process2, import_util, execFileAsync, LIVE_SESSIONS_DIR;
530
594
  var init_claude_live = __esm({
531
595
  "src/claude-live.ts"() {
532
596
  "use strict";
533
- import_fs5 = __toESM(require("fs"));
534
- import_path4 = __toESM(require("path"));
597
+ import_fs6 = __toESM(require("fs"));
598
+ import_path5 = __toESM(require("path"));
535
599
  import_os2 = __toESM(require("os"));
536
- import_child_process = require("child_process");
600
+ import_child_process2 = require("child_process");
537
601
  import_util = require("util");
538
- execFileAsync = (0, import_util.promisify)(import_child_process.execFile);
539
- LIVE_SESSIONS_DIR = import_path4.default.join(import_os2.default.homedir(), ".claude", "sessions");
602
+ execFileAsync = (0, import_util.promisify)(import_child_process2.execFile);
603
+ LIVE_SESSIONS_DIR = import_path5.default.join(import_os2.default.homedir(), ".claude", "sessions");
540
604
  }
541
605
  });
542
606
 
543
607
  // src/tmux.ts
544
608
  function resolveTmuxPath() {
545
609
  if (cachedTmuxPath !== void 0) return cachedTmuxPath;
546
- const dirs = (process.env.PATH || "").split(import_path5.default.delimiter).filter(Boolean);
610
+ const dirs = (process.env.PATH || "").split(import_path6.default.delimiter).filter(Boolean);
547
611
  for (const extra of ["/opt/homebrew/bin", "/usr/local/bin", "/usr/bin", "/bin", "/opt/local/bin"]) {
548
612
  if (!dirs.includes(extra)) dirs.push(extra);
549
613
  }
550
614
  for (const dir of dirs) {
551
- const candidate = import_path5.default.join(dir, "tmux");
615
+ const candidate = import_path6.default.join(dir, "tmux");
552
616
  try {
553
- import_fs6.default.accessSync(candidate, import_fs6.default.constants.X_OK);
617
+ import_fs7.default.accessSync(candidate, import_fs7.default.constants.X_OK);
554
618
  cachedTmuxPath = candidate;
555
619
  return candidate;
556
620
  } catch {
@@ -600,8 +664,8 @@ async function enrichWithPanesAndClaude(sessions2) {
600
664
  const { file, args } = tmuxCmd(["list-panes", "-a", "-F", paneFmt]);
601
665
  const { stdout } = await execFileAsync2(file, args, { timeout: 6e3 });
602
666
  const panes = stdout.split("\n").map((line) => line.replace(/\r$/, "").trim()).filter(Boolean).map((line) => {
603
- const [session, pid, path10, activeFlags] = line.split(" ");
604
- return { session, pid: parseInt(pid, 10) || 0, path: path10 || "", active: activeFlags === "11" };
667
+ const [session, pid, path11, activeFlags] = line.split(" ");
668
+ return { session, pid: parseInt(pid, 10) || 0, path: path11 || "", active: activeFlags === "11" };
605
669
  });
606
670
  const byName = new Map(sessions2.map((s) => [s.name, s]));
607
671
  for (const pane of panes) {
@@ -640,16 +704,16 @@ function buildTmuxLaunch(name, launch) {
640
704
  if (trimmed) args.push(trimmed);
641
705
  return { file: tmuxBin, args };
642
706
  }
643
- var import_child_process2, import_util2, import_fs6, import_path5, execFileAsync2, IS_WIN, cachedTmuxPath;
707
+ var import_child_process3, import_util2, import_fs7, import_path6, execFileAsync2, IS_WIN, cachedTmuxPath;
644
708
  var init_tmux = __esm({
645
709
  "src/tmux.ts"() {
646
710
  "use strict";
647
- import_child_process2 = require("child_process");
711
+ import_child_process3 = require("child_process");
648
712
  import_util2 = require("util");
649
- import_fs6 = __toESM(require("fs"));
650
- import_path5 = __toESM(require("path"));
713
+ import_fs7 = __toESM(require("fs"));
714
+ import_path6 = __toESM(require("path"));
651
715
  init_claude_live();
652
- execFileAsync2 = (0, import_util2.promisify)(import_child_process2.execFile);
716
+ execFileAsync2 = (0, import_util2.promisify)(import_child_process3.execFile);
653
717
  IS_WIN = process.platform === "win32";
654
718
  }
655
719
  });
@@ -659,7 +723,7 @@ function detectShell(preference) {
659
723
  if (preference !== "auto") return preference;
660
724
  if (process.platform === "win32") {
661
725
  try {
662
- (0, import_child_process3.execSync)("where pwsh", { stdio: "ignore" });
726
+ (0, import_child_process4.execSync)("where pwsh", { stdio: "ignore" });
663
727
  return "pwsh.exe";
664
728
  } catch {
665
729
  return "powershell.exe";
@@ -667,25 +731,25 @@ function detectShell(preference) {
667
731
  }
668
732
  return process.env.SHELL || "/bin/bash";
669
733
  }
670
- var import_child_process3;
734
+ var import_child_process4;
671
735
  var init_shell = __esm({
672
736
  "src/shell.ts"() {
673
737
  "use strict";
674
- import_child_process3 = require("child_process");
738
+ import_child_process4 = require("child_process");
675
739
  }
676
740
  });
677
741
 
678
742
  // src/pty-session.ts
679
743
  function ensureInitFiles() {
680
- const versionFile = (0, import_path6.join)(INIT_DIR, ".version");
681
- if ((0, import_fs7.existsSync)(versionFile)) {
744
+ const versionFile = (0, import_path7.join)(INIT_DIR, ".version");
745
+ if ((0, import_fs8.existsSync)(versionFile)) {
682
746
  try {
683
- if ((0, import_fs7.readFileSync)(versionFile, "utf-8").trim() === INIT_VERSION) return;
747
+ if ((0, import_fs8.readFileSync)(versionFile, "utf-8").trim() === INIT_VERSION) return;
684
748
  } catch {
685
749
  }
686
750
  }
687
- (0, import_fs7.mkdirSync)(ZSH_DIR, { recursive: true });
688
- (0, import_fs7.writeFileSync)(BASH_INIT, [
751
+ (0, import_fs8.mkdirSync)(ZSH_DIR, { recursive: true });
752
+ (0, import_fs8.writeFileSync)(BASH_INIT, [
689
753
  "[ -f ~/.bash_profile ] && source ~/.bash_profile",
690
754
  "[ -f ~/.bashrc ] && source ~/.bashrc",
691
755
  "__crc_s=$SECONDS",
@@ -696,11 +760,11 @@ function ensureInitFiles() {
696
760
  `PROMPT_COMMAND='[ "$__crc_armed" = 1 ] && [ \${__crc_elapsed:-0} -ge ${THRESHOLD} ] && printf "\\a"; __crc_armed=1; eval "$__crc_orig_pc"'`,
697
761
  ""
698
762
  ].join("\n"), "utf-8");
699
- (0, import_fs7.writeFileSync)(ZSH_ENV, [
763
+ (0, import_fs8.writeFileSync)(ZSH_ENV, [
700
764
  '[ -f "${ZDOTDIR_ORIG:-$HOME}/.zshenv" ] && source "${ZDOTDIR_ORIG:-$HOME}/.zshenv"',
701
765
  ""
702
766
  ].join("\n"), "utf-8");
703
- (0, import_fs7.writeFileSync)(ZSH_RC, [
767
+ (0, import_fs8.writeFileSync)(ZSH_RC, [
704
768
  '[ -f "${ZDOTDIR_ORIG:-$HOME}/.zshrc" ] && source "${ZDOTDIR_ORIG:-$HOME}/.zshrc"',
705
769
  "__crc_s=$SECONDS",
706
770
  "__crc_armed=0",
@@ -713,36 +777,36 @@ function ensureInitFiles() {
713
777
  "fi",
714
778
  ""
715
779
  ].join("\n"), "utf-8");
716
- (0, import_fs7.writeFileSync)(versionFile, INIT_VERSION, "utf-8");
780
+ (0, import_fs8.writeFileSync)(versionFile, INIT_VERSION, "utf-8");
717
781
  }
718
782
  function resolveCwd(cwd) {
719
783
  const candidates = process.platform === "win32" ? [cwd, (0, import_os3.homedir)(), process.env.USERPROFILE, process.env.HOME] : [cwd, (0, import_os3.homedir)(), process.env.HOME, process.env.USERPROFILE];
720
784
  for (const c of candidates) {
721
785
  if (c) {
722
786
  try {
723
- if ((0, import_fs7.existsSync)(c)) return c;
787
+ if ((0, import_fs8.existsSync)(c)) return c;
724
788
  } catch {
725
789
  }
726
790
  }
727
791
  }
728
792
  return (0, import_os3.homedir)();
729
793
  }
730
- var pty, import_fs7, import_path6, import_os3, THRESHOLD, INIT_DIR, BASH_INIT, ZSH_DIR, ZSH_RC, ZSH_ENV, INIT_VERSION, PtySession;
794
+ var pty, import_fs8, import_path7, import_os3, THRESHOLD, INIT_DIR, BASH_INIT, ZSH_DIR, ZSH_RC, ZSH_ENV, INIT_VERSION, PtySession;
731
795
  var init_pty_session = __esm({
732
796
  "src/pty-session.ts"() {
733
797
  "use strict";
734
798
  pty = __toESM(require("node-pty"));
735
- import_fs7 = require("fs");
736
- import_path6 = require("path");
799
+ import_fs8 = require("fs");
800
+ import_path7 = require("path");
737
801
  import_os3 = require("os");
738
802
  init_src();
739
803
  init_shell();
740
804
  THRESHOLD = 3;
741
- INIT_DIR = (0, import_path6.join)((0, import_os3.tmpdir)(), "crc-shell-init");
742
- BASH_INIT = (0, import_path6.join)(INIT_DIR, "bashrc");
743
- ZSH_DIR = (0, import_path6.join)(INIT_DIR, "zsh");
744
- ZSH_RC = (0, import_path6.join)(ZSH_DIR, ".zshrc");
745
- ZSH_ENV = (0, import_path6.join)(ZSH_DIR, ".zshenv");
805
+ INIT_DIR = (0, import_path7.join)((0, import_os3.tmpdir)(), "crc-shell-init");
806
+ BASH_INIT = (0, import_path7.join)(INIT_DIR, "bashrc");
807
+ ZSH_DIR = (0, import_path7.join)(INIT_DIR, "zsh");
808
+ ZSH_RC = (0, import_path7.join)(ZSH_DIR, ".zshrc");
809
+ ZSH_ENV = (0, import_path7.join)(ZSH_DIR, ".zshenv");
746
810
  INIT_VERSION = "2";
747
811
  PtySession = class {
748
812
  id;
@@ -791,6 +855,9 @@ var init_pty_session = __esm({
791
855
  });
792
856
  }
793
857
  }
858
+ get pid() {
859
+ return this.pty.pid;
860
+ }
794
861
  write(data) {
795
862
  this.pty.write(data);
796
863
  }
@@ -857,6 +924,9 @@ function createTerminalSession(sessionId, cols, rows, shellPreference, cwd, onDa
857
924
  sessions.set(sessionId, session);
858
925
  logger.info({ sessionId, cols, rows }, "PTY session created");
859
926
  }
927
+ function getSessionPid(sessionId) {
928
+ return sessions.get(sessionId)?.pid;
929
+ }
860
930
  function writeToSession(sessionId, data) {
861
931
  sessions.get(sessionId)?.write(data);
862
932
  }
@@ -953,7 +1023,7 @@ function getCpuUsage() {
953
1023
  function getRootPaths() {
954
1024
  if (process.platform === "win32") {
955
1025
  try {
956
- const output = (0, import_child_process4.execSync)("wmic logicaldisk get name", { encoding: "utf-8" });
1026
+ const output = (0, import_child_process5.execSync)("wmic logicaldisk get name", { encoding: "utf-8" });
957
1027
  return output.split("\n").map((l) => l.trim()).filter((l) => /^[A-Z]:$/.test(l)).map((l) => l + "\\");
958
1028
  } catch {
959
1029
  return ["C:\\"];
@@ -972,19 +1042,19 @@ function buildHeartbeat(homeDir) {
972
1042
  memoryUsage: Math.round((totalMem - freeMem) / totalMem * 100),
973
1043
  uptime: Math.round(import_os4.default.uptime()),
974
1044
  activeSessions: getActiveSessionCount(),
975
- pathSeparator: import_path7.default.sep,
1045
+ pathSeparator: import_path8.default.sep,
976
1046
  homeDirectory: homeDir || import_os4.default.homedir(),
977
1047
  rootPaths: getRootPaths(),
978
1048
  capabilities: { terminal: true, fileTransfer: false }
979
1049
  };
980
1050
  }
981
- var import_os4, import_path7, import_child_process4, prevCpu;
1051
+ var import_os4, import_path8, import_child_process5, prevCpu;
982
1052
  var init_heartbeat = __esm({
983
1053
  "src/heartbeat.ts"() {
984
1054
  "use strict";
985
1055
  import_os4 = __toESM(require("os"));
986
- import_path7 = __toESM(require("path"));
987
- import_child_process4 = require("child_process");
1056
+ import_path8 = __toESM(require("path"));
1057
+ import_child_process5 = require("child_process");
988
1058
  init_terminal_manager();
989
1059
  prevCpu = null;
990
1060
  }
@@ -992,28 +1062,28 @@ var init_heartbeat = __esm({
992
1062
 
993
1063
  // src/file-explorer.ts
994
1064
  function isInSecretDir(resolved) {
995
- const rel = import_path8.default.relative(SECRET_DIR, resolved);
996
- return rel === "" || !rel.startsWith(".." + import_path8.default.sep) && rel !== ".." && !import_path8.default.isAbsolute(rel);
1065
+ const rel = import_path9.default.relative(SECRET_DIR, resolved);
1066
+ return rel === "" || !rel.startsWith(".." + import_path9.default.sep) && rel !== ".." && !import_path9.default.isAbsolute(rel);
997
1067
  }
998
1068
  function listDirectory(dirPath) {
999
1069
  try {
1000
- const resolved = import_path8.default.resolve(dirPath);
1070
+ const resolved = import_path9.default.resolve(dirPath);
1001
1071
  if (isInSecretDir(resolved)) {
1002
1072
  return { entries: [], error: "Access denied" };
1003
1073
  }
1004
- if (!import_fs8.default.existsSync(resolved)) {
1074
+ if (!import_fs9.default.existsSync(resolved)) {
1005
1075
  return { entries: [], error: "Path does not exist" };
1006
1076
  }
1007
- const stat = import_fs8.default.statSync(resolved);
1077
+ const stat = import_fs9.default.statSync(resolved);
1008
1078
  if (!stat.isDirectory()) {
1009
1079
  return { entries: [], error: "Not a directory" };
1010
1080
  }
1011
- const dirents = import_fs8.default.readdirSync(resolved, { withFileTypes: true });
1081
+ const dirents = import_fs9.default.readdirSync(resolved, { withFileTypes: true });
1012
1082
  const entries = [];
1013
1083
  for (const dirent of dirents) {
1014
1084
  try {
1015
- const fullPath = import_path8.default.join(resolved, dirent.name);
1016
- const s = import_fs8.default.statSync(fullPath);
1085
+ const fullPath = import_path9.default.join(resolved, dirent.name);
1086
+ const s = import_fs9.default.statSync(fullPath);
1017
1087
  entries.push({
1018
1088
  name: dirent.name,
1019
1089
  isDirectory: dirent.isDirectory(),
@@ -1035,22 +1105,22 @@ function listDirectory(dirPath) {
1035
1105
  }
1036
1106
  async function downloadFile(filePath, serverUrl, secret, agentId) {
1037
1107
  try {
1038
- const resolved = import_path8.default.resolve(filePath);
1108
+ const resolved = import_path9.default.resolve(filePath);
1039
1109
  if (isInSecretDir(resolved)) {
1040
1110
  return { error: "Access denied" };
1041
1111
  }
1042
- if (!import_fs8.default.existsSync(resolved)) {
1112
+ if (!import_fs9.default.existsSync(resolved)) {
1043
1113
  return { error: "File does not exist" };
1044
1114
  }
1045
- const stat = import_fs8.default.statSync(resolved);
1115
+ const stat = import_fs9.default.statSync(resolved);
1046
1116
  if (stat.isDirectory()) {
1047
1117
  return { error: "Cannot download a directory" };
1048
1118
  }
1049
1119
  if (stat.size > FILE_MAX_SIZE) {
1050
1120
  return { error: `File too large (max ${FILE_MAX_SIZE} bytes)` };
1051
1121
  }
1052
- const fileName = import_path8.default.basename(resolved);
1053
- const fileStream = import_fs8.default.createReadStream(resolved);
1122
+ const fileName = import_path9.default.basename(resolved);
1123
+ const fileStream = import_fs9.default.createReadStream(resolved);
1054
1124
  const baseUrl = serverUrl.replace("wss://", "https://").replace("ws://", "http://");
1055
1125
  const url = `${baseUrl}/api/files/receive`;
1056
1126
  const res = await fetch(url, {
@@ -1079,16 +1149,16 @@ async function downloadFile(filePath, serverUrl, secret, agentId) {
1079
1149
  return { error: err.message };
1080
1150
  }
1081
1151
  }
1082
- var import_fs8, import_path8, import_os5, SECRET_DIR;
1152
+ var import_fs9, import_path9, import_os5, SECRET_DIR;
1083
1153
  var init_file_explorer = __esm({
1084
1154
  "src/file-explorer.ts"() {
1085
1155
  "use strict";
1086
- import_fs8 = __toESM(require("fs"));
1087
- import_path8 = __toESM(require("path"));
1156
+ import_fs9 = __toESM(require("fs"));
1157
+ import_path9 = __toESM(require("path"));
1088
1158
  import_os5 = __toESM(require("os"));
1089
1159
  init_logger();
1090
1160
  init_src();
1091
- SECRET_DIR = import_path8.default.join(import_os5.default.homedir(), ".crc-agent");
1161
+ SECRET_DIR = import_path9.default.join(import_os5.default.homedir(), ".crc-agent");
1092
1162
  }
1093
1163
  });
1094
1164
 
@@ -1111,8 +1181,8 @@ async function runCmd(cmd2, shell) {
1111
1181
  }
1112
1182
  function getConfigPath(profile) {
1113
1183
  const file = profile.configFile || `${profile.id}.conf`;
1114
- if (import_path9.default.isAbsolute(file)) return file;
1115
- return import_path9.default.join(VPN_DIR, file);
1184
+ if (import_path10.default.isAbsolute(file)) return file;
1185
+ return import_path10.default.join(VPN_DIR, file);
1116
1186
  }
1117
1187
  async function getScutilStatus(serviceName) {
1118
1188
  const { stdout } = await runCmd(`scutil --nc status '${shq(serviceName)}'`);
@@ -1228,7 +1298,7 @@ async function connectWireGuard(profile) {
1228
1298
  const tunnelName = profile.tunnelName || profile.id;
1229
1299
  if (isWindows) {
1230
1300
  const confPath2 = getConfigPath(profile);
1231
- if (!import_fs9.default.existsSync(confPath2)) {
1301
+ if (!import_fs10.default.existsSync(confPath2)) {
1232
1302
  return `Config file not found: ${confPath2}`;
1233
1303
  }
1234
1304
  const script = `
@@ -1254,7 +1324,7 @@ async function connectWireGuard(profile) {
1254
1324
  async function connectOpenVpn(profile) {
1255
1325
  if (isWindows) {
1256
1326
  const confPath2 = getConfigPath(profile);
1257
- if (!import_fs9.default.existsSync(confPath2)) {
1327
+ if (!import_fs10.default.existsSync(confPath2)) {
1258
1328
  return `Config file not found: ${confPath2}`;
1259
1329
  }
1260
1330
  const connector = "C:\\Program Files\\OpenVPN Connect\\ovpnconnector.exe";
@@ -1281,7 +1351,7 @@ async function connectOpenVpn(profile) {
1281
1351
  return scutilStart(profile.serviceName);
1282
1352
  }
1283
1353
  const confPath = getConfigPath(profile);
1284
- if (!import_fs9.default.existsSync(confPath)) {
1354
+ if (!import_fs10.default.existsSync(confPath)) {
1285
1355
  return `Config file not found: ${confPath}`;
1286
1356
  }
1287
1357
  const { stderr } = await runCmd(`sudo openvpn --config '${shq(confPath)}' --daemon`);
@@ -1290,7 +1360,7 @@ async function connectOpenVpn(profile) {
1290
1360
  async function connectAzure(profile) {
1291
1361
  if (isWindows) {
1292
1362
  const confPath = getConfigPath(profile);
1293
- if (!import_fs9.default.existsSync(confPath)) {
1363
+ if (!import_fs10.default.existsSync(confPath)) {
1294
1364
  return `Config file not found: ${confPath}`;
1295
1365
  }
1296
1366
  await runCmd(`& 'AzureVpn.exe' -i '${psq(confPath)}' 2>&1`);
@@ -1410,19 +1480,19 @@ async function disconnectVpn(configs, profileId) {
1410
1480
  }
1411
1481
  return profiles;
1412
1482
  }
1413
- var import_child_process5, import_util3, import_fs9, import_path9, import_os6, execAsync, isWindows, VPN_DIR;
1483
+ var import_child_process6, import_util3, import_fs10, import_path10, import_os6, execAsync, isWindows, VPN_DIR;
1414
1484
  var init_vpn_manager = __esm({
1415
1485
  "src/vpn-manager.ts"() {
1416
1486
  "use strict";
1417
- import_child_process5 = require("child_process");
1487
+ import_child_process6 = require("child_process");
1418
1488
  import_util3 = require("util");
1419
- import_fs9 = __toESM(require("fs"));
1420
- import_path9 = __toESM(require("path"));
1489
+ import_fs10 = __toESM(require("fs"));
1490
+ import_path10 = __toESM(require("path"));
1421
1491
  import_os6 = __toESM(require("os"));
1422
1492
  init_logger();
1423
- execAsync = (0, import_util3.promisify)(import_child_process5.exec);
1493
+ execAsync = (0, import_util3.promisify)(import_child_process6.exec);
1424
1494
  isWindows = process.platform === "win32";
1425
- VPN_DIR = import_path9.default.join(import_os6.default.homedir(), ".crc-agent", "vpn");
1495
+ VPN_DIR = import_path10.default.join(import_os6.default.homedir(), ".crc-agent", "vpn");
1426
1496
  }
1427
1497
  });
1428
1498
 
@@ -1457,7 +1527,7 @@ var init_claude_path = __esm({
1457
1527
  // src/claude-sessions.ts
1458
1528
  async function parseSessionFile(filePath) {
1459
1529
  try {
1460
- const stream = import_fs10.default.createReadStream(filePath, { encoding: "utf-8" });
1530
+ const stream = import_fs11.default.createReadStream(filePath, { encoding: "utf-8" });
1461
1531
  const rl = import_readline2.default.createInterface({ input: stream, crlfDelay: Infinity });
1462
1532
  let firstMessage = "";
1463
1533
  let lastTimestamp = "";
@@ -1505,17 +1575,17 @@ async function parseSessionFile(filePath) {
1505
1575
  }
1506
1576
  async function listClaudeSessions(filterProjectPath) {
1507
1577
  const sessions2 = [];
1508
- if (!import_fs10.default.existsSync(CLAUDE_PROJECTS_DIR)) return sessions2;
1578
+ if (!import_fs11.default.existsSync(CLAUDE_PROJECTS_DIR)) return sessions2;
1509
1579
  const isDirSafe = (d) => {
1510
1580
  try {
1511
- return import_fs10.default.statSync(import_path10.default.join(CLAUDE_PROJECTS_DIR, d)).isDirectory();
1581
+ return import_fs11.default.statSync(import_path11.default.join(CLAUDE_PROJECTS_DIR, d)).isDirectory();
1512
1582
  } catch {
1513
1583
  return false;
1514
1584
  }
1515
1585
  };
1516
1586
  let allEntries;
1517
1587
  try {
1518
- allEntries = import_fs10.default.readdirSync(CLAUDE_PROJECTS_DIR);
1588
+ allEntries = import_fs11.default.readdirSync(CLAUDE_PROJECTS_DIR);
1519
1589
  } catch {
1520
1590
  return sessions2;
1521
1591
  }
@@ -1528,16 +1598,16 @@ async function listClaudeSessions(filterProjectPath) {
1528
1598
  projectDirs = allEntries.filter((d) => isDirSafe(d) && !d.startsWith("-private-"));
1529
1599
  }
1530
1600
  for (const dirName of projectDirs) {
1531
- const dirPath = import_path10.default.join(CLAUDE_PROJECTS_DIR, dirName);
1601
+ const dirPath = import_path11.default.join(CLAUDE_PROJECTS_DIR, dirName);
1532
1602
  let files;
1533
1603
  try {
1534
- files = import_fs10.default.readdirSync(dirPath).filter((f) => f.endsWith(".jsonl"));
1604
+ files = import_fs11.default.readdirSync(dirPath).filter((f) => f.endsWith(".jsonl"));
1535
1605
  } catch {
1536
1606
  continue;
1537
1607
  }
1538
1608
  for (const file of files) {
1539
1609
  const sessionId = file.replace(".jsonl", "");
1540
- const parsed = await parseSessionFile(import_path10.default.join(dirPath, file));
1610
+ const parsed = await parseSessionFile(import_path11.default.join(dirPath, file));
1541
1611
  if (!parsed) continue;
1542
1612
  const projectPath = parsed.cwd || dirName.replace(/^-/, "/").replace(/-/g, "/");
1543
1613
  sessions2.push({
@@ -1555,31 +1625,31 @@ async function listClaudeSessions(filterProjectPath) {
1555
1625
  sessions2.sort((a, b) => b.lastTimestamp > a.lastTimestamp ? 1 : -1);
1556
1626
  return sessions2;
1557
1627
  }
1558
- var import_fs10, import_path10, import_os7, import_readline2, CLAUDE_PROJECTS_DIR;
1628
+ var import_fs11, import_path11, import_os7, import_readline2, CLAUDE_PROJECTS_DIR;
1559
1629
  var init_claude_sessions = __esm({
1560
1630
  "src/claude-sessions.ts"() {
1561
1631
  "use strict";
1562
- import_fs10 = __toESM(require("fs"));
1563
- import_path10 = __toESM(require("path"));
1632
+ import_fs11 = __toESM(require("fs"));
1633
+ import_path11 = __toESM(require("path"));
1564
1634
  import_os7 = __toESM(require("os"));
1565
1635
  import_readline2 = __toESM(require("readline"));
1566
1636
  init_logger();
1567
1637
  init_claude_path();
1568
- CLAUDE_PROJECTS_DIR = import_path10.default.join(import_os7.default.homedir(), ".claude", "projects");
1638
+ CLAUDE_PROJECTS_DIR = import_path11.default.join(import_os7.default.homedir(), ".claude", "projects");
1569
1639
  }
1570
1640
  });
1571
1641
 
1572
1642
  // src/claude-conversation.ts
1573
1643
  function findProjectDir(projectPath) {
1574
1644
  const encoded = encodeProjectPath(projectPath);
1575
- const dirPath = import_path11.default.join(CLAUDE_PROJECTS_DIR2, encoded);
1576
- if (import_fs11.default.existsSync(dirPath)) return dirPath;
1577
- if (!import_fs11.default.existsSync(CLAUDE_PROJECTS_DIR2)) return null;
1645
+ const dirPath = import_path12.default.join(CLAUDE_PROJECTS_DIR2, encoded);
1646
+ if (import_fs12.default.existsSync(dirPath)) return dirPath;
1647
+ if (!import_fs12.default.existsSync(CLAUDE_PROJECTS_DIR2)) return null;
1578
1648
  let allDirs;
1579
1649
  try {
1580
- allDirs = import_fs11.default.readdirSync(CLAUDE_PROJECTS_DIR2).filter((d) => {
1650
+ allDirs = import_fs12.default.readdirSync(CLAUDE_PROJECTS_DIR2).filter((d) => {
1581
1651
  try {
1582
- return import_fs11.default.statSync(import_path11.default.join(CLAUDE_PROJECTS_DIR2, d)).isDirectory();
1652
+ return import_fs12.default.statSync(import_path12.default.join(CLAUDE_PROJECTS_DIR2, d)).isDirectory();
1583
1653
  } catch {
1584
1654
  return false;
1585
1655
  }
@@ -1589,23 +1659,23 @@ function findProjectDir(projectPath) {
1589
1659
  }
1590
1660
  const matches = findProjectDirs(allDirs, projectPath);
1591
1661
  if (matches.length > 0) {
1592
- return import_path11.default.join(CLAUDE_PROJECTS_DIR2, matches[0]);
1662
+ return import_path12.default.join(CLAUDE_PROJECTS_DIR2, matches[0]);
1593
1663
  }
1594
1664
  return null;
1595
1665
  }
1596
1666
  function findSessionFileById(sessionId) {
1597
1667
  if (!/^[A-Za-z0-9._-]+$/.test(sessionId)) return null;
1598
- if (!import_fs11.default.existsSync(CLAUDE_PROJECTS_DIR2)) return null;
1668
+ if (!import_fs12.default.existsSync(CLAUDE_PROJECTS_DIR2)) return null;
1599
1669
  let dirs;
1600
1670
  try {
1601
- dirs = import_fs11.default.readdirSync(CLAUDE_PROJECTS_DIR2);
1671
+ dirs = import_fs12.default.readdirSync(CLAUDE_PROJECTS_DIR2);
1602
1672
  } catch {
1603
1673
  return null;
1604
1674
  }
1605
1675
  for (const dir of dirs) {
1606
- const candidate = import_path11.default.join(CLAUDE_PROJECTS_DIR2, dir, `${sessionId}.jsonl`);
1676
+ const candidate = import_path12.default.join(CLAUDE_PROJECTS_DIR2, dir, `${sessionId}.jsonl`);
1607
1677
  try {
1608
- if (import_fs11.default.statSync(candidate).isFile()) return candidate;
1678
+ if (import_fs12.default.statSync(candidate).isFile()) return candidate;
1609
1679
  } catch {
1610
1680
  }
1611
1681
  }
@@ -1616,20 +1686,20 @@ function findLatestSessionFile(projectPath) {
1616
1686
  if (!dirPath) return null;
1617
1687
  let entries;
1618
1688
  try {
1619
- entries = import_fs11.default.readdirSync(dirPath);
1689
+ entries = import_fs12.default.readdirSync(dirPath);
1620
1690
  } catch {
1621
1691
  return null;
1622
1692
  }
1623
1693
  const files = entries.filter((f) => f.endsWith(".jsonl") && !f.includes("/")).map((f) => {
1624
1694
  try {
1625
- return { name: f, mtime: import_fs11.default.statSync(import_path11.default.join(dirPath, f)).mtimeMs };
1695
+ return { name: f, mtime: import_fs12.default.statSync(import_path12.default.join(dirPath, f)).mtimeMs };
1626
1696
  } catch {
1627
1697
  return null;
1628
1698
  }
1629
1699
  }).filter((f) => f !== null).sort((a, b) => b.mtime - a.mtime);
1630
1700
  if (files.length === 0) return null;
1631
1701
  return {
1632
- filePath: import_path11.default.join(dirPath, files[0].name),
1702
+ filePath: import_path12.default.join(dirPath, files[0].name),
1633
1703
  sessionId: files[0].name.replace(".jsonl", "")
1634
1704
  };
1635
1705
  }
@@ -1639,8 +1709,8 @@ async function readConversation(projectPath, afterLine = 0, specificSessionId) {
1639
1709
  if (specificSessionId) {
1640
1710
  sessionId = specificSessionId;
1641
1711
  const dirPath = findProjectDir(projectPath);
1642
- const direct = dirPath ? import_path11.default.join(dirPath, `${specificSessionId}.jsonl`) : null;
1643
- if (direct && import_fs11.default.existsSync(direct)) {
1712
+ const direct = dirPath ? import_path12.default.join(dirPath, `${specificSessionId}.jsonl`) : null;
1713
+ if (direct && import_fs12.default.existsSync(direct)) {
1644
1714
  filePath = direct;
1645
1715
  } else {
1646
1716
  const byId = findSessionFileById(specificSessionId);
@@ -1654,7 +1724,7 @@ async function readConversation(projectPath, afterLine = 0, specificSessionId) {
1654
1724
  sessionId = found.sessionId;
1655
1725
  }
1656
1726
  try {
1657
- const stream = import_fs11.default.createReadStream(filePath, { encoding: "utf-8" });
1727
+ const stream = import_fs12.default.createReadStream(filePath, { encoding: "utf-8" });
1658
1728
  const rl = import_readline3.default.createInterface({ input: stream, crlfDelay: Infinity });
1659
1729
  const messages = [];
1660
1730
  let lineNum = 0;
@@ -1735,23 +1805,23 @@ async function readConversation(projectPath, afterLine = 0, specificSessionId) {
1735
1805
  return null;
1736
1806
  }
1737
1807
  }
1738
- var import_fs11, import_path11, import_os8, import_readline3, CLAUDE_PROJECTS_DIR2;
1808
+ var import_fs12, import_path12, import_os8, import_readline3, CLAUDE_PROJECTS_DIR2;
1739
1809
  var init_claude_conversation = __esm({
1740
1810
  "src/claude-conversation.ts"() {
1741
1811
  "use strict";
1742
- import_fs11 = __toESM(require("fs"));
1743
- import_path11 = __toESM(require("path"));
1812
+ import_fs12 = __toESM(require("fs"));
1813
+ import_path12 = __toESM(require("path"));
1744
1814
  import_os8 = __toESM(require("os"));
1745
1815
  import_readline3 = __toESM(require("readline"));
1746
1816
  init_logger();
1747
1817
  init_claude_path();
1748
- CLAUDE_PROJECTS_DIR2 = import_path11.default.join(import_os8.default.homedir(), ".claude", "projects");
1818
+ CLAUDE_PROJECTS_DIR2 = import_path12.default.join(import_os8.default.homedir(), ".claude", "projects");
1749
1819
  }
1750
1820
  });
1751
1821
 
1752
1822
  // src/index.ts
1753
1823
  var index_exports = {};
1754
- var import_socket, config, socket, SESSION_MAX_IDLE_MS, reaperInterval, vpnProfiles;
1824
+ var import_socket, config, lock, socket, SESSION_MAX_IDLE_MS, reaperInterval, vpnProfiles;
1755
1825
  var init_index = __esm({
1756
1826
  "src/index.ts"() {
1757
1827
  "use strict";
@@ -1762,6 +1832,7 @@ var init_index = __esm({
1762
1832
  init_claude_plugin_installer();
1763
1833
  init_local_control();
1764
1834
  init_pty_helper_fix();
1835
+ init_single_instance();
1765
1836
  init_tmux();
1766
1837
  init_heartbeat();
1767
1838
  init_file_explorer();
@@ -1769,12 +1840,20 @@ var init_index = __esm({
1769
1840
  init_claude_sessions();
1770
1841
  init_claude_conversation();
1771
1842
  init_terminal_manager();
1843
+ init_claude_live();
1772
1844
  if (!isConfigured()) {
1773
1845
  console.error("No agent configured. Run: crc-agent setup");
1774
1846
  process.exit(1);
1775
1847
  }
1776
1848
  config = loadConfig();
1777
1849
  logger.info({ agentId: config.agentId, serverUrl: config.serverUrl }, "Starting agent");
1850
+ lock = acquireSingleInstanceLock();
1851
+ if (!lock.ok) {
1852
+ console.error(
1853
+ `Another crc-agent is already running on this machine (PID ${lock.pid}). Stop it first, or set CRC_ALLOW_MULTIPLE=1 to override.`
1854
+ );
1855
+ process.exit(1);
1856
+ }
1778
1857
  ensurePtyHelperExecutable();
1779
1858
  process.on("unhandledRejection", (reason) => {
1780
1859
  logger.error({ reason }, "Unhandled promise rejection (agent kept alive)");
@@ -1963,7 +2042,17 @@ var init_index = __esm({
1963
2042
  });
1964
2043
  socket.on(CLAUDE_CONV_READ, async (payload) => {
1965
2044
  try {
1966
- const result = await readConversation(payload.projectPath, payload.afterLine || 0, payload.sessionId);
2045
+ let sessionId = payload.sessionId;
2046
+ if (!sessionId && payload.terminalSessionId) {
2047
+ const ptyPid = getSessionPid(payload.terminalSessionId);
2048
+ const resolved = ptyPid ? await findClaudeSessionForPtyPid(ptyPid) : null;
2049
+ if (!resolved) {
2050
+ socket.emit(CLAUDE_CONV_DATA, { sessionId: "", messages: [], totalLines: 0 });
2051
+ return;
2052
+ }
2053
+ sessionId = resolved;
2054
+ }
2055
+ const result = await readConversation(payload.projectPath, payload.afterLine || 0, sessionId);
1967
2056
  if (result) {
1968
2057
  socket.emit(CLAUDE_CONV_DATA, {
1969
2058
  sessionId: result.sessionId,