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/index.js CHANGED
@@ -289,20 +289,66 @@ function ensurePtyHelperExecutable() {
289
289
  }
290
290
  }
291
291
 
292
+ // src/single-instance.ts
293
+ var import_fs4 = __toESM(require("fs"));
294
+ var import_path4 = __toESM(require("path"));
295
+ var import_child_process = require("child_process");
296
+ var LOCK_FILE = import_path4.default.join(CONFIG_DIR, "agent.lock");
297
+ function isRunningAgent(pid) {
298
+ try {
299
+ process.kill(pid, 0);
300
+ } catch {
301
+ return false;
302
+ }
303
+ if (process.platform === "win32") return true;
304
+ try {
305
+ const out = (0, import_child_process.execFileSync)("ps", ["-p", String(pid), "-o", "command="], { encoding: "utf8" });
306
+ return /crc-agent|cli-remote-agent|agent[\/\\](dist|src)[\/\\](cli|index)/.test(out);
307
+ } catch {
308
+ return true;
309
+ }
310
+ }
311
+ function acquireSingleInstanceLock() {
312
+ if (process.env.CRC_ALLOW_MULTIPLE === "1") return { ok: true };
313
+ try {
314
+ if (import_fs4.default.existsSync(LOCK_FILE)) {
315
+ const prev = parseInt(import_fs4.default.readFileSync(LOCK_FILE, "utf8").trim(), 10);
316
+ if (prev && prev !== process.pid && isRunningAgent(prev)) {
317
+ return { ok: false, pid: prev };
318
+ }
319
+ }
320
+ import_fs4.default.mkdirSync(CONFIG_DIR, { recursive: true });
321
+ import_fs4.default.writeFileSync(LOCK_FILE, String(process.pid));
322
+ const release = () => {
323
+ try {
324
+ if (parseInt(import_fs4.default.readFileSync(LOCK_FILE, "utf8").trim(), 10) === process.pid) {
325
+ import_fs4.default.unlinkSync(LOCK_FILE);
326
+ }
327
+ } catch {
328
+ }
329
+ };
330
+ process.on("exit", release);
331
+ return { ok: true };
332
+ } catch (err) {
333
+ logger.warn({ error: err?.message }, "Could not manage single-instance lock");
334
+ return { ok: true };
335
+ }
336
+ }
337
+
292
338
  // src/tmux.ts
293
- var import_child_process2 = require("child_process");
339
+ var import_child_process3 = require("child_process");
294
340
  var import_util2 = require("util");
295
- var import_fs5 = __toESM(require("fs"));
296
- var import_path5 = __toESM(require("path"));
341
+ var import_fs6 = __toESM(require("fs"));
342
+ var import_path6 = __toESM(require("path"));
297
343
 
298
344
  // src/claude-live.ts
299
- var import_fs4 = __toESM(require("fs"));
300
- var import_path4 = __toESM(require("path"));
345
+ var import_fs5 = __toESM(require("fs"));
346
+ var import_path5 = __toESM(require("path"));
301
347
  var import_os2 = __toESM(require("os"));
302
- var import_child_process = require("child_process");
348
+ var import_child_process2 = require("child_process");
303
349
  var import_util = require("util");
304
- var execFileAsync = (0, import_util.promisify)(import_child_process.execFile);
305
- var LIVE_SESSIONS_DIR = import_path4.default.join(import_os2.default.homedir(), ".claude", "sessions");
350
+ var execFileAsync = (0, import_util.promisify)(import_child_process2.execFile);
351
+ var LIVE_SESSIONS_DIR = import_path5.default.join(import_os2.default.homedir(), ".claude", "sessions");
306
352
  function isAlive(pid) {
307
353
  try {
308
354
  process.kill(pid, 0);
@@ -314,18 +360,19 @@ function isAlive(pid) {
314
360
  function getLiveClaudeSessions() {
315
361
  let files;
316
362
  try {
317
- files = import_fs4.default.readdirSync(LIVE_SESSIONS_DIR).filter((f) => f.endsWith(".json"));
363
+ files = import_fs5.default.readdirSync(LIVE_SESSIONS_DIR).filter((f) => f.endsWith(".json"));
318
364
  } catch {
319
365
  return [];
320
366
  }
321
367
  const out = [];
322
368
  for (const f of files) {
323
369
  try {
324
- const obj = JSON.parse(import_fs4.default.readFileSync(import_path4.default.join(LIVE_SESSIONS_DIR, f), "utf-8"));
370
+ const obj = JSON.parse(import_fs5.default.readFileSync(import_path5.default.join(LIVE_SESSIONS_DIR, f), "utf-8"));
325
371
  if (typeof obj?.pid !== "number" || !isAlive(obj.pid)) continue;
326
372
  out.push({
327
373
  pid: obj.pid,
328
374
  cwd: typeof obj.cwd === "string" ? obj.cwd : "",
375
+ sessionId: typeof obj.sessionId === "string" ? obj.sessionId : void 0,
329
376
  name: typeof obj.name === "string" ? obj.name : void 0,
330
377
  status: typeof obj.status === "string" ? obj.status : void 0,
331
378
  updatedAt: typeof obj.updatedAt === "number" ? obj.updatedAt : void 0
@@ -356,21 +403,30 @@ function isDescendantOf(pid, ancestor, parents) {
356
403
  }
357
404
  return false;
358
405
  }
406
+ async function findClaudeSessionForPtyPid(ptyPid) {
407
+ const live = getLiveClaudeSessions().filter((s) => s.sessionId);
408
+ if (live.length === 0) return null;
409
+ const parents = await getPidParents();
410
+ for (let i = live.length - 1; i >= 0; i--) {
411
+ if (isDescendantOf(live[i].pid, ptyPid, parents)) return live[i].sessionId;
412
+ }
413
+ return null;
414
+ }
359
415
 
360
416
  // src/tmux.ts
361
- var execFileAsync2 = (0, import_util2.promisify)(import_child_process2.execFile);
417
+ var execFileAsync2 = (0, import_util2.promisify)(import_child_process3.execFile);
362
418
  var IS_WIN = process.platform === "win32";
363
419
  var cachedTmuxPath;
364
420
  function resolveTmuxPath() {
365
421
  if (cachedTmuxPath !== void 0) return cachedTmuxPath;
366
- const dirs = (process.env.PATH || "").split(import_path5.default.delimiter).filter(Boolean);
422
+ const dirs = (process.env.PATH || "").split(import_path6.default.delimiter).filter(Boolean);
367
423
  for (const extra of ["/opt/homebrew/bin", "/usr/local/bin", "/usr/bin", "/bin", "/opt/local/bin"]) {
368
424
  if (!dirs.includes(extra)) dirs.push(extra);
369
425
  }
370
426
  for (const dir of dirs) {
371
- const candidate = import_path5.default.join(dir, "tmux");
427
+ const candidate = import_path6.default.join(dir, "tmux");
372
428
  try {
373
- import_fs5.default.accessSync(candidate, import_fs5.default.constants.X_OK);
429
+ import_fs6.default.accessSync(candidate, import_fs6.default.constants.X_OK);
374
430
  cachedTmuxPath = candidate;
375
431
  return candidate;
376
432
  } catch {
@@ -420,8 +476,8 @@ async function enrichWithPanesAndClaude(sessions2) {
420
476
  const { file, args } = tmuxCmd(["list-panes", "-a", "-F", paneFmt]);
421
477
  const { stdout } = await execFileAsync2(file, args, { timeout: 6e3 });
422
478
  const panes = stdout.split("\n").map((line) => line.replace(/\r$/, "").trim()).filter(Boolean).map((line) => {
423
- const [session, pid, path10, activeFlags] = line.split(" ");
424
- return { session, pid: parseInt(pid, 10) || 0, path: path10 || "", active: activeFlags === "11" };
479
+ const [session, pid, path11, activeFlags] = line.split(" ");
480
+ return { session, pid: parseInt(pid, 10) || 0, path: path11 || "", active: activeFlags === "11" };
425
481
  });
426
482
  const byName = new Map(sessions2.map((s) => [s.name, s]));
427
483
  for (const pane of panes) {
@@ -463,22 +519,22 @@ function buildTmuxLaunch(name, launch) {
463
519
 
464
520
  // src/heartbeat.ts
465
521
  var import_os4 = __toESM(require("os"));
466
- var import_path7 = __toESM(require("path"));
467
- var import_child_process4 = require("child_process");
522
+ var import_path8 = __toESM(require("path"));
523
+ var import_child_process5 = require("child_process");
468
524
 
469
525
  // src/pty-session.ts
470
526
  var pty = __toESM(require("node-pty"));
471
- var import_fs6 = require("fs");
472
- var import_path6 = require("path");
527
+ var import_fs7 = require("fs");
528
+ var import_path7 = require("path");
473
529
  var import_os3 = require("os");
474
530
 
475
531
  // src/shell.ts
476
- var import_child_process3 = require("child_process");
532
+ var import_child_process4 = require("child_process");
477
533
  function detectShell(preference) {
478
534
  if (preference !== "auto") return preference;
479
535
  if (process.platform === "win32") {
480
536
  try {
481
- (0, import_child_process3.execSync)("where pwsh", { stdio: "ignore" });
537
+ (0, import_child_process4.execSync)("where pwsh", { stdio: "ignore" });
482
538
  return "pwsh.exe";
483
539
  } catch {
484
540
  return "powershell.exe";
@@ -489,22 +545,22 @@ function detectShell(preference) {
489
545
 
490
546
  // src/pty-session.ts
491
547
  var THRESHOLD = 3;
492
- var INIT_DIR = (0, import_path6.join)((0, import_os3.tmpdir)(), "crc-shell-init");
493
- var BASH_INIT = (0, import_path6.join)(INIT_DIR, "bashrc");
494
- var ZSH_DIR = (0, import_path6.join)(INIT_DIR, "zsh");
495
- var ZSH_RC = (0, import_path6.join)(ZSH_DIR, ".zshrc");
496
- var ZSH_ENV = (0, import_path6.join)(ZSH_DIR, ".zshenv");
548
+ var INIT_DIR = (0, import_path7.join)((0, import_os3.tmpdir)(), "crc-shell-init");
549
+ var BASH_INIT = (0, import_path7.join)(INIT_DIR, "bashrc");
550
+ var ZSH_DIR = (0, import_path7.join)(INIT_DIR, "zsh");
551
+ var ZSH_RC = (0, import_path7.join)(ZSH_DIR, ".zshrc");
552
+ var ZSH_ENV = (0, import_path7.join)(ZSH_DIR, ".zshenv");
497
553
  var INIT_VERSION = "2";
498
554
  function ensureInitFiles() {
499
- const versionFile = (0, import_path6.join)(INIT_DIR, ".version");
500
- if ((0, import_fs6.existsSync)(versionFile)) {
555
+ const versionFile = (0, import_path7.join)(INIT_DIR, ".version");
556
+ if ((0, import_fs7.existsSync)(versionFile)) {
501
557
  try {
502
- if ((0, import_fs6.readFileSync)(versionFile, "utf-8").trim() === INIT_VERSION) return;
558
+ if ((0, import_fs7.readFileSync)(versionFile, "utf-8").trim() === INIT_VERSION) return;
503
559
  } catch {
504
560
  }
505
561
  }
506
- (0, import_fs6.mkdirSync)(ZSH_DIR, { recursive: true });
507
- (0, import_fs6.writeFileSync)(BASH_INIT, [
562
+ (0, import_fs7.mkdirSync)(ZSH_DIR, { recursive: true });
563
+ (0, import_fs7.writeFileSync)(BASH_INIT, [
508
564
  "[ -f ~/.bash_profile ] && source ~/.bash_profile",
509
565
  "[ -f ~/.bashrc ] && source ~/.bashrc",
510
566
  "__crc_s=$SECONDS",
@@ -515,11 +571,11 @@ function ensureInitFiles() {
515
571
  `PROMPT_COMMAND='[ "$__crc_armed" = 1 ] && [ \${__crc_elapsed:-0} -ge ${THRESHOLD} ] && printf "\\a"; __crc_armed=1; eval "$__crc_orig_pc"'`,
516
572
  ""
517
573
  ].join("\n"), "utf-8");
518
- (0, import_fs6.writeFileSync)(ZSH_ENV, [
574
+ (0, import_fs7.writeFileSync)(ZSH_ENV, [
519
575
  '[ -f "${ZDOTDIR_ORIG:-$HOME}/.zshenv" ] && source "${ZDOTDIR_ORIG:-$HOME}/.zshenv"',
520
576
  ""
521
577
  ].join("\n"), "utf-8");
522
- (0, import_fs6.writeFileSync)(ZSH_RC, [
578
+ (0, import_fs7.writeFileSync)(ZSH_RC, [
523
579
  '[ -f "${ZDOTDIR_ORIG:-$HOME}/.zshrc" ] && source "${ZDOTDIR_ORIG:-$HOME}/.zshrc"',
524
580
  "__crc_s=$SECONDS",
525
581
  "__crc_armed=0",
@@ -532,14 +588,14 @@ function ensureInitFiles() {
532
588
  "fi",
533
589
  ""
534
590
  ].join("\n"), "utf-8");
535
- (0, import_fs6.writeFileSync)(versionFile, INIT_VERSION, "utf-8");
591
+ (0, import_fs7.writeFileSync)(versionFile, INIT_VERSION, "utf-8");
536
592
  }
537
593
  function resolveCwd(cwd) {
538
594
  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];
539
595
  for (const c of candidates) {
540
596
  if (c) {
541
597
  try {
542
- if ((0, import_fs6.existsSync)(c)) return c;
598
+ if ((0, import_fs7.existsSync)(c)) return c;
543
599
  } catch {
544
600
  }
545
601
  }
@@ -593,6 +649,9 @@ var PtySession = class {
593
649
  });
594
650
  }
595
651
  }
652
+ get pid() {
653
+ return this.pty.pid;
654
+ }
596
655
  write(data) {
597
656
  this.pty.write(data);
598
657
  }
@@ -658,6 +717,9 @@ function createTerminalSession(sessionId, cols, rows, shellPreference, cwd, onDa
658
717
  sessions.set(sessionId, session);
659
718
  logger.info({ sessionId, cols, rows }, "PTY session created");
660
719
  }
720
+ function getSessionPid(sessionId) {
721
+ return sessions.get(sessionId)?.pid;
722
+ }
661
723
  function writeToSession(sessionId, data) {
662
724
  sessions.get(sessionId)?.write(data);
663
725
  }
@@ -746,7 +808,7 @@ function getCpuUsage() {
746
808
  function getRootPaths() {
747
809
  if (process.platform === "win32") {
748
810
  try {
749
- const output = (0, import_child_process4.execSync)("wmic logicaldisk get name", { encoding: "utf-8" });
811
+ const output = (0, import_child_process5.execSync)("wmic logicaldisk get name", { encoding: "utf-8" });
750
812
  return output.split("\n").map((l) => l.trim()).filter((l) => /^[A-Z]:$/.test(l)).map((l) => l + "\\");
751
813
  } catch {
752
814
  return ["C:\\"];
@@ -765,7 +827,7 @@ function buildHeartbeat(homeDir) {
765
827
  memoryUsage: Math.round((totalMem - freeMem) / totalMem * 100),
766
828
  uptime: Math.round(import_os4.default.uptime()),
767
829
  activeSessions: getActiveSessionCount(),
768
- pathSeparator: import_path7.default.sep,
830
+ pathSeparator: import_path8.default.sep,
769
831
  homeDirectory: homeDir || import_os4.default.homedir(),
770
832
  rootPaths: getRootPaths(),
771
833
  capabilities: { terminal: true, fileTransfer: false }
@@ -773,33 +835,33 @@ function buildHeartbeat(homeDir) {
773
835
  }
774
836
 
775
837
  // src/file-explorer.ts
776
- var import_fs7 = __toESM(require("fs"));
777
- var import_path8 = __toESM(require("path"));
838
+ var import_fs8 = __toESM(require("fs"));
839
+ var import_path9 = __toESM(require("path"));
778
840
  var import_os5 = __toESM(require("os"));
779
- var SECRET_DIR = import_path8.default.join(import_os5.default.homedir(), ".crc-agent");
841
+ var SECRET_DIR = import_path9.default.join(import_os5.default.homedir(), ".crc-agent");
780
842
  function isInSecretDir(resolved) {
781
- const rel = import_path8.default.relative(SECRET_DIR, resolved);
782
- return rel === "" || !rel.startsWith(".." + import_path8.default.sep) && rel !== ".." && !import_path8.default.isAbsolute(rel);
843
+ const rel = import_path9.default.relative(SECRET_DIR, resolved);
844
+ return rel === "" || !rel.startsWith(".." + import_path9.default.sep) && rel !== ".." && !import_path9.default.isAbsolute(rel);
783
845
  }
784
846
  function listDirectory(dirPath) {
785
847
  try {
786
- const resolved = import_path8.default.resolve(dirPath);
848
+ const resolved = import_path9.default.resolve(dirPath);
787
849
  if (isInSecretDir(resolved)) {
788
850
  return { entries: [], error: "Access denied" };
789
851
  }
790
- if (!import_fs7.default.existsSync(resolved)) {
852
+ if (!import_fs8.default.existsSync(resolved)) {
791
853
  return { entries: [], error: "Path does not exist" };
792
854
  }
793
- const stat = import_fs7.default.statSync(resolved);
855
+ const stat = import_fs8.default.statSync(resolved);
794
856
  if (!stat.isDirectory()) {
795
857
  return { entries: [], error: "Not a directory" };
796
858
  }
797
- const dirents = import_fs7.default.readdirSync(resolved, { withFileTypes: true });
859
+ const dirents = import_fs8.default.readdirSync(resolved, { withFileTypes: true });
798
860
  const entries = [];
799
861
  for (const dirent of dirents) {
800
862
  try {
801
- const fullPath = import_path8.default.join(resolved, dirent.name);
802
- const s = import_fs7.default.statSync(fullPath);
863
+ const fullPath = import_path9.default.join(resolved, dirent.name);
864
+ const s = import_fs8.default.statSync(fullPath);
803
865
  entries.push({
804
866
  name: dirent.name,
805
867
  isDirectory: dirent.isDirectory(),
@@ -821,22 +883,22 @@ function listDirectory(dirPath) {
821
883
  }
822
884
  async function downloadFile(filePath, serverUrl, secret, agentId) {
823
885
  try {
824
- const resolved = import_path8.default.resolve(filePath);
886
+ const resolved = import_path9.default.resolve(filePath);
825
887
  if (isInSecretDir(resolved)) {
826
888
  return { error: "Access denied" };
827
889
  }
828
- if (!import_fs7.default.existsSync(resolved)) {
890
+ if (!import_fs8.default.existsSync(resolved)) {
829
891
  return { error: "File does not exist" };
830
892
  }
831
- const stat = import_fs7.default.statSync(resolved);
893
+ const stat = import_fs8.default.statSync(resolved);
832
894
  if (stat.isDirectory()) {
833
895
  return { error: "Cannot download a directory" };
834
896
  }
835
897
  if (stat.size > FILE_MAX_SIZE) {
836
898
  return { error: `File too large (max ${FILE_MAX_SIZE} bytes)` };
837
899
  }
838
- const fileName = import_path8.default.basename(resolved);
839
- const fileStream = import_fs7.default.createReadStream(resolved);
900
+ const fileName = import_path9.default.basename(resolved);
901
+ const fileStream = import_fs8.default.createReadStream(resolved);
840
902
  const baseUrl = serverUrl.replace("wss://", "https://").replace("ws://", "http://");
841
903
  const url = `${baseUrl}/api/files/receive`;
842
904
  const res = await fetch(url, {
@@ -867,14 +929,14 @@ async function downloadFile(filePath, serverUrl, secret, agentId) {
867
929
  }
868
930
 
869
931
  // src/vpn-manager.ts
870
- var import_child_process5 = require("child_process");
932
+ var import_child_process6 = require("child_process");
871
933
  var import_util3 = require("util");
872
- var import_fs8 = __toESM(require("fs"));
873
- var import_path9 = __toESM(require("path"));
934
+ var import_fs9 = __toESM(require("fs"));
935
+ var import_path10 = __toESM(require("path"));
874
936
  var import_os6 = __toESM(require("os"));
875
- var execAsync = (0, import_util3.promisify)(import_child_process5.exec);
937
+ var execAsync = (0, import_util3.promisify)(import_child_process6.exec);
876
938
  var isWindows = process.platform === "win32";
877
- var VPN_DIR = import_path9.default.join(import_os6.default.homedir(), ".crc-agent", "vpn");
939
+ var VPN_DIR = import_path10.default.join(import_os6.default.homedir(), ".crc-agent", "vpn");
878
940
  function shq(value) {
879
941
  return value.replace(/'/g, "'\\''");
880
942
  }
@@ -893,8 +955,8 @@ async function runCmd(cmd, shell) {
893
955
  }
894
956
  function getConfigPath(profile) {
895
957
  const file = profile.configFile || `${profile.id}.conf`;
896
- if (import_path9.default.isAbsolute(file)) return file;
897
- return import_path9.default.join(VPN_DIR, file);
958
+ if (import_path10.default.isAbsolute(file)) return file;
959
+ return import_path10.default.join(VPN_DIR, file);
898
960
  }
899
961
  async function getScutilStatus(serviceName) {
900
962
  const { stdout } = await runCmd(`scutil --nc status '${shq(serviceName)}'`);
@@ -1010,7 +1072,7 @@ async function connectWireGuard(profile) {
1010
1072
  const tunnelName = profile.tunnelName || profile.id;
1011
1073
  if (isWindows) {
1012
1074
  const confPath2 = getConfigPath(profile);
1013
- if (!import_fs8.default.existsSync(confPath2)) {
1075
+ if (!import_fs9.default.existsSync(confPath2)) {
1014
1076
  return `Config file not found: ${confPath2}`;
1015
1077
  }
1016
1078
  const script = `
@@ -1036,7 +1098,7 @@ async function connectWireGuard(profile) {
1036
1098
  async function connectOpenVpn(profile) {
1037
1099
  if (isWindows) {
1038
1100
  const confPath2 = getConfigPath(profile);
1039
- if (!import_fs8.default.existsSync(confPath2)) {
1101
+ if (!import_fs9.default.existsSync(confPath2)) {
1040
1102
  return `Config file not found: ${confPath2}`;
1041
1103
  }
1042
1104
  const connector = "C:\\Program Files\\OpenVPN Connect\\ovpnconnector.exe";
@@ -1063,7 +1125,7 @@ async function connectOpenVpn(profile) {
1063
1125
  return scutilStart(profile.serviceName);
1064
1126
  }
1065
1127
  const confPath = getConfigPath(profile);
1066
- if (!import_fs8.default.existsSync(confPath)) {
1128
+ if (!import_fs9.default.existsSync(confPath)) {
1067
1129
  return `Config file not found: ${confPath}`;
1068
1130
  }
1069
1131
  const { stderr } = await runCmd(`sudo openvpn --config '${shq(confPath)}' --daemon`);
@@ -1072,7 +1134,7 @@ async function connectOpenVpn(profile) {
1072
1134
  async function connectAzure(profile) {
1073
1135
  if (isWindows) {
1074
1136
  const confPath = getConfigPath(profile);
1075
- if (!import_fs8.default.existsSync(confPath)) {
1137
+ if (!import_fs9.default.existsSync(confPath)) {
1076
1138
  return `Config file not found: ${confPath}`;
1077
1139
  }
1078
1140
  await runCmd(`& 'AzureVpn.exe' -i '${psq(confPath)}' 2>&1`);
@@ -1194,8 +1256,8 @@ async function disconnectVpn(configs, profileId) {
1194
1256
  }
1195
1257
 
1196
1258
  // src/claude-sessions.ts
1197
- var import_fs9 = __toESM(require("fs"));
1198
- var import_path10 = __toESM(require("path"));
1259
+ var import_fs10 = __toESM(require("fs"));
1260
+ var import_path11 = __toESM(require("path"));
1199
1261
  var import_os7 = __toESM(require("os"));
1200
1262
  var import_readline = __toESM(require("readline"));
1201
1263
 
@@ -1223,10 +1285,10 @@ function findProjectDirs(allDirs, filterProjectPath) {
1223
1285
  }
1224
1286
 
1225
1287
  // src/claude-sessions.ts
1226
- var CLAUDE_PROJECTS_DIR = import_path10.default.join(import_os7.default.homedir(), ".claude", "projects");
1288
+ var CLAUDE_PROJECTS_DIR = import_path11.default.join(import_os7.default.homedir(), ".claude", "projects");
1227
1289
  async function parseSessionFile(filePath) {
1228
1290
  try {
1229
- const stream = import_fs9.default.createReadStream(filePath, { encoding: "utf-8" });
1291
+ const stream = import_fs10.default.createReadStream(filePath, { encoding: "utf-8" });
1230
1292
  const rl = import_readline.default.createInterface({ input: stream, crlfDelay: Infinity });
1231
1293
  let firstMessage = "";
1232
1294
  let lastTimestamp = "";
@@ -1274,17 +1336,17 @@ async function parseSessionFile(filePath) {
1274
1336
  }
1275
1337
  async function listClaudeSessions(filterProjectPath) {
1276
1338
  const sessions2 = [];
1277
- if (!import_fs9.default.existsSync(CLAUDE_PROJECTS_DIR)) return sessions2;
1339
+ if (!import_fs10.default.existsSync(CLAUDE_PROJECTS_DIR)) return sessions2;
1278
1340
  const isDirSafe = (d) => {
1279
1341
  try {
1280
- return import_fs9.default.statSync(import_path10.default.join(CLAUDE_PROJECTS_DIR, d)).isDirectory();
1342
+ return import_fs10.default.statSync(import_path11.default.join(CLAUDE_PROJECTS_DIR, d)).isDirectory();
1281
1343
  } catch {
1282
1344
  return false;
1283
1345
  }
1284
1346
  };
1285
1347
  let allEntries;
1286
1348
  try {
1287
- allEntries = import_fs9.default.readdirSync(CLAUDE_PROJECTS_DIR);
1349
+ allEntries = import_fs10.default.readdirSync(CLAUDE_PROJECTS_DIR);
1288
1350
  } catch {
1289
1351
  return sessions2;
1290
1352
  }
@@ -1297,16 +1359,16 @@ async function listClaudeSessions(filterProjectPath) {
1297
1359
  projectDirs = allEntries.filter((d) => isDirSafe(d) && !d.startsWith("-private-"));
1298
1360
  }
1299
1361
  for (const dirName of projectDirs) {
1300
- const dirPath = import_path10.default.join(CLAUDE_PROJECTS_DIR, dirName);
1362
+ const dirPath = import_path11.default.join(CLAUDE_PROJECTS_DIR, dirName);
1301
1363
  let files;
1302
1364
  try {
1303
- files = import_fs9.default.readdirSync(dirPath).filter((f) => f.endsWith(".jsonl"));
1365
+ files = import_fs10.default.readdirSync(dirPath).filter((f) => f.endsWith(".jsonl"));
1304
1366
  } catch {
1305
1367
  continue;
1306
1368
  }
1307
1369
  for (const file of files) {
1308
1370
  const sessionId = file.replace(".jsonl", "");
1309
- const parsed = await parseSessionFile(import_path10.default.join(dirPath, file));
1371
+ const parsed = await parseSessionFile(import_path11.default.join(dirPath, file));
1310
1372
  if (!parsed) continue;
1311
1373
  const projectPath = parsed.cwd || dirName.replace(/^-/, "/").replace(/-/g, "/");
1312
1374
  sessions2.push({
@@ -1326,21 +1388,21 @@ async function listClaudeSessions(filterProjectPath) {
1326
1388
  }
1327
1389
 
1328
1390
  // src/claude-conversation.ts
1329
- var import_fs10 = __toESM(require("fs"));
1330
- var import_path11 = __toESM(require("path"));
1391
+ var import_fs11 = __toESM(require("fs"));
1392
+ var import_path12 = __toESM(require("path"));
1331
1393
  var import_os8 = __toESM(require("os"));
1332
1394
  var import_readline2 = __toESM(require("readline"));
1333
- var CLAUDE_PROJECTS_DIR2 = import_path11.default.join(import_os8.default.homedir(), ".claude", "projects");
1395
+ var CLAUDE_PROJECTS_DIR2 = import_path12.default.join(import_os8.default.homedir(), ".claude", "projects");
1334
1396
  function findProjectDir(projectPath) {
1335
1397
  const encoded = encodeProjectPath(projectPath);
1336
- const dirPath = import_path11.default.join(CLAUDE_PROJECTS_DIR2, encoded);
1337
- if (import_fs10.default.existsSync(dirPath)) return dirPath;
1338
- if (!import_fs10.default.existsSync(CLAUDE_PROJECTS_DIR2)) return null;
1398
+ const dirPath = import_path12.default.join(CLAUDE_PROJECTS_DIR2, encoded);
1399
+ if (import_fs11.default.existsSync(dirPath)) return dirPath;
1400
+ if (!import_fs11.default.existsSync(CLAUDE_PROJECTS_DIR2)) return null;
1339
1401
  let allDirs;
1340
1402
  try {
1341
- allDirs = import_fs10.default.readdirSync(CLAUDE_PROJECTS_DIR2).filter((d) => {
1403
+ allDirs = import_fs11.default.readdirSync(CLAUDE_PROJECTS_DIR2).filter((d) => {
1342
1404
  try {
1343
- return import_fs10.default.statSync(import_path11.default.join(CLAUDE_PROJECTS_DIR2, d)).isDirectory();
1405
+ return import_fs11.default.statSync(import_path12.default.join(CLAUDE_PROJECTS_DIR2, d)).isDirectory();
1344
1406
  } catch {
1345
1407
  return false;
1346
1408
  }
@@ -1350,23 +1412,23 @@ function findProjectDir(projectPath) {
1350
1412
  }
1351
1413
  const matches = findProjectDirs(allDirs, projectPath);
1352
1414
  if (matches.length > 0) {
1353
- return import_path11.default.join(CLAUDE_PROJECTS_DIR2, matches[0]);
1415
+ return import_path12.default.join(CLAUDE_PROJECTS_DIR2, matches[0]);
1354
1416
  }
1355
1417
  return null;
1356
1418
  }
1357
1419
  function findSessionFileById(sessionId) {
1358
1420
  if (!/^[A-Za-z0-9._-]+$/.test(sessionId)) return null;
1359
- if (!import_fs10.default.existsSync(CLAUDE_PROJECTS_DIR2)) return null;
1421
+ if (!import_fs11.default.existsSync(CLAUDE_PROJECTS_DIR2)) return null;
1360
1422
  let dirs;
1361
1423
  try {
1362
- dirs = import_fs10.default.readdirSync(CLAUDE_PROJECTS_DIR2);
1424
+ dirs = import_fs11.default.readdirSync(CLAUDE_PROJECTS_DIR2);
1363
1425
  } catch {
1364
1426
  return null;
1365
1427
  }
1366
1428
  for (const dir of dirs) {
1367
- const candidate = import_path11.default.join(CLAUDE_PROJECTS_DIR2, dir, `${sessionId}.jsonl`);
1429
+ const candidate = import_path12.default.join(CLAUDE_PROJECTS_DIR2, dir, `${sessionId}.jsonl`);
1368
1430
  try {
1369
- if (import_fs10.default.statSync(candidate).isFile()) return candidate;
1431
+ if (import_fs11.default.statSync(candidate).isFile()) return candidate;
1370
1432
  } catch {
1371
1433
  }
1372
1434
  }
@@ -1377,20 +1439,20 @@ function findLatestSessionFile(projectPath) {
1377
1439
  if (!dirPath) return null;
1378
1440
  let entries;
1379
1441
  try {
1380
- entries = import_fs10.default.readdirSync(dirPath);
1442
+ entries = import_fs11.default.readdirSync(dirPath);
1381
1443
  } catch {
1382
1444
  return null;
1383
1445
  }
1384
1446
  const files = entries.filter((f) => f.endsWith(".jsonl") && !f.includes("/")).map((f) => {
1385
1447
  try {
1386
- return { name: f, mtime: import_fs10.default.statSync(import_path11.default.join(dirPath, f)).mtimeMs };
1448
+ return { name: f, mtime: import_fs11.default.statSync(import_path12.default.join(dirPath, f)).mtimeMs };
1387
1449
  } catch {
1388
1450
  return null;
1389
1451
  }
1390
1452
  }).filter((f) => f !== null).sort((a, b) => b.mtime - a.mtime);
1391
1453
  if (files.length === 0) return null;
1392
1454
  return {
1393
- filePath: import_path11.default.join(dirPath, files[0].name),
1455
+ filePath: import_path12.default.join(dirPath, files[0].name),
1394
1456
  sessionId: files[0].name.replace(".jsonl", "")
1395
1457
  };
1396
1458
  }
@@ -1400,8 +1462,8 @@ async function readConversation(projectPath, afterLine = 0, specificSessionId) {
1400
1462
  if (specificSessionId) {
1401
1463
  sessionId = specificSessionId;
1402
1464
  const dirPath = findProjectDir(projectPath);
1403
- const direct = dirPath ? import_path11.default.join(dirPath, `${specificSessionId}.jsonl`) : null;
1404
- if (direct && import_fs10.default.existsSync(direct)) {
1465
+ const direct = dirPath ? import_path12.default.join(dirPath, `${specificSessionId}.jsonl`) : null;
1466
+ if (direct && import_fs11.default.existsSync(direct)) {
1405
1467
  filePath = direct;
1406
1468
  } else {
1407
1469
  const byId = findSessionFileById(specificSessionId);
@@ -1415,7 +1477,7 @@ async function readConversation(projectPath, afterLine = 0, specificSessionId) {
1415
1477
  sessionId = found.sessionId;
1416
1478
  }
1417
1479
  try {
1418
- const stream = import_fs10.default.createReadStream(filePath, { encoding: "utf-8" });
1480
+ const stream = import_fs11.default.createReadStream(filePath, { encoding: "utf-8" });
1419
1481
  const rl = import_readline2.default.createInterface({ input: stream, crlfDelay: Infinity });
1420
1482
  const messages = [];
1421
1483
  let lineNum = 0;
@@ -1504,6 +1566,13 @@ if (!isConfigured()) {
1504
1566
  }
1505
1567
  var config = loadConfig();
1506
1568
  logger.info({ agentId: config.agentId, serverUrl: config.serverUrl }, "Starting agent");
1569
+ var lock = acquireSingleInstanceLock();
1570
+ if (!lock.ok) {
1571
+ console.error(
1572
+ `Another crc-agent is already running on this machine (PID ${lock.pid}). Stop it first, or set CRC_ALLOW_MULTIPLE=1 to override.`
1573
+ );
1574
+ process.exit(1);
1575
+ }
1507
1576
  ensurePtyHelperExecutable();
1508
1577
  process.on("unhandledRejection", (reason) => {
1509
1578
  logger.error({ reason }, "Unhandled promise rejection (agent kept alive)");
@@ -1692,7 +1761,17 @@ socket.on(AGENT_EXEC, async (payload) => {
1692
1761
  });
1693
1762
  socket.on(CLAUDE_CONV_READ, async (payload) => {
1694
1763
  try {
1695
- const result = await readConversation(payload.projectPath, payload.afterLine || 0, payload.sessionId);
1764
+ let sessionId = payload.sessionId;
1765
+ if (!sessionId && payload.terminalSessionId) {
1766
+ const ptyPid = getSessionPid(payload.terminalSessionId);
1767
+ const resolved = ptyPid ? await findClaudeSessionForPtyPid(ptyPid) : null;
1768
+ if (!resolved) {
1769
+ socket.emit(CLAUDE_CONV_DATA, { sessionId: "", messages: [], totalLines: 0 });
1770
+ return;
1771
+ }
1772
+ sessionId = resolved;
1773
+ }
1774
+ const result = await readConversation(payload.projectPath, payload.afterLine || 0, sessionId);
1696
1775
  if (result) {
1697
1776
  socket.emit(CLAUDE_CONV_DATA, {
1698
1777
  sessionId: result.sessionId,