cli-remote-agent 1.0.7 → 1.0.8

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,14 +360,14 @@ 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,
@@ -358,19 +404,19 @@ function isDescendantOf(pid, ancestor, parents) {
358
404
  }
359
405
 
360
406
  // src/tmux.ts
361
- var execFileAsync2 = (0, import_util2.promisify)(import_child_process2.execFile);
407
+ var execFileAsync2 = (0, import_util2.promisify)(import_child_process3.execFile);
362
408
  var IS_WIN = process.platform === "win32";
363
409
  var cachedTmuxPath;
364
410
  function resolveTmuxPath() {
365
411
  if (cachedTmuxPath !== void 0) return cachedTmuxPath;
366
- const dirs = (process.env.PATH || "").split(import_path5.default.delimiter).filter(Boolean);
412
+ const dirs = (process.env.PATH || "").split(import_path6.default.delimiter).filter(Boolean);
367
413
  for (const extra of ["/opt/homebrew/bin", "/usr/local/bin", "/usr/bin", "/bin", "/opt/local/bin"]) {
368
414
  if (!dirs.includes(extra)) dirs.push(extra);
369
415
  }
370
416
  for (const dir of dirs) {
371
- const candidate = import_path5.default.join(dir, "tmux");
417
+ const candidate = import_path6.default.join(dir, "tmux");
372
418
  try {
373
- import_fs5.default.accessSync(candidate, import_fs5.default.constants.X_OK);
419
+ import_fs6.default.accessSync(candidate, import_fs6.default.constants.X_OK);
374
420
  cachedTmuxPath = candidate;
375
421
  return candidate;
376
422
  } catch {
@@ -420,8 +466,8 @@ async function enrichWithPanesAndClaude(sessions2) {
420
466
  const { file, args } = tmuxCmd(["list-panes", "-a", "-F", paneFmt]);
421
467
  const { stdout } = await execFileAsync2(file, args, { timeout: 6e3 });
422
468
  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" };
469
+ const [session, pid, path11, activeFlags] = line.split(" ");
470
+ return { session, pid: parseInt(pid, 10) || 0, path: path11 || "", active: activeFlags === "11" };
425
471
  });
426
472
  const byName = new Map(sessions2.map((s) => [s.name, s]));
427
473
  for (const pane of panes) {
@@ -463,22 +509,22 @@ function buildTmuxLaunch(name, launch) {
463
509
 
464
510
  // src/heartbeat.ts
465
511
  var import_os4 = __toESM(require("os"));
466
- var import_path7 = __toESM(require("path"));
467
- var import_child_process4 = require("child_process");
512
+ var import_path8 = __toESM(require("path"));
513
+ var import_child_process5 = require("child_process");
468
514
 
469
515
  // src/pty-session.ts
470
516
  var pty = __toESM(require("node-pty"));
471
- var import_fs6 = require("fs");
472
- var import_path6 = require("path");
517
+ var import_fs7 = require("fs");
518
+ var import_path7 = require("path");
473
519
  var import_os3 = require("os");
474
520
 
475
521
  // src/shell.ts
476
- var import_child_process3 = require("child_process");
522
+ var import_child_process4 = require("child_process");
477
523
  function detectShell(preference) {
478
524
  if (preference !== "auto") return preference;
479
525
  if (process.platform === "win32") {
480
526
  try {
481
- (0, import_child_process3.execSync)("where pwsh", { stdio: "ignore" });
527
+ (0, import_child_process4.execSync)("where pwsh", { stdio: "ignore" });
482
528
  return "pwsh.exe";
483
529
  } catch {
484
530
  return "powershell.exe";
@@ -489,22 +535,22 @@ function detectShell(preference) {
489
535
 
490
536
  // src/pty-session.ts
491
537
  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");
538
+ var INIT_DIR = (0, import_path7.join)((0, import_os3.tmpdir)(), "crc-shell-init");
539
+ var BASH_INIT = (0, import_path7.join)(INIT_DIR, "bashrc");
540
+ var ZSH_DIR = (0, import_path7.join)(INIT_DIR, "zsh");
541
+ var ZSH_RC = (0, import_path7.join)(ZSH_DIR, ".zshrc");
542
+ var ZSH_ENV = (0, import_path7.join)(ZSH_DIR, ".zshenv");
497
543
  var INIT_VERSION = "2";
498
544
  function ensureInitFiles() {
499
- const versionFile = (0, import_path6.join)(INIT_DIR, ".version");
500
- if ((0, import_fs6.existsSync)(versionFile)) {
545
+ const versionFile = (0, import_path7.join)(INIT_DIR, ".version");
546
+ if ((0, import_fs7.existsSync)(versionFile)) {
501
547
  try {
502
- if ((0, import_fs6.readFileSync)(versionFile, "utf-8").trim() === INIT_VERSION) return;
548
+ if ((0, import_fs7.readFileSync)(versionFile, "utf-8").trim() === INIT_VERSION) return;
503
549
  } catch {
504
550
  }
505
551
  }
506
- (0, import_fs6.mkdirSync)(ZSH_DIR, { recursive: true });
507
- (0, import_fs6.writeFileSync)(BASH_INIT, [
552
+ (0, import_fs7.mkdirSync)(ZSH_DIR, { recursive: true });
553
+ (0, import_fs7.writeFileSync)(BASH_INIT, [
508
554
  "[ -f ~/.bash_profile ] && source ~/.bash_profile",
509
555
  "[ -f ~/.bashrc ] && source ~/.bashrc",
510
556
  "__crc_s=$SECONDS",
@@ -515,11 +561,11 @@ function ensureInitFiles() {
515
561
  `PROMPT_COMMAND='[ "$__crc_armed" = 1 ] && [ \${__crc_elapsed:-0} -ge ${THRESHOLD} ] && printf "\\a"; __crc_armed=1; eval "$__crc_orig_pc"'`,
516
562
  ""
517
563
  ].join("\n"), "utf-8");
518
- (0, import_fs6.writeFileSync)(ZSH_ENV, [
564
+ (0, import_fs7.writeFileSync)(ZSH_ENV, [
519
565
  '[ -f "${ZDOTDIR_ORIG:-$HOME}/.zshenv" ] && source "${ZDOTDIR_ORIG:-$HOME}/.zshenv"',
520
566
  ""
521
567
  ].join("\n"), "utf-8");
522
- (0, import_fs6.writeFileSync)(ZSH_RC, [
568
+ (0, import_fs7.writeFileSync)(ZSH_RC, [
523
569
  '[ -f "${ZDOTDIR_ORIG:-$HOME}/.zshrc" ] && source "${ZDOTDIR_ORIG:-$HOME}/.zshrc"',
524
570
  "__crc_s=$SECONDS",
525
571
  "__crc_armed=0",
@@ -532,14 +578,14 @@ function ensureInitFiles() {
532
578
  "fi",
533
579
  ""
534
580
  ].join("\n"), "utf-8");
535
- (0, import_fs6.writeFileSync)(versionFile, INIT_VERSION, "utf-8");
581
+ (0, import_fs7.writeFileSync)(versionFile, INIT_VERSION, "utf-8");
536
582
  }
537
583
  function resolveCwd(cwd) {
538
584
  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
585
  for (const c of candidates) {
540
586
  if (c) {
541
587
  try {
542
- if ((0, import_fs6.existsSync)(c)) return c;
588
+ if ((0, import_fs7.existsSync)(c)) return c;
543
589
  } catch {
544
590
  }
545
591
  }
@@ -746,7 +792,7 @@ function getCpuUsage() {
746
792
  function getRootPaths() {
747
793
  if (process.platform === "win32") {
748
794
  try {
749
- const output = (0, import_child_process4.execSync)("wmic logicaldisk get name", { encoding: "utf-8" });
795
+ const output = (0, import_child_process5.execSync)("wmic logicaldisk get name", { encoding: "utf-8" });
750
796
  return output.split("\n").map((l) => l.trim()).filter((l) => /^[A-Z]:$/.test(l)).map((l) => l + "\\");
751
797
  } catch {
752
798
  return ["C:\\"];
@@ -765,7 +811,7 @@ function buildHeartbeat(homeDir) {
765
811
  memoryUsage: Math.round((totalMem - freeMem) / totalMem * 100),
766
812
  uptime: Math.round(import_os4.default.uptime()),
767
813
  activeSessions: getActiveSessionCount(),
768
- pathSeparator: import_path7.default.sep,
814
+ pathSeparator: import_path8.default.sep,
769
815
  homeDirectory: homeDir || import_os4.default.homedir(),
770
816
  rootPaths: getRootPaths(),
771
817
  capabilities: { terminal: true, fileTransfer: false }
@@ -773,33 +819,33 @@ function buildHeartbeat(homeDir) {
773
819
  }
774
820
 
775
821
  // src/file-explorer.ts
776
- var import_fs7 = __toESM(require("fs"));
777
- var import_path8 = __toESM(require("path"));
822
+ var import_fs8 = __toESM(require("fs"));
823
+ var import_path9 = __toESM(require("path"));
778
824
  var import_os5 = __toESM(require("os"));
779
- var SECRET_DIR = import_path8.default.join(import_os5.default.homedir(), ".crc-agent");
825
+ var SECRET_DIR = import_path9.default.join(import_os5.default.homedir(), ".crc-agent");
780
826
  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);
827
+ const rel = import_path9.default.relative(SECRET_DIR, resolved);
828
+ return rel === "" || !rel.startsWith(".." + import_path9.default.sep) && rel !== ".." && !import_path9.default.isAbsolute(rel);
783
829
  }
784
830
  function listDirectory(dirPath) {
785
831
  try {
786
- const resolved = import_path8.default.resolve(dirPath);
832
+ const resolved = import_path9.default.resolve(dirPath);
787
833
  if (isInSecretDir(resolved)) {
788
834
  return { entries: [], error: "Access denied" };
789
835
  }
790
- if (!import_fs7.default.existsSync(resolved)) {
836
+ if (!import_fs8.default.existsSync(resolved)) {
791
837
  return { entries: [], error: "Path does not exist" };
792
838
  }
793
- const stat = import_fs7.default.statSync(resolved);
839
+ const stat = import_fs8.default.statSync(resolved);
794
840
  if (!stat.isDirectory()) {
795
841
  return { entries: [], error: "Not a directory" };
796
842
  }
797
- const dirents = import_fs7.default.readdirSync(resolved, { withFileTypes: true });
843
+ const dirents = import_fs8.default.readdirSync(resolved, { withFileTypes: true });
798
844
  const entries = [];
799
845
  for (const dirent of dirents) {
800
846
  try {
801
- const fullPath = import_path8.default.join(resolved, dirent.name);
802
- const s = import_fs7.default.statSync(fullPath);
847
+ const fullPath = import_path9.default.join(resolved, dirent.name);
848
+ const s = import_fs8.default.statSync(fullPath);
803
849
  entries.push({
804
850
  name: dirent.name,
805
851
  isDirectory: dirent.isDirectory(),
@@ -821,22 +867,22 @@ function listDirectory(dirPath) {
821
867
  }
822
868
  async function downloadFile(filePath, serverUrl, secret, agentId) {
823
869
  try {
824
- const resolved = import_path8.default.resolve(filePath);
870
+ const resolved = import_path9.default.resolve(filePath);
825
871
  if (isInSecretDir(resolved)) {
826
872
  return { error: "Access denied" };
827
873
  }
828
- if (!import_fs7.default.existsSync(resolved)) {
874
+ if (!import_fs8.default.existsSync(resolved)) {
829
875
  return { error: "File does not exist" };
830
876
  }
831
- const stat = import_fs7.default.statSync(resolved);
877
+ const stat = import_fs8.default.statSync(resolved);
832
878
  if (stat.isDirectory()) {
833
879
  return { error: "Cannot download a directory" };
834
880
  }
835
881
  if (stat.size > FILE_MAX_SIZE) {
836
882
  return { error: `File too large (max ${FILE_MAX_SIZE} bytes)` };
837
883
  }
838
- const fileName = import_path8.default.basename(resolved);
839
- const fileStream = import_fs7.default.createReadStream(resolved);
884
+ const fileName = import_path9.default.basename(resolved);
885
+ const fileStream = import_fs8.default.createReadStream(resolved);
840
886
  const baseUrl = serverUrl.replace("wss://", "https://").replace("ws://", "http://");
841
887
  const url = `${baseUrl}/api/files/receive`;
842
888
  const res = await fetch(url, {
@@ -867,14 +913,14 @@ async function downloadFile(filePath, serverUrl, secret, agentId) {
867
913
  }
868
914
 
869
915
  // src/vpn-manager.ts
870
- var import_child_process5 = require("child_process");
916
+ var import_child_process6 = require("child_process");
871
917
  var import_util3 = require("util");
872
- var import_fs8 = __toESM(require("fs"));
873
- var import_path9 = __toESM(require("path"));
918
+ var import_fs9 = __toESM(require("fs"));
919
+ var import_path10 = __toESM(require("path"));
874
920
  var import_os6 = __toESM(require("os"));
875
- var execAsync = (0, import_util3.promisify)(import_child_process5.exec);
921
+ var execAsync = (0, import_util3.promisify)(import_child_process6.exec);
876
922
  var isWindows = process.platform === "win32";
877
- var VPN_DIR = import_path9.default.join(import_os6.default.homedir(), ".crc-agent", "vpn");
923
+ var VPN_DIR = import_path10.default.join(import_os6.default.homedir(), ".crc-agent", "vpn");
878
924
  function shq(value) {
879
925
  return value.replace(/'/g, "'\\''");
880
926
  }
@@ -893,8 +939,8 @@ async function runCmd(cmd, shell) {
893
939
  }
894
940
  function getConfigPath(profile) {
895
941
  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);
942
+ if (import_path10.default.isAbsolute(file)) return file;
943
+ return import_path10.default.join(VPN_DIR, file);
898
944
  }
899
945
  async function getScutilStatus(serviceName) {
900
946
  const { stdout } = await runCmd(`scutil --nc status '${shq(serviceName)}'`);
@@ -1010,7 +1056,7 @@ async function connectWireGuard(profile) {
1010
1056
  const tunnelName = profile.tunnelName || profile.id;
1011
1057
  if (isWindows) {
1012
1058
  const confPath2 = getConfigPath(profile);
1013
- if (!import_fs8.default.existsSync(confPath2)) {
1059
+ if (!import_fs9.default.existsSync(confPath2)) {
1014
1060
  return `Config file not found: ${confPath2}`;
1015
1061
  }
1016
1062
  const script = `
@@ -1036,7 +1082,7 @@ async function connectWireGuard(profile) {
1036
1082
  async function connectOpenVpn(profile) {
1037
1083
  if (isWindows) {
1038
1084
  const confPath2 = getConfigPath(profile);
1039
- if (!import_fs8.default.existsSync(confPath2)) {
1085
+ if (!import_fs9.default.existsSync(confPath2)) {
1040
1086
  return `Config file not found: ${confPath2}`;
1041
1087
  }
1042
1088
  const connector = "C:\\Program Files\\OpenVPN Connect\\ovpnconnector.exe";
@@ -1063,7 +1109,7 @@ async function connectOpenVpn(profile) {
1063
1109
  return scutilStart(profile.serviceName);
1064
1110
  }
1065
1111
  const confPath = getConfigPath(profile);
1066
- if (!import_fs8.default.existsSync(confPath)) {
1112
+ if (!import_fs9.default.existsSync(confPath)) {
1067
1113
  return `Config file not found: ${confPath}`;
1068
1114
  }
1069
1115
  const { stderr } = await runCmd(`sudo openvpn --config '${shq(confPath)}' --daemon`);
@@ -1072,7 +1118,7 @@ async function connectOpenVpn(profile) {
1072
1118
  async function connectAzure(profile) {
1073
1119
  if (isWindows) {
1074
1120
  const confPath = getConfigPath(profile);
1075
- if (!import_fs8.default.existsSync(confPath)) {
1121
+ if (!import_fs9.default.existsSync(confPath)) {
1076
1122
  return `Config file not found: ${confPath}`;
1077
1123
  }
1078
1124
  await runCmd(`& 'AzureVpn.exe' -i '${psq(confPath)}' 2>&1`);
@@ -1194,8 +1240,8 @@ async function disconnectVpn(configs, profileId) {
1194
1240
  }
1195
1241
 
1196
1242
  // src/claude-sessions.ts
1197
- var import_fs9 = __toESM(require("fs"));
1198
- var import_path10 = __toESM(require("path"));
1243
+ var import_fs10 = __toESM(require("fs"));
1244
+ var import_path11 = __toESM(require("path"));
1199
1245
  var import_os7 = __toESM(require("os"));
1200
1246
  var import_readline = __toESM(require("readline"));
1201
1247
 
@@ -1223,10 +1269,10 @@ function findProjectDirs(allDirs, filterProjectPath) {
1223
1269
  }
1224
1270
 
1225
1271
  // src/claude-sessions.ts
1226
- var CLAUDE_PROJECTS_DIR = import_path10.default.join(import_os7.default.homedir(), ".claude", "projects");
1272
+ var CLAUDE_PROJECTS_DIR = import_path11.default.join(import_os7.default.homedir(), ".claude", "projects");
1227
1273
  async function parseSessionFile(filePath) {
1228
1274
  try {
1229
- const stream = import_fs9.default.createReadStream(filePath, { encoding: "utf-8" });
1275
+ const stream = import_fs10.default.createReadStream(filePath, { encoding: "utf-8" });
1230
1276
  const rl = import_readline.default.createInterface({ input: stream, crlfDelay: Infinity });
1231
1277
  let firstMessage = "";
1232
1278
  let lastTimestamp = "";
@@ -1274,17 +1320,17 @@ async function parseSessionFile(filePath) {
1274
1320
  }
1275
1321
  async function listClaudeSessions(filterProjectPath) {
1276
1322
  const sessions2 = [];
1277
- if (!import_fs9.default.existsSync(CLAUDE_PROJECTS_DIR)) return sessions2;
1323
+ if (!import_fs10.default.existsSync(CLAUDE_PROJECTS_DIR)) return sessions2;
1278
1324
  const isDirSafe = (d) => {
1279
1325
  try {
1280
- return import_fs9.default.statSync(import_path10.default.join(CLAUDE_PROJECTS_DIR, d)).isDirectory();
1326
+ return import_fs10.default.statSync(import_path11.default.join(CLAUDE_PROJECTS_DIR, d)).isDirectory();
1281
1327
  } catch {
1282
1328
  return false;
1283
1329
  }
1284
1330
  };
1285
1331
  let allEntries;
1286
1332
  try {
1287
- allEntries = import_fs9.default.readdirSync(CLAUDE_PROJECTS_DIR);
1333
+ allEntries = import_fs10.default.readdirSync(CLAUDE_PROJECTS_DIR);
1288
1334
  } catch {
1289
1335
  return sessions2;
1290
1336
  }
@@ -1297,16 +1343,16 @@ async function listClaudeSessions(filterProjectPath) {
1297
1343
  projectDirs = allEntries.filter((d) => isDirSafe(d) && !d.startsWith("-private-"));
1298
1344
  }
1299
1345
  for (const dirName of projectDirs) {
1300
- const dirPath = import_path10.default.join(CLAUDE_PROJECTS_DIR, dirName);
1346
+ const dirPath = import_path11.default.join(CLAUDE_PROJECTS_DIR, dirName);
1301
1347
  let files;
1302
1348
  try {
1303
- files = import_fs9.default.readdirSync(dirPath).filter((f) => f.endsWith(".jsonl"));
1349
+ files = import_fs10.default.readdirSync(dirPath).filter((f) => f.endsWith(".jsonl"));
1304
1350
  } catch {
1305
1351
  continue;
1306
1352
  }
1307
1353
  for (const file of files) {
1308
1354
  const sessionId = file.replace(".jsonl", "");
1309
- const parsed = await parseSessionFile(import_path10.default.join(dirPath, file));
1355
+ const parsed = await parseSessionFile(import_path11.default.join(dirPath, file));
1310
1356
  if (!parsed) continue;
1311
1357
  const projectPath = parsed.cwd || dirName.replace(/^-/, "/").replace(/-/g, "/");
1312
1358
  sessions2.push({
@@ -1326,21 +1372,21 @@ async function listClaudeSessions(filterProjectPath) {
1326
1372
  }
1327
1373
 
1328
1374
  // src/claude-conversation.ts
1329
- var import_fs10 = __toESM(require("fs"));
1330
- var import_path11 = __toESM(require("path"));
1375
+ var import_fs11 = __toESM(require("fs"));
1376
+ var import_path12 = __toESM(require("path"));
1331
1377
  var import_os8 = __toESM(require("os"));
1332
1378
  var import_readline2 = __toESM(require("readline"));
1333
- var CLAUDE_PROJECTS_DIR2 = import_path11.default.join(import_os8.default.homedir(), ".claude", "projects");
1379
+ var CLAUDE_PROJECTS_DIR2 = import_path12.default.join(import_os8.default.homedir(), ".claude", "projects");
1334
1380
  function findProjectDir(projectPath) {
1335
1381
  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;
1382
+ const dirPath = import_path12.default.join(CLAUDE_PROJECTS_DIR2, encoded);
1383
+ if (import_fs11.default.existsSync(dirPath)) return dirPath;
1384
+ if (!import_fs11.default.existsSync(CLAUDE_PROJECTS_DIR2)) return null;
1339
1385
  let allDirs;
1340
1386
  try {
1341
- allDirs = import_fs10.default.readdirSync(CLAUDE_PROJECTS_DIR2).filter((d) => {
1387
+ allDirs = import_fs11.default.readdirSync(CLAUDE_PROJECTS_DIR2).filter((d) => {
1342
1388
  try {
1343
- return import_fs10.default.statSync(import_path11.default.join(CLAUDE_PROJECTS_DIR2, d)).isDirectory();
1389
+ return import_fs11.default.statSync(import_path12.default.join(CLAUDE_PROJECTS_DIR2, d)).isDirectory();
1344
1390
  } catch {
1345
1391
  return false;
1346
1392
  }
@@ -1350,23 +1396,23 @@ function findProjectDir(projectPath) {
1350
1396
  }
1351
1397
  const matches = findProjectDirs(allDirs, projectPath);
1352
1398
  if (matches.length > 0) {
1353
- return import_path11.default.join(CLAUDE_PROJECTS_DIR2, matches[0]);
1399
+ return import_path12.default.join(CLAUDE_PROJECTS_DIR2, matches[0]);
1354
1400
  }
1355
1401
  return null;
1356
1402
  }
1357
1403
  function findSessionFileById(sessionId) {
1358
1404
  if (!/^[A-Za-z0-9._-]+$/.test(sessionId)) return null;
1359
- if (!import_fs10.default.existsSync(CLAUDE_PROJECTS_DIR2)) return null;
1405
+ if (!import_fs11.default.existsSync(CLAUDE_PROJECTS_DIR2)) return null;
1360
1406
  let dirs;
1361
1407
  try {
1362
- dirs = import_fs10.default.readdirSync(CLAUDE_PROJECTS_DIR2);
1408
+ dirs = import_fs11.default.readdirSync(CLAUDE_PROJECTS_DIR2);
1363
1409
  } catch {
1364
1410
  return null;
1365
1411
  }
1366
1412
  for (const dir of dirs) {
1367
- const candidate = import_path11.default.join(CLAUDE_PROJECTS_DIR2, dir, `${sessionId}.jsonl`);
1413
+ const candidate = import_path12.default.join(CLAUDE_PROJECTS_DIR2, dir, `${sessionId}.jsonl`);
1368
1414
  try {
1369
- if (import_fs10.default.statSync(candidate).isFile()) return candidate;
1415
+ if (import_fs11.default.statSync(candidate).isFile()) return candidate;
1370
1416
  } catch {
1371
1417
  }
1372
1418
  }
@@ -1377,20 +1423,20 @@ function findLatestSessionFile(projectPath) {
1377
1423
  if (!dirPath) return null;
1378
1424
  let entries;
1379
1425
  try {
1380
- entries = import_fs10.default.readdirSync(dirPath);
1426
+ entries = import_fs11.default.readdirSync(dirPath);
1381
1427
  } catch {
1382
1428
  return null;
1383
1429
  }
1384
1430
  const files = entries.filter((f) => f.endsWith(".jsonl") && !f.includes("/")).map((f) => {
1385
1431
  try {
1386
- return { name: f, mtime: import_fs10.default.statSync(import_path11.default.join(dirPath, f)).mtimeMs };
1432
+ return { name: f, mtime: import_fs11.default.statSync(import_path12.default.join(dirPath, f)).mtimeMs };
1387
1433
  } catch {
1388
1434
  return null;
1389
1435
  }
1390
1436
  }).filter((f) => f !== null).sort((a, b) => b.mtime - a.mtime);
1391
1437
  if (files.length === 0) return null;
1392
1438
  return {
1393
- filePath: import_path11.default.join(dirPath, files[0].name),
1439
+ filePath: import_path12.default.join(dirPath, files[0].name),
1394
1440
  sessionId: files[0].name.replace(".jsonl", "")
1395
1441
  };
1396
1442
  }
@@ -1400,8 +1446,8 @@ async function readConversation(projectPath, afterLine = 0, specificSessionId) {
1400
1446
  if (specificSessionId) {
1401
1447
  sessionId = specificSessionId;
1402
1448
  const dirPath = findProjectDir(projectPath);
1403
- const direct = dirPath ? import_path11.default.join(dirPath, `${specificSessionId}.jsonl`) : null;
1404
- if (direct && import_fs10.default.existsSync(direct)) {
1449
+ const direct = dirPath ? import_path12.default.join(dirPath, `${specificSessionId}.jsonl`) : null;
1450
+ if (direct && import_fs11.default.existsSync(direct)) {
1405
1451
  filePath = direct;
1406
1452
  } else {
1407
1453
  const byId = findSessionFileById(specificSessionId);
@@ -1415,7 +1461,7 @@ async function readConversation(projectPath, afterLine = 0, specificSessionId) {
1415
1461
  sessionId = found.sessionId;
1416
1462
  }
1417
1463
  try {
1418
- const stream = import_fs10.default.createReadStream(filePath, { encoding: "utf-8" });
1464
+ const stream = import_fs11.default.createReadStream(filePath, { encoding: "utf-8" });
1419
1465
  const rl = import_readline2.default.createInterface({ input: stream, crlfDelay: Infinity });
1420
1466
  const messages = [];
1421
1467
  let lineNum = 0;
@@ -1504,6 +1550,13 @@ if (!isConfigured()) {
1504
1550
  }
1505
1551
  var config = loadConfig();
1506
1552
  logger.info({ agentId: config.agentId, serverUrl: config.serverUrl }, "Starting agent");
1553
+ var lock = acquireSingleInstanceLock();
1554
+ if (!lock.ok) {
1555
+ console.error(
1556
+ `Another crc-agent is already running on this machine (PID ${lock.pid}). Stop it first, or set CRC_ALLOW_MULTIPLE=1 to override.`
1557
+ );
1558
+ process.exit(1);
1559
+ }
1507
1560
  ensurePtyHelperExecutable();
1508
1561
  process.on("unhandledRejection", (reason) => {
1509
1562
  logger.error({ reason }, "Unhandled promise rejection (agent kept alive)");