@akms/mcp-ssh 0.1.6 → 0.1.8

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/lib/cli.cjs CHANGED
@@ -2,7 +2,7 @@
2
2
  'use strict';
3
3
 
4
4
  var stdio_js = require('@modelcontextprotocol/sdk/server/stdio.js');
5
- var fs6 = require('fs');
5
+ var fs7 = require('fs');
6
6
  var os5 = require('os');
7
7
  var path = require('path');
8
8
  var child_process = require('child_process');
@@ -11,12 +11,14 @@ var stream = require('stream');
11
11
  var string_decoder = require('string_decoder');
12
12
  var ssh2 = require('ssh2');
13
13
  var timers = require('timers');
14
+ var util = require('util');
15
+ var smolToml = require('smol-toml');
14
16
  var mcp_js = require('@modelcontextprotocol/sdk/server/mcp.js');
15
17
  var zod = require('zod');
16
18
 
17
19
  function _interopDefault (e) { return e && e.__esModule ? e : { default: e }; }
18
20
 
19
- var fs6__default = /*#__PURE__*/_interopDefault(fs6);
21
+ var fs7__default = /*#__PURE__*/_interopDefault(fs7);
20
22
  var os5__default = /*#__PURE__*/_interopDefault(os5);
21
23
  var path__default = /*#__PURE__*/_interopDefault(path);
22
24
  var ssh2__default = /*#__PURE__*/_interopDefault(ssh2);
@@ -36,7 +38,7 @@ var CWD_MARKER = "__AKMS_SSH_CWD__";
36
38
  var MAX_DIRECTORY_ENTRIES = 1e3;
37
39
  var SERVER_NAME = "akms-mcp-ssh";
38
40
  var PACKAGE_NAME = "@akms/mcp-ssh";
39
- var SERVER_VERSION = "0.1.6";
41
+ var SERVER_VERSION = "0.1.8";
40
42
 
41
43
  // src/_defs/command-rules.ts
42
44
  var CRITICAL_PATH_PATTERN = new RegExp(
@@ -622,7 +624,7 @@ function findExecutable(name) {
622
624
  for (const directory of directories) {
623
625
  for (const suffix of suffixes) {
624
626
  const candidate = path__default.default.join(directory, `${name}${suffix}`);
625
- if (fs6__default.default.existsSync(candidate) == true && fs6__default.default.statSync(candidate).isFile() == true) {
627
+ if (fs7__default.default.existsSync(candidate) == true && fs7__default.default.statSync(candidate).isFile() == true) {
626
628
  return candidate;
627
629
  }
628
630
  }
@@ -763,7 +765,7 @@ function loadHostProfile(env = process.env) {
763
765
  if (keyPath != null) {
764
766
  const expandedKeyPath = expandHomePath(keyPath);
765
767
  profile.privateKeyPath = expandedKeyPath;
766
- if (fs6__default.default.existsSync(expandedKeyPath) == false) {
768
+ if (fs7__default.default.existsSync(expandedKeyPath) == false) {
767
769
  _logger.warn(`${SINGLE_HOST_ENV.KEY} does not exist; connections will fail`, {
768
770
  private_key_path: expandedKeyPath
769
771
  });
@@ -795,7 +797,7 @@ function loadHostProfile(env = process.env) {
795
797
  if (jumpKeyPath != null) {
796
798
  profile.jump.privateKeyPath = expandHomePath(jumpKeyPath);
797
799
  profile.jump.passphrase = readEnv(env, SINGLE_HOST_ENV.JUMP_PASSPHRASE);
798
- if (fs6__default.default.existsSync(profile.jump.privateKeyPath) == false) {
800
+ if (fs7__default.default.existsSync(profile.jump.privateKeyPath) == false) {
799
801
  _logger.warn(`${SINGLE_HOST_ENV.JUMP_KEY} does not exist; connections will fail`, {
800
802
  private_key_path: profile.jump.privateKeyPath
801
803
  });
@@ -1281,14 +1283,14 @@ function inspectRemotePath(remotePath, policy, access) {
1281
1283
  function toRealPath(inputPath) {
1282
1284
  const _logger = $logger.child({ context: "toRealPath" });
1283
1285
  try {
1284
- return fs6__default.default.realpathSync.native(inputPath);
1286
+ return fs7__default.default.realpathSync.native(inputPath);
1285
1287
  } catch (ex) {
1286
1288
  _logger.debug("path does not resolve, falling back to its parent directory", {
1287
1289
  reason: ex instanceof Error ? ex.name : "unknown"
1288
1290
  });
1289
1291
  }
1290
1292
  try {
1291
- const parentRealPath = fs6__default.default.realpathSync.native(path__default.default.dirname(inputPath));
1293
+ const parentRealPath = fs7__default.default.realpathSync.native(path__default.default.dirname(inputPath));
1292
1294
  return path__default.default.join(parentRealPath, path__default.default.basename(inputPath));
1293
1295
  } catch (ex) {
1294
1296
  _logger.debug("parent directory does not resolve either, comparing lexically", {
@@ -1380,7 +1382,7 @@ function readPrivateKey(label, keyPath) {
1380
1382
  return cached;
1381
1383
  }
1382
1384
  try {
1383
- const contents = fs6__default.default.readFileSync(keyPath);
1385
+ const contents = fs7__default.default.readFileSync(keyPath);
1384
1386
  PRIVATE_KEY_CACHE.set(keyPath, contents);
1385
1387
  return contents;
1386
1388
  } catch (ex) {
@@ -1649,7 +1651,7 @@ var SshConnection = class {
1649
1651
  * @throws {Error} If the connection was closed, authentication fails, the host is
1650
1652
  * unreachable, the handshake times out, or the carrier cannot be opened.
1651
1653
  */
1652
- async connect() {
1654
+ async connect(onProgress) {
1653
1655
  if (this.disposed == true) {
1654
1656
  throw new Error(`ssh connection for '${this.profile.name}' has been closed; open a new session`);
1655
1657
  }
@@ -1671,6 +1673,9 @@ var SshConnection = class {
1671
1673
  this.observedFingerprint = fingerprint;
1672
1674
  });
1673
1675
  this.connecting = (async () => {
1676
+ if (this.profile.jump != null || this.profile.proxyCommand != null) {
1677
+ onProgress?.("transport");
1678
+ }
1674
1679
  const transport = await openTransport(this.profile, (fingerprint) => {
1675
1680
  this.observedJumpFingerprint = fingerprint;
1676
1681
  });
@@ -1681,6 +1686,7 @@ var SshConnection = class {
1681
1686
  if (transport != null) {
1682
1687
  connectConfig.sock = transport.sock;
1683
1688
  }
1689
+ onProgress?.("host");
1684
1690
  await new Promise((resolve, reject) => {
1685
1691
  const client = new Client();
1686
1692
  this.pendingClient = client;
@@ -2331,7 +2337,7 @@ async function writeRemoteFile(connection, args) {
2331
2337
  }
2332
2338
  async function uploadFile(connection, localPath, remotePath) {
2333
2339
  requirePermittedPath(connection, remotePath, "write");
2334
- const localStats = fs6__default.default.statSync(localPath);
2340
+ const localStats = fs7__default.default.statSync(localPath);
2335
2341
  if (localStats.isFile() == false) {
2336
2342
  throw new Error(`local path is not a file: ${localPath}`);
2337
2343
  }
@@ -2368,7 +2374,7 @@ async function downloadFile(connection, remotePath, localPath) {
2368
2374
  });
2369
2375
  });
2370
2376
  });
2371
- const localStats = fs6__default.default.statSync(localPath);
2377
+ const localStats = fs7__default.default.statSync(localPath);
2372
2378
  const _logger = $logger.child({
2373
2379
  context: "downloadFile",
2374
2380
  profile: connection.profile.name,
@@ -2379,283 +2385,6 @@ async function downloadFile(connection, remotePath, localPath) {
2379
2385
  return localStats.size;
2380
2386
  }
2381
2387
 
2382
- // src/modules/manage/connection-check.ts
2383
- function profileFromEnv(output, env) {
2384
- try {
2385
- const profile = loadHostProfile(env);
2386
- if (profile == null) {
2387
- output.write(" The entry has no host.\n");
2388
- }
2389
- return profile;
2390
- } catch (ex) {
2391
- let detail = String(ex);
2392
- if (ex instanceof Error) {
2393
- detail = ex.message;
2394
- }
2395
- output.write(` The server would refuse to start with this entry: ${detail}
2396
- `);
2397
- return null;
2398
- }
2399
- }
2400
- function describeAuth(profile) {
2401
- if (profile.privateKeyPath != null) {
2402
- return `key ${profile.privateKeyPath}`;
2403
- }
2404
- if (profile.password != null) {
2405
- return "password";
2406
- }
2407
- return "ssh agent";
2408
- }
2409
- async function runConnectionCheck(profile, output) {
2410
- const endpoint = `${profile.username}@${profile.host}:${profile.port}`;
2411
- const auth = describeAuth(profile);
2412
- output.write(`Checking ${profile.name} \u2192 ${endpoint}
2413
- `);
2414
- if (profile.proxyCommand != null) {
2415
- output.write(` via proxy command: ${profile.proxyCommand}
2416
- `);
2417
- }
2418
- if (profile.jump != null) {
2419
- output.write(` via jump host: ${profile.jump.username}@${profile.jump.host}:${profile.jump.port}
2420
- `);
2421
- }
2422
- output.write("\n");
2423
- const connection = new SshConnection(profile);
2424
- const startedAt = Date.now();
2425
- let connected = false;
2426
- let detail = "";
2427
- try {
2428
- await connection.connect();
2429
- connected = true;
2430
- } catch (ex) {
2431
- detail = String(ex);
2432
- if (ex instanceof Error) {
2433
- detail = ex.message;
2434
- }
2435
- } finally {
2436
- connection.disconnect();
2437
- }
2438
- const durationMs = Date.now() - startedAt;
2439
- if (connected == true) {
2440
- output.write(` OK ${endpoint} (${auth}, ${durationMs}ms)
2441
- `);
2442
- } else {
2443
- output.write(` FAILED ${endpoint} (${auth})
2444
- `);
2445
- output.write(` ${detail}
2446
- `);
2447
- }
2448
- if (connection.hostKeyFingerprint != null) {
2449
- let pinNote = `not pinned \u2014 set ${SINGLE_HOST_ENV.HOST_KEY} to it to detect a changed key`;
2450
- if (profile.hostKeyFingerprint != null) {
2451
- pinNote = "pinned";
2452
- }
2453
- output.write(` host key ${connection.hostKeyFingerprint} (${pinNote})
2454
- `);
2455
- }
2456
- if (connection.jumpHostKeyFingerprint != null) {
2457
- let pinNote = `not pinned \u2014 set ${SINGLE_HOST_ENV.JUMP_HOST_KEY} to it to detect a changed key`;
2458
- if (profile.jump?.hostKeyFingerprint != null) {
2459
- pinNote = "pinned";
2460
- }
2461
- output.write(` jump host key ${connection.jumpHostKeyFingerprint} (${pinNote})
2462
- `);
2463
- }
2464
- return {
2465
- connected,
2466
- hostKeyFingerprint: connection.hostKeyFingerprint,
2467
- jumpHostKeyFingerprint: connection.jumpHostKeyFingerprint
2468
- };
2469
- }
2470
-
2471
- // src/modules/manage/entry.ts
2472
- var MANAGED_ENV_NAMES = [
2473
- SINGLE_HOST_ENV.HOST,
2474
- SINGLE_HOST_ENV.PORT,
2475
- SINGLE_HOST_ENV.USER,
2476
- SINGLE_HOST_ENV.KEY,
2477
- SINGLE_HOST_ENV.PASSPHRASE,
2478
- SINGLE_HOST_ENV.PASSWORD,
2479
- SINGLE_HOST_ENV.DESCRIPTION,
2480
- SINGLE_HOST_ENV.JUMP_HOST,
2481
- SINGLE_HOST_ENV.JUMP_PORT,
2482
- SINGLE_HOST_ENV.JUMP_USER,
2483
- SINGLE_HOST_ENV.PROXY_COMMAND,
2484
- SINGLE_HOST_ENV.READONLY,
2485
- SINGLE_HOST_ENV.ALLOWED_PATHS,
2486
- SINGLE_HOST_ENV.HOST_KEY,
2487
- SINGLE_HOST_ENV.JUMP_HOST_KEY
2488
- ];
2489
- var JUMP_ENV_PREFIX = "SSH_JUMP_";
2490
- var SERVER_NAME_PATTERN = /^[A-Za-z0-9_-]{1,48}$/;
2491
- function isOurEntry(entry) {
2492
- if (entry.args.includes(PACKAGE_NAME) == true) {
2493
- return true;
2494
- }
2495
- const commandBase = entry.command.replace(/\\/g, "/").split("/").pop() ?? "";
2496
- return commandBase === SERVER_NAME || commandBase.startsWith(`${SERVER_NAME}.`) == true;
2497
- }
2498
- function launchCommand() {
2499
- if (process.platform === "win32") {
2500
- return { command: "cmd", args: ["/c", "npx", "-y", PACKAGE_NAME] };
2501
- }
2502
- return { command: "npx", args: ["-y", PACKAGE_NAME] };
2503
- }
2504
- function answersToEntry(answers) {
2505
- const env = {};
2506
- env[SINGLE_HOST_ENV.HOST] = answers.host;
2507
- if (answers.port !== DEFAULT_SSH_PORT) {
2508
- env[SINGLE_HOST_ENV.PORT] = String(answers.port);
2509
- }
2510
- env[SINGLE_HOST_ENV.USER] = answers.username;
2511
- if (answers.auth === "key" && answers.keyPath != null) {
2512
- env[SINGLE_HOST_ENV.KEY] = answers.keyPath;
2513
- if (answers.passphrase != null && answers.passphrase !== "") {
2514
- env[SINGLE_HOST_ENV.PASSPHRASE] = answers.passphrase;
2515
- }
2516
- }
2517
- if (answers.auth === "password" && answers.password != null) {
2518
- env[SINGLE_HOST_ENV.PASSWORD] = answers.password;
2519
- }
2520
- if (answers.hostKeyFingerprint != null) {
2521
- env[SINGLE_HOST_ENV.HOST_KEY] = answers.hostKeyFingerprint;
2522
- }
2523
- if (answers.description != null && answers.description !== "") {
2524
- env[SINGLE_HOST_ENV.DESCRIPTION] = answers.description;
2525
- }
2526
- if (answers.jumpHost != null) {
2527
- env[SINGLE_HOST_ENV.JUMP_HOST] = answers.jumpHost;
2528
- if (answers.jumpPort != null && answers.jumpPort !== DEFAULT_SSH_PORT) {
2529
- env[SINGLE_HOST_ENV.JUMP_PORT] = String(answers.jumpPort);
2530
- }
2531
- if (answers.jumpUser != null && answers.jumpUser !== answers.username) {
2532
- env[SINGLE_HOST_ENV.JUMP_USER] = answers.jumpUser;
2533
- }
2534
- if (answers.jumpHostKeyFingerprint != null) {
2535
- env[SINGLE_HOST_ENV.JUMP_HOST_KEY] = answers.jumpHostKeyFingerprint;
2536
- }
2537
- }
2538
- if (answers.proxyCommand != null && answers.proxyCommand !== "") {
2539
- env[SINGLE_HOST_ENV.PROXY_COMMAND] = answers.proxyCommand;
2540
- }
2541
- if (answers.readonly == true) {
2542
- env[SINGLE_HOST_ENV.READONLY] = "true";
2543
- }
2544
- if (answers.allowedPaths.length > 0) {
2545
- env[SINGLE_HOST_ENV.ALLOWED_PATHS] = answers.allowedPaths.join(",");
2546
- }
2547
- for (const [name, value] of Object.entries(answers.extraEnv)) {
2548
- if (MANAGED_ENV_NAMES.includes(name) == true) {
2549
- continue;
2550
- }
2551
- if (answers.jumpHost == null && name.startsWith(JUMP_ENV_PREFIX) == true) {
2552
- continue;
2553
- }
2554
- env[name] = value;
2555
- }
2556
- const launch = launchCommand();
2557
- return {
2558
- command: launch.command,
2559
- args: launch.args,
2560
- env
2561
- };
2562
- }
2563
- function readPort(value) {
2564
- if (value == null) {
2565
- return DEFAULT_SSH_PORT;
2566
- }
2567
- const parsed = Number(value);
2568
- if (Number.isInteger(parsed) == false || parsed <= 0) {
2569
- return DEFAULT_SSH_PORT;
2570
- }
2571
- return parsed;
2572
- }
2573
- function isTruthy(value) {
2574
- if (value == null) {
2575
- return false;
2576
- }
2577
- return TRUTHY_ENV_VALUES.includes(value.trim().toLowerCase());
2578
- }
2579
- function entryToAnswers(name, entry) {
2580
- const env = entry.env;
2581
- const host = env[SINGLE_HOST_ENV.HOST] ?? "";
2582
- const username = env[SINGLE_HOST_ENV.USER] ?? "";
2583
- let auth = "agent";
2584
- if (env[SINGLE_HOST_ENV.KEY] != null) {
2585
- auth = "key";
2586
- } else if (env[SINGLE_HOST_ENV.PASSWORD] != null) {
2587
- auth = "password";
2588
- }
2589
- let allowedPaths = [];
2590
- const allowedPathsValue = env[SINGLE_HOST_ENV.ALLOWED_PATHS];
2591
- if (allowedPathsValue != null) {
2592
- allowedPaths = allowedPathsValue.split(",").map((item) => item.trim()).filter((item) => item !== "");
2593
- }
2594
- const extraEnv = {};
2595
- for (const [variable, value] of Object.entries(env)) {
2596
- if (MANAGED_ENV_NAMES.includes(variable) == false) {
2597
- extraEnv[variable] = value;
2598
- }
2599
- }
2600
- const answers = {
2601
- name,
2602
- host,
2603
- port: readPort(env[SINGLE_HOST_ENV.PORT]),
2604
- username,
2605
- auth,
2606
- keyPath: env[SINGLE_HOST_ENV.KEY],
2607
- passphrase: env[SINGLE_HOST_ENV.PASSPHRASE],
2608
- password: env[SINGLE_HOST_ENV.PASSWORD],
2609
- description: env[SINGLE_HOST_ENV.DESCRIPTION],
2610
- proxyCommand: env[SINGLE_HOST_ENV.PROXY_COMMAND],
2611
- readonly: isTruthy(env[SINGLE_HOST_ENV.READONLY]),
2612
- allowedPaths,
2613
- hostKeyFingerprint: env[SINGLE_HOST_ENV.HOST_KEY],
2614
- extraEnv
2615
- };
2616
- const jumpHost = env[SINGLE_HOST_ENV.JUMP_HOST];
2617
- if (jumpHost != null) {
2618
- answers.jumpHost = jumpHost;
2619
- answers.jumpPort = readPort(env[SINGLE_HOST_ENV.JUMP_PORT]);
2620
- answers.jumpUser = env[SINGLE_HOST_ENV.JUMP_USER] ?? username;
2621
- answers.jumpHostKeyFingerprint = env[SINGLE_HOST_ENV.JUMP_HOST_KEY];
2622
- }
2623
- return answers;
2624
- }
2625
- function summarizeEntry(entry) {
2626
- const env = entry.env;
2627
- const host = env[SINGLE_HOST_ENV.HOST] ?? "?";
2628
- const port = env[SINGLE_HOST_ENV.PORT] ?? String(DEFAULT_SSH_PORT);
2629
- const username = env[SINGLE_HOST_ENV.USER] ?? "?";
2630
- let auth = "agent";
2631
- if (env[SINGLE_HOST_ENV.KEY] != null) {
2632
- auth = "key";
2633
- } else if (env[SINGLE_HOST_ENV.PASSWORD] != null) {
2634
- auth = "password";
2635
- }
2636
- const notes = [];
2637
- if (env[SINGLE_HOST_ENV.JUMP_HOST] != null) {
2638
- notes.push(`jump ${env[SINGLE_HOST_ENV.JUMP_HOST]}`);
2639
- }
2640
- if (env[SINGLE_HOST_ENV.PROXY_COMMAND] != null) {
2641
- notes.push("proxy");
2642
- }
2643
- if (isTruthy(env[SINGLE_HOST_ENV.READONLY]) == true) {
2644
- notes.push("readonly");
2645
- }
2646
- if (env[SINGLE_HOST_ENV.ALLOWED_PATHS] != null) {
2647
- notes.push(`paths ${env[SINGLE_HOST_ENV.ALLOWED_PATHS]}`);
2648
- }
2649
- if (env[SINGLE_HOST_ENV.HOST_KEY] != null) {
2650
- notes.push("pinned");
2651
- }
2652
- return {
2653
- endpoint: `${username}@${host}:${port}`,
2654
- auth,
2655
- notes: notes.join(", ")
2656
- };
2657
- }
2658
-
2659
2388
  // src/modules/manage/prompter.ts
2660
2389
  var PromptClosedError = class extends Error {
2661
2390
  constructor() {
@@ -2832,6 +2561,17 @@ var Prompter = class {
2832
2561
  get paint() {
2833
2562
  return this.style;
2834
2563
  }
2564
+ /** Keeps a report visible until Enter on a TTY; Esc goes back. Pipes need no extra answer. */
2565
+ async pause() {
2566
+ if (this.interactive == false) {
2567
+ return;
2568
+ }
2569
+ if (this.inputEnded == true) {
2570
+ throw new PromptClosedError();
2571
+ }
2572
+ const prompt = this.fitLine([{ text: "Press Enter to continue", paint: this.style.dim }], this.columns() - 1);
2573
+ await this.readText(prompt, false);
2574
+ }
2835
2575
  /**
2836
2576
  * Free-text prompt. Enter accepts `defaultValue` when one is shown.
2837
2577
  *
@@ -3277,9 +3017,435 @@ var Prompter = class {
3277
3017
  draw(buildLines());
3278
3018
  });
3279
3019
  }
3280
- };
3281
-
3282
- // src/modules/manage/host-form.ts
3020
+ };
3021
+
3022
+ // src/modules/manage/check-progress.ts
3023
+ var ESC2 = "\x1B";
3024
+ var CheckProgress = class {
3025
+ constructor(output) {
3026
+ this.output = output;
3027
+ this.unicode = supportsUnicode();
3028
+ this.frames = this.unicode ? ["\u280B", "\u2819", "\u2839", "\u2838", "\u283C", "\u2834", "\u2826", "\u2827", "\u2807", "\u280F"] : ["-", "\\", "|", "/"];
3029
+ this.timer = null;
3030
+ this.label = null;
3031
+ this.startedAt = 0;
3032
+ this.frame = 0;
3033
+ this.enabled = output.isTTY === true && process.env.TERM !== "dumb";
3034
+ this.color = process.env.NO_COLOR == null;
3035
+ }
3036
+ /** Advancing means the preceding phase actually completed successfully. */
3037
+ start(label) {
3038
+ if (this.enabled == false) {
3039
+ return;
3040
+ }
3041
+ this.finish(true);
3042
+ this.label = label;
3043
+ this.startedAt = Date.now();
3044
+ this.frame = 0;
3045
+ this.draw(this.frames[0], 36);
3046
+ this.timer = setInterval(() => {
3047
+ this.frame = (this.frame + 1) % this.frames.length;
3048
+ this.draw(this.frames[this.frame], 36);
3049
+ }, 80);
3050
+ this.timer.unref();
3051
+ }
3052
+ /** Leaves the completed phase and its duration visible. */
3053
+ finish(ok) {
3054
+ if (this.label == null) {
3055
+ return;
3056
+ }
3057
+ this.clearTimer();
3058
+ const symbol = ok ? this.unicode ? "\u2714" : "OK" : this.unicode ? "\u2716" : "X";
3059
+ this.draw(symbol, ok ? 32 : 31);
3060
+ this.output.write("\n");
3061
+ this.label = null;
3062
+ }
3063
+ /** Clears an unfinished phase without recording a result. */
3064
+ stop() {
3065
+ this.clearTimer();
3066
+ if (this.label != null) {
3067
+ this.output.write(`\r${ESC2}[2K`);
3068
+ this.label = null;
3069
+ }
3070
+ }
3071
+ clearTimer() {
3072
+ if (this.timer != null) {
3073
+ clearInterval(this.timer);
3074
+ this.timer = null;
3075
+ }
3076
+ }
3077
+ draw(symbol, color) {
3078
+ const columns = this.output.columns;
3079
+ const width = Math.max(0, (columns != null && columns > 0 ? columns : 80) - 1);
3080
+ const elapsed = ` ${((Date.now() - this.startedAt) / 1e3).toFixed(1)}s`;
3081
+ const prefix = ` ${symbol} `;
3082
+ const labelWidth = Math.min(48, Math.max(0, width - displayWidth(prefix) - displayWidth(elapsed)));
3083
+ const label = this.fit(this.label ?? "", labelWidth);
3084
+ const line = this.fit(`${prefix}${label}${" ".repeat(labelWidth - displayWidth(label))}${elapsed}`, width);
3085
+ const painted = this.color ? `${ESC2}[${color}m${line}${ESC2}[39m` : line;
3086
+ this.output.write(`\r${ESC2}[2K${painted}`);
3087
+ }
3088
+ fit(text, width) {
3089
+ if (displayWidth(text) <= width) {
3090
+ return text;
3091
+ }
3092
+ if (width < 1) {
3093
+ return "";
3094
+ }
3095
+ let result = "";
3096
+ let used = 0;
3097
+ for (const character of text) {
3098
+ const size = displayWidth(character);
3099
+ if (used + size > width - 1) {
3100
+ break;
3101
+ }
3102
+ result += character;
3103
+ used += size;
3104
+ }
3105
+ return `${result}${this.unicode ? "\u2026" : "~"}`;
3106
+ }
3107
+ };
3108
+
3109
+ // src/modules/manage/connection-check.ts
3110
+ function profileFromEnv(output, env) {
3111
+ try {
3112
+ const profile = loadHostProfile(env);
3113
+ if (profile == null) {
3114
+ output.write(" The entry has no host.\n");
3115
+ }
3116
+ return profile;
3117
+ } catch (ex) {
3118
+ let detail = String(ex);
3119
+ if (ex instanceof Error) {
3120
+ detail = ex.message;
3121
+ }
3122
+ output.write(` The server would refuse to start with this entry: ${detail}
3123
+ `);
3124
+ return null;
3125
+ }
3126
+ }
3127
+ function describeAuth(profile) {
3128
+ if (profile.privateKeyPath != null) {
3129
+ return `key ${profile.privateKeyPath}`;
3130
+ }
3131
+ if (profile.password != null) {
3132
+ return "password";
3133
+ }
3134
+ return "ssh agent";
3135
+ }
3136
+ async function runConnectionCheck(profile, output) {
3137
+ const endpoint = `${profile.username}@${profile.host}:${profile.port}`;
3138
+ const auth = describeAuth(profile);
3139
+ output.write(`Checking ${profile.name} \u2192 ${endpoint}
3140
+ `);
3141
+ if (profile.proxyCommand != null) {
3142
+ output.write(` via proxy command: ${profile.proxyCommand}
3143
+ `);
3144
+ }
3145
+ if (profile.jump != null) {
3146
+ output.write(` via jump host: ${profile.jump.username}@${profile.jump.host}:${profile.jump.port}
3147
+ `);
3148
+ }
3149
+ output.write("\n");
3150
+ const connection = new SshConnection(profile);
3151
+ const progress = new CheckProgress(output);
3152
+ const startedAt = Date.now();
3153
+ let connected = false;
3154
+ let detail = "";
3155
+ try {
3156
+ progress.start("Preparing SSH connection");
3157
+ await connection.connect((stage) => {
3158
+ if (stage === "transport") {
3159
+ progress.start(profile.jump != null ? "Jump host connection and forwarding" : "Starting proxy connection");
3160
+ } else {
3161
+ progress.start("SSH handshake and authentication");
3162
+ }
3163
+ });
3164
+ connected = true;
3165
+ } catch (ex) {
3166
+ detail = String(ex);
3167
+ if (ex instanceof Error) {
3168
+ detail = ex.message;
3169
+ }
3170
+ } finally {
3171
+ progress.finish(connected);
3172
+ connection.disconnect();
3173
+ }
3174
+ const durationMs = Date.now() - startedAt;
3175
+ if (connected == true) {
3176
+ output.write(` OK ${endpoint} (${auth}, ${durationMs}ms)
3177
+ `);
3178
+ } else {
3179
+ output.write(` FAILED ${endpoint} (${auth})
3180
+ `);
3181
+ output.write(` ${detail}
3182
+ `);
3183
+ }
3184
+ if (connection.hostKeyFingerprint != null) {
3185
+ let pinNote = `not pinned \u2014 set ${SINGLE_HOST_ENV.HOST_KEY} to it to detect a changed key`;
3186
+ if (profile.hostKeyFingerprint != null) {
3187
+ pinNote = "pinned";
3188
+ }
3189
+ output.write(` host key ${connection.hostKeyFingerprint} (${pinNote})
3190
+ `);
3191
+ }
3192
+ if (connection.jumpHostKeyFingerprint != null) {
3193
+ let pinNote = `not pinned \u2014 set ${SINGLE_HOST_ENV.JUMP_HOST_KEY} to it to detect a changed key`;
3194
+ if (profile.jump?.hostKeyFingerprint != null) {
3195
+ pinNote = "pinned";
3196
+ }
3197
+ output.write(` jump host key ${connection.jumpHostKeyFingerprint} (${pinNote})
3198
+ `);
3199
+ }
3200
+ return {
3201
+ connected,
3202
+ hostKeyFingerprint: connection.hostKeyFingerprint,
3203
+ jumpHostKeyFingerprint: connection.jumpHostKeyFingerprint
3204
+ };
3205
+ }
3206
+
3207
+ // src/modules/manage/copy-entry.ts
3208
+ async function copyEntryToClient(prompter, source, targets, name, entry) {
3209
+ const destinations = targets.filter((target) => target.id !== source.id);
3210
+ if (destinations.length === 0) {
3211
+ return;
3212
+ }
3213
+ for (; ; ) {
3214
+ const items = destinations.map((target) => ({
3215
+ value: target.id,
3216
+ key: target.id,
3217
+ label: target.label,
3218
+ hint: target.detected ? target.location : `${target.location} \xB7 not found on this machine`
3219
+ }));
3220
+ items.push({ separator: true }, { value: "back", key: "b", label: "Back" });
3221
+ const picked = await prompter.select(`Copy ${name} to which client?`, items, { cancelValue: "back" });
3222
+ const destination = destinations.find((target) => target.id === picked);
3223
+ if (destination == null) {
3224
+ return;
3225
+ }
3226
+ const exists = destination.hasEntry?.(name) ?? Object.hasOwn(destination.readEntries(), name);
3227
+ if (exists) {
3228
+ try {
3229
+ const overwrite = await prompter.askYesNo(
3230
+ `${name} already exists in ${destination.label} (${destination.location}). Overwrite it?`,
3231
+ false
3232
+ );
3233
+ if (overwrite == false) {
3234
+ prompter.write(" Not copied; the destination entry was kept.\n\n");
3235
+ return;
3236
+ }
3237
+ } catch (ex) {
3238
+ if (ex instanceof PromptBackError == false) {
3239
+ throw ex;
3240
+ }
3241
+ continue;
3242
+ }
3243
+ }
3244
+ const result = destination.writeEntry(name, {
3245
+ command: entry.command,
3246
+ args: [...entry.args],
3247
+ env: { ...entry.env }
3248
+ }, { replace: true });
3249
+ if (result.status === "written") {
3250
+ prompter.write(` ${prompter.paint.success(`Copied ${name} to ${destination.label}`)} ${prompter.paint.dim(result.detail)}
3251
+ `);
3252
+ prompter.write(prompter.paint.dim(" Restart the destination client (or reconnect it from its MCP menu) to pick up the change.\n\n"));
3253
+ } else {
3254
+ prompter.write(` Not copied: ${result.detail}
3255
+
3256
+ `);
3257
+ }
3258
+ return;
3259
+ }
3260
+ }
3261
+
3262
+ // src/modules/manage/entry.ts
3263
+ var MANAGED_ENV_NAMES = [
3264
+ SINGLE_HOST_ENV.HOST,
3265
+ SINGLE_HOST_ENV.PORT,
3266
+ SINGLE_HOST_ENV.USER,
3267
+ SINGLE_HOST_ENV.KEY,
3268
+ SINGLE_HOST_ENV.PASSPHRASE,
3269
+ SINGLE_HOST_ENV.PASSWORD,
3270
+ SINGLE_HOST_ENV.DESCRIPTION,
3271
+ SINGLE_HOST_ENV.JUMP_HOST,
3272
+ SINGLE_HOST_ENV.JUMP_PORT,
3273
+ SINGLE_HOST_ENV.JUMP_USER,
3274
+ SINGLE_HOST_ENV.PROXY_COMMAND,
3275
+ SINGLE_HOST_ENV.READONLY,
3276
+ SINGLE_HOST_ENV.ALLOWED_PATHS,
3277
+ SINGLE_HOST_ENV.HOST_KEY,
3278
+ SINGLE_HOST_ENV.JUMP_HOST_KEY
3279
+ ];
3280
+ var JUMP_ENV_PREFIX = "SSH_JUMP_";
3281
+ var SERVER_NAME_PATTERN = /^[A-Za-z0-9_-]{1,48}$/;
3282
+ function isOurEntry(entry) {
3283
+ if (entry.args.includes(PACKAGE_NAME) == true) {
3284
+ return true;
3285
+ }
3286
+ const commandBase = entry.command.replace(/\\/g, "/").split("/").pop() ?? "";
3287
+ return commandBase === SERVER_NAME || commandBase.startsWith(`${SERVER_NAME}.`) == true;
3288
+ }
3289
+ function launchCommand() {
3290
+ if (process.platform === "win32") {
3291
+ return { command: "cmd", args: ["/c", "npx", "-y", PACKAGE_NAME] };
3292
+ }
3293
+ return { command: "npx", args: ["-y", PACKAGE_NAME] };
3294
+ }
3295
+ function answersToEntry(answers) {
3296
+ const env = {};
3297
+ env[SINGLE_HOST_ENV.HOST] = answers.host;
3298
+ if (answers.port !== DEFAULT_SSH_PORT) {
3299
+ env[SINGLE_HOST_ENV.PORT] = String(answers.port);
3300
+ }
3301
+ env[SINGLE_HOST_ENV.USER] = answers.username;
3302
+ if (answers.auth === "key" && answers.keyPath != null) {
3303
+ env[SINGLE_HOST_ENV.KEY] = answers.keyPath;
3304
+ if (answers.passphrase != null && answers.passphrase !== "") {
3305
+ env[SINGLE_HOST_ENV.PASSPHRASE] = answers.passphrase;
3306
+ }
3307
+ }
3308
+ if (answers.auth === "password" && answers.password != null) {
3309
+ env[SINGLE_HOST_ENV.PASSWORD] = answers.password;
3310
+ }
3311
+ if (answers.hostKeyFingerprint != null) {
3312
+ env[SINGLE_HOST_ENV.HOST_KEY] = answers.hostKeyFingerprint;
3313
+ }
3314
+ if (answers.description != null && answers.description !== "") {
3315
+ env[SINGLE_HOST_ENV.DESCRIPTION] = answers.description;
3316
+ }
3317
+ if (answers.jumpHost != null) {
3318
+ env[SINGLE_HOST_ENV.JUMP_HOST] = answers.jumpHost;
3319
+ if (answers.jumpPort != null && answers.jumpPort !== DEFAULT_SSH_PORT) {
3320
+ env[SINGLE_HOST_ENV.JUMP_PORT] = String(answers.jumpPort);
3321
+ }
3322
+ if (answers.jumpUser != null && answers.jumpUser !== answers.username) {
3323
+ env[SINGLE_HOST_ENV.JUMP_USER] = answers.jumpUser;
3324
+ }
3325
+ if (answers.jumpHostKeyFingerprint != null) {
3326
+ env[SINGLE_HOST_ENV.JUMP_HOST_KEY] = answers.jumpHostKeyFingerprint;
3327
+ }
3328
+ }
3329
+ if (answers.proxyCommand != null && answers.proxyCommand !== "") {
3330
+ env[SINGLE_HOST_ENV.PROXY_COMMAND] = answers.proxyCommand;
3331
+ }
3332
+ if (answers.readonly == true) {
3333
+ env[SINGLE_HOST_ENV.READONLY] = "true";
3334
+ }
3335
+ if (answers.allowedPaths.length > 0) {
3336
+ env[SINGLE_HOST_ENV.ALLOWED_PATHS] = answers.allowedPaths.join(",");
3337
+ }
3338
+ for (const [name, value] of Object.entries(answers.extraEnv)) {
3339
+ if (MANAGED_ENV_NAMES.includes(name) == true) {
3340
+ continue;
3341
+ }
3342
+ if (answers.jumpHost == null && name.startsWith(JUMP_ENV_PREFIX) == true) {
3343
+ continue;
3344
+ }
3345
+ env[name] = value;
3346
+ }
3347
+ const launch = launchCommand();
3348
+ return {
3349
+ command: launch.command,
3350
+ args: launch.args,
3351
+ env
3352
+ };
3353
+ }
3354
+ function readPort(value) {
3355
+ if (value == null) {
3356
+ return DEFAULT_SSH_PORT;
3357
+ }
3358
+ const parsed = Number(value);
3359
+ if (Number.isInteger(parsed) == false || parsed <= 0) {
3360
+ return DEFAULT_SSH_PORT;
3361
+ }
3362
+ return parsed;
3363
+ }
3364
+ function isTruthy(value) {
3365
+ if (value == null) {
3366
+ return false;
3367
+ }
3368
+ return TRUTHY_ENV_VALUES.includes(value.trim().toLowerCase());
3369
+ }
3370
+ function entryToAnswers(name, entry) {
3371
+ const env = entry.env;
3372
+ const host = env[SINGLE_HOST_ENV.HOST] ?? "";
3373
+ const username = env[SINGLE_HOST_ENV.USER] ?? "";
3374
+ let auth = "agent";
3375
+ if (env[SINGLE_HOST_ENV.KEY] != null) {
3376
+ auth = "key";
3377
+ } else if (env[SINGLE_HOST_ENV.PASSWORD] != null) {
3378
+ auth = "password";
3379
+ }
3380
+ let allowedPaths = [];
3381
+ const allowedPathsValue = env[SINGLE_HOST_ENV.ALLOWED_PATHS];
3382
+ if (allowedPathsValue != null) {
3383
+ allowedPaths = allowedPathsValue.split(",").map((item) => item.trim()).filter((item) => item !== "");
3384
+ }
3385
+ const extraEnv = {};
3386
+ for (const [variable, value] of Object.entries(env)) {
3387
+ if (MANAGED_ENV_NAMES.includes(variable) == false) {
3388
+ extraEnv[variable] = value;
3389
+ }
3390
+ }
3391
+ const answers = {
3392
+ name,
3393
+ host,
3394
+ port: readPort(env[SINGLE_HOST_ENV.PORT]),
3395
+ username,
3396
+ auth,
3397
+ keyPath: env[SINGLE_HOST_ENV.KEY],
3398
+ passphrase: env[SINGLE_HOST_ENV.PASSPHRASE],
3399
+ password: env[SINGLE_HOST_ENV.PASSWORD],
3400
+ description: env[SINGLE_HOST_ENV.DESCRIPTION],
3401
+ proxyCommand: env[SINGLE_HOST_ENV.PROXY_COMMAND],
3402
+ readonly: isTruthy(env[SINGLE_HOST_ENV.READONLY]),
3403
+ allowedPaths,
3404
+ hostKeyFingerprint: env[SINGLE_HOST_ENV.HOST_KEY],
3405
+ extraEnv
3406
+ };
3407
+ const jumpHost = env[SINGLE_HOST_ENV.JUMP_HOST];
3408
+ if (jumpHost != null) {
3409
+ answers.jumpHost = jumpHost;
3410
+ answers.jumpPort = readPort(env[SINGLE_HOST_ENV.JUMP_PORT]);
3411
+ answers.jumpUser = env[SINGLE_HOST_ENV.JUMP_USER] ?? username;
3412
+ answers.jumpHostKeyFingerprint = env[SINGLE_HOST_ENV.JUMP_HOST_KEY];
3413
+ }
3414
+ return answers;
3415
+ }
3416
+ function summarizeEntry(entry) {
3417
+ const env = entry.env;
3418
+ const host = env[SINGLE_HOST_ENV.HOST] ?? "?";
3419
+ const port = env[SINGLE_HOST_ENV.PORT] ?? String(DEFAULT_SSH_PORT);
3420
+ const username = env[SINGLE_HOST_ENV.USER] ?? "?";
3421
+ let auth = "agent";
3422
+ if (env[SINGLE_HOST_ENV.KEY] != null) {
3423
+ auth = "key";
3424
+ } else if (env[SINGLE_HOST_ENV.PASSWORD] != null) {
3425
+ auth = "password";
3426
+ }
3427
+ const notes = [];
3428
+ if (env[SINGLE_HOST_ENV.JUMP_HOST] != null) {
3429
+ notes.push(`jump ${env[SINGLE_HOST_ENV.JUMP_HOST]}`);
3430
+ }
3431
+ if (env[SINGLE_HOST_ENV.PROXY_COMMAND] != null) {
3432
+ notes.push("proxy");
3433
+ }
3434
+ if (isTruthy(env[SINGLE_HOST_ENV.READONLY]) == true) {
3435
+ notes.push("readonly");
3436
+ }
3437
+ if (env[SINGLE_HOST_ENV.ALLOWED_PATHS] != null) {
3438
+ notes.push(`paths ${env[SINGLE_HOST_ENV.ALLOWED_PATHS]}`);
3439
+ }
3440
+ if (env[SINGLE_HOST_ENV.HOST_KEY] != null) {
3441
+ notes.push("pinned");
3442
+ }
3443
+ return {
3444
+ endpoint: `${username}@${host}:${port}`,
3445
+ auth,
3446
+ notes: notes.join(", ")
3447
+ };
3448
+ }
3283
3449
  var PREFERRED_KEY_NAMES = ["id_ed25519", "id_ecdsa", "id_rsa"];
3284
3450
  var NON_KEY_FILE_NAMES = ["config", "known_hosts", "known_hosts.old", "authorized_keys", "environment"];
3285
3451
  var MAX_KEY_FILE_BYTES = 64 * 1024;
@@ -3323,7 +3489,7 @@ function findPrivateKeys() {
3323
3489
  const sshDirectory = path__default.default.join(os5__default.default.homedir(), ".ssh");
3324
3490
  let names = [];
3325
3491
  try {
3326
- names = fs6__default.default.readdirSync(sshDirectory);
3492
+ names = fs7__default.default.readdirSync(sshDirectory);
3327
3493
  } catch {
3328
3494
  return [];
3329
3495
  }
@@ -3335,14 +3501,14 @@ function findPrivateKeys() {
3335
3501
  const fullPath = path__default.default.join(sshDirectory, name);
3336
3502
  let head = "";
3337
3503
  try {
3338
- const stat = fs6__default.default.statSync(fullPath);
3504
+ const stat = fs7__default.default.statSync(fullPath);
3339
3505
  if (stat.isFile() == false || stat.size > MAX_KEY_FILE_BYTES) {
3340
3506
  continue;
3341
3507
  }
3342
- const descriptor = fs6__default.default.openSync(fullPath, "r");
3508
+ const descriptor = fs7__default.default.openSync(fullPath, "r");
3343
3509
  const buffer = Buffer.alloc(64);
3344
- const bytesRead = fs6__default.default.readSync(descriptor, buffer, 0, buffer.length, 0);
3345
- fs6__default.default.closeSync(descriptor);
3510
+ const bytesRead = fs7__default.default.readSync(descriptor, buffer, 0, buffer.length, 0);
3511
+ fs7__default.default.closeSync(descriptor);
3346
3512
  head = buffer.subarray(0, bytesRead).toString("utf8");
3347
3513
  } catch {
3348
3514
  continue;
@@ -3354,7 +3520,7 @@ function findPrivateKeys() {
3354
3520
  }
3355
3521
  let hint = "";
3356
3522
  try {
3357
- const publicKey = fs6__default.default.readFileSync(`${fullPath}.pub`, "utf8").trim().split(/\s+/);
3523
+ const publicKey = fs7__default.default.readFileSync(`${fullPath}.pub`, "utf8").trim().split(/\s+/);
3358
3524
  hint = publicKey[0].replace(/^ssh-/, "");
3359
3525
  if (publicKey.length > 2) {
3360
3526
  hint += ` \xB7 ${publicKey.slice(2).join(" ")}`;
@@ -3423,7 +3589,7 @@ async function askKeyPath(prompter, current) {
3423
3589
  }
3424
3590
  for (; ; ) {
3425
3591
  const typed = await prompter.askRequired("Private key path");
3426
- if (fs6__default.default.existsSync(expandHomePath(typed)) == true) {
3592
+ if (fs7__default.default.existsSync(expandHomePath(typed)) == true) {
3427
3593
  return typed;
3428
3594
  }
3429
3595
  prompter.write(` ${expandHomePath(typed)} does not exist.
@@ -3635,10 +3801,12 @@ async function testConnection(prompter, output, state) {
3635
3801
  if (profile == null) {
3636
3802
  output.write("\n");
3637
3803
  state.lastTest = "failed";
3804
+ await prompter.pause();
3638
3805
  return false;
3639
3806
  }
3640
3807
  const result = await runConnectionCheck(profile, output);
3641
3808
  output.write("\n");
3809
+ await prompter.pause();
3642
3810
  if (result.connected == true) {
3643
3811
  if (result.hostKeyFingerprint != null && state.answers.hostKeyFingerprint == null) {
3644
3812
  const pin = await prompter.askYesNo("Pin this host key? (a changed key then aborts the connection)", true);
@@ -3955,6 +4123,65 @@ async function runHostForm(prompter, output, options) {
3955
4123
  prompter.transient = previousTransient;
3956
4124
  }
3957
4125
  }
4126
+ function readExisting(filePath) {
4127
+ try {
4128
+ return fs7__default.default.readFileSync(filePath, "utf8");
4129
+ } catch (error) {
4130
+ if (error.code === "ENOENT") {
4131
+ return null;
4132
+ }
4133
+ throw error;
4134
+ }
4135
+ }
4136
+ function updateConfigFile(filePath, update) {
4137
+ let destination = filePath;
4138
+ let symbolicLink = false;
4139
+ try {
4140
+ symbolicLink = fs7__default.default.lstatSync(filePath).isSymbolicLink();
4141
+ } catch (error) {
4142
+ if (error.code !== "ENOENT") {
4143
+ throw error;
4144
+ }
4145
+ }
4146
+ if (symbolicLink) {
4147
+ destination = fs7__default.default.realpathSync(filePath);
4148
+ }
4149
+ const before = readExisting(destination);
4150
+ const updated = update(before ?? "");
4151
+ if (before != null) {
4152
+ fs7__default.default.accessSync(destination, fs7__default.default.constants.W_OK);
4153
+ }
4154
+ fs7__default.default.mkdirSync(path__default.default.dirname(destination), { recursive: true });
4155
+ const temporary = path__default.default.join(path__default.default.dirname(destination), `.${path__default.default.basename(destination)}.${crypto.randomUUID()}.tmp`);
4156
+ const mode = before == null ? 384 : fs7__default.default.statSync(destination).mode & 511;
4157
+ let descriptor;
4158
+ let ownsTemporary = false;
4159
+ try {
4160
+ descriptor = fs7__default.default.openSync(temporary, "wx", mode);
4161
+ ownsTemporary = true;
4162
+ fs7__default.default.writeFileSync(descriptor, updated, "utf8");
4163
+ fs7__default.default.fsyncSync(descriptor);
4164
+ fs7__default.default.closeSync(descriptor);
4165
+ descriptor = void 0;
4166
+ if (readExisting(destination) !== before) {
4167
+ throw new Error("The config changed while saving; reload it and try again.");
4168
+ }
4169
+ fs7__default.default.renameSync(temporary, destination);
4170
+ ownsTemporary = false;
4171
+ } finally {
4172
+ try {
4173
+ if (descriptor != null) {
4174
+ fs7__default.default.closeSync(descriptor);
4175
+ }
4176
+ } finally {
4177
+ if (ownsTemporary) {
4178
+ fs7__default.default.unlinkSync(temporary);
4179
+ }
4180
+ }
4181
+ }
4182
+ }
4183
+
4184
+ // src/modules/manage/targets/json-file.ts
3958
4185
  function toMcpServerEntry(value) {
3959
4186
  if (typeof value !== "object" || value == null) {
3960
4187
  return null;
@@ -3967,7 +4194,7 @@ function toMcpServerEntry(value) {
3967
4194
  if (Array.isArray(record.args) == true) {
3968
4195
  args = record.args.filter((item) => typeof item === "string");
3969
4196
  }
3970
- const env = {};
4197
+ const env = /* @__PURE__ */ Object.create(null);
3971
4198
  if (typeof record.env === "object" && record.env != null) {
3972
4199
  for (const [key, item] of Object.entries(record.env)) {
3973
4200
  if (typeof item === "string") {
@@ -3977,16 +4204,22 @@ function toMcpServerEntry(value) {
3977
4204
  }
3978
4205
  return { command: record.command, args, env };
3979
4206
  }
3980
- function readMcpServersJson(document) {
3981
- if (typeof document !== "object" || document == null || Array.isArray(document) == true) {
4207
+ function readRawMcpServersJson(document) {
4208
+ if (typeof document !== "object" || document == null || Array.isArray(document)) {
3982
4209
  throw new Error("the config file is not a JSON object");
3983
4210
  }
3984
- const entries = {};
3985
4211
  const servers = document.mcpServers;
3986
- if (typeof servers !== "object" || servers == null) {
3987
- return entries;
4212
+ if (servers === void 0) {
4213
+ return {};
4214
+ }
4215
+ if (typeof servers !== "object" || servers == null || Array.isArray(servers)) {
4216
+ throw new Error("mcpServers is not a JSON object");
3988
4217
  }
3989
- for (const [name, value] of Object.entries(servers)) {
4218
+ return servers;
4219
+ }
4220
+ function readMcpServersJson(document) {
4221
+ const entries = /* @__PURE__ */ Object.create(null);
4222
+ for (const [name, value] of Object.entries(readRawMcpServersJson(document))) {
3990
4223
  const entry = toMcpServerEntry(value);
3991
4224
  if (entry != null) {
3992
4225
  entries[name] = entry;
@@ -3994,11 +4227,11 @@ function readMcpServersJson(document) {
3994
4227
  }
3995
4228
  return entries;
3996
4229
  }
3997
- function readDocument(filePath) {
3998
- if (fs6__default.default.existsSync(filePath) == false) {
4230
+ function readDocument(filePath, contents) {
4231
+ if (contents == null && fs7__default.default.existsSync(filePath) == false) {
3999
4232
  return {};
4000
4233
  }
4001
- const text = fs6__default.default.readFileSync(filePath, "utf8");
4234
+ const text = contents ?? fs7__default.default.readFileSync(filePath, "utf8");
4002
4235
  if (text.trim() === "") {
4003
4236
  return {};
4004
4237
  }
@@ -4019,39 +4252,35 @@ function readDocument(filePath) {
4019
4252
  }
4020
4253
  function createJsonFileTarget(options) {
4021
4254
  const { id, label, filePath } = options;
4022
- function writeDocument(document) {
4023
- fs6__default.default.mkdirSync(path__default.default.dirname(filePath), { recursive: true });
4024
- fs6__default.default.writeFileSync(filePath, `${JSON.stringify(document, null, 4)}
4025
- `, "utf8");
4026
- }
4027
4255
  return {
4028
4256
  id,
4029
4257
  label,
4030
4258
  location: filePath,
4031
- detected: fs6__default.default.existsSync(filePath),
4259
+ detected: fs7__default.default.existsSync(filePath),
4032
4260
  readEntries() {
4033
4261
  const document = readDocument(filePath);
4034
4262
  return readMcpServersJson(document);
4035
4263
  },
4264
+ hasEntry(name) {
4265
+ return Object.hasOwn(readRawMcpServersJson(readDocument(filePath)), name);
4266
+ },
4036
4267
  writeEntry(name, entry) {
4037
- const document = readDocument(filePath);
4038
- let servers = document.mcpServers;
4039
- if (typeof servers !== "object" || servers == null || Array.isArray(servers) == true) {
4040
- servers = {};
4041
- }
4042
- servers[name] = entry;
4043
- document.mcpServers = servers;
4044
- writeDocument(document);
4268
+ updateConfigFile(filePath, (contents) => {
4269
+ const document = readDocument(filePath, contents);
4270
+ const servers = Object.assign(/* @__PURE__ */ Object.create(null), readRawMcpServersJson(document));
4271
+ servers[name] = entry;
4272
+ document.mcpServers = servers;
4273
+ return JSON.stringify(document, null, 4) + "\n";
4274
+ });
4045
4275
  return { status: "written", detail: filePath };
4046
4276
  },
4047
4277
  removeEntry(name) {
4048
- const document = readDocument(filePath);
4049
- const servers = document.mcpServers;
4050
- if (typeof servers !== "object" || servers == null || Array.isArray(servers) == true) {
4051
- return { status: "written", detail: filePath };
4052
- }
4053
- delete servers[name];
4054
- writeDocument(document);
4278
+ updateConfigFile(filePath, (contents) => {
4279
+ const document = readDocument(filePath, contents);
4280
+ const servers = readRawMcpServersJson(document);
4281
+ delete servers[name];
4282
+ return JSON.stringify(document, null, 4) + "\n";
4283
+ });
4055
4284
  return { status: "written", detail: filePath };
4056
4285
  }
4057
4286
  };
@@ -4103,11 +4332,11 @@ function runClaude(executable, args) {
4103
4332
  }
4104
4333
  return { status: "written", detail: CLAUDE_CODE_CONFIG_PATH };
4105
4334
  }
4106
- function readClaudeEntries() {
4107
- if (fs6__default.default.existsSync(CLAUDE_CODE_CONFIG_PATH) == false) {
4335
+ function readRawClaudeEntries() {
4336
+ if (fs7__default.default.existsSync(CLAUDE_CODE_CONFIG_PATH) == false) {
4108
4337
  return {};
4109
4338
  }
4110
- const text = fs6__default.default.readFileSync(CLAUDE_CODE_CONFIG_PATH, "utf8");
4339
+ const text = fs7__default.default.readFileSync(CLAUDE_CODE_CONFIG_PATH, "utf8");
4111
4340
  let document;
4112
4341
  try {
4113
4342
  document = JSON.parse(text);
@@ -4118,7 +4347,7 @@ function readClaudeEntries() {
4118
4347
  }
4119
4348
  throw new Error(`${CLAUDE_CODE_CONFIG_PATH} is not valid JSON: ${detail}`);
4120
4349
  }
4121
- return readMcpServersJson(document);
4350
+ return readRawMcpServersJson(document);
4122
4351
  }
4123
4352
  function createClaudeCodeTarget() {
4124
4353
  const executable = findExecutable("claude");
@@ -4126,28 +4355,35 @@ function createClaudeCodeTarget() {
4126
4355
  id: "claude-code",
4127
4356
  label: "Claude Code (user scope)",
4128
4357
  location: CLAUDE_CODE_CONFIG_PATH,
4129
- detected: executable != null || fs6__default.default.existsSync(CLAUDE_CODE_CONFIG_PATH) == true,
4130
- readEntries: readClaudeEntries,
4358
+ detected: executable != null || fs7__default.default.existsSync(CLAUDE_CODE_CONFIG_PATH) == true,
4359
+ readEntries() {
4360
+ return readMcpServersJson({ mcpServers: readRawClaudeEntries() });
4361
+ },
4362
+ hasEntry(name) {
4363
+ return Object.hasOwn(readRawClaudeEntries(), name);
4364
+ },
4131
4365
  writeEntry(name, entry) {
4132
4366
  if (executable == null) {
4133
4367
  return {
4134
4368
  status: "unavailable",
4135
- detail: `the claude CLI is not on PATH \u2014 run this yourself:
4136
- ${formatClaudeAddCommand(name, entry)}`
4369
+ detail: "the claude CLI is not on PATH \u2014 install it, or use Print to get a command to run yourself"
4137
4370
  };
4138
4371
  }
4139
- const previous = readClaudeEntries()[name];
4140
- if (previous != null) {
4372
+ const previousEntries = readRawClaudeEntries();
4373
+ const hadPrevious = Object.hasOwn(previousEntries, name);
4374
+ const previous = previousEntries[name];
4375
+ if (hadPrevious) {
4141
4376
  const removed = runClaude(executable, ["mcp", "remove", name, "-s", "user"]);
4142
4377
  if (removed.status === "unavailable") {
4143
4378
  return removed;
4144
4379
  }
4145
4380
  }
4146
4381
  const added = runClaude(executable, ["mcp", "add-json", name, toClaudeAddJsonPayload(entry), "-s", "user"]);
4147
- if (added.status === "unavailable" && previous != null) {
4148
- runClaude(executable, ["mcp", "add-json", name, toClaudeAddJsonPayload(previous), "-s", "user"]);
4382
+ if (added.status === "unavailable" && hadPrevious) {
4383
+ const restored = runClaude(executable, ["mcp", "add-json", name, JSON.stringify(previous), "-s", "user"]);
4384
+ const recovery = restored.status === "written" ? "the previous entry was restored" : `could not restore the previous entry: ${restored.detail}`;
4149
4385
  return { status: "unavailable", detail: `${added.detail}
4150
- the previous entry was restored` };
4386
+ ${recovery}` };
4151
4387
  }
4152
4388
  return added;
4153
4389
  },
@@ -4164,194 +4400,121 @@ function createClaudeCodeTarget() {
4164
4400
  };
4165
4401
  }
4166
4402
 
4167
- // src/modules/manage/targets/toml-subset.ts
4168
- var BARE_KEY_PATTERN = /^[A-Za-z0-9_-]+$/;
4169
- var ANY_HEADER_PATTERN = /^\s*\[/;
4170
- var SERVER_HEADER_PATTERN = /^\s*\[\s*mcp_servers\s*\.\s*([A-Za-z0-9_-]+|"(?:[^"\\]|\\.)*")\s*((?:\.[^\]]*)?)\]\s*(?:#.*)?$/;
4171
- var KEY_VALUE_PATTERN = /^\s*([A-Za-z0-9_-]+|"(?:[^"\\]|\\.)*")\s*=\s*(.*)$/;
4172
- var OWNED_TABLE_KEYS = ["command", "args", "env"];
4173
- var TomlValueParser = class {
4174
- constructor(text) {
4175
- this.text = text;
4176
- this.position = 0;
4177
- }
4178
- parse() {
4179
- const value = this.parseValue();
4180
- return value;
4181
- }
4182
- skipWhitespace() {
4183
- while (this.position < this.text.length && /\s/.test(this.text[this.position]) == true) {
4184
- this.position += 1;
4185
- }
4186
- }
4187
- parseValue() {
4188
- this.skipWhitespace();
4189
- const current = this.text[this.position];
4190
- if (current === '"') {
4191
- return this.parseBasicString();
4192
- }
4193
- if (current === "'") {
4194
- return this.parseLiteralString();
4195
- }
4196
- if (current === "[") {
4197
- return this.parseArray();
4198
- }
4199
- if (current === "{") {
4200
- return this.parseInlineTable();
4201
- }
4202
- throw new Error(`unsupported toml value at offset ${this.position}`);
4203
- }
4204
- parseBasicString() {
4205
- this.position += 1;
4206
- let result = "";
4207
- while (this.position < this.text.length) {
4208
- const current = this.text[this.position];
4209
- if (current === '"') {
4210
- this.position += 1;
4211
- return result;
4212
- }
4213
- if (current !== "\\") {
4214
- result += current;
4215
- this.position += 1;
4216
- continue;
4217
- }
4218
- const escaped = this.text[this.position + 1];
4219
- this.position += 2;
4220
- if (escaped === "n") {
4221
- result += "\n";
4222
- } else if (escaped === "t") {
4223
- result += " ";
4224
- } else if (escaped === "r") {
4225
- result += "\r";
4226
- } else if (escaped === "u" || escaped === "U") {
4227
- let length = 4;
4228
- if (escaped === "U") {
4229
- length = 8;
4403
+ // src/modules/manage/targets/toml-lines.ts
4404
+ function lineState() {
4405
+ return { quote: null, multiline: false, depth: 0 };
4406
+ }
4407
+ function scanTomlLine(line, state) {
4408
+ for (let index = 0; index < line.length; index += 1) {
4409
+ const character = line[index];
4410
+ if (state.quote != null) {
4411
+ if (character === "\\" && state.quote === '"') {
4412
+ index += 1;
4413
+ } else if (character === state.quote) {
4414
+ if (!state.multiline) {
4415
+ state.quote = null;
4416
+ } else if (line.slice(index, index + 3) === state.quote.repeat(3)) {
4417
+ while (line[index + 1] === character) {
4418
+ index += 1;
4419
+ }
4420
+ state.quote = null;
4421
+ state.multiline = false;
4230
4422
  }
4231
- const hex = this.text.slice(this.position, this.position + length);
4232
- result += String.fromCodePoint(Number.parseInt(hex, 16));
4233
- this.position += length;
4234
- } else {
4235
- result += escaped;
4236
4423
  }
4424
+ continue;
4237
4425
  }
4238
- throw new Error("unterminated toml string");
4239
- }
4240
- parseLiteralString() {
4241
- const end = this.text.indexOf("'", this.position + 1);
4242
- if (end < 0) {
4243
- throw new Error("unterminated toml literal string");
4426
+ if (character === "#") {
4427
+ break;
4244
4428
  }
4245
- const result = this.text.slice(this.position + 1, end);
4246
- this.position = end + 1;
4247
- return result;
4248
- }
4249
- parseArray() {
4250
- this.position += 1;
4251
- const items = [];
4252
- for (; ; ) {
4253
- this.skipWhitespace();
4254
- if (this.position >= this.text.length) {
4255
- throw new Error("unterminated toml array");
4256
- }
4257
- if (this.text[this.position] === "]") {
4258
- this.position += 1;
4259
- return items;
4260
- }
4261
- items.push(this.parseValue());
4262
- this.skipWhitespace();
4263
- if (this.text[this.position] === ",") {
4264
- this.position += 1;
4429
+ if (character === '"' || character === "'") {
4430
+ state.quote = character;
4431
+ state.multiline = line.slice(index, index + 3) === character.repeat(3);
4432
+ if (state.multiline) {
4433
+ index += 2;
4265
4434
  }
4435
+ } else if (character === "[" || character === "{") {
4436
+ state.depth += 1;
4437
+ } else if (character === "]" || character === "}") {
4438
+ state.depth -= 1;
4266
4439
  }
4267
4440
  }
4268
- parseInlineTable() {
4269
- this.position += 1;
4270
- const table = {};
4271
- for (; ; ) {
4272
- this.skipWhitespace();
4273
- if (this.position >= this.text.length) {
4274
- throw new Error("unterminated toml inline table");
4275
- }
4276
- if (this.text[this.position] === "}") {
4277
- this.position += 1;
4278
- return table;
4279
- }
4280
- let key;
4281
- if (this.text[this.position] === '"') {
4282
- key = this.parseBasicString();
4283
- } else {
4284
- const match = /^[A-Za-z0-9_-]+/.exec(this.text.slice(this.position));
4285
- if (match == null) {
4286
- throw new Error(`invalid inline table key at offset ${this.position}`);
4287
- }
4288
- key = match[0];
4289
- this.position += key.length;
4290
- }
4291
- this.skipWhitespace();
4292
- if (this.text[this.position] !== "=") {
4293
- throw new Error(`expected '=' at offset ${this.position}`);
4294
- }
4295
- this.position += 1;
4296
- table[key] = this.parseValue();
4297
- this.skipWhitespace();
4298
- if (this.text[this.position] === ",") {
4299
- this.position += 1;
4300
- }
4301
- }
4441
+ }
4442
+ function isTable(value) {
4443
+ return value != null && typeof value === "object" && !Array.isArray(value) && !(value instanceof Date);
4444
+ }
4445
+ function parseConfigToml(text) {
4446
+ let document;
4447
+ try {
4448
+ document = smolToml.parse(text, { integersAsBigInt: true });
4449
+ } catch (error) {
4450
+ const location = error instanceof smolToml.TomlError ? ` at line ${error.line}, column ${error.column}` : "";
4451
+ throw new Error(`Invalid TOML${location}; fix the config before changing it.`);
4302
4452
  }
4303
- };
4304
- function unquoteKey(rawKey) {
4305
- if (rawKey.startsWith('"') == false) {
4306
- return rawKey;
4453
+ if (Object.hasOwn(document, "mcp_servers") && !isTable(document.mcp_servers)) {
4454
+ throw new Error("mcp_servers must be a TOML table.");
4307
4455
  }
4308
- const parsed = new TomlValueParser(rawKey).parse();
4309
- return parsed;
4456
+ return document;
4310
4457
  }
4311
- function bracketBalance(text) {
4312
- let balance = 0;
4313
- let quote = null;
4314
- for (let index = 0; index < text.length; index += 1) {
4315
- const current = text[index];
4316
- if (quote != null) {
4317
- if (current === "\\" && quote === '"') {
4318
- index += 1;
4319
- } else if (current === quote) {
4320
- quote = null;
4321
- }
4322
- continue;
4323
- }
4324
- if (current === '"' || current === "'") {
4325
- quote = current;
4326
- } else if (current === "#") {
4327
- break;
4328
- } else if (current === "[" || current === "{") {
4329
- balance += 1;
4330
- } else if (current === "]" || current === "}") {
4331
- balance -= 1;
4458
+ function readTomlEntries(text) {
4459
+ return readMcpServersJson({ mcpServers: parseConfigToml(text).mcp_servers ?? {} });
4460
+ }
4461
+ function hasTomlEntry(text, name) {
4462
+ return Object.hasOwn(parseConfigToml(text).mcp_servers ?? {}, name);
4463
+ }
4464
+ function normalize(document) {
4465
+ const servers = document.mcp_servers;
4466
+ if (servers == null) {
4467
+ return;
4468
+ }
4469
+ for (const server of Object.values(servers)) {
4470
+ if (isTable(server) && isTable(server.env) && Object.keys(server.env).length === 0) {
4471
+ delete server.env;
4332
4472
  }
4333
4473
  }
4334
- return balance;
4474
+ if (Object.keys(servers).length === 0) {
4475
+ delete document.mcp_servers;
4476
+ }
4335
4477
  }
4336
- function stripComment(text) {
4337
- let quote = null;
4338
- for (let index = 0; index < text.length; index += 1) {
4339
- const current = text[index];
4340
- if (quote != null) {
4341
- if (current === "\\" && quote === '"') {
4342
- index += 1;
4343
- } else if (current === quote) {
4344
- quote = null;
4345
- }
4346
- continue;
4347
- }
4348
- if (current === '"' || current === "'") {
4349
- quote = current;
4350
- } else if (current === "#") {
4351
- return text.slice(0, index);
4352
- }
4478
+ function verifyTomlEdit(before, after, name, entry) {
4479
+ const expected = parseConfigToml(before);
4480
+ const servers = expected.mcp_servers ?? /* @__PURE__ */ Object.create(null);
4481
+ if (entry == null) {
4482
+ delete servers[name];
4483
+ } else {
4484
+ const previous = Object.hasOwn(servers, name) && isTable(servers[name]) ? servers[name] : {};
4485
+ Object.defineProperty(servers, name, {
4486
+ value: Object.assign(/* @__PURE__ */ Object.create(null), previous, { command: entry.command, args: [...entry.args], env: Object.assign(/* @__PURE__ */ Object.create(null), entry.env) }),
4487
+ configurable: true,
4488
+ enumerable: true,
4489
+ writable: true
4490
+ });
4491
+ }
4492
+ expected.mcp_servers = servers;
4493
+ let actual;
4494
+ try {
4495
+ actual = parseConfigToml(after);
4496
+ } catch {
4497
+ throw new Error("Cannot safely edit this TOML layout. Use [mcp_servers.<name>] tables; nothing was written.");
4498
+ }
4499
+ normalize(expected);
4500
+ normalize(actual);
4501
+ if (!util.isDeepStrictEqual(expected, actual)) {
4502
+ throw new Error("Cannot safely edit this TOML layout without changing other settings; nothing was written.");
4503
+ }
4504
+ return after;
4505
+ }
4506
+
4507
+ // src/modules/manage/targets/toml-subset.ts
4508
+ var BARE_KEY_PATTERN = /^[A-Za-z0-9_-]+$/;
4509
+ var ANY_HEADER_PATTERN = /^\s*\[/;
4510
+ var SERVER_HEADER_PATTERN = /^\s*\[\s*(?:mcp_servers|"mcp_servers"|'mcp_servers')\s*\.\s*([A-Za-z0-9_-]+|"(?:[^"\\]|\\.)*"|'[^']*')\s*((?:\.[^\]]*)?)\]\s*(?:#.*)?$/;
4511
+ var KEY_VALUE_PATTERN = /^\s*([A-Za-z0-9_-]+|"(?:[^"\\]|\\.)*"|'[^']*')\s*=\s*(.*)$/;
4512
+ var OWNED_TABLE_KEYS = ["command", "args", "env"];
4513
+ function unquoteKey(rawKey) {
4514
+ if (rawKey.startsWith('"') || rawKey.startsWith("'")) {
4515
+ return parseConfigToml(`value = ${rawKey}`).value;
4353
4516
  }
4354
- return text;
4517
+ return rawKey;
4355
4518
  }
4356
4519
  function readStatements(body) {
4357
4520
  const statements = [];
@@ -4363,28 +4526,25 @@ function readStatements(body) {
4363
4526
  continue;
4364
4527
  }
4365
4528
  const start = index;
4366
- let rawValue = stripComment(match[2]);
4367
- let balance = bracketBalance(rawValue);
4529
+ const state = lineState();
4530
+ scanTomlLine(match[2], state);
4368
4531
  index += 1;
4369
- while (balance > 0 && index < body.length) {
4370
- const continuation = stripComment(body[index]);
4371
- rawValue += `
4372
- ${continuation}`;
4373
- balance += bracketBalance(continuation);
4532
+ while ((state.quote != null || state.depth > 0) && index < body.length) {
4533
+ scanTomlLine(body[index], state);
4374
4534
  index += 1;
4375
4535
  }
4376
- statements.push({ key: unquoteKey(match[1]), rawValue, start, end: index });
4536
+ statements.push({ key: unquoteKey(match[1]), start, end: index });
4377
4537
  }
4378
4538
  return statements;
4379
4539
  }
4380
4540
  function splitSections(lines) {
4381
4541
  const sections = [{ header: null, serverName: null, subPath: "", body: [] }];
4382
- let balance = 0;
4542
+ const state = lineState();
4383
4543
  for (const line of lines) {
4384
4544
  const current = sections[sections.length - 1];
4385
- if (balance > 0) {
4545
+ if (state.quote != null || state.depth > 0) {
4386
4546
  current.body.push(line);
4387
- balance += bracketBalance(stripComment(line));
4547
+ scanTomlLine(line, state);
4388
4548
  continue;
4389
4549
  }
4390
4550
  if (ANY_HEADER_PATTERN.test(line) == true) {
@@ -4399,73 +4559,18 @@ function splitSections(lines) {
4399
4559
  continue;
4400
4560
  }
4401
4561
  current.body.push(line);
4402
- const keyValueMatch = KEY_VALUE_PATTERN.exec(line);
4403
- if (keyValueMatch != null) {
4404
- balance = bracketBalance(stripComment(keyValueMatch[2]));
4405
- }
4562
+ scanTomlLine(line, state);
4406
4563
  }
4407
4564
  return sections;
4408
4565
  }
4409
- function parseStatementValue(statement) {
4410
- try {
4411
- const value = new TomlValueParser(statement.rawValue).parse();
4412
- return value;
4413
- } catch {
4414
- return null;
4415
- }
4566
+ function hasMcpServerToml(text, name) {
4567
+ return hasTomlEntry(text, name);
4416
4568
  }
4417
4569
  function parseMcpServersToml(text) {
4418
- const sections = splitSections(text.split(/\r?\n/));
4419
- const entries = {};
4420
- const envTables = {};
4421
- for (const section of sections) {
4422
- if (section.serverName == null) {
4423
- continue;
4424
- }
4425
- if (section.subPath === "env") {
4426
- const envTable = {};
4427
- for (const statement of readStatements(section.body)) {
4428
- const value = parseStatementValue(statement);
4429
- if (typeof value === "string") {
4430
- envTable[statement.key] = value;
4431
- }
4432
- }
4433
- envTables[section.serverName] = envTable;
4434
- continue;
4435
- }
4436
- if (section.subPath !== "") {
4437
- continue;
4438
- }
4439
- let command = null;
4440
- let args = [];
4441
- const env = {};
4442
- for (const statement of readStatements(section.body)) {
4443
- const value = parseStatementValue(statement);
4444
- if (statement.key === "command" && typeof value === "string") {
4445
- command = value;
4446
- } else if (statement.key === "args" && Array.isArray(value) == true) {
4447
- args = value.filter((item) => typeof item === "string");
4448
- } else if (statement.key === "env" && value != null && typeof value === "object" && Array.isArray(value) == false) {
4449
- for (const [key, item] of Object.entries(value)) {
4450
- if (typeof item === "string") {
4451
- env[key] = item;
4452
- }
4453
- }
4454
- }
4455
- }
4456
- if (command != null) {
4457
- entries[section.serverName] = { command, args, env };
4458
- }
4459
- }
4460
- for (const [name, envTable] of Object.entries(envTables)) {
4461
- if (entries[name] != null) {
4462
- Object.assign(entries[name].env, envTable);
4463
- }
4464
- }
4465
- return entries;
4570
+ return readTomlEntries(text);
4466
4571
  }
4467
4572
  function quoteTomlString(value) {
4468
- const escaped = value.replace(/\\/g, "\\\\").replace(/"/g, '\\"').replace(/\n/g, "\\n").replace(/\r/g, "\\r").replace(/\t/g, "\\t");
4573
+ const escaped = value.replace(/\\/g, "\\\\").replace(/"/g, '\\"').replace(/\n/g, "\\n").replace(/\r/g, "\\r").replace(/\t/g, "\\t").replace(/[\u0000-\u0008\u000b\u000c\u000e-\u001f\u007f]/g, (character) => `\\u${character.charCodeAt(0).toString(16).padStart(4, "0")}`);
4469
4574
  return `"${escaped}"`;
4470
4575
  }
4471
4576
  function formatTomlKey(key) {
@@ -4509,6 +4614,11 @@ function joinSections(sections, eol) {
4509
4614
  return lines.join(eol);
4510
4615
  }
4511
4616
  function writeMcpServerToml(text, name, entry) {
4617
+ parseConfigToml(text);
4618
+ const updated = editMcpServerToml(text, name, entry);
4619
+ return verifyTomlEdit(text, updated, name, entry);
4620
+ }
4621
+ function editMcpServerToml(text, name, entry) {
4512
4622
  let eol = "\n";
4513
4623
  if (text.includes("\r\n") == true) {
4514
4624
  eol = "\r\n";
@@ -4581,34 +4691,36 @@ function writeMcpServerToml(text, name, entry) {
4581
4691
  var CODEX_CONFIG_PATH = path__default.default.join(os5__default.default.homedir(), ".codex", "config.toml");
4582
4692
  function createCodexTarget() {
4583
4693
  function readText() {
4584
- if (fs6__default.default.existsSync(CODEX_CONFIG_PATH) == false) {
4694
+ if (fs7__default.default.existsSync(CODEX_CONFIG_PATH) == false) {
4585
4695
  return "";
4586
4696
  }
4587
- return fs6__default.default.readFileSync(CODEX_CONFIG_PATH, "utf8");
4588
- }
4589
- function writeText(text) {
4590
- fs6__default.default.mkdirSync(path__default.default.dirname(CODEX_CONFIG_PATH), { recursive: true });
4591
- fs6__default.default.writeFileSync(CODEX_CONFIG_PATH, text, "utf8");
4592
- return { status: "written", detail: CODEX_CONFIG_PATH };
4697
+ return fs7__default.default.readFileSync(CODEX_CONFIG_PATH, "utf8");
4593
4698
  }
4594
4699
  return {
4595
4700
  id: "codex",
4596
4701
  label: "Codex CLI",
4597
4702
  location: CODEX_CONFIG_PATH,
4598
- detected: findExecutable("codex") != null || fs6__default.default.existsSync(CODEX_CONFIG_PATH) == true,
4703
+ detected: findExecutable("codex") != null || fs7__default.default.existsSync(CODEX_CONFIG_PATH) == true,
4599
4704
  readEntries() {
4600
4705
  const text = readText();
4601
4706
  return parseMcpServersToml(text);
4602
4707
  },
4603
- writeEntry(name, entry) {
4604
- const text = readText();
4605
- const updated = writeMcpServerToml(text, name, entry);
4606
- return writeText(updated);
4708
+ hasEntry(name) {
4709
+ return hasMcpServerToml(readText(), name);
4710
+ },
4711
+ writeEntry(name, entry, options) {
4712
+ updateConfigFile(CODEX_CONFIG_PATH, (original) => {
4713
+ let text = original;
4714
+ if (options?.replace) {
4715
+ text = writeMcpServerToml(text, name, null);
4716
+ }
4717
+ return writeMcpServerToml(text, name, entry);
4718
+ });
4719
+ return { status: "written", detail: CODEX_CONFIG_PATH };
4607
4720
  },
4608
4721
  removeEntry(name) {
4609
- const text = readText();
4610
- const updated = writeMcpServerToml(text, name, null);
4611
- return writeText(updated);
4722
+ updateConfigFile(CODEX_CONFIG_PATH, (text) => writeMcpServerToml(text, name, null));
4723
+ return { status: "written", detail: CODEX_CONFIG_PATH };
4612
4724
  }
4613
4725
  };
4614
4726
  }
@@ -4747,10 +4859,12 @@ async function checkHost(prompter, output, target, name, entry) {
4747
4859
  const profile = profileFromEnv(output, entry.env);
4748
4860
  if (profile == null) {
4749
4861
  output.write("\n");
4862
+ await prompter.pause();
4750
4863
  return;
4751
4864
  }
4752
4865
  const result = await runConnectionCheck(profile, output);
4753
4866
  output.write("\n");
4867
+ await prompter.pause();
4754
4868
  if (result.connected == false) {
4755
4869
  return;
4756
4870
  }
@@ -4814,13 +4928,14 @@ function buildHostItems(names, entries) {
4814
4928
  };
4815
4929
  });
4816
4930
  }
4817
- async function manageHost(prompter, output, target, entries, name) {
4931
+ async function manageHost(prompter, output, target, targets, entries, name) {
4818
4932
  const action = await prompter.select(
4819
4933
  name,
4820
4934
  [
4821
4935
  { value: "edit", key: "e", label: "Edit", hint: "open its settings" },
4822
4936
  { value: "check", key: "c", label: "Check the connection", hint: "connect now and offer to pin the host key" },
4823
4937
  { value: "print", key: "p", label: "Print the entry", hint: "JSON, Codex TOML and a claude command, ready to paste" },
4938
+ ...targets.some((candidate) => candidate.id !== target.id) ? [{ value: "copy", key: "o", label: "Copy to another client", hint: "keep this entry and copy its settings" }] : [],
4824
4939
  { value: "remove", key: "r", label: "Remove", hint: `from ${target.label}` },
4825
4940
  { separator: true },
4826
4941
  { value: "back", key: "b", label: "Back" }
@@ -4839,6 +4954,10 @@ async function manageHost(prompter, output, target, entries, name) {
4839
4954
  printHost(prompter, name, entries[name]);
4840
4955
  return;
4841
4956
  }
4957
+ if (action === "copy") {
4958
+ await copyEntryToClient(prompter, target, targets, name, entries[name]);
4959
+ return;
4960
+ }
4842
4961
  if (action === "remove") {
4843
4962
  const remove = await prompter.askYesNo(`Remove ${name} from ${target.location}?`, false);
4844
4963
  if (remove == true) {
@@ -4940,7 +5059,7 @@ ${paint.bold(target.label)} ${paint.dim(target.location)}
4940
5059
  continue;
4941
5060
  }
4942
5061
  if (picked.startsWith(HOST_VALUE_PREFIX) == true) {
4943
- await manageHost(prompter, output, target, entries, picked.slice(HOST_VALUE_PREFIX.length));
5062
+ await manageHost(prompter, output, target, targets, entries, picked.slice(HOST_VALUE_PREFIX.length));
4944
5063
  }
4945
5064
  } catch (ex) {
4946
5065
  if (ex instanceof PromptClosedError) {
@@ -5393,7 +5512,7 @@ ${file.content}`);
5393
5512
  const { profile, session: openSession } = resolveProfile(context, { session });
5394
5513
  const resolvedLocalPath = expandHomePath(localPath);
5395
5514
  requirePermittedLocalPath(resolvedLocalPath, "read");
5396
- if (fs6__default.default.existsSync(resolvedLocalPath) == false) {
5515
+ if (fs7__default.default.existsSync(resolvedLocalPath) == false) {
5397
5516
  return textResult(`local file not found: ${resolvedLocalPath}`);
5398
5517
  }
5399
5518
  const transferredBytes = await withHostConnection(profile, openSession, async (connection) => {
@@ -5433,7 +5552,7 @@ ${file.content}`);
5433
5552
  const { profile, session: openSession } = resolveProfile(context, { session });
5434
5553
  const resolvedLocalPath = expandHomePath(localPath);
5435
5554
  requirePermittedLocalPath(resolvedLocalPath, "write");
5436
- fs6__default.default.mkdirSync(path__default.default.dirname(resolvedLocalPath), { recursive: true });
5555
+ fs7__default.default.mkdirSync(path__default.default.dirname(resolvedLocalPath), { recursive: true });
5437
5556
  const transferredBytes = await withHostConnection(profile, openSession, async (connection) => {
5438
5557
  return await downloadFile(connection, remotePath, resolvedLocalPath);
5439
5558
  });