cli-remote-agent 1.0.4 → 1.0.6

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
@@ -119,7 +119,18 @@ function decodeToken(token) {
119
119
  function ask(rl, question) {
120
120
  return new Promise((resolve) => rl.question(question, (answer) => resolve(answer)));
121
121
  }
122
+ function normalizeServerUrl(url) {
123
+ const fixed = url.trim().replace(/^(wss?|https?)\/\//i, (_m, proto) => `${proto.toLowerCase()}://`);
124
+ try {
125
+ const u = new URL(fixed);
126
+ if (!/^(wss?:|https?:)$/.test(u.protocol)) throw new Error("unsupported protocol");
127
+ } catch {
128
+ throw new Error(`Invalid server URL: "${url}" (expected e.g. wss://crc.example.com)`);
129
+ }
130
+ return fixed;
131
+ }
122
132
  function writeConfig(cfg) {
133
+ cfg.serverUrl = normalizeServerUrl(cfg.serverUrl);
123
134
  import_fs2.default.mkdirSync(CONFIG_DIR, { recursive: true });
124
135
  import_fs2.default.writeFileSync(CONFIG_FILE, JSON.stringify(cfg, null, 2));
125
136
  console.log(`Saved. Start the agent with: crc-agent`);
@@ -485,8 +496,27 @@ var init_claude_live = __esm({
485
496
  });
486
497
 
487
498
  // src/tmux.ts
499
+ function resolveTmuxPath() {
500
+ if (cachedTmuxPath !== void 0) return cachedTmuxPath;
501
+ const dirs = (process.env.PATH || "").split(import_path4.default.delimiter).filter(Boolean);
502
+ for (const extra of ["/opt/homebrew/bin", "/usr/local/bin", "/usr/bin", "/bin", "/opt/local/bin"]) {
503
+ if (!dirs.includes(extra)) dirs.push(extra);
504
+ }
505
+ for (const dir of dirs) {
506
+ const candidate = import_path4.default.join(dir, "tmux");
507
+ try {
508
+ import_fs5.default.accessSync(candidate, import_fs5.default.constants.X_OK);
509
+ cachedTmuxPath = candidate;
510
+ return candidate;
511
+ } catch {
512
+ }
513
+ }
514
+ cachedTmuxPath = null;
515
+ return null;
516
+ }
488
517
  function tmuxCmd(args) {
489
- return IS_WIN ? { file: "wsl.exe", args: ["tmux", ...args] } : { file: "tmux", args };
518
+ if (IS_WIN) return { file: "wsl.exe", args: ["tmux", ...args] };
519
+ return { file: resolveTmuxPath() || "tmux", args };
490
520
  }
491
521
  async function listTmuxSessions() {
492
522
  const fmt = "#{session_name} #{session_windows} #{session_attached} #{session_activity}";
@@ -525,8 +555,8 @@ async function enrichWithPanesAndClaude(sessions2) {
525
555
  const { file, args } = tmuxCmd(["list-panes", "-a", "-F", paneFmt]);
526
556
  const { stdout } = await execFileAsync2(file, args, { timeout: 6e3 });
527
557
  const panes = stdout.split("\n").map((line) => line.replace(/\r$/, "").trim()).filter(Boolean).map((line) => {
528
- const [session, pid, path8, activeFlags] = line.split(" ");
529
- return { session, pid: parseInt(pid, 10) || 0, path: path8 || "", active: activeFlags === "11" };
558
+ const [session, pid, path9, activeFlags] = line.split(" ");
559
+ return { session, pid: parseInt(pid, 10) || 0, path: path9 || "", active: activeFlags === "11" };
530
560
  });
531
561
  const byName = new Map(sessions2.map((s) => [s.name, s]));
532
562
  for (const pane of panes) {
@@ -548,26 +578,31 @@ async function enrichWithPanesAndClaude(sessions2) {
548
578
  } catch {
549
579
  }
550
580
  }
551
- function shq(v) {
552
- return `'${v.replace(/'/g, `'\\''`)}'`;
553
- }
554
- function buildTmuxLaunch(name, launch, shell) {
581
+ function buildTmuxLaunch(name, launch) {
555
582
  const trimmed = launch && launch.trim() ? launch.trim() : void 0;
556
583
  if (IS_WIN) {
557
- const args = ["tmux", "new-session", "-A", "-s", name];
558
- if (trimmed) args.push(trimmed);
559
- return { file: "wsl.exe", args };
560
- }
561
- let cmd2 = `tmux set-option -g aggressive-resize on 2>/dev/null; exec tmux new-session -A -s ${shq(name)}`;
562
- if (trimmed) cmd2 += ` ${shq(trimmed)}`;
563
- return { file: shell, args: ["-l", "-c", cmd2] };
584
+ const args2 = ["tmux", "new-session", "-A", "-s", name];
585
+ if (trimmed) args2.push(trimmed);
586
+ return { file: "wsl.exe", args: args2 };
587
+ }
588
+ const tmuxBin = resolveTmuxPath();
589
+ if (!tmuxBin) return null;
590
+ execFileAsync2(tmuxBin, ["set-option", "-g", "aggressive-resize", "on"], { timeout: 4e3 }).catch(
591
+ () => {
592
+ }
593
+ );
594
+ const args = ["new-session", "-A", "-s", name];
595
+ if (trimmed) args.push(trimmed);
596
+ return { file: tmuxBin, args };
564
597
  }
565
- var import_child_process2, import_util2, execFileAsync2, IS_WIN;
598
+ var import_child_process2, import_util2, import_fs5, import_path4, execFileAsync2, IS_WIN, cachedTmuxPath;
566
599
  var init_tmux = __esm({
567
600
  "src/tmux.ts"() {
568
601
  "use strict";
569
602
  import_child_process2 = require("child_process");
570
603
  import_util2 = require("util");
604
+ import_fs5 = __toESM(require("fs"));
605
+ import_path4 = __toESM(require("path"));
571
606
  init_claude_live();
572
607
  execFileAsync2 = (0, import_util2.promisify)(import_child_process2.execFile);
573
608
  IS_WIN = process.platform === "win32";
@@ -597,15 +632,15 @@ var init_shell = __esm({
597
632
 
598
633
  // src/pty-session.ts
599
634
  function ensureInitFiles() {
600
- const versionFile = (0, import_path4.join)(INIT_DIR, ".version");
601
- if ((0, import_fs5.existsSync)(versionFile)) {
635
+ const versionFile = (0, import_path5.join)(INIT_DIR, ".version");
636
+ if ((0, import_fs6.existsSync)(versionFile)) {
602
637
  try {
603
- if ((0, import_fs5.readFileSync)(versionFile, "utf-8").trim() === INIT_VERSION) return;
638
+ if ((0, import_fs6.readFileSync)(versionFile, "utf-8").trim() === INIT_VERSION) return;
604
639
  } catch {
605
640
  }
606
641
  }
607
- (0, import_fs5.mkdirSync)(ZSH_DIR, { recursive: true });
608
- (0, import_fs5.writeFileSync)(BASH_INIT, [
642
+ (0, import_fs6.mkdirSync)(ZSH_DIR, { recursive: true });
643
+ (0, import_fs6.writeFileSync)(BASH_INIT, [
609
644
  "[ -f ~/.bash_profile ] && source ~/.bash_profile",
610
645
  "[ -f ~/.bashrc ] && source ~/.bashrc",
611
646
  "__crc_s=$SECONDS",
@@ -616,11 +651,11 @@ function ensureInitFiles() {
616
651
  `PROMPT_COMMAND='[ "$__crc_armed" = 1 ] && [ \${__crc_elapsed:-0} -ge ${THRESHOLD} ] && printf "\\a"; __crc_armed=1; eval "$__crc_orig_pc"'`,
617
652
  ""
618
653
  ].join("\n"), "utf-8");
619
- (0, import_fs5.writeFileSync)(ZSH_ENV, [
654
+ (0, import_fs6.writeFileSync)(ZSH_ENV, [
620
655
  '[ -f "${ZDOTDIR_ORIG:-$HOME}/.zshenv" ] && source "${ZDOTDIR_ORIG:-$HOME}/.zshenv"',
621
656
  ""
622
657
  ].join("\n"), "utf-8");
623
- (0, import_fs5.writeFileSync)(ZSH_RC, [
658
+ (0, import_fs6.writeFileSync)(ZSH_RC, [
624
659
  '[ -f "${ZDOTDIR_ORIG:-$HOME}/.zshrc" ] && source "${ZDOTDIR_ORIG:-$HOME}/.zshrc"',
625
660
  "__crc_s=$SECONDS",
626
661
  "__crc_armed=0",
@@ -633,36 +668,36 @@ function ensureInitFiles() {
633
668
  "fi",
634
669
  ""
635
670
  ].join("\n"), "utf-8");
636
- (0, import_fs5.writeFileSync)(versionFile, INIT_VERSION, "utf-8");
671
+ (0, import_fs6.writeFileSync)(versionFile, INIT_VERSION, "utf-8");
637
672
  }
638
673
  function resolveCwd(cwd) {
639
674
  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];
640
675
  for (const c of candidates) {
641
676
  if (c) {
642
677
  try {
643
- if ((0, import_fs5.existsSync)(c)) return c;
678
+ if ((0, import_fs6.existsSync)(c)) return c;
644
679
  } catch {
645
680
  }
646
681
  }
647
682
  }
648
683
  return (0, import_os3.homedir)();
649
684
  }
650
- var pty, import_fs5, import_path4, import_os3, THRESHOLD, INIT_DIR, BASH_INIT, ZSH_DIR, ZSH_RC, ZSH_ENV, INIT_VERSION, PtySession;
685
+ var pty, import_fs6, import_path5, import_os3, THRESHOLD, INIT_DIR, BASH_INIT, ZSH_DIR, ZSH_RC, ZSH_ENV, INIT_VERSION, PtySession;
651
686
  var init_pty_session = __esm({
652
687
  "src/pty-session.ts"() {
653
688
  "use strict";
654
689
  pty = __toESM(require("node-pty"));
655
- import_fs5 = require("fs");
656
- import_path4 = require("path");
690
+ import_fs6 = require("fs");
691
+ import_path5 = require("path");
657
692
  import_os3 = require("os");
658
693
  init_src();
659
694
  init_shell();
660
695
  THRESHOLD = 3;
661
- INIT_DIR = (0, import_path4.join)((0, import_os3.tmpdir)(), "crc-shell-init");
662
- BASH_INIT = (0, import_path4.join)(INIT_DIR, "bashrc");
663
- ZSH_DIR = (0, import_path4.join)(INIT_DIR, "zsh");
664
- ZSH_RC = (0, import_path4.join)(ZSH_DIR, ".zshrc");
665
- ZSH_ENV = (0, import_path4.join)(ZSH_DIR, ".zshenv");
696
+ INIT_DIR = (0, import_path5.join)((0, import_os3.tmpdir)(), "crc-shell-init");
697
+ BASH_INIT = (0, import_path5.join)(INIT_DIR, "bashrc");
698
+ ZSH_DIR = (0, import_path5.join)(INIT_DIR, "zsh");
699
+ ZSH_RC = (0, import_path5.join)(ZSH_DIR, ".zshrc");
700
+ ZSH_ENV = (0, import_path5.join)(ZSH_DIR, ".zshenv");
666
701
  INIT_VERSION = "2";
667
702
  PtySession = class {
668
703
  id;
@@ -892,18 +927,18 @@ function buildHeartbeat(homeDir) {
892
927
  memoryUsage: Math.round((totalMem - freeMem) / totalMem * 100),
893
928
  uptime: Math.round(import_os4.default.uptime()),
894
929
  activeSessions: getActiveSessionCount(),
895
- pathSeparator: import_path5.default.sep,
930
+ pathSeparator: import_path6.default.sep,
896
931
  homeDirectory: homeDir || import_os4.default.homedir(),
897
932
  rootPaths: getRootPaths(),
898
933
  capabilities: { terminal: true, fileTransfer: false }
899
934
  };
900
935
  }
901
- var import_os4, import_path5, import_child_process4, prevCpu;
936
+ var import_os4, import_path6, import_child_process4, prevCpu;
902
937
  var init_heartbeat = __esm({
903
938
  "src/heartbeat.ts"() {
904
939
  "use strict";
905
940
  import_os4 = __toESM(require("os"));
906
- import_path5 = __toESM(require("path"));
941
+ import_path6 = __toESM(require("path"));
907
942
  import_child_process4 = require("child_process");
908
943
  init_terminal_manager();
909
944
  prevCpu = null;
@@ -912,28 +947,28 @@ var init_heartbeat = __esm({
912
947
 
913
948
  // src/file-explorer.ts
914
949
  function isInSecretDir(resolved) {
915
- const rel = import_path6.default.relative(SECRET_DIR, resolved);
916
- return rel === "" || !rel.startsWith(".." + import_path6.default.sep) && rel !== ".." && !import_path6.default.isAbsolute(rel);
950
+ const rel = import_path7.default.relative(SECRET_DIR, resolved);
951
+ return rel === "" || !rel.startsWith(".." + import_path7.default.sep) && rel !== ".." && !import_path7.default.isAbsolute(rel);
917
952
  }
918
953
  function listDirectory(dirPath) {
919
954
  try {
920
- const resolved = import_path6.default.resolve(dirPath);
955
+ const resolved = import_path7.default.resolve(dirPath);
921
956
  if (isInSecretDir(resolved)) {
922
957
  return { entries: [], error: "Access denied" };
923
958
  }
924
- if (!import_fs6.default.existsSync(resolved)) {
959
+ if (!import_fs7.default.existsSync(resolved)) {
925
960
  return { entries: [], error: "Path does not exist" };
926
961
  }
927
- const stat = import_fs6.default.statSync(resolved);
962
+ const stat = import_fs7.default.statSync(resolved);
928
963
  if (!stat.isDirectory()) {
929
964
  return { entries: [], error: "Not a directory" };
930
965
  }
931
- const dirents = import_fs6.default.readdirSync(resolved, { withFileTypes: true });
966
+ const dirents = import_fs7.default.readdirSync(resolved, { withFileTypes: true });
932
967
  const entries = [];
933
968
  for (const dirent of dirents) {
934
969
  try {
935
- const fullPath = import_path6.default.join(resolved, dirent.name);
936
- const s = import_fs6.default.statSync(fullPath);
970
+ const fullPath = import_path7.default.join(resolved, dirent.name);
971
+ const s = import_fs7.default.statSync(fullPath);
937
972
  entries.push({
938
973
  name: dirent.name,
939
974
  isDirectory: dirent.isDirectory(),
@@ -955,22 +990,22 @@ function listDirectory(dirPath) {
955
990
  }
956
991
  async function downloadFile(filePath, serverUrl, secret, agentId) {
957
992
  try {
958
- const resolved = import_path6.default.resolve(filePath);
993
+ const resolved = import_path7.default.resolve(filePath);
959
994
  if (isInSecretDir(resolved)) {
960
995
  return { error: "Access denied" };
961
996
  }
962
- if (!import_fs6.default.existsSync(resolved)) {
997
+ if (!import_fs7.default.existsSync(resolved)) {
963
998
  return { error: "File does not exist" };
964
999
  }
965
- const stat = import_fs6.default.statSync(resolved);
1000
+ const stat = import_fs7.default.statSync(resolved);
966
1001
  if (stat.isDirectory()) {
967
1002
  return { error: "Cannot download a directory" };
968
1003
  }
969
1004
  if (stat.size > FILE_MAX_SIZE) {
970
1005
  return { error: `File too large (max ${FILE_MAX_SIZE} bytes)` };
971
1006
  }
972
- const fileName = import_path6.default.basename(resolved);
973
- const fileStream = import_fs6.default.createReadStream(resolved);
1007
+ const fileName = import_path7.default.basename(resolved);
1008
+ const fileStream = import_fs7.default.createReadStream(resolved);
974
1009
  const baseUrl = serverUrl.replace("wss://", "https://").replace("ws://", "http://");
975
1010
  const url = `${baseUrl}/api/files/receive`;
976
1011
  const res = await fetch(url, {
@@ -999,21 +1034,21 @@ async function downloadFile(filePath, serverUrl, secret, agentId) {
999
1034
  return { error: err.message };
1000
1035
  }
1001
1036
  }
1002
- var import_fs6, import_path6, import_os5, SECRET_DIR;
1037
+ var import_fs7, import_path7, import_os5, SECRET_DIR;
1003
1038
  var init_file_explorer = __esm({
1004
1039
  "src/file-explorer.ts"() {
1005
1040
  "use strict";
1006
- import_fs6 = __toESM(require("fs"));
1007
- import_path6 = __toESM(require("path"));
1041
+ import_fs7 = __toESM(require("fs"));
1042
+ import_path7 = __toESM(require("path"));
1008
1043
  import_os5 = __toESM(require("os"));
1009
1044
  init_logger();
1010
1045
  init_src();
1011
- SECRET_DIR = import_path6.default.join(import_os5.default.homedir(), ".crc-agent");
1046
+ SECRET_DIR = import_path7.default.join(import_os5.default.homedir(), ".crc-agent");
1012
1047
  }
1013
1048
  });
1014
1049
 
1015
1050
  // src/vpn-manager.ts
1016
- function shq2(value) {
1051
+ function shq(value) {
1017
1052
  return value.replace(/'/g, "'\\''");
1018
1053
  }
1019
1054
  function psq(value) {
@@ -1031,11 +1066,11 @@ async function runCmd(cmd2, shell) {
1031
1066
  }
1032
1067
  function getConfigPath(profile) {
1033
1068
  const file = profile.configFile || `${profile.id}.conf`;
1034
- if (import_path7.default.isAbsolute(file)) return file;
1035
- return import_path7.default.join(VPN_DIR, file);
1069
+ if (import_path8.default.isAbsolute(file)) return file;
1070
+ return import_path8.default.join(VPN_DIR, file);
1036
1071
  }
1037
1072
  async function getScutilStatus(serviceName) {
1038
- const { stdout } = await runCmd(`scutil --nc status '${shq2(serviceName)}'`);
1073
+ const { stdout } = await runCmd(`scutil --nc status '${shq(serviceName)}'`);
1039
1074
  const firstLine = stdout.trim().split("\n")[0];
1040
1075
  if (firstLine === "Connected") return "connected";
1041
1076
  if (firstLine === "Connecting") return "connecting";
@@ -1043,11 +1078,11 @@ async function getScutilStatus(serviceName) {
1043
1078
  return "disconnected";
1044
1079
  }
1045
1080
  async function scutilStart(serviceName) {
1046
- const { stderr } = await runCmd(`scutil --nc start '${shq2(serviceName)}'`);
1081
+ const { stderr } = await runCmd(`scutil --nc start '${shq(serviceName)}'`);
1047
1082
  return stderr || null;
1048
1083
  }
1049
1084
  async function scutilStop(serviceName) {
1050
- const { stderr } = await runCmd(`scutil --nc stop '${shq2(serviceName)}'`);
1085
+ const { stderr } = await runCmd(`scutil --nc stop '${shq(serviceName)}'`);
1051
1086
  return stderr || null;
1052
1087
  }
1053
1088
  async function runOsascript(script) {
@@ -1096,7 +1131,7 @@ async function getWireGuardStatus(profile) {
1096
1131
  return getScutilStatus(profile.serviceName);
1097
1132
  }
1098
1133
  const tunnelName = profile.tunnelName || profile.id;
1099
- const { stdout } = await runCmd(`wg show '${shq2(tunnelName)}' 2>/dev/null`);
1134
+ const { stdout } = await runCmd(`wg show '${shq(tunnelName)}' 2>/dev/null`);
1100
1135
  return stdout.trim() ? "connected" : "disconnected";
1101
1136
  }
1102
1137
  async function getOpenVpnStatus(profile) {
@@ -1148,7 +1183,7 @@ async function connectWireGuard(profile) {
1148
1183
  const tunnelName = profile.tunnelName || profile.id;
1149
1184
  if (isWindows) {
1150
1185
  const confPath2 = getConfigPath(profile);
1151
- if (!import_fs7.default.existsSync(confPath2)) {
1186
+ if (!import_fs8.default.existsSync(confPath2)) {
1152
1187
  return `Config file not found: ${confPath2}`;
1153
1188
  }
1154
1189
  const script = `
@@ -1168,13 +1203,13 @@ async function connectWireGuard(profile) {
1168
1203
  return scutilStart(profile.serviceName);
1169
1204
  }
1170
1205
  const confPath = getConfigPath(profile);
1171
- const { stderr } = await runCmd(`sudo wg-quick up '${shq2(confPath)}'`);
1206
+ const { stderr } = await runCmd(`sudo wg-quick up '${shq(confPath)}'`);
1172
1207
  return stderr || null;
1173
1208
  }
1174
1209
  async function connectOpenVpn(profile) {
1175
1210
  if (isWindows) {
1176
1211
  const confPath2 = getConfigPath(profile);
1177
- if (!import_fs7.default.existsSync(confPath2)) {
1212
+ if (!import_fs8.default.existsSync(confPath2)) {
1178
1213
  return `Config file not found: ${confPath2}`;
1179
1214
  }
1180
1215
  const connector = "C:\\Program Files\\OpenVPN Connect\\ovpnconnector.exe";
@@ -1201,16 +1236,16 @@ async function connectOpenVpn(profile) {
1201
1236
  return scutilStart(profile.serviceName);
1202
1237
  }
1203
1238
  const confPath = getConfigPath(profile);
1204
- if (!import_fs7.default.existsSync(confPath)) {
1239
+ if (!import_fs8.default.existsSync(confPath)) {
1205
1240
  return `Config file not found: ${confPath}`;
1206
1241
  }
1207
- const { stderr } = await runCmd(`sudo openvpn --config '${shq2(confPath)}' --daemon`);
1242
+ const { stderr } = await runCmd(`sudo openvpn --config '${shq(confPath)}' --daemon`);
1208
1243
  return stderr || null;
1209
1244
  }
1210
1245
  async function connectAzure(profile) {
1211
1246
  if (isWindows) {
1212
1247
  const confPath = getConfigPath(profile);
1213
- if (!import_fs7.default.existsSync(confPath)) {
1248
+ if (!import_fs8.default.existsSync(confPath)) {
1214
1249
  return `Config file not found: ${confPath}`;
1215
1250
  }
1216
1251
  await runCmd(`& 'AzureVpn.exe' -i '${psq(confPath)}' 2>&1`);
@@ -1254,7 +1289,7 @@ async function disconnectWireGuard(profile) {
1254
1289
  return scutilStop(profile.serviceName);
1255
1290
  }
1256
1291
  const confPath = getConfigPath(profile);
1257
- const { stderr } = await runCmd(`sudo wg-quick down '${shq2(confPath)}'`);
1292
+ const { stderr } = await runCmd(`sudo wg-quick down '${shq(confPath)}'`);
1258
1293
  return stderr || null;
1259
1294
  }
1260
1295
  async function disconnectOpenVpn(profile) {
@@ -1330,19 +1365,19 @@ async function disconnectVpn(configs, profileId) {
1330
1365
  }
1331
1366
  return profiles;
1332
1367
  }
1333
- var import_child_process5, import_util3, import_fs7, import_path7, import_os6, execAsync, isWindows, VPN_DIR;
1368
+ var import_child_process5, import_util3, import_fs8, import_path8, import_os6, execAsync, isWindows, VPN_DIR;
1334
1369
  var init_vpn_manager = __esm({
1335
1370
  "src/vpn-manager.ts"() {
1336
1371
  "use strict";
1337
1372
  import_child_process5 = require("child_process");
1338
1373
  import_util3 = require("util");
1339
- import_fs7 = __toESM(require("fs"));
1340
- import_path7 = __toESM(require("path"));
1374
+ import_fs8 = __toESM(require("fs"));
1375
+ import_path8 = __toESM(require("path"));
1341
1376
  import_os6 = __toESM(require("os"));
1342
1377
  init_logger();
1343
1378
  execAsync = (0, import_util3.promisify)(import_child_process5.exec);
1344
1379
  isWindows = process.platform === "win32";
1345
- VPN_DIR = import_path7.default.join(import_os6.default.homedir(), ".crc-agent", "vpn");
1380
+ VPN_DIR = import_path8.default.join(import_os6.default.homedir(), ".crc-agent", "vpn");
1346
1381
  }
1347
1382
  });
1348
1383
 
@@ -1377,7 +1412,7 @@ var init_claude_path = __esm({
1377
1412
  // src/claude-sessions.ts
1378
1413
  async function parseSessionFile(filePath) {
1379
1414
  try {
1380
- const stream = import_fs8.default.createReadStream(filePath, { encoding: "utf-8" });
1415
+ const stream = import_fs9.default.createReadStream(filePath, { encoding: "utf-8" });
1381
1416
  const rl = import_readline2.default.createInterface({ input: stream, crlfDelay: Infinity });
1382
1417
  let firstMessage = "";
1383
1418
  let lastTimestamp = "";
@@ -1425,17 +1460,17 @@ async function parseSessionFile(filePath) {
1425
1460
  }
1426
1461
  async function listClaudeSessions(filterProjectPath) {
1427
1462
  const sessions2 = [];
1428
- if (!import_fs8.default.existsSync(CLAUDE_PROJECTS_DIR)) return sessions2;
1463
+ if (!import_fs9.default.existsSync(CLAUDE_PROJECTS_DIR)) return sessions2;
1429
1464
  const isDirSafe = (d) => {
1430
1465
  try {
1431
- return import_fs8.default.statSync(import_path8.default.join(CLAUDE_PROJECTS_DIR, d)).isDirectory();
1466
+ return import_fs9.default.statSync(import_path9.default.join(CLAUDE_PROJECTS_DIR, d)).isDirectory();
1432
1467
  } catch {
1433
1468
  return false;
1434
1469
  }
1435
1470
  };
1436
1471
  let allEntries;
1437
1472
  try {
1438
- allEntries = import_fs8.default.readdirSync(CLAUDE_PROJECTS_DIR);
1473
+ allEntries = import_fs9.default.readdirSync(CLAUDE_PROJECTS_DIR);
1439
1474
  } catch {
1440
1475
  return sessions2;
1441
1476
  }
@@ -1448,16 +1483,16 @@ async function listClaudeSessions(filterProjectPath) {
1448
1483
  projectDirs = allEntries.filter((d) => isDirSafe(d) && !d.startsWith("-private-"));
1449
1484
  }
1450
1485
  for (const dirName of projectDirs) {
1451
- const dirPath = import_path8.default.join(CLAUDE_PROJECTS_DIR, dirName);
1486
+ const dirPath = import_path9.default.join(CLAUDE_PROJECTS_DIR, dirName);
1452
1487
  let files;
1453
1488
  try {
1454
- files = import_fs8.default.readdirSync(dirPath).filter((f) => f.endsWith(".jsonl"));
1489
+ files = import_fs9.default.readdirSync(dirPath).filter((f) => f.endsWith(".jsonl"));
1455
1490
  } catch {
1456
1491
  continue;
1457
1492
  }
1458
1493
  for (const file of files) {
1459
1494
  const sessionId = file.replace(".jsonl", "");
1460
- const parsed = await parseSessionFile(import_path8.default.join(dirPath, file));
1495
+ const parsed = await parseSessionFile(import_path9.default.join(dirPath, file));
1461
1496
  if (!parsed) continue;
1462
1497
  const projectPath = parsed.cwd || dirName.replace(/^-/, "/").replace(/-/g, "/");
1463
1498
  sessions2.push({
@@ -1475,31 +1510,31 @@ async function listClaudeSessions(filterProjectPath) {
1475
1510
  sessions2.sort((a, b) => b.lastTimestamp > a.lastTimestamp ? 1 : -1);
1476
1511
  return sessions2;
1477
1512
  }
1478
- var import_fs8, import_path8, import_os7, import_readline2, CLAUDE_PROJECTS_DIR;
1513
+ var import_fs9, import_path9, import_os7, import_readline2, CLAUDE_PROJECTS_DIR;
1479
1514
  var init_claude_sessions = __esm({
1480
1515
  "src/claude-sessions.ts"() {
1481
1516
  "use strict";
1482
- import_fs8 = __toESM(require("fs"));
1483
- import_path8 = __toESM(require("path"));
1517
+ import_fs9 = __toESM(require("fs"));
1518
+ import_path9 = __toESM(require("path"));
1484
1519
  import_os7 = __toESM(require("os"));
1485
1520
  import_readline2 = __toESM(require("readline"));
1486
1521
  init_logger();
1487
1522
  init_claude_path();
1488
- CLAUDE_PROJECTS_DIR = import_path8.default.join(import_os7.default.homedir(), ".claude", "projects");
1523
+ CLAUDE_PROJECTS_DIR = import_path9.default.join(import_os7.default.homedir(), ".claude", "projects");
1489
1524
  }
1490
1525
  });
1491
1526
 
1492
1527
  // src/claude-conversation.ts
1493
1528
  function findProjectDir(projectPath) {
1494
1529
  const encoded = encodeProjectPath(projectPath);
1495
- const dirPath = import_path9.default.join(CLAUDE_PROJECTS_DIR2, encoded);
1496
- if (import_fs9.default.existsSync(dirPath)) return dirPath;
1497
- if (!import_fs9.default.existsSync(CLAUDE_PROJECTS_DIR2)) return null;
1530
+ const dirPath = import_path10.default.join(CLAUDE_PROJECTS_DIR2, encoded);
1531
+ if (import_fs10.default.existsSync(dirPath)) return dirPath;
1532
+ if (!import_fs10.default.existsSync(CLAUDE_PROJECTS_DIR2)) return null;
1498
1533
  let allDirs;
1499
1534
  try {
1500
- allDirs = import_fs9.default.readdirSync(CLAUDE_PROJECTS_DIR2).filter((d) => {
1535
+ allDirs = import_fs10.default.readdirSync(CLAUDE_PROJECTS_DIR2).filter((d) => {
1501
1536
  try {
1502
- return import_fs9.default.statSync(import_path9.default.join(CLAUDE_PROJECTS_DIR2, d)).isDirectory();
1537
+ return import_fs10.default.statSync(import_path10.default.join(CLAUDE_PROJECTS_DIR2, d)).isDirectory();
1503
1538
  } catch {
1504
1539
  return false;
1505
1540
  }
@@ -1509,23 +1544,23 @@ function findProjectDir(projectPath) {
1509
1544
  }
1510
1545
  const matches = findProjectDirs(allDirs, projectPath);
1511
1546
  if (matches.length > 0) {
1512
- return import_path9.default.join(CLAUDE_PROJECTS_DIR2, matches[0]);
1547
+ return import_path10.default.join(CLAUDE_PROJECTS_DIR2, matches[0]);
1513
1548
  }
1514
1549
  return null;
1515
1550
  }
1516
1551
  function findSessionFileById(sessionId) {
1517
1552
  if (!/^[A-Za-z0-9._-]+$/.test(sessionId)) return null;
1518
- if (!import_fs9.default.existsSync(CLAUDE_PROJECTS_DIR2)) return null;
1553
+ if (!import_fs10.default.existsSync(CLAUDE_PROJECTS_DIR2)) return null;
1519
1554
  let dirs;
1520
1555
  try {
1521
- dirs = import_fs9.default.readdirSync(CLAUDE_PROJECTS_DIR2);
1556
+ dirs = import_fs10.default.readdirSync(CLAUDE_PROJECTS_DIR2);
1522
1557
  } catch {
1523
1558
  return null;
1524
1559
  }
1525
1560
  for (const dir of dirs) {
1526
- const candidate = import_path9.default.join(CLAUDE_PROJECTS_DIR2, dir, `${sessionId}.jsonl`);
1561
+ const candidate = import_path10.default.join(CLAUDE_PROJECTS_DIR2, dir, `${sessionId}.jsonl`);
1527
1562
  try {
1528
- if (import_fs9.default.statSync(candidate).isFile()) return candidate;
1563
+ if (import_fs10.default.statSync(candidate).isFile()) return candidate;
1529
1564
  } catch {
1530
1565
  }
1531
1566
  }
@@ -1536,20 +1571,20 @@ function findLatestSessionFile(projectPath) {
1536
1571
  if (!dirPath) return null;
1537
1572
  let entries;
1538
1573
  try {
1539
- entries = import_fs9.default.readdirSync(dirPath);
1574
+ entries = import_fs10.default.readdirSync(dirPath);
1540
1575
  } catch {
1541
1576
  return null;
1542
1577
  }
1543
1578
  const files = entries.filter((f) => f.endsWith(".jsonl") && !f.includes("/")).map((f) => {
1544
1579
  try {
1545
- return { name: f, mtime: import_fs9.default.statSync(import_path9.default.join(dirPath, f)).mtimeMs };
1580
+ return { name: f, mtime: import_fs10.default.statSync(import_path10.default.join(dirPath, f)).mtimeMs };
1546
1581
  } catch {
1547
1582
  return null;
1548
1583
  }
1549
1584
  }).filter((f) => f !== null).sort((a, b) => b.mtime - a.mtime);
1550
1585
  if (files.length === 0) return null;
1551
1586
  return {
1552
- filePath: import_path9.default.join(dirPath, files[0].name),
1587
+ filePath: import_path10.default.join(dirPath, files[0].name),
1553
1588
  sessionId: files[0].name.replace(".jsonl", "")
1554
1589
  };
1555
1590
  }
@@ -1559,8 +1594,8 @@ async function readConversation(projectPath, afterLine = 0, specificSessionId) {
1559
1594
  if (specificSessionId) {
1560
1595
  sessionId = specificSessionId;
1561
1596
  const dirPath = findProjectDir(projectPath);
1562
- const direct = dirPath ? import_path9.default.join(dirPath, `${specificSessionId}.jsonl`) : null;
1563
- if (direct && import_fs9.default.existsSync(direct)) {
1597
+ const direct = dirPath ? import_path10.default.join(dirPath, `${specificSessionId}.jsonl`) : null;
1598
+ if (direct && import_fs10.default.existsSync(direct)) {
1564
1599
  filePath = direct;
1565
1600
  } else {
1566
1601
  const byId = findSessionFileById(specificSessionId);
@@ -1574,7 +1609,7 @@ async function readConversation(projectPath, afterLine = 0, specificSessionId) {
1574
1609
  sessionId = found.sessionId;
1575
1610
  }
1576
1611
  try {
1577
- const stream = import_fs9.default.createReadStream(filePath, { encoding: "utf-8" });
1612
+ const stream = import_fs10.default.createReadStream(filePath, { encoding: "utf-8" });
1578
1613
  const rl = import_readline3.default.createInterface({ input: stream, crlfDelay: Infinity });
1579
1614
  const messages = [];
1580
1615
  let lineNum = 0;
@@ -1655,17 +1690,17 @@ async function readConversation(projectPath, afterLine = 0, specificSessionId) {
1655
1690
  return null;
1656
1691
  }
1657
1692
  }
1658
- var import_fs9, import_path9, import_os8, import_readline3, CLAUDE_PROJECTS_DIR2;
1693
+ var import_fs10, import_path10, import_os8, import_readline3, CLAUDE_PROJECTS_DIR2;
1659
1694
  var init_claude_conversation = __esm({
1660
1695
  "src/claude-conversation.ts"() {
1661
1696
  "use strict";
1662
- import_fs9 = __toESM(require("fs"));
1663
- import_path9 = __toESM(require("path"));
1697
+ import_fs10 = __toESM(require("fs"));
1698
+ import_path10 = __toESM(require("path"));
1664
1699
  import_os8 = __toESM(require("os"));
1665
1700
  import_readline3 = __toESM(require("readline"));
1666
1701
  init_logger();
1667
1702
  init_claude_path();
1668
- CLAUDE_PROJECTS_DIR2 = import_path9.default.join(import_os8.default.homedir(), ".claude", "projects");
1703
+ CLAUDE_PROJECTS_DIR2 = import_path10.default.join(import_os8.default.homedir(), ".claude", "projects");
1669
1704
  }
1670
1705
  });
1671
1706
 
@@ -1682,7 +1717,6 @@ var init_index = __esm({
1682
1717
  init_claude_plugin_installer();
1683
1718
  init_local_control();
1684
1719
  init_tmux();
1685
- init_shell();
1686
1720
  init_heartbeat();
1687
1721
  init_file_explorer();
1688
1722
  init_vpn_manager();
@@ -1754,7 +1788,18 @@ var init_index = __esm({
1754
1788
  socket.on(TERMINAL_OPEN, (payload) => {
1755
1789
  const { sessionId, cols, rows, tmux, launch } = payload;
1756
1790
  if (!sessionId) return;
1757
- const launchSpec = tmux ? buildTmuxLaunch(tmux, launch, detectShell(config.shell)) : void 0;
1791
+ let launchSpec;
1792
+ if (tmux) {
1793
+ const spec = buildTmuxLaunch(tmux, launch);
1794
+ if (!spec) {
1795
+ const msg = "\r\n\x1B[31mtmux is not installed on this machine.\x1B[0m\r\nInstall it to use tmux mirroring:\r\n macOS: brew install tmux\r\n Linux: sudo apt install tmux (or your package manager)\r\n\r\n";
1796
+ socket.emit(TERMINAL_OUTPUT, { sessionId, data: msg });
1797
+ socket.emit(TERMINAL_EXIT, { sessionId, exitCode: 1 });
1798
+ logger.warn({ sessionId, tmux }, "tmux launch requested but tmux not found");
1799
+ return;
1800
+ }
1801
+ launchSpec = spec;
1802
+ }
1758
1803
  createTerminalSession(
1759
1804
  sessionId,
1760
1805
  cols,