@akms/mcp-ssh 0.1.1 → 0.1.2

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
@@ -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.1";
38
+ var SERVER_VERSION = "0.1.2";
39
39
 
40
40
  // src/_defs/command-rules.ts
41
41
  var CRITICAL_PATH_PATTERN = new RegExp(
@@ -2368,6 +2368,23 @@ async function downloadFile(connection, remotePath, localPath) {
2368
2368
  }
2369
2369
 
2370
2370
  // src/modules/manage/connection-check.ts
2371
+ function profileFromEnv(output, env) {
2372
+ try {
2373
+ const profile = loadHostProfile(env);
2374
+ if (profile == null) {
2375
+ output.write(" The entry has no host.\n");
2376
+ }
2377
+ return profile;
2378
+ } catch (ex) {
2379
+ let detail = String(ex);
2380
+ if (ex instanceof Error) {
2381
+ detail = ex.message;
2382
+ }
2383
+ output.write(` The server would refuse to start with this entry: ${detail}
2384
+ `);
2385
+ return null;
2386
+ }
2387
+ }
2371
2388
  function describeAuth(profile) {
2372
2389
  if (profile.privateKeyPath != null) {
2373
2390
  return `key ${profile.privateKeyPath}`;
@@ -2626,6 +2643,654 @@ function summarizeEntry(entry) {
2626
2643
  notes: notes.join(", ")
2627
2644
  };
2628
2645
  }
2646
+ var PREFERRED_KEY_NAMES = ["id_ed25519", "id_ecdsa", "id_rsa"];
2647
+ var NON_KEY_FILE_NAMES = ["config", "known_hosts", "known_hosts.old", "authorized_keys", "environment"];
2648
+ var MAX_KEY_FILE_BYTES = 64 * 1024;
2649
+ var CLEAR_ANSWER = "-";
2650
+ var OTHER_KEY_PATH = "\0other";
2651
+ var FIELD_NAME_WIDTH = 17;
2652
+ var SUB_FIELDS = ["key", "passphrase", "password", "jump-host", "jump-port", "jump-user", "proxy"];
2653
+ var FIELD_NAMES = {
2654
+ "host": "Host",
2655
+ "port": "Port",
2656
+ "user": "User",
2657
+ "name": "Server name",
2658
+ "auth": "Authentication",
2659
+ "key": "Private key",
2660
+ "passphrase": "Passphrase",
2661
+ "password": "Password",
2662
+ "route": "Route",
2663
+ "jump-host": "Jump host",
2664
+ "jump-port": "Jump port",
2665
+ "jump-user": "Jump user",
2666
+ "proxy": "Proxy command",
2667
+ "description": "Description",
2668
+ "readonly": "Read-only",
2669
+ "paths": "File paths",
2670
+ "host-key": "Host key",
2671
+ "jump-host-key": "Jump host key"
2672
+ };
2673
+ var AUTH_LABELS = {
2674
+ key: "Private key file",
2675
+ password: "Password",
2676
+ agent: "SSH agent"
2677
+ };
2678
+ var ROUTE_LABELS = {
2679
+ "direct": "Directly",
2680
+ "jump": "Through a jump host",
2681
+ "proxy": "Through a proxy command",
2682
+ "jump-proxy": "Proxy command, then a jump host"
2683
+ };
2684
+ var REQUIRED_MARK = "(required)";
2685
+ function findPrivateKeys() {
2686
+ const sshDirectory = path__default.default.join(os5__default.default.homedir(), ".ssh");
2687
+ let names = [];
2688
+ try {
2689
+ names = fs6__default.default.readdirSync(sshDirectory);
2690
+ } catch {
2691
+ return [];
2692
+ }
2693
+ const keys = [];
2694
+ for (const name of names) {
2695
+ if (name.endsWith(".pub") == true || NON_KEY_FILE_NAMES.includes(name) == true) {
2696
+ continue;
2697
+ }
2698
+ const fullPath = path__default.default.join(sshDirectory, name);
2699
+ let head = "";
2700
+ try {
2701
+ const stat = fs6__default.default.statSync(fullPath);
2702
+ if (stat.isFile() == false || stat.size > MAX_KEY_FILE_BYTES) {
2703
+ continue;
2704
+ }
2705
+ const descriptor = fs6__default.default.openSync(fullPath, "r");
2706
+ const buffer = Buffer.alloc(64);
2707
+ const bytesRead = fs6__default.default.readSync(descriptor, buffer, 0, buffer.length, 0);
2708
+ fs6__default.default.closeSync(descriptor);
2709
+ head = buffer.subarray(0, bytesRead).toString("utf8");
2710
+ } catch {
2711
+ continue;
2712
+ }
2713
+ const isPemKey = head.startsWith("-----BEGIN") == true && head.includes("PRIVATE KEY") == true;
2714
+ const isPuttyKey = head.startsWith("PuTTY-User-Key-File") == true;
2715
+ if (isPemKey == false && isPuttyKey == false) {
2716
+ continue;
2717
+ }
2718
+ let hint = "";
2719
+ try {
2720
+ const publicKey = fs6__default.default.readFileSync(`${fullPath}.pub`, "utf8").trim().split(/\s+/);
2721
+ hint = publicKey[0].replace(/^ssh-/, "");
2722
+ if (publicKey.length > 2) {
2723
+ hint += ` \xB7 ${publicKey.slice(2).join(" ")}`;
2724
+ }
2725
+ } catch {
2726
+ }
2727
+ keys.push({ path: `~/.ssh/${name}`, hint });
2728
+ }
2729
+ const rank = (key) => {
2730
+ const index = PREFERRED_KEY_NAMES.indexOf(path__default.default.basename(key.path));
2731
+ if (index < 0) {
2732
+ return PREFERRED_KEY_NAMES.length;
2733
+ }
2734
+ return index;
2735
+ };
2736
+ return keys.sort((left, right) => rank(left) - rank(right) || left.path.localeCompare(right.path));
2737
+ }
2738
+ async function askPort(prompter, question, current) {
2739
+ for (; ; ) {
2740
+ const answer = await prompter.ask(question, String(current));
2741
+ const parsed = Number(answer);
2742
+ if (Number.isInteger(parsed) == true && parsed > 0 && parsed <= 65535) {
2743
+ return parsed;
2744
+ }
2745
+ prompter.write(" Enter a port number between 1 and 65535.\n");
2746
+ }
2747
+ }
2748
+ async function askOptional(prompter, question, current) {
2749
+ const answer = await prompter.ask(question, current);
2750
+ if (answer === "" || answer === CLEAR_ANSWER) {
2751
+ return void 0;
2752
+ }
2753
+ return answer;
2754
+ }
2755
+ async function askOptionalSecret(prompter, question, current) {
2756
+ let suffix = "(empty for none)";
2757
+ if (current != null) {
2758
+ suffix = `(enter keeps current, ${CLEAR_ANSWER} clears)`;
2759
+ }
2760
+ const answer = await prompter.askSecret(`${question} ${prompter.paint.dim(suffix)}`);
2761
+ if (answer === CLEAR_ANSWER) {
2762
+ return void 0;
2763
+ }
2764
+ if (answer === "") {
2765
+ return current;
2766
+ }
2767
+ return answer;
2768
+ }
2769
+ async function askKeyPath(prompter, current) {
2770
+ const found = findPrivateKeys();
2771
+ let picked = OTHER_KEY_PATH;
2772
+ if (found.length > 0 || current != null) {
2773
+ const items = [];
2774
+ if (current != null && found.some((key) => key.path === current) == false) {
2775
+ items.push({ value: current, label: current, hint: "current" });
2776
+ }
2777
+ for (const key of found) {
2778
+ items.push({ value: key.path, label: key.path, hint: key.hint });
2779
+ }
2780
+ items.push({ separator: true });
2781
+ items.push({ value: OTHER_KEY_PATH, key: "other", label: "Another path\u2026" });
2782
+ picked = await prompter.select("Private key", items, { defaultValue: current ?? found[0]?.path });
2783
+ }
2784
+ if (picked !== OTHER_KEY_PATH) {
2785
+ return picked;
2786
+ }
2787
+ for (; ; ) {
2788
+ const typed = await prompter.askRequired("Private key path");
2789
+ if (fs6__default.default.existsSync(expandHomePath(typed)) == true) {
2790
+ return typed;
2791
+ }
2792
+ prompter.write(` ${expandHomePath(typed)} does not exist.
2793
+ `);
2794
+ const useAnyway = await prompter.askYesNo("Use this path anyway?", false);
2795
+ if (useAnyway == true) {
2796
+ return typed;
2797
+ }
2798
+ }
2799
+ }
2800
+ function routeUsesJump(route) {
2801
+ return route === "jump" || route === "jump-proxy";
2802
+ }
2803
+ function routeUsesProxy(route) {
2804
+ return route === "proxy" || route === "jump-proxy";
2805
+ }
2806
+ function deriveServerName(host) {
2807
+ return `ssh-${host}`.replace(/[^A-Za-z0-9_-]+/g, "-").slice(0, 48);
2808
+ }
2809
+ function finalizeAnswers(state) {
2810
+ const answers = { ...state.answers };
2811
+ if (routeUsesJump(state.route) == false) {
2812
+ answers.jumpHost = void 0;
2813
+ answers.jumpPort = void 0;
2814
+ answers.jumpUser = void 0;
2815
+ answers.jumpHostKeyFingerprint = void 0;
2816
+ }
2817
+ if (routeUsesProxy(state.route) == false) {
2818
+ answers.proxyCommand = void 0;
2819
+ }
2820
+ if (answers.auth !== "key") {
2821
+ answers.keyPath = void 0;
2822
+ answers.passphrase = void 0;
2823
+ }
2824
+ if (answers.auth !== "password") {
2825
+ answers.password = void 0;
2826
+ }
2827
+ return answers;
2828
+ }
2829
+ function snapshotOf(state) {
2830
+ return JSON.stringify(finalizeAnswers(state));
2831
+ }
2832
+ function missingFields(state) {
2833
+ const { answers } = state;
2834
+ const missing = [];
2835
+ if (answers.host === "") {
2836
+ missing.push("host");
2837
+ }
2838
+ if (answers.username === "") {
2839
+ missing.push("user");
2840
+ }
2841
+ if (answers.auth === "key" && answers.keyPath == null) {
2842
+ missing.push("key");
2843
+ }
2844
+ if (answers.auth === "password" && answers.password == null) {
2845
+ missing.push("password");
2846
+ }
2847
+ if (routeUsesJump(state.route) == true && answers.jumpHost == null) {
2848
+ missing.push("jump-host");
2849
+ }
2850
+ if (routeUsesProxy(state.route) == true && answers.proxyCommand == null) {
2851
+ missing.push("proxy");
2852
+ }
2853
+ return missing;
2854
+ }
2855
+ function describePin(fingerprint) {
2856
+ if (fingerprint == null) {
2857
+ return "not pinned";
2858
+ }
2859
+ return `pinned \xB7 ${fingerprint.slice(0, 19)}\u2026`;
2860
+ }
2861
+ function valueOrRequired(value) {
2862
+ if (value == null || value === "") {
2863
+ return REQUIRED_MARK;
2864
+ }
2865
+ return value;
2866
+ }
2867
+ function fieldRow(id, value, hint) {
2868
+ let name = FIELD_NAMES[id];
2869
+ if (SUB_FIELDS.includes(id) == true) {
2870
+ name = ` ${name}`;
2871
+ }
2872
+ return {
2873
+ value: id,
2874
+ key: id,
2875
+ label: `${name.padEnd(FIELD_NAME_WIDTH)}${value}`,
2876
+ hint
2877
+ };
2878
+ }
2879
+ function buildRows(state, destination) {
2880
+ const { answers } = state;
2881
+ const rows = [];
2882
+ rows.push(fieldRow("host", valueOrRequired(answers.host)));
2883
+ rows.push(fieldRow("port", String(answers.port)));
2884
+ rows.push(fieldRow("user", valueOrRequired(answers.username)));
2885
+ let nameValue = answers.name;
2886
+ let nameHint = "what the agent sees";
2887
+ if (state.nameTouched == false) {
2888
+ nameHint = "follows the host until you change it";
2889
+ }
2890
+ if (nameValue === "") {
2891
+ nameValue = "ssh-<host>";
2892
+ }
2893
+ rows.push(fieldRow("name", nameValue, nameHint));
2894
+ rows.push(fieldRow("auth", AUTH_LABELS[answers.auth]));
2895
+ if (answers.auth === "key") {
2896
+ rows.push(fieldRow("key", valueOrRequired(answers.keyPath)));
2897
+ let passphrase = "none";
2898
+ if (answers.passphrase != null) {
2899
+ passphrase = "set";
2900
+ }
2901
+ rows.push(fieldRow("passphrase", passphrase));
2902
+ }
2903
+ if (answers.auth === "password") {
2904
+ let password = REQUIRED_MARK;
2905
+ if (answers.password != null) {
2906
+ password = "set";
2907
+ }
2908
+ rows.push(fieldRow("password", password, "stored in the config file as plain text"));
2909
+ }
2910
+ rows.push(fieldRow("route", ROUTE_LABELS[state.route]));
2911
+ if (routeUsesJump(state.route) == true) {
2912
+ rows.push(fieldRow("jump-host", valueOrRequired(answers.jumpHost)));
2913
+ rows.push(fieldRow("jump-port", String(answers.jumpPort ?? DEFAULT_SSH_PORT)));
2914
+ let jumpUser = answers.jumpUser;
2915
+ if (jumpUser == null || jumpUser === answers.username) {
2916
+ let loginUser = answers.username;
2917
+ if (loginUser === "") {
2918
+ loginUser = "\u2026";
2919
+ }
2920
+ jumpUser = `same as login (${loginUser})`;
2921
+ }
2922
+ rows.push(fieldRow("jump-user", jumpUser));
2923
+ }
2924
+ if (routeUsesProxy(state.route) == true) {
2925
+ rows.push(fieldRow("proxy", valueOrRequired(answers.proxyCommand), "%h %p %r \u2192 first hop"));
2926
+ }
2927
+ rows.push(fieldRow("description", answers.description ?? "none", "shown to the agent"));
2928
+ let readonly = "no";
2929
+ if (answers.readonly == true) {
2930
+ readonly = "yes";
2931
+ }
2932
+ rows.push(fieldRow("readonly", readonly, "enter toggles"));
2933
+ let paths = "anywhere";
2934
+ if (answers.allowedPaths.length > 0) {
2935
+ paths = answers.allowedPaths.join(", ");
2936
+ }
2937
+ rows.push(fieldRow("paths", paths, "where the file tools may read and write"));
2938
+ rows.push(fieldRow("host-key", describePin(answers.hostKeyFingerprint)));
2939
+ if (routeUsesJump(state.route) == true) {
2940
+ rows.push(fieldRow("jump-host-key", describePin(answers.jumpHostKeyFingerprint)));
2941
+ }
2942
+ rows.push({ separator: true });
2943
+ let testHint = "not run yet";
2944
+ if (state.lastTest === "ok") {
2945
+ testHint = "last run: OK";
2946
+ } else if (state.lastTest === "failed") {
2947
+ testHint = "last run: failed";
2948
+ }
2949
+ rows.push({ value: "test", key: "test", label: "Test the connection", hint: testHint });
2950
+ rows.push({ value: "save", key: "save", label: "Save", hint: `to ${destination}` });
2951
+ rows.push({ value: "cancel", key: "cancel", label: "Cancel" });
2952
+ return rows;
2953
+ }
2954
+ function nextCursor(state, destination, edited) {
2955
+ if (edited === "readonly") {
2956
+ return edited;
2957
+ }
2958
+ const values = buildRows(state, destination).flatMap((row) => {
2959
+ if ("value" in row) {
2960
+ return [row.value];
2961
+ }
2962
+ return [];
2963
+ });
2964
+ let index = values.indexOf(edited) + 1;
2965
+ if (SUB_FIELDS.includes(edited) == false) {
2966
+ while (index < values.length && SUB_FIELDS.includes(values[index]) == true) {
2967
+ index += 1;
2968
+ }
2969
+ }
2970
+ const next = values[index];
2971
+ if (next == null || next === "test") {
2972
+ return "save";
2973
+ }
2974
+ return next;
2975
+ }
2976
+ async function testConnection(prompter, output, state) {
2977
+ const answers = finalizeAnswers(state);
2978
+ output.write("\n");
2979
+ const profile = profileFromEnv(output, answersToEntry(answers).env);
2980
+ if (profile == null) {
2981
+ output.write("\n");
2982
+ state.lastTest = "failed";
2983
+ return false;
2984
+ }
2985
+ const result = await runConnectionCheck(profile, output);
2986
+ output.write("\n");
2987
+ if (result.connected == true) {
2988
+ if (result.hostKeyFingerprint != null && state.answers.hostKeyFingerprint == null) {
2989
+ const pin = await prompter.askYesNo("Pin this host key? (a changed key then aborts the connection)", true);
2990
+ if (pin == true) {
2991
+ state.answers.hostKeyFingerprint = result.hostKeyFingerprint;
2992
+ }
2993
+ }
2994
+ if (result.jumpHostKeyFingerprint != null && routeUsesJump(state.route) == true && state.answers.jumpHostKeyFingerprint == null) {
2995
+ const pin = await prompter.askYesNo("Pin the jump host key too?", true);
2996
+ if (pin == true) {
2997
+ state.answers.jumpHostKeyFingerprint = result.jumpHostKeyFingerprint;
2998
+ }
2999
+ }
3000
+ state.lastTest = "ok";
3001
+ state.testedSnapshot = snapshotOf(state);
3002
+ return true;
3003
+ }
3004
+ const hostKeyChanged = result.hostKeyFingerprint != null && state.answers.hostKeyFingerprint != null && result.hostKeyFingerprint !== state.answers.hostKeyFingerprint;
3005
+ if (hostKeyChanged == true) {
3006
+ output.write(" The host presented a different key than the pinned one. Re-pin only if you know it was rebuilt.\n");
3007
+ const repin = await prompter.askYesNo(`Pin ${result.hostKeyFingerprint} instead?`, false);
3008
+ if (repin == true) {
3009
+ state.answers.hostKeyFingerprint = result.hostKeyFingerprint ?? void 0;
3010
+ output.write(" Pinned the new key \u2014 test again to confirm.\n");
3011
+ }
3012
+ }
3013
+ const jumpKeyChanged = result.jumpHostKeyFingerprint != null && state.answers.jumpHostKeyFingerprint != null && result.jumpHostKeyFingerprint !== state.answers.jumpHostKeyFingerprint;
3014
+ if (jumpKeyChanged == true) {
3015
+ output.write(" The jump host presented a different key than the pinned one. Re-pin only if you know it was rebuilt.\n");
3016
+ const repin = await prompter.askYesNo(`Pin ${result.jumpHostKeyFingerprint} instead?`, false);
3017
+ if (repin == true) {
3018
+ state.answers.jumpHostKeyFingerprint = result.jumpHostKeyFingerprint ?? void 0;
3019
+ output.write(" Pinned the new key \u2014 test again to confirm.\n");
3020
+ }
3021
+ }
3022
+ state.lastTest = "failed";
3023
+ return false;
3024
+ }
3025
+ async function editField(prompter, output, state, id) {
3026
+ const { answers } = state;
3027
+ if (id === "host") {
3028
+ let current;
3029
+ if (answers.host !== "") {
3030
+ current = answers.host;
3031
+ }
3032
+ answers.host = await prompter.askRequired("Host (hostname or IP)", current);
3033
+ if (state.nameTouched == false) {
3034
+ answers.name = deriveServerName(answers.host);
3035
+ }
3036
+ return;
3037
+ }
3038
+ if (id === "port") {
3039
+ answers.port = await askPort(prompter, "Port", answers.port);
3040
+ return;
3041
+ }
3042
+ if (id === "user") {
3043
+ let current;
3044
+ if (answers.username !== "") {
3045
+ current = answers.username;
3046
+ }
3047
+ answers.username = await prompter.askRequired("Login user", current);
3048
+ return;
3049
+ }
3050
+ if (id === "name") {
3051
+ let current;
3052
+ if (answers.name !== "") {
3053
+ current = answers.name;
3054
+ }
3055
+ for (; ; ) {
3056
+ const name = await prompter.askRequired("Server name the agent sees", current);
3057
+ if (SERVER_NAME_PATTERN.test(name) == true) {
3058
+ answers.name = name;
3059
+ state.nameTouched = true;
3060
+ return;
3061
+ }
3062
+ prompter.write(" Use letters, digits, '-' and '_' only (max 48) \u2014 the name becomes part of every tool name.\n");
3063
+ }
3064
+ }
3065
+ if (id === "auth") {
3066
+ answers.auth = await prompter.select(
3067
+ "Authentication",
3068
+ [
3069
+ { value: "key", label: AUTH_LABELS.key, hint: "only the path is stored \u2014 recommended" },
3070
+ { value: "password", label: AUTH_LABELS.password, hint: "stored in the config file as plain text" },
3071
+ { value: "agent", label: AUTH_LABELS.agent, hint: "keys already loaded in ssh-agent" }
3072
+ ],
3073
+ { defaultValue: answers.auth }
3074
+ );
3075
+ if (answers.auth === "key" && answers.keyPath == null) {
3076
+ answers.keyPath = await askKeyPath(prompter, void 0);
3077
+ }
3078
+ if (answers.auth === "password" && answers.password == null) {
3079
+ answers.password = await askOptionalSecret(prompter, "Password", void 0);
3080
+ }
3081
+ return;
3082
+ }
3083
+ if (id === "key") {
3084
+ answers.keyPath = await askKeyPath(prompter, answers.keyPath);
3085
+ return;
3086
+ }
3087
+ if (id === "passphrase") {
3088
+ answers.passphrase = await askOptionalSecret(prompter, "Key passphrase", answers.passphrase);
3089
+ return;
3090
+ }
3091
+ if (id === "password") {
3092
+ answers.password = await askOptionalSecret(prompter, "Password", answers.password);
3093
+ return;
3094
+ }
3095
+ if (id === "route") {
3096
+ state.route = await prompter.select(
3097
+ "Route",
3098
+ [
3099
+ { value: "direct", label: ROUTE_LABELS.direct },
3100
+ { value: "jump", label: ROUTE_LABELS.jump, hint: "ssh -J" },
3101
+ { value: "proxy", label: ROUTE_LABELS.proxy, hint: "e.g. cloudflared access ssh" },
3102
+ { value: "jump-proxy", label: ROUTE_LABELS["jump-proxy"], hint: "cloudflared to the bastion, then on to the host" }
3103
+ ],
3104
+ { defaultValue: state.route }
3105
+ );
3106
+ if (routeUsesJump(state.route) == true && answers.jumpHost == null) {
3107
+ answers.jumpHost = await prompter.askRequired("Jump host");
3108
+ }
3109
+ if (routeUsesProxy(state.route) == true && answers.proxyCommand == null) {
3110
+ answers.proxyCommand = await prompter.askRequired("Proxy command", "cloudflared access ssh --hostname %h");
3111
+ }
3112
+ return;
3113
+ }
3114
+ if (id === "jump-host") {
3115
+ answers.jumpHost = await prompter.askRequired("Jump host", answers.jumpHost);
3116
+ return;
3117
+ }
3118
+ if (id === "jump-port") {
3119
+ answers.jumpPort = await askPort(prompter, "Jump host port", answers.jumpPort ?? DEFAULT_SSH_PORT);
3120
+ return;
3121
+ }
3122
+ if (id === "jump-user") {
3123
+ answers.jumpUser = await askOptional(prompter, "Jump host user (empty = same as the login user)", answers.jumpUser);
3124
+ return;
3125
+ }
3126
+ if (id === "proxy") {
3127
+ output.write(prompter.paint.dim(" %h / %p / %r expand to the first hop's host, port and user.\n"));
3128
+ answers.proxyCommand = await prompter.askRequired("Proxy command", answers.proxyCommand ?? "cloudflared access ssh --hostname %h");
3129
+ return;
3130
+ }
3131
+ if (id === "description") {
3132
+ answers.description = await askOptional(prompter, "Description shown to the agent", answers.description);
3133
+ return;
3134
+ }
3135
+ if (id === "readonly") {
3136
+ answers.readonly = answers.readonly == false;
3137
+ return;
3138
+ }
3139
+ if (id === "paths") {
3140
+ let current;
3141
+ if (answers.allowedPaths.length > 0) {
3142
+ current = answers.allowedPaths.join(",");
3143
+ }
3144
+ const typed = await askOptional(prompter, "Limit file tools to path prefixes (comma-separated, empty = anywhere)", current);
3145
+ answers.allowedPaths = [];
3146
+ if (typed != null) {
3147
+ answers.allowedPaths = typed.split(",").map((item) => item.trim()).filter((item) => item !== "");
3148
+ }
3149
+ return;
3150
+ }
3151
+ const isJump = id === "jump-host-key";
3152
+ let pinned = answers.hostKeyFingerprint;
3153
+ if (isJump == true) {
3154
+ pinned = answers.jumpHostKeyFingerprint;
3155
+ }
3156
+ if (pinned != null) {
3157
+ const unpin = await prompter.askYesNo(`Unpin ${pinned}?`, false);
3158
+ if (unpin == true && isJump == true) {
3159
+ answers.jumpHostKeyFingerprint = void 0;
3160
+ } else if (unpin == true) {
3161
+ answers.hostKeyFingerprint = void 0;
3162
+ }
3163
+ return;
3164
+ }
3165
+ const missing = missingFields(state);
3166
+ if (missing.length > 0) {
3167
+ output.write(` Fill in ${missing.map((field) => FIELD_NAMES[field]).join(", ")} first \u2014 the key is pinned by testing the connection.
3168
+ `);
3169
+ return;
3170
+ }
3171
+ await testConnection(prompter, output, state);
3172
+ }
3173
+ async function runHostForm(prompter, output, options) {
3174
+ const { existing, takenNames, destination } = options;
3175
+ let state;
3176
+ if (existing != null) {
3177
+ let route = "direct";
3178
+ if (existing.jumpHost != null && existing.proxyCommand != null) {
3179
+ route = "jump-proxy";
3180
+ } else if (existing.jumpHost != null) {
3181
+ route = "jump";
3182
+ } else if (existing.proxyCommand != null) {
3183
+ route = "proxy";
3184
+ }
3185
+ state = { answers: { ...existing }, route, nameTouched: true, lastTest: "never", testedSnapshot: null };
3186
+ } else {
3187
+ const foundKeys = findPrivateKeys();
3188
+ state = {
3189
+ answers: {
3190
+ name: "",
3191
+ host: "",
3192
+ port: DEFAULT_SSH_PORT,
3193
+ username: "",
3194
+ auth: "key",
3195
+ keyPath: foundKeys[0]?.path,
3196
+ readonly: false,
3197
+ allowedPaths: [],
3198
+ extraEnv: {}
3199
+ },
3200
+ route: "direct",
3201
+ nameTouched: false,
3202
+ lastTest: "never",
3203
+ testedSnapshot: null
3204
+ };
3205
+ }
3206
+ const initialSnapshot = snapshotOf(state);
3207
+ const originalName = existing?.name;
3208
+ let title = "New host";
3209
+ if (existing != null) {
3210
+ title = `Editing ${existing.name}`;
3211
+ }
3212
+ output.write(prompter.paint.dim(`
3213
+ Pick a setting to change it. In a text field, Enter keeps the value shown and ${CLEAR_ANSWER} clears it.
3214
+
3215
+ `));
3216
+ const previousTransient = prompter.transient;
3217
+ prompter.transient = true;
3218
+ try {
3219
+ let cursor = "host";
3220
+ for (; ; ) {
3221
+ const picked = await prompter.select(title, buildRows(state, destination), { defaultValue: cursor, cancelValue: "cancel" });
3222
+ if (picked === "cancel") {
3223
+ if (snapshotOf(state) !== initialSnapshot) {
3224
+ const discard = await prompter.askYesNo("Discard the changes?", false);
3225
+ if (discard == false) {
3226
+ continue;
3227
+ }
3228
+ }
3229
+ return null;
3230
+ }
3231
+ const missing = missingFields(state);
3232
+ if (picked === "test") {
3233
+ if (missing.length > 0) {
3234
+ output.write(` Fill in ${missing.map((field) => FIELD_NAMES[field]).join(", ")} first.
3235
+ `);
3236
+ cursor = missing[0];
3237
+ continue;
3238
+ }
3239
+ const connected = await testConnection(prompter, output, state);
3240
+ cursor = "test";
3241
+ if (connected == true) {
3242
+ cursor = "save";
3243
+ }
3244
+ continue;
3245
+ }
3246
+ if (picked === "save") {
3247
+ if (missing.length > 0) {
3248
+ output.write(` Fill in ${missing.map((field) => FIELD_NAMES[field]).join(", ")} first.
3249
+ `);
3250
+ cursor = missing[0];
3251
+ continue;
3252
+ }
3253
+ const name = state.answers.name;
3254
+ if (name !== originalName && takenNames.includes(name) == true) {
3255
+ const replace = await prompter.askYesNo(`"${name}" already exists in this file. Replace it?`, false);
3256
+ if (replace == false) {
3257
+ cursor = "name";
3258
+ continue;
3259
+ }
3260
+ }
3261
+ if (snapshotOf(state) !== state.testedSnapshot) {
3262
+ const testFirst = await prompter.askYesNo("Test the connection before saving?", true);
3263
+ if (testFirst == true) {
3264
+ const connected = await testConnection(prompter, output, state);
3265
+ if (connected == false) {
3266
+ const next = await prompter.select(
3267
+ "The check failed. What now?",
3268
+ [
3269
+ { value: "back", key: "back", label: "Back to the settings" },
3270
+ { value: "save", key: "save", label: "Save anyway", hint: "the host may be unreachable from here" },
3271
+ { value: "discard", key: "discard", label: "Discard", hint: "nothing is written" }
3272
+ ],
3273
+ { defaultValue: "back", cancelValue: "back" }
3274
+ );
3275
+ if (next === "discard") {
3276
+ return null;
3277
+ }
3278
+ if (next === "back") {
3279
+ cursor = "test";
3280
+ continue;
3281
+ }
3282
+ }
3283
+ }
3284
+ }
3285
+ return finalizeAnswers(state);
3286
+ }
3287
+ await editField(prompter, output, state, picked);
3288
+ cursor = nextCursor(state, destination, picked);
3289
+ }
3290
+ } finally {
3291
+ prompter.transient = previousTransient;
3292
+ }
3293
+ }
2629
3294
 
2630
3295
  // src/modules/manage/prompter.ts
2631
3296
  var PromptClosedError = class extends Error {
@@ -2740,6 +3405,12 @@ var Prompter = class {
2740
3405
  this.inputEnded = false;
2741
3406
  /** Set while a key-driven prompt (select, secret) owns the input. */
2742
3407
  this.keyHandler = null;
3408
+ /**
3409
+ * When true, an answered prompt leaves nothing behind on a TTY instead of its
3410
+ * `✔ question · answer` line. The settings form sets it: the form itself shows every
3411
+ * value, so it can redraw in place rather than scroll a trail of edits.
3412
+ */
3413
+ this.transient = false;
2743
3414
  const ttyInput = input;
2744
3415
  const ttyOutput = output;
2745
3416
  this.interactive = ttyInput.isTTY == true && ttyInput.setRawMode != null && ttyOutput.isTTY == true;
@@ -3039,6 +3710,9 @@ var Prompter = class {
3039
3710
  }
3040
3711
  /** The one-line record an answered prompt leaves behind on a TTY. */
3041
3712
  writeAnswered(question, answer) {
3713
+ if (this.transient == true) {
3714
+ return;
3715
+ }
3042
3716
  this.output.write(`${this.style.success(this.symbols.tick)} ${question} ${this.style.dim("\xB7")} ${this.style.accent(answer)}
3043
3717
  `);
3044
3718
  }
@@ -3171,242 +3845,6 @@ var Prompter = class {
3171
3845
  });
3172
3846
  }
3173
3847
  };
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;
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
- }
3232
- async function askPort(prompter, question, current) {
3233
- for (; ; ) {
3234
- const answer = await prompter.ask(question, String(current));
3235
- const parsed = Number(answer);
3236
- if (Number.isInteger(parsed) == true && parsed > 0 && parsed <= 65535) {
3237
- return parsed;
3238
- }
3239
- prompter.write(" Enter a port number between 1 and 65535.\n");
3240
- }
3241
- }
3242
- async function askOptional(prompter, question, current) {
3243
- const answer = await prompter.ask(question, current);
3244
- if (answer === "" || answer === CLEAR_ANSWER) {
3245
- return void 0;
3246
- }
3247
- return answer;
3248
- }
3249
- async function askOptionalSecret(prompter, question, current) {
3250
- let suffix = "(empty for none)";
3251
- if (current != null) {
3252
- suffix = `(enter keeps current, ${CLEAR_ANSWER} clears)`;
3253
- }
3254
- const answer = await prompter.askSecret(`${question} ${prompter.paint.dim(suffix)}`);
3255
- if (answer === CLEAR_ANSWER) {
3256
- return void 0;
3257
- }
3258
- if (answer === "") {
3259
- return current;
3260
- }
3261
- return answer;
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
- }
3294
- async function askHostQuestions(prompter, existing) {
3295
- if (existing != null) {
3296
- prompter.write(prompter.paint.dim(` Enter keeps the value in brackets; ${CLEAR_ANSWER} clears an optional one.
3297
-
3298
- `));
3299
- }
3300
- const host = await prompter.askRequired("Host (hostname or IP)", existing?.host);
3301
- const port = await askPort(prompter, "Port", existing?.port ?? DEFAULT_SSH_PORT);
3302
- const username = await prompter.askRequired("Login user", existing?.username);
3303
- let name = "";
3304
- const defaultName = existing?.name ?? `ssh-${host}`.replace(/[^A-Za-z0-9_-]+/g, "-").slice(0, 48);
3305
- for (; ; ) {
3306
- name = await prompter.askRequired("Server name the agent sees", defaultName);
3307
- if (SERVER_NAME_PATTERN.test(name) == true) {
3308
- break;
3309
- }
3310
- prompter.write(" Use letters, digits, '-' and '_' only (max 48) \u2014 the name becomes part of every tool name.\n");
3311
- }
3312
- const auth = await prompter.select(
3313
- "Authentication",
3314
- [
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" }
3318
- ],
3319
- { defaultValue: existing?.auth ?? "key" }
3320
- );
3321
- let keyPath;
3322
- let passphrase;
3323
- let password;
3324
- if (auth === "key") {
3325
- keyPath = await askKeyPath(prompter, existing?.keyPath);
3326
- passphrase = await askOptionalSecret(prompter, "Key passphrase", existing?.passphrase);
3327
- }
3328
- if (auth === "password") {
3329
- password = await askOptionalSecret(prompter, "Password", existing?.password);
3330
- if (password == null) {
3331
- prompter.write(" No password given \u2014 falling back to the ssh agent.\n");
3332
- }
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
- );
3352
- let jumpHost;
3353
- let jumpPort;
3354
- let jumpUser;
3355
- let proxyCommand;
3356
- if (route === "jump" || route === "jump-proxy") {
3357
- jumpHost = await prompter.askRequired("Jump host", existing?.jumpHost);
3358
- jumpPort = await askPort(prompter, "Jump host port", existing?.jumpPort ?? DEFAULT_SSH_PORT);
3359
- jumpUser = await prompter.askRequired("Jump host user", existing?.jumpUser ?? username);
3360
- }
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"));
3363
- proxyCommand = await prompter.askRequired("Proxy command", existing?.proxyCommand ?? "cloudflared access ssh --hostname %h");
3364
- }
3365
- const description = await askOptional(prompter, "Description shown to the agent", existing?.description);
3366
- const readonly = await prompter.askYesNo("Read-only? (rejects writes, redirections, uploads)", existing?.readonly ?? false);
3367
- let currentAllowedPaths;
3368
- if (existing != null && existing.allowedPaths.length > 0) {
3369
- currentAllowedPaths = existing.allowedPaths.join(",");
3370
- }
3371
- let allowedPaths = [];
3372
- const allowedPathsAnswer = await askOptional(
3373
- prompter,
3374
- "Limit file tools to path prefixes (comma-separated, empty = anywhere)",
3375
- currentAllowedPaths
3376
- );
3377
- if (allowedPathsAnswer != null) {
3378
- allowedPaths = allowedPathsAnswer.split(",").map((item) => item.trim()).filter((item) => item !== "");
3379
- }
3380
- let resolvedAuth = auth;
3381
- if (auth === "password" && password == null) {
3382
- resolvedAuth = "agent";
3383
- }
3384
- const answers = {
3385
- name,
3386
- host,
3387
- port,
3388
- username,
3389
- auth: resolvedAuth,
3390
- keyPath,
3391
- passphrase,
3392
- password,
3393
- description,
3394
- jumpHost,
3395
- jumpPort,
3396
- jumpUser,
3397
- proxyCommand,
3398
- readonly,
3399
- allowedPaths,
3400
- extraEnv: existing?.extraEnv ?? {}
3401
- };
3402
- if (existing != null && existing.host === host && existing.port === port) {
3403
- answers.hostKeyFingerprint = existing.hostKeyFingerprint;
3404
- }
3405
- if (existing != null && existing.jumpHost === jumpHost && existing.jumpPort === jumpPort) {
3406
- answers.jumpHostKeyFingerprint = existing.jumpHostKeyFingerprint;
3407
- }
3408
- return answers;
3409
- }
3410
3848
  function toMcpServerEntry(value) {
3411
3849
  if (typeof value !== "object" || value == null) {
3412
3850
  return null;
@@ -4134,113 +4572,20 @@ function reportWrite(prompter, result, action) {
4134
4572
 
4135
4573
  `);
4136
4574
  }
4137
- function profileFromEntry(output, entry) {
4138
- try {
4139
- const profile = loadHostProfile(entry.env);
4140
- if (profile == null) {
4141
- output.write(" The entry has no host.\n");
4142
- }
4143
- return profile;
4144
- } catch (ex) {
4145
- output.write(` The server would refuse to start with this entry: ${errorMessage(ex)}
4146
- `);
4147
- return null;
4148
- }
4149
- }
4150
- async function verifyAnswers(prompter, output, initial) {
4151
- let answers = initial;
4152
- for (; ; ) {
4153
- const testNow = await prompter.askYesNo("Test the connection now?", true);
4154
- if (testNow == false) {
4155
- return answers;
4156
- }
4157
- output.write("\n");
4158
- const profile = profileFromEntry(output, answersToEntry(answers));
4159
- if (profile != null) {
4160
- const result = await runConnectionCheck(profile, output);
4161
- output.write("\n");
4162
- if (result.connected == true) {
4163
- if (result.hostKeyFingerprint != null && answers.hostKeyFingerprint == null) {
4164
- const pin = await prompter.askYesNo("Pin this host key? (a changed key then aborts the connection)", true);
4165
- if (pin == true) {
4166
- answers = { ...answers, hostKeyFingerprint: result.hostKeyFingerprint };
4167
- }
4168
- }
4169
- if (result.jumpHostKeyFingerprint != null && answers.jumpHost != null && answers.jumpHostKeyFingerprint == null) {
4170
- const pin = await prompter.askYesNo("Pin the jump host key too?", true);
4171
- if (pin == true) {
4172
- answers = { ...answers, jumpHostKeyFingerprint: result.jumpHostKeyFingerprint };
4173
- }
4174
- }
4175
- return answers;
4176
- }
4177
- const hostKeyChanged = result.hostKeyFingerprint != null && answers.hostKeyFingerprint != null && result.hostKeyFingerprint !== answers.hostKeyFingerprint;
4178
- if (hostKeyChanged == true) {
4179
- output.write(" The host presented a different key than the pinned one. Re-pin only if you know it was rebuilt.\n");
4180
- const repin = await prompter.askYesNo(`Pin ${result.hostKeyFingerprint} instead?`, false);
4181
- if (repin == true) {
4182
- answers = { ...answers, hostKeyFingerprint: result.hostKeyFingerprint ?? void 0 };
4183
- continue;
4184
- }
4185
- }
4186
- const jumpKeyChanged = result.jumpHostKeyFingerprint != null && answers.jumpHostKeyFingerprint != null && result.jumpHostKeyFingerprint !== answers.jumpHostKeyFingerprint;
4187
- if (jumpKeyChanged == true) {
4188
- output.write(" The jump host presented a different key than the pinned one. Re-pin only if you know it was rebuilt.\n");
4189
- const repin = await prompter.askYesNo(`Pin ${result.jumpHostKeyFingerprint} instead?`, false);
4190
- if (repin == true) {
4191
- answers = { ...answers, jumpHostKeyFingerprint: result.jumpHostKeyFingerprint ?? void 0 };
4192
- continue;
4193
- }
4194
- }
4195
- }
4196
- const next = await prompter.select(
4197
- "The check failed. What now?",
4198
- [
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" }
4203
- ],
4204
- { defaultValue: "edit", cancelValue: "cancel" }
4205
- );
4206
- if (next === "cancel") {
4207
- return null;
4208
- }
4209
- if (next === "write") {
4210
- return answers;
4211
- }
4212
- if (next === "edit") {
4213
- output.write("\n");
4214
- answers = await askHostQuestions(prompter, answers);
4215
- }
4216
- }
4217
- }
4218
4575
  async function addOrEditHost(prompter, output, target, entries, existingName) {
4219
4576
  let existing;
4220
4577
  if (existingName != null) {
4221
4578
  existing = entryToAnswers(existingName, entries[existingName]);
4222
- output.write(`
4223
- ${prompter.paint.bold(`Editing ${existingName}`)}
4224
- `);
4225
- } else {
4226
- output.write(`
4227
- ${prompter.paint.bold("Adding a host")}
4228
- `);
4229
4579
  }
4230
- const asked = await askHostQuestions(prompter, existing);
4231
- output.write("\n");
4232
- const answers = await verifyAnswers(prompter, output, asked);
4580
+ const answers = await runHostForm(prompter, output, {
4581
+ existing,
4582
+ takenNames: Object.keys(entries),
4583
+ destination: target.label
4584
+ });
4233
4585
  if (answers == null) {
4234
4586
  output.write(" Cancelled \u2014 nothing was written.\n\n");
4235
4587
  return;
4236
4588
  }
4237
- if (answers.name !== existingName && entries[answers.name] != null) {
4238
- const replace = await prompter.askYesNo(`"${answers.name}" already exists in this file. Replace it?`, false);
4239
- if (replace == false) {
4240
- output.write(" Cancelled \u2014 nothing was written.\n\n");
4241
- return;
4242
- }
4243
- }
4244
4589
  const entry = answersToEntry(answers);
4245
4590
  output.write(`
4246
4591
  ${prompter.paint.dim(`This goes into ${target.location}:`)}
@@ -4267,7 +4612,7 @@ ${prompter.paint.bold("Adding a host")}
4267
4612
  }
4268
4613
  async function checkHost(prompter, output, target, name, entry) {
4269
4614
  output.write("\n");
4270
- const profile = profileFromEntry(output, entry);
4615
+ const profile = profileFromEnv(output, entry.env);
4271
4616
  if (profile == null) {
4272
4617
  output.write("\n");
4273
4618
  return;
@@ -4341,7 +4686,7 @@ async function manageHost(prompter, output, target, entries, name) {
4341
4686
  const action = await prompter.select(
4342
4687
  name,
4343
4688
  [
4344
- { value: "edit", key: "e", label: "Edit", hint: "change the answers, re-check, re-write" },
4689
+ { value: "edit", key: "e", label: "Edit", hint: "open its settings" },
4345
4690
  { value: "check", key: "c", label: "Check the connection", hint: "connect now and offer to pin the host key" },
4346
4691
  { value: "print", key: "p", label: "Print the entry", hint: "JSON, Codex TOML and a claude command, ready to paste" },
4347
4692
  { value: "remove", key: "r", label: "Remove", hint: `from ${target.label}` },