@akms/mcp-ssh 0.1.0 → 0.1.1

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/lib/cli.cjs CHANGED
@@ -2,8 +2,8 @@
2
2
  'use strict';
3
3
 
4
4
  var stdio_js = require('@modelcontextprotocol/sdk/server/stdio.js');
5
- var fs7 = require('fs');
6
- var os4 = require('os');
5
+ var fs6 = require('fs');
6
+ var os5 = require('os');
7
7
  var path = require('path');
8
8
  var child_process = require('child_process');
9
9
  var crypto = require('crypto');
@@ -16,8 +16,8 @@ var zod = require('zod');
16
16
 
17
17
  function _interopDefault (e) { return e && e.__esModule ? e : { default: e }; }
18
18
 
19
- var fs7__default = /*#__PURE__*/_interopDefault(fs7);
20
- var os4__default = /*#__PURE__*/_interopDefault(os4);
19
+ var fs6__default = /*#__PURE__*/_interopDefault(fs6);
20
+ var os5__default = /*#__PURE__*/_interopDefault(os5);
21
21
  var path__default = /*#__PURE__*/_interopDefault(path);
22
22
  var ssh2__default = /*#__PURE__*/_interopDefault(ssh2);
23
23
 
@@ -35,7 +35,7 @@ var CWD_MARKER = "__AKMS_SSH_CWD__";
35
35
  var MAX_DIRECTORY_ENTRIES = 1e3;
36
36
  var SERVER_NAME = "akms-mcp-ssh";
37
37
  var PACKAGE_NAME = "@akms/mcp-ssh";
38
- var SERVER_VERSION = "0.1.0";
38
+ var SERVER_VERSION = "0.1.1";
39
39
 
40
40
  // src/_defs/command-rules.ts
41
41
  var CRITICAL_PATH_PATTERN = new RegExp(
@@ -610,7 +610,7 @@ function findExecutable(name) {
610
610
  for (const directory of directories) {
611
611
  for (const suffix of suffixes) {
612
612
  const candidate = path__default.default.join(directory, `${name}${suffix}`);
613
- if (fs7__default.default.existsSync(candidate) == true && fs7__default.default.statSync(candidate).isFile() == true) {
613
+ if (fs6__default.default.existsSync(candidate) == true && fs6__default.default.statSync(candidate).isFile() == true) {
614
614
  return candidate;
615
615
  }
616
616
  }
@@ -620,9 +620,9 @@ function findExecutable(name) {
620
620
  function expandHomePath(inputPath) {
621
621
  let expanded = inputPath;
622
622
  if (inputPath === "~") {
623
- expanded = os4__default.default.homedir();
623
+ expanded = os5__default.default.homedir();
624
624
  } else if (inputPath.startsWith("~/") == true || inputPath.startsWith("~\\") == true) {
625
- expanded = path__default.default.join(os4__default.default.homedir(), inputPath.slice(2));
625
+ expanded = path__default.default.join(os5__default.default.homedir(), inputPath.slice(2));
626
626
  }
627
627
  return path__default.default.resolve(expanded);
628
628
  }
@@ -751,7 +751,7 @@ function loadHostProfile(env = process.env) {
751
751
  if (keyPath != null) {
752
752
  const expandedKeyPath = expandHomePath(keyPath);
753
753
  profile.privateKeyPath = expandedKeyPath;
754
- if (fs7__default.default.existsSync(expandedKeyPath) == false) {
754
+ if (fs6__default.default.existsSync(expandedKeyPath) == false) {
755
755
  _logger.warn(`${SINGLE_HOST_ENV.KEY} does not exist; connections will fail`, {
756
756
  private_key_path: expandedKeyPath
757
757
  });
@@ -783,7 +783,7 @@ function loadHostProfile(env = process.env) {
783
783
  if (jumpKeyPath != null) {
784
784
  profile.jump.privateKeyPath = expandHomePath(jumpKeyPath);
785
785
  profile.jump.passphrase = readEnv(env, SINGLE_HOST_ENV.JUMP_PASSPHRASE);
786
- if (fs7__default.default.existsSync(profile.jump.privateKeyPath) == false) {
786
+ if (fs6__default.default.existsSync(profile.jump.privateKeyPath) == false) {
787
787
  _logger.warn(`${SINGLE_HOST_ENV.JUMP_KEY} does not exist; connections will fail`, {
788
788
  private_key_path: profile.jump.privateKeyPath
789
789
  });
@@ -1269,14 +1269,14 @@ function inspectRemotePath(remotePath, policy, access) {
1269
1269
  function toRealPath(inputPath) {
1270
1270
  const _logger = $logger.child({ context: "toRealPath" });
1271
1271
  try {
1272
- return fs7__default.default.realpathSync.native(inputPath);
1272
+ return fs6__default.default.realpathSync.native(inputPath);
1273
1273
  } catch (ex) {
1274
1274
  _logger.debug("path does not resolve, falling back to its parent directory", {
1275
1275
  reason: ex instanceof Error ? ex.name : "unknown"
1276
1276
  });
1277
1277
  }
1278
1278
  try {
1279
- const parentRealPath = fs7__default.default.realpathSync.native(path__default.default.dirname(inputPath));
1279
+ const parentRealPath = fs6__default.default.realpathSync.native(path__default.default.dirname(inputPath));
1280
1280
  return path__default.default.join(parentRealPath, path__default.default.basename(inputPath));
1281
1281
  } catch (ex) {
1282
1282
  _logger.debug("parent directory does not resolve either, comparing lexically", {
@@ -1368,7 +1368,7 @@ function readPrivateKey(label, keyPath) {
1368
1368
  return cached;
1369
1369
  }
1370
1370
  try {
1371
- const contents = fs7__default.default.readFileSync(keyPath);
1371
+ const contents = fs6__default.default.readFileSync(keyPath);
1372
1372
  PRIVATE_KEY_CACHE.set(keyPath, contents);
1373
1373
  return contents;
1374
1374
  } catch (ex) {
@@ -2319,7 +2319,7 @@ async function writeRemoteFile(connection, args) {
2319
2319
  }
2320
2320
  async function uploadFile(connection, localPath, remotePath) {
2321
2321
  requirePermittedPath(connection, remotePath, "write");
2322
- const localStats = fs7__default.default.statSync(localPath);
2322
+ const localStats = fs6__default.default.statSync(localPath);
2323
2323
  if (localStats.isFile() == false) {
2324
2324
  throw new Error(`local path is not a file: ${localPath}`);
2325
2325
  }
@@ -2356,7 +2356,7 @@ async function downloadFile(connection, remotePath, localPath) {
2356
2356
  });
2357
2357
  });
2358
2358
  });
2359
- const localStats = fs7__default.default.statSync(localPath);
2359
+ const localStats = fs6__default.default.statSync(localPath);
2360
2360
  const _logger = $logger.child({
2361
2361
  context: "downloadFile",
2362
2362
  profile: connection.profile.name,
@@ -2634,36 +2634,152 @@ var PromptClosedError = class extends Error {
2634
2634
  this.name = "PromptClosedError";
2635
2635
  }
2636
2636
  };
2637
+ var ESC = "\x1B";
2638
+ var KEY_UP = [`${ESC}[A`, `${ESC}OA`, "k"];
2639
+ var KEY_DOWN = [`${ESC}[B`, `${ESC}OB`, "j"];
2640
+ var KEY_HOME = [`${ESC}[H`, `${ESC}OH`, `${ESC}[1~`];
2641
+ var KEY_END = [`${ESC}[F`, `${ESC}OF`, `${ESC}[4~`];
2642
+ var KEY_ENTER = ["\r", "\n"];
2643
+ var KEY_BACKSPACE = ["\x7F", "\b"];
2644
+ var KEY_CTRL_C = "";
2645
+ var KEY_CTRL_D = "";
2646
+ function splitKeys(chunk) {
2647
+ const keys = [];
2648
+ let index = 0;
2649
+ while (index < chunk.length) {
2650
+ const isSequence = chunk[index] === ESC && (chunk[index + 1] === "[" || chunk[index + 1] === "O");
2651
+ if (isSequence == true) {
2652
+ let end = index + 2;
2653
+ while (end < chunk.length && /[0-9;]/.test(chunk[end]) == true) {
2654
+ end += 1;
2655
+ }
2656
+ keys.push(chunk.slice(index, end + 1));
2657
+ index = end + 1;
2658
+ continue;
2659
+ }
2660
+ const codePoint = chunk.codePointAt(index) ?? 0;
2661
+ const character = String.fromCodePoint(codePoint);
2662
+ keys.push(character);
2663
+ index += character.length;
2664
+ }
2665
+ return keys;
2666
+ }
2667
+ function charWidth(codePoint) {
2668
+ if (codePoint < 4352) {
2669
+ return 1;
2670
+ }
2671
+ const isWide = codePoint <= 4447 || codePoint === 9001 || codePoint === 9002 || codePoint >= 11904 && codePoint <= 42191 && codePoint !== 12351 || codePoint >= 44032 && codePoint <= 55203 || codePoint >= 63744 && codePoint <= 64255 || codePoint >= 65072 && codePoint <= 65103 || codePoint >= 65280 && codePoint <= 65376 || codePoint >= 65504 && codePoint <= 65510 || codePoint >= 127744 && codePoint <= 128591 || codePoint >= 129280 && codePoint <= 129535 || codePoint >= 131072 && codePoint <= 262141;
2672
+ if (isWide == true) {
2673
+ return 2;
2674
+ }
2675
+ return 1;
2676
+ }
2677
+ function displayWidth(text) {
2678
+ let width = 0;
2679
+ for (const character of text) {
2680
+ width += charWidth(character.codePointAt(0) ?? 0);
2681
+ }
2682
+ return width;
2683
+ }
2684
+ function padToWidth(text, width) {
2685
+ const missing = width - displayWidth(text);
2686
+ if (missing <= 0) {
2687
+ return text;
2688
+ }
2689
+ return `${text}${" ".repeat(missing)}`;
2690
+ }
2691
+ function groupLabelWidths(items) {
2692
+ const widths = items.map(() => 0);
2693
+ let groupStart = 0;
2694
+ for (let index = 0; index <= items.length; index += 1) {
2695
+ const atBoundary = index === items.length || "value" in items[index] == false;
2696
+ if (atBoundary == false) {
2697
+ continue;
2698
+ }
2699
+ let groupWidth = 0;
2700
+ for (let member = groupStart; member < index; member += 1) {
2701
+ const item = items[member];
2702
+ groupWidth = Math.max(groupWidth, displayWidth(item.label));
2703
+ }
2704
+ for (let member = groupStart; member < index; member += 1) {
2705
+ widths[member] = groupWidth;
2706
+ }
2707
+ groupStart = index + 1;
2708
+ }
2709
+ return widths;
2710
+ }
2711
+ function supportsUnicode() {
2712
+ if (process.platform !== "win32") {
2713
+ return true;
2714
+ }
2715
+ return process.env.WT_SESSION != null || process.env.TERM_PROGRAM === "vscode" || process.env.TERMINAL_EMULATOR === "JetBrains-JediTerm" || process.env.ConEmuTask != null;
2716
+ }
2717
+ function createStyle(enabled) {
2718
+ const paint = (open, close) => {
2719
+ return (text) => {
2720
+ if (enabled == false || text === "") {
2721
+ return text;
2722
+ }
2723
+ return `${ESC}[${open}m${text}${ESC}[${close}m`;
2724
+ };
2725
+ };
2726
+ return {
2727
+ accent: paint(36, 39),
2728
+ success: paint(32, 39),
2729
+ dim: paint(2, 22),
2730
+ bold: paint(1, 22)
2731
+ };
2732
+ }
2637
2733
  var Prompter = class {
2638
2734
  constructor(input, output) {
2639
2735
  this.input = input;
2640
2736
  this.output = output;
2641
2737
  this.lineBuffer = "";
2642
2738
  this.pendingLines = [];
2643
- this.waiter = null;
2739
+ this.lineWaiter = null;
2644
2740
  this.inputEnded = false;
2645
- this.secretMode = false;
2646
- this.secretBuffer = "";
2741
+ /** Set while a key-driven prompt (select, secret) owns the input. */
2742
+ this.keyHandler = null;
2743
+ const ttyInput = input;
2744
+ const ttyOutput = output;
2745
+ this.interactive = ttyInput.isTTY == true && ttyInput.setRawMode != null && ttyOutput.isTTY == true;
2746
+ this.style = createStyle(this.interactive == true && process.env.NO_COLOR == null);
2747
+ if (supportsUnicode() == true) {
2748
+ this.symbols = { question: "?", pointer: "\u276F", tick: "\u2714", rule: "\u2500" };
2749
+ } else {
2750
+ this.symbols = { question: "?", pointer: ">", tick: "\u221A", rule: "-" };
2751
+ }
2647
2752
  input.setEncoding("utf8");
2648
2753
  input.on("data", (chunk) => {
2649
2754
  this.onData(chunk);
2650
2755
  });
2651
2756
  input.on("end", () => {
2652
2757
  this.inputEnded = true;
2653
- if (this.waiter != null) {
2654
- const waiter = this.waiter;
2655
- this.waiter = null;
2758
+ if (this.lineWaiter != null) {
2759
+ const waiter = this.lineWaiter;
2760
+ this.lineWaiter = null;
2656
2761
  waiter(null);
2657
2762
  }
2763
+ if (this.keyHandler != null) {
2764
+ this.keyHandler(KEY_CTRL_D);
2765
+ }
2658
2766
  });
2659
2767
  }
2660
2768
  /** Stops reading so the process can exit once the command is done. */
2661
2769
  close() {
2770
+ if (this.interactive == true) {
2771
+ this.input.setRawMode?.(false);
2772
+ this.output.write(`${ESC}[?25h`);
2773
+ }
2662
2774
  this.input.pause();
2663
2775
  }
2664
2776
  write(text) {
2665
2777
  this.output.write(text);
2666
2778
  }
2779
+ /** Styling for callers that print around the prompts; plain text off a TTY. */
2780
+ get paint() {
2781
+ return this.style;
2782
+ }
2667
2783
  /**
2668
2784
  * Free-text prompt. Enter accepts `defaultValue` when one is shown.
2669
2785
  *
@@ -2672,18 +2788,20 @@ var Prompter = class {
2672
2788
  async ask(question, defaultValue) {
2673
2789
  let suffix = "";
2674
2790
  if (defaultValue != null && defaultValue !== "") {
2675
- suffix = ` [${defaultValue}]`;
2791
+ suffix = ` ${this.style.dim(`(${defaultValue})`)}`;
2676
2792
  }
2677
- this.output.write(`${question}${suffix}: `);
2793
+ const prompt = `${this.style.accent(this.symbols.question)} ${this.style.bold(question)}${suffix} `;
2794
+ this.output.write(prompt);
2678
2795
  const line = await this.readLine();
2679
2796
  if (line == null) {
2680
2797
  throw new PromptClosedError();
2681
2798
  }
2682
- const trimmed = line.trim();
2683
- if (trimmed === "" && defaultValue != null) {
2684
- return defaultValue;
2799
+ let answer = line.trim();
2800
+ if (answer === "" && defaultValue != null) {
2801
+ answer = defaultValue;
2685
2802
  }
2686
- return trimmed;
2803
+ this.collapseAnswered(prompt, line, question, answer);
2804
+ return answer;
2687
2805
  }
2688
2806
  /** Like `ask`, re-asking until something non-empty is entered. */
2689
2807
  async askRequired(question, defaultValue) {
@@ -2692,77 +2810,115 @@ var Prompter = class {
2692
2810
  if (answer !== "") {
2693
2811
  return answer;
2694
2812
  }
2695
- this.output.write(" A value is required.\n");
2813
+ this.output.write(` ${this.style.dim("A value is required.")}
2814
+ `);
2696
2815
  }
2697
2816
  }
2698
2817
  async askYesNo(question, defaultValue) {
2699
- let hint = "[y/N]";
2818
+ let hint = "y/N";
2700
2819
  if (defaultValue == true) {
2701
- hint = "[Y/n]";
2820
+ hint = "Y/n";
2702
2821
  }
2703
2822
  for (; ; ) {
2704
- this.output.write(`${question} ${hint}: `);
2823
+ const prompt = `${this.style.accent(this.symbols.question)} ${this.style.bold(question)} ${this.style.dim(`(${hint})`)} `;
2824
+ this.output.write(prompt);
2705
2825
  const line = await this.readLine();
2706
2826
  if (line == null) {
2707
2827
  throw new PromptClosedError();
2708
2828
  }
2709
2829
  const normalized = line.trim().toLowerCase();
2830
+ let answer = null;
2710
2831
  if (normalized === "") {
2711
- return defaultValue;
2712
- }
2713
- if (normalized === "y" || normalized === "yes") {
2714
- return true;
2832
+ answer = defaultValue;
2833
+ } else if (normalized === "y" || normalized === "yes") {
2834
+ answer = true;
2835
+ } else if (normalized === "n" || normalized === "no") {
2836
+ answer = false;
2715
2837
  }
2716
- if (normalized === "n" || normalized === "no") {
2717
- return false;
2838
+ if (answer != null) {
2839
+ let shown = "no";
2840
+ if (answer == true) {
2841
+ shown = "yes";
2842
+ }
2843
+ this.collapseAnswered(prompt, line, question, shown);
2844
+ return answer;
2718
2845
  }
2719
- this.output.write(" Answer y or n.\n");
2846
+ this.output.write(` ${this.style.dim("Answer y or n.")}
2847
+ `);
2720
2848
  }
2721
2849
  }
2722
- /** Lists the choices one per line and accepts a key, case-insensitively. */
2723
- async askChoice(question, choices, defaultKey) {
2724
- this.output.write(`${question}
2725
- `);
2726
- for (const choice of choices) {
2727
- this.output.write(` ${choice.key}) ${choice.label}
2728
- `);
2850
+ /**
2851
+ * Picks one of `items`. On a TTY: ↑/↓ (or k/j) move, 1–9 jump, Enter picks, Esc returns
2852
+ * `cancelValue`. Off a TTY: a numbered list, answered by number, key or label.
2853
+ *
2854
+ * @throws {PromptClosedError} On Ctrl+C or when the input closes.
2855
+ */
2856
+ async select(question, items, options) {
2857
+ const choices = items.filter((item) => "value" in item);
2858
+ if (choices.length === 0) {
2859
+ throw new Error("select needs at least one choice");
2729
2860
  }
2730
- for (; ; ) {
2731
- const answer = await this.ask("Choice", defaultKey);
2732
- const matched = choices.find((choice) => choice.key.toLowerCase() === answer.toLowerCase());
2733
- if (matched != null) {
2734
- return matched.key;
2735
- }
2736
- this.output.write(` Pick one of ${choices.map((choice) => choice.key).join(", ")}.
2737
- `);
2861
+ let startIndex = items.findIndex((item) => "value" in item && item.value === options?.defaultValue);
2862
+ if (startIndex < 0) {
2863
+ startIndex = items.findIndex((item) => "value" in item);
2864
+ }
2865
+ if (this.interactive == true) {
2866
+ const picked2 = await this.selectWithKeys(question, items, startIndex, options?.cancelValue);
2867
+ return picked2;
2738
2868
  }
2869
+ const picked = await this.selectWithLines(question, items, startIndex);
2870
+ return picked;
2739
2871
  }
2740
2872
  /**
2741
2873
  * Reads a value without echoing it on a TTY. Empty means "none", or "keep" when the
2742
2874
  * caller says a value already exists.
2875
+ *
2876
+ * @throws {PromptClosedError} On Ctrl+C or when the input closes.
2743
2877
  */
2744
2878
  async askSecret(question) {
2745
- const rawModeStream = this.input;
2746
- if (rawModeStream.isTTY != true || rawModeStream.setRawMode == null) {
2879
+ if (this.interactive == false) {
2747
2880
  const answer = await this.ask(question);
2748
2881
  return answer;
2749
2882
  }
2750
- this.output.write(`${question}: `);
2751
- this.secretBuffer = "";
2752
- this.secretMode = true;
2753
- rawModeStream.setRawMode(true);
2754
- try {
2755
- const line = await this.readLine();
2756
- if (line == null) {
2757
- throw new PromptClosedError();
2758
- }
2759
- return line;
2760
- } finally {
2761
- rawModeStream.setRawMode(false);
2762
- this.secretMode = false;
2763
- this.output.write("\n");
2764
- }
2883
+ const prompt = `${this.style.accent(this.symbols.question)} ${this.style.bold(question)} `;
2884
+ this.output.write(prompt);
2885
+ const ttyInput = this.input;
2886
+ return new Promise((resolve, reject) => {
2887
+ let buffer = "";
2888
+ const finish = () => {
2889
+ this.keyHandler = null;
2890
+ ttyInput.setRawMode?.(false);
2891
+ this.output.write(`\r${ESC}[2K`);
2892
+ };
2893
+ this.keyHandler = (key) => {
2894
+ if (key === KEY_CTRL_C || key === KEY_CTRL_D) {
2895
+ finish();
2896
+ reject(new PromptClosedError());
2897
+ return;
2898
+ }
2899
+ if (KEY_ENTER.includes(key) == true) {
2900
+ finish();
2901
+ let shown = this.style.dim("(empty)");
2902
+ if (buffer !== "") {
2903
+ shown = "\u2022".repeat(Math.min(buffer.length, 8));
2904
+ }
2905
+ this.writeAnswered(question, shown);
2906
+ resolve(buffer);
2907
+ return;
2908
+ }
2909
+ if (KEY_BACKSPACE.includes(key) == true) {
2910
+ buffer = Array.from(buffer).slice(0, -1).join("");
2911
+ return;
2912
+ }
2913
+ if (key.startsWith(ESC) == true) {
2914
+ return;
2915
+ }
2916
+ buffer += key;
2917
+ };
2918
+ ttyInput.setRawMode?.(true);
2919
+ });
2765
2920
  }
2921
+ // ── line mode ──────────────────────────────────────────────────────────
2766
2922
  readLine() {
2767
2923
  const queued = this.pendingLines.shift();
2768
2924
  if (queued != null) {
@@ -2772,36 +2928,16 @@ var Prompter = class {
2772
2928
  return Promise.resolve(null);
2773
2929
  }
2774
2930
  return new Promise((resolve) => {
2775
- this.waiter = resolve;
2931
+ this.lineWaiter = resolve;
2776
2932
  });
2777
2933
  }
2778
- deliver(line) {
2779
- if (this.waiter != null) {
2780
- const waiter = this.waiter;
2781
- this.waiter = null;
2782
- waiter(line);
2783
- return;
2784
- }
2785
- this.pendingLines.push(line);
2786
- }
2787
2934
  onData(chunk) {
2788
- if (this.secretMode == true) {
2789
- for (const character of chunk) {
2790
- if (character === "\r" || character === "\n") {
2791
- const secret = this.secretBuffer;
2792
- this.secretBuffer = "";
2793
- this.deliver(secret);
2794
- continue;
2795
- }
2796
- if (character === "") {
2797
- this.output.write("\n");
2798
- process.exit(130);
2799
- }
2800
- if (character === "\x7F" || character === "\b") {
2801
- this.secretBuffer = this.secretBuffer.slice(0, -1);
2802
- continue;
2935
+ if (this.keyHandler != null) {
2936
+ for (const key of splitKeys(chunk)) {
2937
+ if (this.keyHandler == null) {
2938
+ break;
2803
2939
  }
2804
- this.secretBuffer += character;
2940
+ this.keyHandler(key);
2805
2941
  }
2806
2942
  return;
2807
2943
  }
@@ -2810,13 +2946,289 @@ var Prompter = class {
2810
2946
  while (newlineIndex >= 0) {
2811
2947
  const line = this.lineBuffer.slice(0, newlineIndex).replace(/\r$/, "");
2812
2948
  this.lineBuffer = this.lineBuffer.slice(newlineIndex + 1);
2813
- this.deliver(line);
2949
+ if (this.lineWaiter != null) {
2950
+ const waiter = this.lineWaiter;
2951
+ this.lineWaiter = null;
2952
+ waiter(line);
2953
+ } else {
2954
+ this.pendingLines.push(line);
2955
+ }
2814
2956
  newlineIndex = this.lineBuffer.indexOf("\n");
2815
2957
  }
2816
2958
  }
2959
+ async selectWithLines(question, items, startIndex) {
2960
+ const choices = items.filter((item) => "value" in item);
2961
+ const labelWidths = groupLabelWidths(items);
2962
+ this.output.write(`${this.symbols.question} ${question}
2963
+ `);
2964
+ let number = 0;
2965
+ for (let index = 0; index < items.length; index += 1) {
2966
+ const item = items[index];
2967
+ if ("value" in item == false) {
2968
+ this.output.write("\n");
2969
+ continue;
2970
+ }
2971
+ number += 1;
2972
+ let line = ` ${String(number).padStart(2)}) ${padToWidth(item.label, labelWidths[index])}`;
2973
+ if (item.hint != null && item.hint !== "") {
2974
+ line += ` ${item.hint}`;
2975
+ }
2976
+ this.output.write(`${line.trimEnd()}
2977
+ `);
2978
+ }
2979
+ const defaultChoice = items[startIndex];
2980
+ const defaultNumber = choices.indexOf(defaultChoice) + 1;
2981
+ for (; ; ) {
2982
+ this.output.write(`Choice (${defaultNumber}): `);
2983
+ const line = await this.readLine();
2984
+ if (line == null) {
2985
+ throw new PromptClosedError();
2986
+ }
2987
+ const answer = line.trim();
2988
+ if (answer === "") {
2989
+ return defaultChoice.value;
2990
+ }
2991
+ const index = Number(answer);
2992
+ if (Number.isInteger(index) == true && index >= 1 && index <= choices.length) {
2993
+ return choices[index - 1].value;
2994
+ }
2995
+ const lowered = answer.toLowerCase();
2996
+ const matched = choices.find((choice) => {
2997
+ const key = choice.key ?? choice.value;
2998
+ return key.toLowerCase() === lowered || choice.label.toLowerCase() === lowered;
2999
+ });
3000
+ if (matched != null) {
3001
+ return matched.value;
3002
+ }
3003
+ this.output.write(` Pick a number from 1 to ${choices.length}.
3004
+ `);
3005
+ }
3006
+ }
3007
+ // ── TTY rendering ──────────────────────────────────────────────────────
3008
+ columns() {
3009
+ const columns = this.output.columns;
3010
+ if (columns == null || columns < 20) {
3011
+ return 80;
3012
+ }
3013
+ return columns;
3014
+ }
3015
+ /** Joins styled parts, cutting the text (not the styles) once `width` columns are used. */
3016
+ fitLine(parts, width) {
3017
+ let used = 0;
3018
+ let line = "";
3019
+ for (const part of parts) {
3020
+ let text = "";
3021
+ for (const character of part.text) {
3022
+ const characterWidth = charWidth(character.codePointAt(0) ?? 0);
3023
+ if (used + characterWidth > width) {
3024
+ break;
3025
+ }
3026
+ used += characterWidth;
3027
+ text += character;
3028
+ }
3029
+ if (part.paint != null) {
3030
+ line += part.paint(text);
3031
+ } else {
3032
+ line += text;
3033
+ }
3034
+ if (used >= width) {
3035
+ break;
3036
+ }
3037
+ }
3038
+ return line;
3039
+ }
3040
+ /** The one-line record an answered prompt leaves behind on a TTY. */
3041
+ writeAnswered(question, answer) {
3042
+ this.output.write(`${this.style.success(this.symbols.tick)} ${question} ${this.style.dim("\xB7")} ${this.style.accent(answer)}
3043
+ `);
3044
+ }
3045
+ /**
3046
+ * Replaces a cooked-mode prompt and its echoed answer with the one-line record. The
3047
+ * rows are worked out from the widths, since a long answer wraps.
3048
+ */
3049
+ collapseAnswered(prompt, typed, question, answer) {
3050
+ if (this.interactive == false) {
3051
+ return;
3052
+ }
3053
+ const plainPrompt = prompt.replace(/\u001b\[[0-9;]*m/g, "");
3054
+ const used = displayWidth(plainPrompt) + displayWidth(typed);
3055
+ const rows = Math.max(1, Math.ceil(used / this.columns()));
3056
+ this.output.write(`${ESC}[${rows}A\r${ESC}[J`);
3057
+ let shown = answer;
3058
+ if (shown === "") {
3059
+ shown = this.style.dim("(empty)");
3060
+ }
3061
+ this.writeAnswered(question, shown);
3062
+ }
3063
+ selectWithKeys(question, items, startIndex, cancelValue) {
3064
+ const ttyInput = this.input;
3065
+ const selectableIndexes = items.flatMap((item, index) => {
3066
+ if ("value" in item) {
3067
+ return [index];
3068
+ }
3069
+ return [];
3070
+ });
3071
+ const labelWidths = groupLabelWidths(items);
3072
+ const ruleWidth = Math.min(Math.max(...labelWidths), 24);
3073
+ let active = startIndex;
3074
+ let renderedLines = 0;
3075
+ const draw = (lines) => {
3076
+ let text = "";
3077
+ if (renderedLines > 1) {
3078
+ text += `${ESC}[${renderedLines - 1}A`;
3079
+ }
3080
+ text += `\r${ESC}[J${lines.join("\n")}`;
3081
+ this.output.write(text);
3082
+ renderedLines = lines.length;
3083
+ };
3084
+ const buildLines = () => {
3085
+ const width = this.columns() - 1;
3086
+ const lines = [
3087
+ this.fitLine([
3088
+ { text: this.symbols.question, paint: this.style.accent },
3089
+ { text: " " },
3090
+ { text: question, paint: this.style.bold }
3091
+ ], width)
3092
+ ];
3093
+ for (let index = 0; index < items.length; index += 1) {
3094
+ const item = items[index];
3095
+ if ("value" in item == false) {
3096
+ lines.push(this.fitLine([{ text: ` ${this.symbols.rule.repeat(ruleWidth)}`, paint: this.style.dim }], width));
3097
+ continue;
3098
+ }
3099
+ const isActive = index === active;
3100
+ const parts = [];
3101
+ if (isActive == true) {
3102
+ parts.push({ text: `${this.symbols.pointer} `, paint: this.style.accent });
3103
+ parts.push({ text: padToWidth(item.label, labelWidths[index]), paint: this.style.accent });
3104
+ } else {
3105
+ parts.push({ text: " " });
3106
+ parts.push({ text: padToWidth(item.label, labelWidths[index]) });
3107
+ }
3108
+ if (item.hint != null && item.hint !== "") {
3109
+ parts.push({ text: ` ${item.hint}`, paint: this.style.dim });
3110
+ }
3111
+ lines.push(this.fitLine(parts, width));
3112
+ }
3113
+ let footer = "\u2191\u2193 move \xB7 enter select";
3114
+ if (cancelValue != null) {
3115
+ footer += " \xB7 esc back";
3116
+ }
3117
+ lines.push(this.fitLine([{ text: footer, paint: this.style.dim }], width));
3118
+ return lines;
3119
+ };
3120
+ const move = (step) => {
3121
+ const position = selectableIndexes.indexOf(active);
3122
+ const next = (position + step + selectableIndexes.length) % selectableIndexes.length;
3123
+ active = selectableIndexes[next];
3124
+ };
3125
+ return new Promise((resolve, reject) => {
3126
+ const finish = () => {
3127
+ this.keyHandler = null;
3128
+ ttyInput.setRawMode?.(false);
3129
+ draw([]);
3130
+ this.output.write(`${ESC}[?25h`);
3131
+ };
3132
+ this.keyHandler = (key) => {
3133
+ if (key === KEY_CTRL_C || key === KEY_CTRL_D) {
3134
+ finish();
3135
+ reject(new PromptClosedError());
3136
+ return;
3137
+ }
3138
+ if (KEY_ENTER.includes(key) == true) {
3139
+ const picked = items[active];
3140
+ finish();
3141
+ this.writeAnswered(question, picked.label);
3142
+ resolve(picked.value);
3143
+ return;
3144
+ }
3145
+ if (key === ESC) {
3146
+ if (cancelValue == null) {
3147
+ return;
3148
+ }
3149
+ finish();
3150
+ resolve(cancelValue);
3151
+ return;
3152
+ }
3153
+ if (KEY_UP.includes(key) == true) {
3154
+ move(-1);
3155
+ } else if (KEY_DOWN.includes(key) == true) {
3156
+ move(1);
3157
+ } else if (KEY_HOME.includes(key) == true) {
3158
+ active = selectableIndexes[0];
3159
+ } else if (KEY_END.includes(key) == true) {
3160
+ active = selectableIndexes[selectableIndexes.length - 1];
3161
+ } else if (/^[1-9]$/.test(key) == true && Number(key) <= selectableIndexes.length) {
3162
+ active = selectableIndexes[Number(key) - 1];
3163
+ } else {
3164
+ return;
3165
+ }
3166
+ draw(buildLines());
3167
+ };
3168
+ ttyInput.setRawMode?.(true);
3169
+ this.output.write(`${ESC}[?25l`);
3170
+ draw(buildLines());
3171
+ });
3172
+ }
2817
3173
  };
2818
- var DEFAULT_KEY_CANDIDATES = ["~/.ssh/id_ed25519", "~/.ssh/id_ecdsa", "~/.ssh/id_rsa"];
3174
+ var PREFERRED_KEY_NAMES = ["id_ed25519", "id_ecdsa", "id_rsa"];
3175
+ var NON_KEY_FILE_NAMES = ["config", "known_hosts", "known_hosts.old", "authorized_keys", "environment"];
3176
+ var MAX_KEY_FILE_BYTES = 64 * 1024;
2819
3177
  var CLEAR_ANSWER = "-";
3178
+ var OTHER_KEY_PATH = "\0other";
3179
+ function findPrivateKeys() {
3180
+ const sshDirectory = path__default.default.join(os5__default.default.homedir(), ".ssh");
3181
+ let names = [];
3182
+ try {
3183
+ names = fs6__default.default.readdirSync(sshDirectory);
3184
+ } catch {
3185
+ return [];
3186
+ }
3187
+ const keys = [];
3188
+ for (const name of names) {
3189
+ if (name.endsWith(".pub") == true || NON_KEY_FILE_NAMES.includes(name) == true) {
3190
+ continue;
3191
+ }
3192
+ const fullPath = path__default.default.join(sshDirectory, name);
3193
+ let head = "";
3194
+ try {
3195
+ const stat = fs6__default.default.statSync(fullPath);
3196
+ if (stat.isFile() == false || stat.size > MAX_KEY_FILE_BYTES) {
3197
+ continue;
3198
+ }
3199
+ const descriptor = fs6__default.default.openSync(fullPath, "r");
3200
+ const buffer = Buffer.alloc(64);
3201
+ const bytesRead = fs6__default.default.readSync(descriptor, buffer, 0, buffer.length, 0);
3202
+ fs6__default.default.closeSync(descriptor);
3203
+ head = buffer.subarray(0, bytesRead).toString("utf8");
3204
+ } catch {
3205
+ continue;
3206
+ }
3207
+ const isPemKey = head.startsWith("-----BEGIN") == true && head.includes("PRIVATE KEY") == true;
3208
+ const isPuttyKey = head.startsWith("PuTTY-User-Key-File") == true;
3209
+ if (isPemKey == false && isPuttyKey == false) {
3210
+ continue;
3211
+ }
3212
+ let hint = "";
3213
+ try {
3214
+ const publicKey = fs6__default.default.readFileSync(`${fullPath}.pub`, "utf8").trim().split(/\s+/);
3215
+ hint = publicKey[0].replace(/^ssh-/, "");
3216
+ if (publicKey.length > 2) {
3217
+ hint += ` \xB7 ${publicKey.slice(2).join(" ")}`;
3218
+ }
3219
+ } catch {
3220
+ }
3221
+ keys.push({ path: `~/.ssh/${name}`, hint });
3222
+ }
3223
+ const rank = (key) => {
3224
+ const index = PREFERRED_KEY_NAMES.indexOf(path__default.default.basename(key.path));
3225
+ if (index < 0) {
3226
+ return PREFERRED_KEY_NAMES.length;
3227
+ }
3228
+ return index;
3229
+ };
3230
+ return keys.sort((left, right) => rank(left) - rank(right) || left.path.localeCompare(right.path));
3231
+ }
2820
3232
  async function askPort(prompter, question, current) {
2821
3233
  for (; ; ) {
2822
3234
  const answer = await prompter.ask(question, String(current));
@@ -2835,11 +3247,11 @@ async function askOptional(prompter, question, current) {
2835
3247
  return answer;
2836
3248
  }
2837
3249
  async function askOptionalSecret(prompter, question, current) {
2838
- let suffix = " (empty for none)";
3250
+ let suffix = "(empty for none)";
2839
3251
  if (current != null) {
2840
- suffix = ` [keep current, ${CLEAR_ANSWER} to clear]`;
3252
+ suffix = `(enter keeps current, ${CLEAR_ANSWER} clears)`;
2841
3253
  }
2842
- const answer = await prompter.askSecret(`${question}${suffix}`);
3254
+ const answer = await prompter.askSecret(`${question} ${prompter.paint.dim(suffix)}`);
2843
3255
  if (answer === CLEAR_ANSWER) {
2844
3256
  return void 0;
2845
3257
  }
@@ -2848,11 +3260,42 @@ async function askOptionalSecret(prompter, question, current) {
2848
3260
  }
2849
3261
  return answer;
2850
3262
  }
3263
+ async function askKeyPath(prompter, current) {
3264
+ const found = findPrivateKeys();
3265
+ let picked = OTHER_KEY_PATH;
3266
+ if (found.length > 0 || current != null) {
3267
+ const items = [];
3268
+ if (current != null && found.some((key) => key.path === current) == false) {
3269
+ items.push({ value: current, label: current, hint: "current" });
3270
+ }
3271
+ for (const key of found) {
3272
+ items.push({ value: key.path, label: key.path, hint: key.hint });
3273
+ }
3274
+ items.push({ separator: true });
3275
+ items.push({ value: OTHER_KEY_PATH, key: "other", label: "Another path\u2026" });
3276
+ picked = await prompter.select("Private key", items, { defaultValue: current ?? found[0]?.path });
3277
+ }
3278
+ if (picked !== OTHER_KEY_PATH) {
3279
+ return picked;
3280
+ }
3281
+ for (; ; ) {
3282
+ const typed = await prompter.askRequired("Private key path");
3283
+ if (fs6__default.default.existsSync(expandHomePath(typed)) == true) {
3284
+ return typed;
3285
+ }
3286
+ prompter.write(` ${expandHomePath(typed)} does not exist.
3287
+ `);
3288
+ const useAnyway = await prompter.askYesNo("Use this path anyway?", false);
3289
+ if (useAnyway == true) {
3290
+ return typed;
3291
+ }
3292
+ }
3293
+ }
2851
3294
  async function askHostQuestions(prompter, existing) {
2852
3295
  if (existing != null) {
2853
- prompter.write(` Enter keeps the value in brackets; ${CLEAR_ANSWER} clears an optional one.
3296
+ prompter.write(prompter.paint.dim(` Enter keeps the value in brackets; ${CLEAR_ANSWER} clears an optional one.
2854
3297
 
2855
- `);
3298
+ `));
2856
3299
  }
2857
3300
  const host = await prompter.askRequired("Host (hostname or IP)", existing?.host);
2858
3301
  const port = await askPort(prompter, "Port", existing?.port ?? DEFAULT_SSH_PORT);
@@ -2866,60 +3309,60 @@ async function askHostQuestions(prompter, existing) {
2866
3309
  }
2867
3310
  prompter.write(" Use letters, digits, '-' and '_' only (max 48) \u2014 the name becomes part of every tool name.\n");
2868
3311
  }
2869
- const auth = await prompter.askChoice(
3312
+ const auth = await prompter.select(
2870
3313
  "Authentication",
2871
3314
  [
2872
- { key: "key", label: "private key file (recommended \u2014 only the path is stored)" },
2873
- { key: "password", label: "password (stored in the config file in plain text)" },
2874
- { key: "agent", label: "ssh agent (SSH_AUTH_SOCK / OpenSSH agent pipe)" }
3315
+ { value: "key", label: "Private key file", hint: "only the path is stored \u2014 recommended" },
3316
+ { value: "password", label: "Password", hint: "stored in the config file as plain text" },
3317
+ { value: "agent", label: "SSH agent", hint: "keys already loaded in ssh-agent" }
2875
3318
  ],
2876
- existing?.auth ?? "key"
3319
+ { defaultValue: existing?.auth ?? "key" }
2877
3320
  );
2878
3321
  let keyPath;
2879
3322
  let passphrase;
2880
3323
  let password;
2881
3324
  if (auth === "key") {
2882
- let defaultKeyPath = existing?.keyPath;
2883
- if (defaultKeyPath == null) {
2884
- defaultKeyPath = DEFAULT_KEY_CANDIDATES.find((candidate) => fs7__default.default.existsSync(expandHomePath(candidate)) == true);
2885
- }
2886
- for (; ; ) {
2887
- keyPath = await prompter.askRequired("Private key path", defaultKeyPath);
2888
- if (fs7__default.default.existsSync(expandHomePath(keyPath)) == true) {
2889
- break;
2890
- }
2891
- prompter.write(` ${expandHomePath(keyPath)} does not exist.
2892
- `);
2893
- const useAnyway = await prompter.askYesNo("Use this path anyway?", false);
2894
- if (useAnyway == true) {
2895
- break;
2896
- }
2897
- }
3325
+ keyPath = await askKeyPath(prompter, existing?.keyPath);
2898
3326
  passphrase = await askOptionalSecret(prompter, "Key passphrase", existing?.passphrase);
2899
3327
  }
2900
3328
  if (auth === "password") {
2901
- prompter.write(" The password is written into the config file as plain text. A key file is safer.\n");
2902
3329
  password = await askOptionalSecret(prompter, "Password", existing?.password);
2903
3330
  if (password == null) {
2904
3331
  prompter.write(" No password given \u2014 falling back to the ssh agent.\n");
2905
3332
  }
2906
3333
  }
3334
+ let currentRoute = "direct";
3335
+ if (existing?.jumpHost != null && existing.proxyCommand != null) {
3336
+ currentRoute = "jump-proxy";
3337
+ } else if (existing?.jumpHost != null) {
3338
+ currentRoute = "jump";
3339
+ } else if (existing?.proxyCommand != null) {
3340
+ currentRoute = "proxy";
3341
+ }
3342
+ const route = await prompter.select(
3343
+ "How is it reached?",
3344
+ [
3345
+ { value: "direct", label: "Directly" },
3346
+ { value: "jump", label: "Through a jump host", hint: "ssh -J" },
3347
+ { value: "proxy", label: "Through a proxy command", hint: "e.g. cloudflared access ssh" },
3348
+ { value: "jump-proxy", label: "Proxy command, then a jump host", hint: "cloudflared to the bastion, then on to the host" }
3349
+ ],
3350
+ { defaultValue: currentRoute }
3351
+ );
2907
3352
  let jumpHost;
2908
3353
  let jumpPort;
2909
3354
  let jumpUser;
2910
- const viaJump = await prompter.askYesNo("Reach it through a jump host (ssh -J)?", existing?.jumpHost != null);
2911
- if (viaJump == true) {
3355
+ let proxyCommand;
3356
+ if (route === "jump" || route === "jump-proxy") {
2912
3357
  jumpHost = await prompter.askRequired("Jump host", existing?.jumpHost);
2913
3358
  jumpPort = await askPort(prompter, "Jump host port", existing?.jumpPort ?? DEFAULT_SSH_PORT);
2914
3359
  jumpUser = await prompter.askRequired("Jump host user", existing?.jumpUser ?? username);
2915
3360
  }
2916
- let proxyCommand;
2917
- const viaProxy = await prompter.askYesNo("Start a proxy command for the first hop (e.g. cloudflared)?", existing?.proxyCommand != null);
2918
- if (viaProxy == true) {
2919
- prompter.write(" %h / %p / %r expand to the first hop's host, port and user.\n");
3361
+ if (route === "proxy" || route === "jump-proxy") {
3362
+ prompter.write(prompter.paint.dim(" %h / %p / %r expand to the first hop's host, port and user.\n"));
2920
3363
  proxyCommand = await prompter.askRequired("Proxy command", existing?.proxyCommand ?? "cloudflared access ssh --hostname %h");
2921
3364
  }
2922
- const description = await askOptional(prompter, "Description shown to the agent (what the host is for)", existing?.description);
3365
+ const description = await askOptional(prompter, "Description shown to the agent", existing?.description);
2923
3366
  const readonly = await prompter.askYesNo("Read-only? (rejects writes, redirections, uploads)", existing?.readonly ?? false);
2924
3367
  let currentAllowedPaths;
2925
3368
  if (existing != null && existing.allowedPaths.length > 0) {
@@ -2928,7 +3371,7 @@ async function askHostQuestions(prompter, existing) {
2928
3371
  let allowedPaths = [];
2929
3372
  const allowedPathsAnswer = await askOptional(
2930
3373
  prompter,
2931
- "Restrict file tools to these path prefixes (comma-separated, empty = unrestricted)",
3374
+ "Limit file tools to path prefixes (comma-separated, empty = anywhere)",
2932
3375
  currentAllowedPaths
2933
3376
  );
2934
3377
  if (allowedPathsAnswer != null) {
@@ -3004,10 +3447,10 @@ function readMcpServersJson(document) {
3004
3447
  return entries;
3005
3448
  }
3006
3449
  function readDocument(filePath) {
3007
- if (fs7__default.default.existsSync(filePath) == false) {
3450
+ if (fs6__default.default.existsSync(filePath) == false) {
3008
3451
  return {};
3009
3452
  }
3010
- const text = fs7__default.default.readFileSync(filePath, "utf8");
3453
+ const text = fs6__default.default.readFileSync(filePath, "utf8");
3011
3454
  if (text.trim() === "") {
3012
3455
  return {};
3013
3456
  }
@@ -3029,15 +3472,15 @@ function readDocument(filePath) {
3029
3472
  function createJsonFileTarget(options) {
3030
3473
  const { id, label, filePath } = options;
3031
3474
  function writeDocument(document) {
3032
- fs7__default.default.mkdirSync(path__default.default.dirname(filePath), { recursive: true });
3033
- fs7__default.default.writeFileSync(filePath, `${JSON.stringify(document, null, 4)}
3475
+ fs6__default.default.mkdirSync(path__default.default.dirname(filePath), { recursive: true });
3476
+ fs6__default.default.writeFileSync(filePath, `${JSON.stringify(document, null, 4)}
3034
3477
  `, "utf8");
3035
3478
  }
3036
3479
  return {
3037
3480
  id,
3038
3481
  label,
3039
3482
  location: filePath,
3040
- detected: fs7__default.default.existsSync(filePath),
3483
+ detected: fs6__default.default.existsSync(filePath),
3041
3484
  readEntries() {
3042
3485
  const document = readDocument(filePath);
3043
3486
  return readMcpServersJson(document);
@@ -3067,7 +3510,7 @@ function createJsonFileTarget(options) {
3067
3510
  }
3068
3511
 
3069
3512
  // src/modules/manage/targets/claude-code.ts
3070
- var CLAUDE_CODE_CONFIG_PATH = path__default.default.join(os4__default.default.homedir(), ".claude.json");
3513
+ var CLAUDE_CODE_CONFIG_PATH = path__default.default.join(os5__default.default.homedir(), ".claude.json");
3071
3514
  var CMD_META_CHARACTER_PATTERN = /([()\][%!^"`<>&|;, *?])/g;
3072
3515
  function toClaudeAddJsonPayload(entry) {
3073
3516
  return JSON.stringify({ type: "stdio", command: entry.command, args: entry.args, env: entry.env });
@@ -3113,10 +3556,10 @@ function runClaude(executable, args) {
3113
3556
  return { status: "written", detail: CLAUDE_CODE_CONFIG_PATH };
3114
3557
  }
3115
3558
  function readClaudeEntries() {
3116
- if (fs7__default.default.existsSync(CLAUDE_CODE_CONFIG_PATH) == false) {
3559
+ if (fs6__default.default.existsSync(CLAUDE_CODE_CONFIG_PATH) == false) {
3117
3560
  return {};
3118
3561
  }
3119
- const text = fs7__default.default.readFileSync(CLAUDE_CODE_CONFIG_PATH, "utf8");
3562
+ const text = fs6__default.default.readFileSync(CLAUDE_CODE_CONFIG_PATH, "utf8");
3120
3563
  let document;
3121
3564
  try {
3122
3565
  document = JSON.parse(text);
@@ -3135,7 +3578,7 @@ function createClaudeCodeTarget() {
3135
3578
  id: "claude-code",
3136
3579
  label: "Claude Code (user scope)",
3137
3580
  location: CLAUDE_CODE_CONFIG_PATH,
3138
- detected: executable != null || fs7__default.default.existsSync(CLAUDE_CODE_CONFIG_PATH) == true,
3581
+ detected: executable != null || fs6__default.default.existsSync(CLAUDE_CODE_CONFIG_PATH) == true,
3139
3582
  readEntries: readClaudeEntries,
3140
3583
  writeEntry(name, entry) {
3141
3584
  if (executable == null) {
@@ -3587,24 +4030,24 @@ function writeMcpServerToml(text, name, entry) {
3587
4030
  }
3588
4031
 
3589
4032
  // src/modules/manage/targets/codex.ts
3590
- var CODEX_CONFIG_PATH = path__default.default.join(os4__default.default.homedir(), ".codex", "config.toml");
4033
+ var CODEX_CONFIG_PATH = path__default.default.join(os5__default.default.homedir(), ".codex", "config.toml");
3591
4034
  function createCodexTarget() {
3592
4035
  function readText() {
3593
- if (fs7__default.default.existsSync(CODEX_CONFIG_PATH) == false) {
4036
+ if (fs6__default.default.existsSync(CODEX_CONFIG_PATH) == false) {
3594
4037
  return "";
3595
4038
  }
3596
- return fs7__default.default.readFileSync(CODEX_CONFIG_PATH, "utf8");
4039
+ return fs6__default.default.readFileSync(CODEX_CONFIG_PATH, "utf8");
3597
4040
  }
3598
4041
  function writeText(text) {
3599
- fs7__default.default.mkdirSync(path__default.default.dirname(CODEX_CONFIG_PATH), { recursive: true });
3600
- fs7__default.default.writeFileSync(CODEX_CONFIG_PATH, text, "utf8");
4042
+ fs6__default.default.mkdirSync(path__default.default.dirname(CODEX_CONFIG_PATH), { recursive: true });
4043
+ fs6__default.default.writeFileSync(CODEX_CONFIG_PATH, text, "utf8");
3601
4044
  return { status: "written", detail: CODEX_CONFIG_PATH };
3602
4045
  }
3603
4046
  return {
3604
4047
  id: "codex",
3605
4048
  label: "Codex CLI",
3606
4049
  location: CODEX_CONFIG_PATH,
3607
- detected: findExecutable("codex") != null || fs7__default.default.existsSync(CODEX_CONFIG_PATH) == true,
4050
+ detected: findExecutable("codex") != null || fs6__default.default.existsSync(CODEX_CONFIG_PATH) == true,
3608
4051
  readEntries() {
3609
4052
  const text = readText();
3610
4053
  return parseMcpServersToml(text);
@@ -3625,20 +4068,20 @@ function createCodexTarget() {
3625
4068
  // src/modules/manage/targets/index.ts
3626
4069
  function claudeDesktopConfigPath() {
3627
4070
  if (process.platform === "win32") {
3628
- const appData = process.env.APPDATA ?? path__default.default.join(os4__default.default.homedir(), "AppData", "Roaming");
4071
+ const appData = process.env.APPDATA ?? path__default.default.join(os5__default.default.homedir(), "AppData", "Roaming");
3629
4072
  return path__default.default.join(appData, "Claude", "claude_desktop_config.json");
3630
4073
  }
3631
4074
  if (process.platform === "darwin") {
3632
- return path__default.default.join(os4__default.default.homedir(), "Library", "Application Support", "Claude", "claude_desktop_config.json");
4075
+ return path__default.default.join(os5__default.default.homedir(), "Library", "Application Support", "Claude", "claude_desktop_config.json");
3633
4076
  }
3634
- return path__default.default.join(os4__default.default.homedir(), ".config", "Claude", "claude_desktop_config.json");
4077
+ return path__default.default.join(os5__default.default.homedir(), ".config", "Claude", "claude_desktop_config.json");
3635
4078
  }
3636
4079
  function listConfigTargets() {
3637
4080
  const targets = [
3638
4081
  createClaudeCodeTarget(),
3639
4082
  createCodexTarget(),
3640
4083
  createJsonFileTarget({ id: "claude-desktop", label: "Claude Desktop", filePath: claudeDesktopConfigPath() }),
3641
- createJsonFileTarget({ id: "cursor", label: "Cursor", filePath: path__default.default.join(os4__default.default.homedir(), ".cursor", "mcp.json") })
4084
+ createJsonFileTarget({ id: "cursor", label: "Cursor", filePath: path__default.default.join(os5__default.default.homedir(), ".cursor", "mcp.json") })
3642
4085
  ];
3643
4086
  const detected = targets.filter((target) => target.detected == true);
3644
4087
  const others = targets.filter((target) => target.detected == false);
@@ -3652,6 +4095,7 @@ var SECRET_ENV_NAMES = [
3652
4095
  SINGLE_HOST_ENV.JUMP_PASSWORD,
3653
4096
  SINGLE_HOST_ENV.JUMP_PASSPHRASE
3654
4097
  ];
4098
+ var HOST_VALUE_PREFIX = "host:";
3655
4099
  function errorMessage(error) {
3656
4100
  if (error instanceof Error) {
3657
4101
  return error.message;
@@ -3679,14 +4123,14 @@ function formatForTarget(target, name, entry) {
3679
4123
  function indent(text) {
3680
4124
  return text.replace(/\n$/, "").split("\n").map((line) => ` ${line}`).join("\n");
3681
4125
  }
3682
- function reportWrite(output, result, action) {
4126
+ function reportWrite(prompter, result, action) {
3683
4127
  if (result.status === "written") {
3684
- output.write(` ${action} \u2014 ${result.detail}
4128
+ prompter.write(` ${prompter.paint.success(action)} ${prompter.paint.dim(result.detail)}
3685
4129
  `);
3686
- output.write(" Restart the client (or reconnect it from its MCP menu) to pick up the change.\n\n");
4130
+ prompter.write(prompter.paint.dim(" Restart the client (or reconnect it from its MCP menu) to pick up the change.\n\n"));
3687
4131
  return;
3688
4132
  }
3689
- output.write(` Not ${action.toLowerCase()}: ${result.detail}
4133
+ prompter.write(` Not ${action.toLowerCase()}: ${result.detail}
3690
4134
 
3691
4135
  `);
3692
4136
  }
@@ -3717,13 +4161,13 @@ async function verifyAnswers(prompter, output, initial) {
3717
4161
  output.write("\n");
3718
4162
  if (result.connected == true) {
3719
4163
  if (result.hostKeyFingerprint != null && answers.hostKeyFingerprint == null) {
3720
- const pin = await prompter.askYesNo(`Pin the host key ${result.hostKeyFingerprint}? (a changed key then aborts the connection)`, true);
4164
+ const pin = await prompter.askYesNo("Pin this host key? (a changed key then aborts the connection)", true);
3721
4165
  if (pin == true) {
3722
4166
  answers = { ...answers, hostKeyFingerprint: result.hostKeyFingerprint };
3723
4167
  }
3724
4168
  }
3725
4169
  if (result.jumpHostKeyFingerprint != null && answers.jumpHost != null && answers.jumpHostKeyFingerprint == null) {
3726
- const pin = await prompter.askYesNo(`Pin the jump host key ${result.jumpHostKeyFingerprint}?`, true);
4170
+ const pin = await prompter.askYesNo("Pin the jump host key too?", true);
3727
4171
  if (pin == true) {
3728
4172
  answers = { ...answers, jumpHostKeyFingerprint: result.jumpHostKeyFingerprint };
3729
4173
  }
@@ -3749,23 +4193,23 @@ async function verifyAnswers(prompter, output, initial) {
3749
4193
  }
3750
4194
  }
3751
4195
  }
3752
- const next = await prompter.askChoice(
3753
- "What now?",
4196
+ const next = await prompter.select(
4197
+ "The check failed. What now?",
3754
4198
  [
3755
- { key: "r", label: "retry the check" },
3756
- { key: "e", label: "edit the answers" },
3757
- { key: "w", label: "write it anyway" },
3758
- { key: "c", label: "cancel" }
4199
+ { value: "edit", key: "e", label: "Change the answers" },
4200
+ { value: "retry", key: "r", label: "Try again", hint: "e.g. after starting a tunnel" },
4201
+ { value: "write", key: "w", label: "Write it anyway", hint: "the host may be unreachable from here" },
4202
+ { value: "cancel", key: "c", label: "Cancel", hint: "nothing is written" }
3759
4203
  ],
3760
- "e"
4204
+ { defaultValue: "edit", cancelValue: "cancel" }
3761
4205
  );
3762
- if (next === "c") {
4206
+ if (next === "cancel") {
3763
4207
  return null;
3764
4208
  }
3765
- if (next === "w") {
4209
+ if (next === "write") {
3766
4210
  return answers;
3767
4211
  }
3768
- if (next === "e") {
4212
+ if (next === "edit") {
3769
4213
  output.write("\n");
3770
4214
  answers = await askHostQuestions(prompter, answers);
3771
4215
  }
@@ -3776,10 +4220,12 @@ async function addOrEditHost(prompter, output, target, entries, existingName) {
3776
4220
  if (existingName != null) {
3777
4221
  existing = entryToAnswers(existingName, entries[existingName]);
3778
4222
  output.write(`
3779
- Editing ${existingName}
4223
+ ${prompter.paint.bold(`Editing ${existingName}`)}
3780
4224
  `);
3781
4225
  } else {
3782
- output.write("\nAdding a host\n");
4226
+ output.write(`
4227
+ ${prompter.paint.bold("Adding a host")}
4228
+ `);
3783
4229
  }
3784
4230
  const asked = await askHostQuestions(prompter, existing);
3785
4231
  output.write("\n");
@@ -3797,7 +4243,7 @@ Editing ${existingName}
3797
4243
  }
3798
4244
  const entry = answersToEntry(answers);
3799
4245
  output.write(`
3800
- This goes into ${target.location}:
4246
+ ${prompter.paint.dim(`This goes into ${target.location}:`)}
3801
4247
 
3802
4248
  `);
3803
4249
  output.write(`${indent(formatForTarget(target, answers.name, maskSecrets(entry)))}
@@ -3805,11 +4251,11 @@ Editing ${existingName}
3805
4251
  `);
3806
4252
  const confirmed = await prompter.askYesNo("Write it?", true);
3807
4253
  if (confirmed == false) {
3808
- output.write(" Not written. The print view (p) gives the same entry ready to paste.\n\n");
4254
+ output.write(" Not written. Pick the host and choose Print to get the entry ready to paste.\n\n");
3809
4255
  return;
3810
4256
  }
3811
4257
  const written = target.writeEntry(answers.name, entry);
3812
- reportWrite(output, written, "Written");
4258
+ reportWrite(prompter, written, "Written");
3813
4259
  if (written.status === "written" && existingName != null && existingName !== answers.name) {
3814
4260
  const removed = target.removeEntry(existingName);
3815
4261
  if (removed.status === "unavailable") {
@@ -3834,14 +4280,14 @@ async function checkHost(prompter, output, target, name, entry) {
3834
4280
  const env = { ...entry.env };
3835
4281
  let changed = false;
3836
4282
  if (result.hostKeyFingerprint != null && env[SINGLE_HOST_ENV.HOST_KEY] == null) {
3837
- const pin = await prompter.askYesNo(`Pin the host key ${result.hostKeyFingerprint}?`, true);
4283
+ const pin = await prompter.askYesNo("Pin this host key?", true);
3838
4284
  if (pin == true) {
3839
4285
  env[SINGLE_HOST_ENV.HOST_KEY] = result.hostKeyFingerprint;
3840
4286
  changed = true;
3841
4287
  }
3842
4288
  }
3843
4289
  if (result.jumpHostKeyFingerprint != null && env[SINGLE_HOST_ENV.JUMP_HOST_KEY] == null) {
3844
- const pin = await prompter.askYesNo(`Pin the jump host key ${result.jumpHostKeyFingerprint}?`, true);
4290
+ const pin = await prompter.askYesNo("Pin the jump host key too?", true);
3845
4291
  if (pin == true) {
3846
4292
  env[SINGLE_HOST_ENV.JUMP_HOST_KEY] = result.jumpHostKeyFingerprint;
3847
4293
  changed = true;
@@ -3849,76 +4295,79 @@ async function checkHost(prompter, output, target, name, entry) {
3849
4295
  }
3850
4296
  if (changed == true) {
3851
4297
  const written = target.writeEntry(name, { ...entry, env });
3852
- reportWrite(output, written, "Pinned");
4298
+ reportWrite(prompter, written, "Pinned");
3853
4299
  }
3854
4300
  }
3855
- function printHost(output, name, entry) {
3856
- output.write('\n JSON \u2014 Claude Desktop, Cursor, .mcp.json (inside "mcpServers"):\n\n');
3857
- output.write(`${indent(`"${name}": ${JSON.stringify(entry, null, 4)}`)}
4301
+ function printHost(prompter, name, entry) {
4302
+ prompter.write(`
4303
+ ${prompter.paint.dim('JSON \u2014 Claude Desktop, Cursor, .mcp.json (inside "mcpServers"):')}
3858
4304
 
3859
4305
  `);
3860
- output.write(" TOML \u2014 Codex (~/.codex/config.toml):\n\n");
3861
- output.write(`${indent(renderMcpServerTable(name, entry))}
4306
+ prompter.write(`${indent(`"${name}": ${JSON.stringify(entry, null, 4)}`)}
3862
4307
 
3863
4308
  `);
3864
- output.write(" Claude Code CLI (bash / zsh / Git Bash):\n\n");
3865
- output.write(`${indent(formatClaudeAddCommand(name, entry))}
4309
+ prompter.write(` ${prompter.paint.dim("TOML \u2014 Codex (~/.codex/config.toml):")}
3866
4310
 
3867
4311
  `);
3868
- }
3869
- async function pickHost(prompter, output, names) {
3870
- if (names.length === 0) {
3871
- output.write(" No hosts yet \u2014 add one with a.\n\n");
3872
- return null;
3873
- }
3874
- if (names.length === 1) {
3875
- return names[0];
3876
- }
3877
- for (; ; ) {
3878
- const answer = await prompter.ask("Which host (number or name, empty to go back)");
3879
- if (answer === "") {
3880
- return null;
3881
- }
3882
- const index = Number(answer);
3883
- if (Number.isInteger(index) == true && index >= 1 && index <= names.length) {
3884
- return names[index - 1];
3885
- }
3886
- if (names.includes(answer) == true) {
3887
- return answer;
3888
- }
3889
- output.write(` No host "${answer}".
4312
+ prompter.write(`${indent(renderMcpServerTable(name, entry))}
4313
+
3890
4314
  `);
3891
- }
3892
- }
3893
- function renderHostList(output, target, entries) {
3894
- const names = Object.keys(entries).filter((name) => isOurEntry(entries[name]) == true);
3895
- const otherCount = Object.keys(entries).length - names.length;
3896
- output.write(`
3897
- ${target.label} \u2014 ${target.location}
4315
+ prompter.write(` ${prompter.paint.dim("Claude Code CLI (bash / zsh / Git Bash):")}
3898
4316
 
3899
4317
  `);
3900
- if (names.length === 0) {
3901
- output.write(" No SSH hosts registered here yet.\n");
3902
- } else {
3903
- const rows = names.map((name, index) => {
3904
- const summary = summarizeEntry(entries[name]);
3905
- return [String(index + 1), name, summary.endpoint, summary.auth, summary.notes];
3906
- });
3907
- const header = ["#", "Server", "Endpoint", "Auth", "Notes"];
3908
- const widths = header.map((title, column) => Math.max(title.length, ...rows.map((row) => row[column].length)));
3909
- for (const row of [header, ...rows]) {
3910
- const line = row.map((cell, column) => cell.padEnd(widths[column])).join(" ").trimEnd();
3911
- output.write(` ${line}
4318
+ prompter.write(`${indent(formatClaudeAddCommand(name, entry))}
4319
+
3912
4320
  `);
4321
+ }
4322
+ function buildHostItems(names, entries) {
4323
+ const nameWidth = Math.max(...names.map((name) => displayWidth(name)));
4324
+ const summaries = names.map((name) => summarizeEntry(entries[name]));
4325
+ const endpointWidth = Math.max(...summaries.map((summary) => displayWidth(summary.endpoint)));
4326
+ return names.map((name, index) => {
4327
+ const summary = summaries[index];
4328
+ const hintParts = [summary.auth];
4329
+ if (summary.notes !== "") {
4330
+ hintParts.push(summary.notes);
3913
4331
  }
4332
+ return {
4333
+ value: `${HOST_VALUE_PREFIX}${name}`,
4334
+ key: name,
4335
+ label: `${name.padEnd(nameWidth)} ${summary.endpoint.padEnd(endpointWidth)}`,
4336
+ hint: hintParts.join(" \xB7 ")
4337
+ };
4338
+ });
4339
+ }
4340
+ async function manageHost(prompter, output, target, entries, name) {
4341
+ const action = await prompter.select(
4342
+ name,
4343
+ [
4344
+ { value: "edit", key: "e", label: "Edit", hint: "change the answers, re-check, re-write" },
4345
+ { value: "check", key: "c", label: "Check the connection", hint: "connect now and offer to pin the host key" },
4346
+ { value: "print", key: "p", label: "Print the entry", hint: "JSON, Codex TOML and a claude command, ready to paste" },
4347
+ { value: "remove", key: "r", label: "Remove", hint: `from ${target.label}` },
4348
+ { separator: true },
4349
+ { value: "back", key: "b", label: "Back" }
4350
+ ],
4351
+ { defaultValue: "edit", cancelValue: "back" }
4352
+ );
4353
+ if (action === "edit") {
4354
+ await addOrEditHost(prompter, output, target, entries, name);
4355
+ return;
3914
4356
  }
3915
- if (otherCount > 0) {
3916
- output.write(`
3917
- ${otherCount} other MCP server(s) in this file are not managed here.
3918
- `);
4357
+ if (action === "check") {
4358
+ await checkHost(prompter, output, target, name, entries[name]);
4359
+ return;
4360
+ }
4361
+ if (action === "print") {
4362
+ printHost(prompter, name, entries[name]);
4363
+ return;
4364
+ }
4365
+ if (action === "remove") {
4366
+ const remove = await prompter.askYesNo(`Remove ${name} from ${target.location}?`, false);
4367
+ if (remove == true) {
4368
+ reportWrite(prompter, target.removeEntry(name), "Removed");
4369
+ }
3919
4370
  }
3920
- output.write("\n");
3921
- return names;
3922
4371
  }
3923
4372
  async function runConfigCommand(options) {
3924
4373
  const { input, output } = options;
@@ -3934,81 +4383,88 @@ async function runConfigCommand(options) {
3934
4383
  target = requested;
3935
4384
  }
3936
4385
  const prompter = new Prompter(input, output);
4386
+ const paint = prompter.paint;
3937
4387
  try {
3938
4388
  for (; ; ) {
3939
4389
  let entries = {};
3940
4390
  let names = [];
3941
- let readable = true;
4391
+ let readError = null;
3942
4392
  try {
3943
4393
  entries = target.readEntries();
3944
- names = renderHostList(output, target, entries);
4394
+ names = Object.keys(entries).filter((name) => isOurEntry(entries[name]) == true);
3945
4395
  } catch (ex) {
3946
- readable = false;
3947
- output.write(`
3948
- ${target.label} \u2014 ${target.location}
3949
-
3950
- Cannot read it: ${errorMessage(ex)}
4396
+ readError = errorMessage(ex);
4397
+ }
4398
+ output.write(`
4399
+ ${paint.bold(target.label)} ${paint.dim(target.location)}
4400
+ `);
4401
+ if (readError != null) {
4402
+ output.write(` Cannot read it: ${readError}
3951
4403
  Fix the file by hand, or switch to another client.
3952
-
3953
4404
  `);
4405
+ } else {
4406
+ const otherCount = Object.keys(entries).length - names.length;
4407
+ if (otherCount > 0) {
4408
+ output.write(paint.dim(` ${otherCount} other MCP server(s) in this file are not managed here.
4409
+ `));
4410
+ }
4411
+ }
4412
+ output.write("\n");
4413
+ const items = [];
4414
+ if (names.length > 0) {
4415
+ items.push(...buildHostItems(names, entries));
4416
+ items.push({ separator: true });
3954
4417
  }
3955
- let menu = " t) switch client q) quit";
3956
- if (readable == true && names.length > 0) {
3957
- menu = " a) add e) edit r) remove c) check p) print t) switch client q) quit";
3958
- } else if (readable == true) {
3959
- menu = " a) add t) switch client q) quit";
4418
+ if (readError == null) {
4419
+ items.push({ value: "add", key: "a", label: "Add a host" });
3960
4420
  }
3961
- output.write(`${menu}
3962
- `);
3963
- const action = (await prompter.ask("Choice")).toLowerCase();
3964
- if (action === "q" || action === "quit") {
4421
+ const otherTargets = targets.filter((candidate) => candidate.id !== target.id);
4422
+ if (otherTargets.length > 0) {
4423
+ const detectedOthers = otherTargets.filter((candidate) => candidate.detected == true).map((candidate) => candidate.label);
4424
+ let switchHint = "";
4425
+ if (detectedOthers.length > 0) {
4426
+ switchHint = `also found: ${detectedOthers.join(", ")}`;
4427
+ }
4428
+ items.push({ value: "switch", key: "t", label: "Switch client", hint: switchHint });
4429
+ }
4430
+ items.push({ value: "quit", key: "q", label: "Quit" });
4431
+ let question = "No SSH hosts here yet";
4432
+ if (names.length === 1) {
4433
+ question = "1 SSH host \u2014 pick it to manage it";
4434
+ } else if (names.length > 1) {
4435
+ question = `${names.length} SSH hosts \u2014 pick one to manage it`;
4436
+ }
4437
+ let defaultValue = "add";
4438
+ if (names.length > 0) {
4439
+ defaultValue = `${HOST_VALUE_PREFIX}${names[0]}`;
4440
+ }
4441
+ const picked = await prompter.select(question, items, { defaultValue, cancelValue: "quit" });
4442
+ if (picked === "quit") {
3965
4443
  return 0;
3966
4444
  }
3967
- if (action === "t") {
3968
- const picked = await prompter.askChoice(
4445
+ if (picked === "switch") {
4446
+ const switched = await prompter.select(
3969
4447
  "Which client?",
3970
4448
  targets.map((candidate) => {
3971
- let mark = "";
3972
- if (candidate.detected == true) {
3973
- mark = " (detected)";
4449
+ let hint = candidate.location;
4450
+ if (candidate.detected == false) {
4451
+ hint += " \xB7 not found on this machine";
3974
4452
  }
3975
- return { key: candidate.id, label: `${candidate.label} \u2014 ${candidate.location}${mark}` };
4453
+ return { value: candidate.id, label: candidate.label, hint };
3976
4454
  }),
3977
- target.id
4455
+ { defaultValue: target.id, cancelValue: target.id }
3978
4456
  );
3979
- target = targets.find((candidate) => candidate.id === picked) ?? target;
3980
- continue;
3981
- }
3982
- if (readable == false) {
3983
- output.write(" Not available while the file cannot be read.\n");
4457
+ target = targets.find((candidate) => candidate.id === switched) ?? target;
3984
4458
  continue;
3985
4459
  }
3986
4460
  try {
3987
- if (action === "a") {
4461
+ if (picked === "add") {
3988
4462
  await addOrEditHost(prompter, output, target, entries);
3989
4463
  continue;
3990
4464
  }
3991
- if (action === "e" || action === "r" || action === "c" || action === "p") {
3992
- const name = await pickHost(prompter, output, names);
3993
- if (name == null) {
3994
- continue;
3995
- }
3996
- if (action === "e") {
3997
- await addOrEditHost(prompter, output, target, entries, name);
3998
- } else if (action === "r") {
3999
- const remove = await prompter.askYesNo(`Remove ${name} from ${target.location}?`, false);
4000
- if (remove == true) {
4001
- reportWrite(output, target.removeEntry(name), "Removed");
4002
- }
4003
- } else if (action === "c") {
4004
- await checkHost(prompter, output, target, name, entries[name]);
4005
- } else {
4006
- printHost(output, name, entries[name]);
4007
- }
4008
- continue;
4465
+ if (picked.startsWith(HOST_VALUE_PREFIX) == true) {
4466
+ await manageHost(prompter, output, target, entries, picked.slice(HOST_VALUE_PREFIX.length));
4009
4467
  }
4010
- output.write(` Unknown choice "${action}".
4011
- `);
4012
4468
  } catch (ex) {
4013
4469
  if (ex instanceof PromptClosedError) {
4014
4470
  throw ex;
@@ -4457,7 +4913,7 @@ ${file.content}`);
4457
4913
  const { profile, session: openSession } = resolveProfile(context, { session });
4458
4914
  const resolvedLocalPath = expandHomePath(localPath);
4459
4915
  requirePermittedLocalPath(resolvedLocalPath, "read");
4460
- if (fs7__default.default.existsSync(resolvedLocalPath) == false) {
4916
+ if (fs6__default.default.existsSync(resolvedLocalPath) == false) {
4461
4917
  return textResult(`local file not found: ${resolvedLocalPath}`);
4462
4918
  }
4463
4919
  const transferredBytes = await withHostConnection(profile, openSession, async (connection) => {
@@ -4497,7 +4953,7 @@ ${file.content}`);
4497
4953
  const { profile, session: openSession } = resolveProfile(context, { session });
4498
4954
  const resolvedLocalPath = expandHomePath(localPath);
4499
4955
  requirePermittedLocalPath(resolvedLocalPath, "write");
4500
- fs7__default.default.mkdirSync(path__default.default.dirname(resolvedLocalPath), { recursive: true });
4956
+ fs6__default.default.mkdirSync(path__default.default.dirname(resolvedLocalPath), { recursive: true });
4501
4957
  const transferredBytes = await withHostConnection(profile, openSession, async (connection) => {
4502
4958
  return await downloadFile(connection, remotePath, resolvedLocalPath);
4503
4959
  });