@akms/mcp-ssh 0.1.7 → 0.1.9

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,24 +2,24 @@
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');
9
9
  var crypto = require('crypto');
10
10
  var stream = require('stream');
11
11
  var string_decoder = require('string_decoder');
12
- var ssh2 = require('ssh2');
13
12
  var timers = require('timers');
13
+ var util = require('util');
14
+ var smolToml = require('smol-toml');
14
15
  var mcp_js = require('@modelcontextprotocol/sdk/server/mcp.js');
15
16
  var zod = require('zod');
16
17
 
17
18
  function _interopDefault (e) { return e && e.__esModule ? e : { default: e }; }
18
19
 
19
- var fs6__default = /*#__PURE__*/_interopDefault(fs6);
20
+ var fs7__default = /*#__PURE__*/_interopDefault(fs7);
20
21
  var os5__default = /*#__PURE__*/_interopDefault(os5);
21
22
  var path__default = /*#__PURE__*/_interopDefault(path);
22
- var ssh2__default = /*#__PURE__*/_interopDefault(ssh2);
23
23
 
24
24
  // src/_defs/defaults.ts
25
25
  var LOG_LEVEL_ENV = "SSH_MCP_LOG_LEVEL";
@@ -36,7 +36,7 @@ var CWD_MARKER = "__AKMS_SSH_CWD__";
36
36
  var MAX_DIRECTORY_ENTRIES = 1e3;
37
37
  var SERVER_NAME = "akms-mcp-ssh";
38
38
  var PACKAGE_NAME = "@akms/mcp-ssh";
39
- var SERVER_VERSION = "0.1.7";
39
+ var SERVER_VERSION = "0.1.9";
40
40
 
41
41
  // src/_defs/command-rules.ts
42
42
  var CRITICAL_PATH_PATTERN = new RegExp(
@@ -622,7 +622,7 @@ function findExecutable(name) {
622
622
  for (const directory of directories) {
623
623
  for (const suffix of suffixes) {
624
624
  const candidate = path__default.default.join(directory, `${name}${suffix}`);
625
- if (fs6__default.default.existsSync(candidate) == true && fs6__default.default.statSync(candidate).isFile() == true) {
625
+ if (fs7__default.default.existsSync(candidate) == true && fs7__default.default.statSync(candidate).isFile() == true) {
626
626
  return candidate;
627
627
  }
628
628
  }
@@ -763,7 +763,7 @@ function loadHostProfile(env = process.env) {
763
763
  if (keyPath != null) {
764
764
  const expandedKeyPath = expandHomePath(keyPath);
765
765
  profile.privateKeyPath = expandedKeyPath;
766
- if (fs6__default.default.existsSync(expandedKeyPath) == false) {
766
+ if (fs7__default.default.existsSync(expandedKeyPath) == false) {
767
767
  _logger.warn(`${SINGLE_HOST_ENV.KEY} does not exist; connections will fail`, {
768
768
  private_key_path: expandedKeyPath
769
769
  });
@@ -795,7 +795,7 @@ function loadHostProfile(env = process.env) {
795
795
  if (jumpKeyPath != null) {
796
796
  profile.jump.privateKeyPath = expandHomePath(jumpKeyPath);
797
797
  profile.jump.passphrase = readEnv(env, SINGLE_HOST_ENV.JUMP_PASSPHRASE);
798
- if (fs6__default.default.existsSync(profile.jump.privateKeyPath) == false) {
798
+ if (fs7__default.default.existsSync(profile.jump.privateKeyPath) == false) {
799
799
  _logger.warn(`${SINGLE_HOST_ENV.JUMP_KEY} does not exist; connections will fail`, {
800
800
  private_key_path: profile.jump.privateKeyPath
801
801
  });
@@ -1281,14 +1281,14 @@ function inspectRemotePath(remotePath, policy, access) {
1281
1281
  function toRealPath(inputPath) {
1282
1282
  const _logger = $logger.child({ context: "toRealPath" });
1283
1283
  try {
1284
- return fs6__default.default.realpathSync.native(inputPath);
1284
+ return fs7__default.default.realpathSync.native(inputPath);
1285
1285
  } catch (ex) {
1286
1286
  _logger.debug("path does not resolve, falling back to its parent directory", {
1287
1287
  reason: ex instanceof Error ? ex.name : "unknown"
1288
1288
  });
1289
1289
  }
1290
1290
  try {
1291
- const parentRealPath = fs6__default.default.realpathSync.native(path__default.default.dirname(inputPath));
1291
+ const parentRealPath = fs7__default.default.realpathSync.native(path__default.default.dirname(inputPath));
1292
1292
  return path__default.default.join(parentRealPath, path__default.default.basename(inputPath));
1293
1293
  } catch (ex) {
1294
1294
  _logger.debug("parent directory does not resolve either, comparing lexically", {
@@ -1360,7 +1360,6 @@ var GuardRejectionError = class extends Error {
1360
1360
  };
1361
1361
 
1362
1362
  // src/modules/ssh/ssh-connection.ts
1363
- var { Client } = ssh2__default.default;
1364
1363
  var OUTPUT_HARD_CAP_MULTIPLIER = 4;
1365
1364
  var PRIVATE_KEY_CACHE = /* @__PURE__ */ new Map();
1366
1365
  var WINDOWS_AGENT_PIPE = "\\\\.\\pipe\\openssh-ssh-agent";
@@ -1380,7 +1379,7 @@ function readPrivateKey(label, keyPath) {
1380
1379
  return cached;
1381
1380
  }
1382
1381
  try {
1383
- const contents = fs6__default.default.readFileSync(keyPath);
1382
+ const contents = fs7__default.default.readFileSync(keyPath);
1384
1383
  PRIVATE_KEY_CACHE.set(keyPath, contents);
1385
1384
  return contents;
1386
1385
  } catch (ex) {
@@ -1502,7 +1501,7 @@ function openProxyCommand(profile, hop) {
1502
1501
  }
1503
1502
  };
1504
1503
  }
1505
- function openJumpTransport(profile, jump, proxy, onHostKeySeen) {
1504
+ function openJumpTransport(profile, jump, proxy, onHostKeySeen, Client) {
1506
1505
  const label = `${profile.name} jump host`;
1507
1506
  const endpoint = `${jump.username}@${jump.host}:${jump.port}`;
1508
1507
  const _logger = $logger.child({ context: "openJumpTransport", profile: profile.name, jump_host: jump.host });
@@ -1567,7 +1566,7 @@ function openJumpTransport(profile, jump, proxy, onHostKeySeen) {
1567
1566
  jumpClient.connect(jumpConfig);
1568
1567
  });
1569
1568
  }
1570
- async function openTransport(profile, onJumpHostKeySeen) {
1569
+ async function openTransport(profile, onJumpHostKeySeen, Client) {
1571
1570
  let proxy = null;
1572
1571
  if (profile.proxyCommand != null) {
1573
1572
  proxy = openProxyCommand(profile, profile.jump ?? profile);
@@ -1576,7 +1575,7 @@ async function openTransport(profile, onJumpHostKeySeen) {
1576
1575
  return proxy;
1577
1576
  }
1578
1577
  try {
1579
- const jumpTransport = await openJumpTransport(profile, profile.jump, proxy, onJumpHostKeySeen);
1578
+ const jumpTransport = await openJumpTransport(profile, profile.jump, proxy, onJumpHostKeySeen, Client);
1580
1579
  return jumpTransport;
1581
1580
  } catch (ex) {
1582
1581
  proxy?.close();
@@ -1640,6 +1639,11 @@ var SshConnection = class {
1640
1639
  get wasDropped() {
1641
1640
  return this.droppedAfterReady;
1642
1641
  }
1642
+ assertNotDisposed() {
1643
+ if (this.disposed == true) {
1644
+ throw new Error(`ssh connection for '${this.profile.name}' has been closed; open a new session`);
1645
+ }
1646
+ }
1643
1647
  /**
1644
1648
  * Opens the connection, or joins the in-flight attempt when one is already running.
1645
1649
  *
@@ -1650,9 +1654,7 @@ var SshConnection = class {
1650
1654
  * unreachable, the handshake times out, or the carrier cannot be opened.
1651
1655
  */
1652
1656
  async connect(onProgress) {
1653
- if (this.disposed == true) {
1654
- throw new Error(`ssh connection for '${this.profile.name}' has been closed; open a new session`);
1655
- }
1657
+ this.assertNotDisposed();
1656
1658
  if (this.client != null) {
1657
1659
  return;
1658
1660
  }
@@ -1671,12 +1673,14 @@ var SshConnection = class {
1671
1673
  this.observedFingerprint = fingerprint;
1672
1674
  });
1673
1675
  this.connecting = (async () => {
1676
+ const { default: { Client } } = await import('ssh2');
1677
+ this.assertNotDisposed();
1674
1678
  if (this.profile.jump != null || this.profile.proxyCommand != null) {
1675
1679
  onProgress?.("transport");
1676
1680
  }
1677
1681
  const transport = await openTransport(this.profile, (fingerprint) => {
1678
1682
  this.observedJumpFingerprint = fingerprint;
1679
- });
1683
+ }, Client);
1680
1684
  if (this.disposed == true) {
1681
1685
  transport?.close();
1682
1686
  throw new Error(`ssh connection for '${this.profile.name}' was closed during the handshake`);
@@ -2335,7 +2339,7 @@ async function writeRemoteFile(connection, args) {
2335
2339
  }
2336
2340
  async function uploadFile(connection, localPath, remotePath) {
2337
2341
  requirePermittedPath(connection, remotePath, "write");
2338
- const localStats = fs6__default.default.statSync(localPath);
2342
+ const localStats = fs7__default.default.statSync(localPath);
2339
2343
  if (localStats.isFile() == false) {
2340
2344
  throw new Error(`local path is not a file: ${localPath}`);
2341
2345
  }
@@ -2372,7 +2376,7 @@ async function downloadFile(connection, remotePath, localPath) {
2372
2376
  });
2373
2377
  });
2374
2378
  });
2375
- const localStats = fs6__default.default.statSync(localPath);
2379
+ const localStats = fs7__default.default.statSync(localPath);
2376
2380
  const _logger = $logger.child({
2377
2381
  context: "downloadFile",
2378
2382
  profile: connection.profile.name,
@@ -3202,6 +3206,61 @@ async function runConnectionCheck(profile, output) {
3202
3206
  };
3203
3207
  }
3204
3208
 
3209
+ // src/modules/manage/copy-entry.ts
3210
+ async function copyEntryToClient(prompter, source, targets, name, entry) {
3211
+ const destinations = targets.filter((target) => target.id !== source.id);
3212
+ if (destinations.length === 0) {
3213
+ return;
3214
+ }
3215
+ for (; ; ) {
3216
+ const items = destinations.map((target) => ({
3217
+ value: target.id,
3218
+ key: target.id,
3219
+ label: target.label,
3220
+ hint: target.detected ? target.location : `${target.location} \xB7 not found on this machine`
3221
+ }));
3222
+ items.push({ separator: true }, { value: "back", key: "b", label: "Back" });
3223
+ const picked = await prompter.select(`Copy ${name} to which client?`, items, { cancelValue: "back" });
3224
+ const destination = destinations.find((target) => target.id === picked);
3225
+ if (destination == null) {
3226
+ return;
3227
+ }
3228
+ const exists = destination.hasEntry?.(name) ?? Object.hasOwn(destination.readEntries(), name);
3229
+ if (exists) {
3230
+ try {
3231
+ const overwrite = await prompter.askYesNo(
3232
+ `${name} already exists in ${destination.label} (${destination.location}). Overwrite it?`,
3233
+ false
3234
+ );
3235
+ if (overwrite == false) {
3236
+ prompter.write(" Not copied; the destination entry was kept.\n\n");
3237
+ return;
3238
+ }
3239
+ } catch (ex) {
3240
+ if (ex instanceof PromptBackError == false) {
3241
+ throw ex;
3242
+ }
3243
+ continue;
3244
+ }
3245
+ }
3246
+ const result = destination.writeEntry(name, {
3247
+ command: entry.command,
3248
+ args: [...entry.args],
3249
+ env: { ...entry.env }
3250
+ }, { replace: true });
3251
+ if (result.status === "written") {
3252
+ prompter.write(` ${prompter.paint.success(`Copied ${name} to ${destination.label}`)} ${prompter.paint.dim(result.detail)}
3253
+ `);
3254
+ prompter.write(prompter.paint.dim(" Restart the destination client (or reconnect it from its MCP menu) to pick up the change.\n\n"));
3255
+ } else {
3256
+ prompter.write(` Not copied: ${result.detail}
3257
+
3258
+ `);
3259
+ }
3260
+ return;
3261
+ }
3262
+ }
3263
+
3205
3264
  // src/modules/manage/entry.ts
3206
3265
  var MANAGED_ENV_NAMES = [
3207
3266
  SINGLE_HOST_ENV.HOST,
@@ -3432,7 +3491,7 @@ function findPrivateKeys() {
3432
3491
  const sshDirectory = path__default.default.join(os5__default.default.homedir(), ".ssh");
3433
3492
  let names = [];
3434
3493
  try {
3435
- names = fs6__default.default.readdirSync(sshDirectory);
3494
+ names = fs7__default.default.readdirSync(sshDirectory);
3436
3495
  } catch {
3437
3496
  return [];
3438
3497
  }
@@ -3444,14 +3503,14 @@ function findPrivateKeys() {
3444
3503
  const fullPath = path__default.default.join(sshDirectory, name);
3445
3504
  let head = "";
3446
3505
  try {
3447
- const stat = fs6__default.default.statSync(fullPath);
3506
+ const stat = fs7__default.default.statSync(fullPath);
3448
3507
  if (stat.isFile() == false || stat.size > MAX_KEY_FILE_BYTES) {
3449
3508
  continue;
3450
3509
  }
3451
- const descriptor = fs6__default.default.openSync(fullPath, "r");
3510
+ const descriptor = fs7__default.default.openSync(fullPath, "r");
3452
3511
  const buffer = Buffer.alloc(64);
3453
- const bytesRead = fs6__default.default.readSync(descriptor, buffer, 0, buffer.length, 0);
3454
- fs6__default.default.closeSync(descriptor);
3512
+ const bytesRead = fs7__default.default.readSync(descriptor, buffer, 0, buffer.length, 0);
3513
+ fs7__default.default.closeSync(descriptor);
3455
3514
  head = buffer.subarray(0, bytesRead).toString("utf8");
3456
3515
  } catch {
3457
3516
  continue;
@@ -3463,7 +3522,7 @@ function findPrivateKeys() {
3463
3522
  }
3464
3523
  let hint = "";
3465
3524
  try {
3466
- const publicKey = fs6__default.default.readFileSync(`${fullPath}.pub`, "utf8").trim().split(/\s+/);
3525
+ const publicKey = fs7__default.default.readFileSync(`${fullPath}.pub`, "utf8").trim().split(/\s+/);
3467
3526
  hint = publicKey[0].replace(/^ssh-/, "");
3468
3527
  if (publicKey.length > 2) {
3469
3528
  hint += ` \xB7 ${publicKey.slice(2).join(" ")}`;
@@ -3532,7 +3591,7 @@ async function askKeyPath(prompter, current) {
3532
3591
  }
3533
3592
  for (; ; ) {
3534
3593
  const typed = await prompter.askRequired("Private key path");
3535
- if (fs6__default.default.existsSync(expandHomePath(typed)) == true) {
3594
+ if (fs7__default.default.existsSync(expandHomePath(typed)) == true) {
3536
3595
  return typed;
3537
3596
  }
3538
3597
  prompter.write(` ${expandHomePath(typed)} does not exist.
@@ -4066,6 +4125,65 @@ async function runHostForm(prompter, output, options) {
4066
4125
  prompter.transient = previousTransient;
4067
4126
  }
4068
4127
  }
4128
+ function readExisting(filePath) {
4129
+ try {
4130
+ return fs7__default.default.readFileSync(filePath, "utf8");
4131
+ } catch (error) {
4132
+ if (error.code === "ENOENT") {
4133
+ return null;
4134
+ }
4135
+ throw error;
4136
+ }
4137
+ }
4138
+ function updateConfigFile(filePath, update) {
4139
+ let destination = filePath;
4140
+ let symbolicLink = false;
4141
+ try {
4142
+ symbolicLink = fs7__default.default.lstatSync(filePath).isSymbolicLink();
4143
+ } catch (error) {
4144
+ if (error.code !== "ENOENT") {
4145
+ throw error;
4146
+ }
4147
+ }
4148
+ if (symbolicLink) {
4149
+ destination = fs7__default.default.realpathSync(filePath);
4150
+ }
4151
+ const before = readExisting(destination);
4152
+ const updated = update(before ?? "");
4153
+ if (before != null) {
4154
+ fs7__default.default.accessSync(destination, fs7__default.default.constants.W_OK);
4155
+ }
4156
+ fs7__default.default.mkdirSync(path__default.default.dirname(destination), { recursive: true });
4157
+ const temporary = path__default.default.join(path__default.default.dirname(destination), `.${path__default.default.basename(destination)}.${crypto.randomUUID()}.tmp`);
4158
+ const mode = before == null ? 384 : fs7__default.default.statSync(destination).mode & 511;
4159
+ let descriptor;
4160
+ let ownsTemporary = false;
4161
+ try {
4162
+ descriptor = fs7__default.default.openSync(temporary, "wx", mode);
4163
+ ownsTemporary = true;
4164
+ fs7__default.default.writeFileSync(descriptor, updated, "utf8");
4165
+ fs7__default.default.fsyncSync(descriptor);
4166
+ fs7__default.default.closeSync(descriptor);
4167
+ descriptor = void 0;
4168
+ if (readExisting(destination) !== before) {
4169
+ throw new Error("The config changed while saving; reload it and try again.");
4170
+ }
4171
+ fs7__default.default.renameSync(temporary, destination);
4172
+ ownsTemporary = false;
4173
+ } finally {
4174
+ try {
4175
+ if (descriptor != null) {
4176
+ fs7__default.default.closeSync(descriptor);
4177
+ }
4178
+ } finally {
4179
+ if (ownsTemporary) {
4180
+ fs7__default.default.unlinkSync(temporary);
4181
+ }
4182
+ }
4183
+ }
4184
+ }
4185
+
4186
+ // src/modules/manage/targets/json-file.ts
4069
4187
  function toMcpServerEntry(value) {
4070
4188
  if (typeof value !== "object" || value == null) {
4071
4189
  return null;
@@ -4078,7 +4196,7 @@ function toMcpServerEntry(value) {
4078
4196
  if (Array.isArray(record.args) == true) {
4079
4197
  args = record.args.filter((item) => typeof item === "string");
4080
4198
  }
4081
- const env = {};
4199
+ const env = /* @__PURE__ */ Object.create(null);
4082
4200
  if (typeof record.env === "object" && record.env != null) {
4083
4201
  for (const [key, item] of Object.entries(record.env)) {
4084
4202
  if (typeof item === "string") {
@@ -4088,16 +4206,22 @@ function toMcpServerEntry(value) {
4088
4206
  }
4089
4207
  return { command: record.command, args, env };
4090
4208
  }
4091
- function readMcpServersJson(document) {
4092
- if (typeof document !== "object" || document == null || Array.isArray(document) == true) {
4209
+ function readRawMcpServersJson(document) {
4210
+ if (typeof document !== "object" || document == null || Array.isArray(document)) {
4093
4211
  throw new Error("the config file is not a JSON object");
4094
4212
  }
4095
- const entries = {};
4096
4213
  const servers = document.mcpServers;
4097
- if (typeof servers !== "object" || servers == null) {
4098
- return entries;
4214
+ if (servers === void 0) {
4215
+ return {};
4216
+ }
4217
+ if (typeof servers !== "object" || servers == null || Array.isArray(servers)) {
4218
+ throw new Error("mcpServers is not a JSON object");
4099
4219
  }
4100
- for (const [name, value] of Object.entries(servers)) {
4220
+ return servers;
4221
+ }
4222
+ function readMcpServersJson(document) {
4223
+ const entries = /* @__PURE__ */ Object.create(null);
4224
+ for (const [name, value] of Object.entries(readRawMcpServersJson(document))) {
4101
4225
  const entry = toMcpServerEntry(value);
4102
4226
  if (entry != null) {
4103
4227
  entries[name] = entry;
@@ -4105,11 +4229,11 @@ function readMcpServersJson(document) {
4105
4229
  }
4106
4230
  return entries;
4107
4231
  }
4108
- function readDocument(filePath) {
4109
- if (fs6__default.default.existsSync(filePath) == false) {
4232
+ function readDocument(filePath, contents) {
4233
+ if (contents == null && fs7__default.default.existsSync(filePath) == false) {
4110
4234
  return {};
4111
4235
  }
4112
- const text = fs6__default.default.readFileSync(filePath, "utf8");
4236
+ const text = contents ?? fs7__default.default.readFileSync(filePath, "utf8");
4113
4237
  if (text.trim() === "") {
4114
4238
  return {};
4115
4239
  }
@@ -4130,39 +4254,35 @@ function readDocument(filePath) {
4130
4254
  }
4131
4255
  function createJsonFileTarget(options) {
4132
4256
  const { id, label, filePath } = options;
4133
- function writeDocument(document) {
4134
- fs6__default.default.mkdirSync(path__default.default.dirname(filePath), { recursive: true });
4135
- fs6__default.default.writeFileSync(filePath, `${JSON.stringify(document, null, 4)}
4136
- `, "utf8");
4137
- }
4138
4257
  return {
4139
4258
  id,
4140
4259
  label,
4141
4260
  location: filePath,
4142
- detected: fs6__default.default.existsSync(filePath),
4261
+ detected: fs7__default.default.existsSync(filePath),
4143
4262
  readEntries() {
4144
4263
  const document = readDocument(filePath);
4145
4264
  return readMcpServersJson(document);
4146
4265
  },
4266
+ hasEntry(name) {
4267
+ return Object.hasOwn(readRawMcpServersJson(readDocument(filePath)), name);
4268
+ },
4147
4269
  writeEntry(name, entry) {
4148
- const document = readDocument(filePath);
4149
- let servers = document.mcpServers;
4150
- if (typeof servers !== "object" || servers == null || Array.isArray(servers) == true) {
4151
- servers = {};
4152
- }
4153
- servers[name] = entry;
4154
- document.mcpServers = servers;
4155
- writeDocument(document);
4270
+ updateConfigFile(filePath, (contents) => {
4271
+ const document = readDocument(filePath, contents);
4272
+ const servers = Object.assign(/* @__PURE__ */ Object.create(null), readRawMcpServersJson(document));
4273
+ servers[name] = entry;
4274
+ document.mcpServers = servers;
4275
+ return JSON.stringify(document, null, 4) + "\n";
4276
+ });
4156
4277
  return { status: "written", detail: filePath };
4157
4278
  },
4158
4279
  removeEntry(name) {
4159
- const document = readDocument(filePath);
4160
- const servers = document.mcpServers;
4161
- if (typeof servers !== "object" || servers == null || Array.isArray(servers) == true) {
4162
- return { status: "written", detail: filePath };
4163
- }
4164
- delete servers[name];
4165
- writeDocument(document);
4280
+ updateConfigFile(filePath, (contents) => {
4281
+ const document = readDocument(filePath, contents);
4282
+ const servers = readRawMcpServersJson(document);
4283
+ delete servers[name];
4284
+ return JSON.stringify(document, null, 4) + "\n";
4285
+ });
4166
4286
  return { status: "written", detail: filePath };
4167
4287
  }
4168
4288
  };
@@ -4214,11 +4334,11 @@ function runClaude(executable, args) {
4214
4334
  }
4215
4335
  return { status: "written", detail: CLAUDE_CODE_CONFIG_PATH };
4216
4336
  }
4217
- function readClaudeEntries() {
4218
- if (fs6__default.default.existsSync(CLAUDE_CODE_CONFIG_PATH) == false) {
4337
+ function readRawClaudeEntries() {
4338
+ if (fs7__default.default.existsSync(CLAUDE_CODE_CONFIG_PATH) == false) {
4219
4339
  return {};
4220
4340
  }
4221
- const text = fs6__default.default.readFileSync(CLAUDE_CODE_CONFIG_PATH, "utf8");
4341
+ const text = fs7__default.default.readFileSync(CLAUDE_CODE_CONFIG_PATH, "utf8");
4222
4342
  let document;
4223
4343
  try {
4224
4344
  document = JSON.parse(text);
@@ -4229,7 +4349,7 @@ function readClaudeEntries() {
4229
4349
  }
4230
4350
  throw new Error(`${CLAUDE_CODE_CONFIG_PATH} is not valid JSON: ${detail}`);
4231
4351
  }
4232
- return readMcpServersJson(document);
4352
+ return readRawMcpServersJson(document);
4233
4353
  }
4234
4354
  function createClaudeCodeTarget() {
4235
4355
  const executable = findExecutable("claude");
@@ -4237,28 +4357,35 @@ function createClaudeCodeTarget() {
4237
4357
  id: "claude-code",
4238
4358
  label: "Claude Code (user scope)",
4239
4359
  location: CLAUDE_CODE_CONFIG_PATH,
4240
- detected: executable != null || fs6__default.default.existsSync(CLAUDE_CODE_CONFIG_PATH) == true,
4241
- readEntries: readClaudeEntries,
4360
+ detected: executable != null || fs7__default.default.existsSync(CLAUDE_CODE_CONFIG_PATH) == true,
4361
+ readEntries() {
4362
+ return readMcpServersJson({ mcpServers: readRawClaudeEntries() });
4363
+ },
4364
+ hasEntry(name) {
4365
+ return Object.hasOwn(readRawClaudeEntries(), name);
4366
+ },
4242
4367
  writeEntry(name, entry) {
4243
4368
  if (executable == null) {
4244
4369
  return {
4245
4370
  status: "unavailable",
4246
- detail: `the claude CLI is not on PATH \u2014 run this yourself:
4247
- ${formatClaudeAddCommand(name, entry)}`
4371
+ detail: "the claude CLI is not on PATH \u2014 install it, or use Print to get a command to run yourself"
4248
4372
  };
4249
4373
  }
4250
- const previous = readClaudeEntries()[name];
4251
- if (previous != null) {
4374
+ const previousEntries = readRawClaudeEntries();
4375
+ const hadPrevious = Object.hasOwn(previousEntries, name);
4376
+ const previous = previousEntries[name];
4377
+ if (hadPrevious) {
4252
4378
  const removed = runClaude(executable, ["mcp", "remove", name, "-s", "user"]);
4253
4379
  if (removed.status === "unavailable") {
4254
4380
  return removed;
4255
4381
  }
4256
4382
  }
4257
4383
  const added = runClaude(executable, ["mcp", "add-json", name, toClaudeAddJsonPayload(entry), "-s", "user"]);
4258
- if (added.status === "unavailable" && previous != null) {
4259
- runClaude(executable, ["mcp", "add-json", name, toClaudeAddJsonPayload(previous), "-s", "user"]);
4384
+ if (added.status === "unavailable" && hadPrevious) {
4385
+ const restored = runClaude(executable, ["mcp", "add-json", name, JSON.stringify(previous), "-s", "user"]);
4386
+ const recovery = restored.status === "written" ? "the previous entry was restored" : `could not restore the previous entry: ${restored.detail}`;
4260
4387
  return { status: "unavailable", detail: `${added.detail}
4261
- the previous entry was restored` };
4388
+ ${recovery}` };
4262
4389
  }
4263
4390
  return added;
4264
4391
  },
@@ -4275,194 +4402,121 @@ function createClaudeCodeTarget() {
4275
4402
  };
4276
4403
  }
4277
4404
 
4278
- // src/modules/manage/targets/toml-subset.ts
4279
- var BARE_KEY_PATTERN = /^[A-Za-z0-9_-]+$/;
4280
- var ANY_HEADER_PATTERN = /^\s*\[/;
4281
- var SERVER_HEADER_PATTERN = /^\s*\[\s*mcp_servers\s*\.\s*([A-Za-z0-9_-]+|"(?:[^"\\]|\\.)*")\s*((?:\.[^\]]*)?)\]\s*(?:#.*)?$/;
4282
- var KEY_VALUE_PATTERN = /^\s*([A-Za-z0-9_-]+|"(?:[^"\\]|\\.)*")\s*=\s*(.*)$/;
4283
- var OWNED_TABLE_KEYS = ["command", "args", "env"];
4284
- var TomlValueParser = class {
4285
- constructor(text) {
4286
- this.text = text;
4287
- this.position = 0;
4288
- }
4289
- parse() {
4290
- const value = this.parseValue();
4291
- return value;
4292
- }
4293
- skipWhitespace() {
4294
- while (this.position < this.text.length && /\s/.test(this.text[this.position]) == true) {
4295
- this.position += 1;
4296
- }
4297
- }
4298
- parseValue() {
4299
- this.skipWhitespace();
4300
- const current = this.text[this.position];
4301
- if (current === '"') {
4302
- return this.parseBasicString();
4303
- }
4304
- if (current === "'") {
4305
- return this.parseLiteralString();
4306
- }
4307
- if (current === "[") {
4308
- return this.parseArray();
4309
- }
4310
- if (current === "{") {
4311
- return this.parseInlineTable();
4312
- }
4313
- throw new Error(`unsupported toml value at offset ${this.position}`);
4314
- }
4315
- parseBasicString() {
4316
- this.position += 1;
4317
- let result = "";
4318
- while (this.position < this.text.length) {
4319
- const current = this.text[this.position];
4320
- if (current === '"') {
4321
- this.position += 1;
4322
- return result;
4323
- }
4324
- if (current !== "\\") {
4325
- result += current;
4326
- this.position += 1;
4327
- continue;
4328
- }
4329
- const escaped = this.text[this.position + 1];
4330
- this.position += 2;
4331
- if (escaped === "n") {
4332
- result += "\n";
4333
- } else if (escaped === "t") {
4334
- result += " ";
4335
- } else if (escaped === "r") {
4336
- result += "\r";
4337
- } else if (escaped === "u" || escaped === "U") {
4338
- let length = 4;
4339
- if (escaped === "U") {
4340
- length = 8;
4405
+ // src/modules/manage/targets/toml-lines.ts
4406
+ function lineState() {
4407
+ return { quote: null, multiline: false, depth: 0 };
4408
+ }
4409
+ function scanTomlLine(line, state) {
4410
+ for (let index = 0; index < line.length; index += 1) {
4411
+ const character = line[index];
4412
+ if (state.quote != null) {
4413
+ if (character === "\\" && state.quote === '"') {
4414
+ index += 1;
4415
+ } else if (character === state.quote) {
4416
+ if (!state.multiline) {
4417
+ state.quote = null;
4418
+ } else if (line.slice(index, index + 3) === state.quote.repeat(3)) {
4419
+ while (line[index + 1] === character) {
4420
+ index += 1;
4421
+ }
4422
+ state.quote = null;
4423
+ state.multiline = false;
4341
4424
  }
4342
- const hex = this.text.slice(this.position, this.position + length);
4343
- result += String.fromCodePoint(Number.parseInt(hex, 16));
4344
- this.position += length;
4345
- } else {
4346
- result += escaped;
4347
4425
  }
4426
+ continue;
4348
4427
  }
4349
- throw new Error("unterminated toml string");
4350
- }
4351
- parseLiteralString() {
4352
- const end = this.text.indexOf("'", this.position + 1);
4353
- if (end < 0) {
4354
- throw new Error("unterminated toml literal string");
4428
+ if (character === "#") {
4429
+ break;
4355
4430
  }
4356
- const result = this.text.slice(this.position + 1, end);
4357
- this.position = end + 1;
4358
- return result;
4359
- }
4360
- parseArray() {
4361
- this.position += 1;
4362
- const items = [];
4363
- for (; ; ) {
4364
- this.skipWhitespace();
4365
- if (this.position >= this.text.length) {
4366
- throw new Error("unterminated toml array");
4367
- }
4368
- if (this.text[this.position] === "]") {
4369
- this.position += 1;
4370
- return items;
4371
- }
4372
- items.push(this.parseValue());
4373
- this.skipWhitespace();
4374
- if (this.text[this.position] === ",") {
4375
- this.position += 1;
4431
+ if (character === '"' || character === "'") {
4432
+ state.quote = character;
4433
+ state.multiline = line.slice(index, index + 3) === character.repeat(3);
4434
+ if (state.multiline) {
4435
+ index += 2;
4376
4436
  }
4437
+ } else if (character === "[" || character === "{") {
4438
+ state.depth += 1;
4439
+ } else if (character === "]" || character === "}") {
4440
+ state.depth -= 1;
4377
4441
  }
4378
4442
  }
4379
- parseInlineTable() {
4380
- this.position += 1;
4381
- const table = {};
4382
- for (; ; ) {
4383
- this.skipWhitespace();
4384
- if (this.position >= this.text.length) {
4385
- throw new Error("unterminated toml inline table");
4386
- }
4387
- if (this.text[this.position] === "}") {
4388
- this.position += 1;
4389
- return table;
4390
- }
4391
- let key;
4392
- if (this.text[this.position] === '"') {
4393
- key = this.parseBasicString();
4394
- } else {
4395
- const match = /^[A-Za-z0-9_-]+/.exec(this.text.slice(this.position));
4396
- if (match == null) {
4397
- throw new Error(`invalid inline table key at offset ${this.position}`);
4398
- }
4399
- key = match[0];
4400
- this.position += key.length;
4401
- }
4402
- this.skipWhitespace();
4403
- if (this.text[this.position] !== "=") {
4404
- throw new Error(`expected '=' at offset ${this.position}`);
4405
- }
4406
- this.position += 1;
4407
- table[key] = this.parseValue();
4408
- this.skipWhitespace();
4409
- if (this.text[this.position] === ",") {
4410
- this.position += 1;
4411
- }
4412
- }
4443
+ }
4444
+ function isTable(value) {
4445
+ return value != null && typeof value === "object" && !Array.isArray(value) && !(value instanceof Date);
4446
+ }
4447
+ function parseConfigToml(text) {
4448
+ let document;
4449
+ try {
4450
+ document = smolToml.parse(text, { integersAsBigInt: true });
4451
+ } catch (error) {
4452
+ const location = error instanceof smolToml.TomlError ? ` at line ${error.line}, column ${error.column}` : "";
4453
+ throw new Error(`Invalid TOML${location}; fix the config before changing it.`);
4413
4454
  }
4414
- };
4415
- function unquoteKey(rawKey) {
4416
- if (rawKey.startsWith('"') == false) {
4417
- return rawKey;
4455
+ if (Object.hasOwn(document, "mcp_servers") && !isTable(document.mcp_servers)) {
4456
+ throw new Error("mcp_servers must be a TOML table.");
4418
4457
  }
4419
- const parsed = new TomlValueParser(rawKey).parse();
4420
- return parsed;
4458
+ return document;
4421
4459
  }
4422
- function bracketBalance(text) {
4423
- let balance = 0;
4424
- let quote = null;
4425
- for (let index = 0; index < text.length; index += 1) {
4426
- const current = text[index];
4427
- if (quote != null) {
4428
- if (current === "\\" && quote === '"') {
4429
- index += 1;
4430
- } else if (current === quote) {
4431
- quote = null;
4432
- }
4433
- continue;
4434
- }
4435
- if (current === '"' || current === "'") {
4436
- quote = current;
4437
- } else if (current === "#") {
4438
- break;
4439
- } else if (current === "[" || current === "{") {
4440
- balance += 1;
4441
- } else if (current === "]" || current === "}") {
4442
- balance -= 1;
4460
+ function readTomlEntries(text) {
4461
+ return readMcpServersJson({ mcpServers: parseConfigToml(text).mcp_servers ?? {} });
4462
+ }
4463
+ function hasTomlEntry(text, name) {
4464
+ return Object.hasOwn(parseConfigToml(text).mcp_servers ?? {}, name);
4465
+ }
4466
+ function normalize(document) {
4467
+ const servers = document.mcp_servers;
4468
+ if (servers == null) {
4469
+ return;
4470
+ }
4471
+ for (const server of Object.values(servers)) {
4472
+ if (isTable(server) && isTable(server.env) && Object.keys(server.env).length === 0) {
4473
+ delete server.env;
4443
4474
  }
4444
4475
  }
4445
- return balance;
4476
+ if (Object.keys(servers).length === 0) {
4477
+ delete document.mcp_servers;
4478
+ }
4446
4479
  }
4447
- function stripComment(text) {
4448
- let quote = null;
4449
- for (let index = 0; index < text.length; index += 1) {
4450
- const current = text[index];
4451
- if (quote != null) {
4452
- if (current === "\\" && quote === '"') {
4453
- index += 1;
4454
- } else if (current === quote) {
4455
- quote = null;
4456
- }
4457
- continue;
4458
- }
4459
- if (current === '"' || current === "'") {
4460
- quote = current;
4461
- } else if (current === "#") {
4462
- return text.slice(0, index);
4463
- }
4480
+ function verifyTomlEdit(before, after, name, entry) {
4481
+ const expected = parseConfigToml(before);
4482
+ const servers = expected.mcp_servers ?? /* @__PURE__ */ Object.create(null);
4483
+ if (entry == null) {
4484
+ delete servers[name];
4485
+ } else {
4486
+ const previous = Object.hasOwn(servers, name) && isTable(servers[name]) ? servers[name] : {};
4487
+ Object.defineProperty(servers, name, {
4488
+ value: Object.assign(/* @__PURE__ */ Object.create(null), previous, { command: entry.command, args: [...entry.args], env: Object.assign(/* @__PURE__ */ Object.create(null), entry.env) }),
4489
+ configurable: true,
4490
+ enumerable: true,
4491
+ writable: true
4492
+ });
4493
+ }
4494
+ expected.mcp_servers = servers;
4495
+ let actual;
4496
+ try {
4497
+ actual = parseConfigToml(after);
4498
+ } catch {
4499
+ throw new Error("Cannot safely edit this TOML layout. Use [mcp_servers.<name>] tables; nothing was written.");
4500
+ }
4501
+ normalize(expected);
4502
+ normalize(actual);
4503
+ if (!util.isDeepStrictEqual(expected, actual)) {
4504
+ throw new Error("Cannot safely edit this TOML layout without changing other settings; nothing was written.");
4505
+ }
4506
+ return after;
4507
+ }
4508
+
4509
+ // src/modules/manage/targets/toml-subset.ts
4510
+ var BARE_KEY_PATTERN = /^[A-Za-z0-9_-]+$/;
4511
+ var ANY_HEADER_PATTERN = /^\s*\[/;
4512
+ var SERVER_HEADER_PATTERN = /^\s*\[\s*(?:mcp_servers|"mcp_servers"|'mcp_servers')\s*\.\s*([A-Za-z0-9_-]+|"(?:[^"\\]|\\.)*"|'[^']*')\s*((?:\.[^\]]*)?)\]\s*(?:#.*)?$/;
4513
+ var KEY_VALUE_PATTERN = /^\s*([A-Za-z0-9_-]+|"(?:[^"\\]|\\.)*"|'[^']*')\s*=\s*(.*)$/;
4514
+ var OWNED_TABLE_KEYS = ["command", "args", "env"];
4515
+ function unquoteKey(rawKey) {
4516
+ if (rawKey.startsWith('"') || rawKey.startsWith("'")) {
4517
+ return parseConfigToml(`value = ${rawKey}`).value;
4464
4518
  }
4465
- return text;
4519
+ return rawKey;
4466
4520
  }
4467
4521
  function readStatements(body) {
4468
4522
  const statements = [];
@@ -4474,28 +4528,25 @@ function readStatements(body) {
4474
4528
  continue;
4475
4529
  }
4476
4530
  const start = index;
4477
- let rawValue = stripComment(match[2]);
4478
- let balance = bracketBalance(rawValue);
4531
+ const state = lineState();
4532
+ scanTomlLine(match[2], state);
4479
4533
  index += 1;
4480
- while (balance > 0 && index < body.length) {
4481
- const continuation = stripComment(body[index]);
4482
- rawValue += `
4483
- ${continuation}`;
4484
- balance += bracketBalance(continuation);
4534
+ while ((state.quote != null || state.depth > 0) && index < body.length) {
4535
+ scanTomlLine(body[index], state);
4485
4536
  index += 1;
4486
4537
  }
4487
- statements.push({ key: unquoteKey(match[1]), rawValue, start, end: index });
4538
+ statements.push({ key: unquoteKey(match[1]), start, end: index });
4488
4539
  }
4489
4540
  return statements;
4490
4541
  }
4491
4542
  function splitSections(lines) {
4492
4543
  const sections = [{ header: null, serverName: null, subPath: "", body: [] }];
4493
- let balance = 0;
4544
+ const state = lineState();
4494
4545
  for (const line of lines) {
4495
4546
  const current = sections[sections.length - 1];
4496
- if (balance > 0) {
4547
+ if (state.quote != null || state.depth > 0) {
4497
4548
  current.body.push(line);
4498
- balance += bracketBalance(stripComment(line));
4549
+ scanTomlLine(line, state);
4499
4550
  continue;
4500
4551
  }
4501
4552
  if (ANY_HEADER_PATTERN.test(line) == true) {
@@ -4510,73 +4561,18 @@ function splitSections(lines) {
4510
4561
  continue;
4511
4562
  }
4512
4563
  current.body.push(line);
4513
- const keyValueMatch = KEY_VALUE_PATTERN.exec(line);
4514
- if (keyValueMatch != null) {
4515
- balance = bracketBalance(stripComment(keyValueMatch[2]));
4516
- }
4564
+ scanTomlLine(line, state);
4517
4565
  }
4518
4566
  return sections;
4519
4567
  }
4520
- function parseStatementValue(statement) {
4521
- try {
4522
- const value = new TomlValueParser(statement.rawValue).parse();
4523
- return value;
4524
- } catch {
4525
- return null;
4526
- }
4568
+ function hasMcpServerToml(text, name) {
4569
+ return hasTomlEntry(text, name);
4527
4570
  }
4528
4571
  function parseMcpServersToml(text) {
4529
- const sections = splitSections(text.split(/\r?\n/));
4530
- const entries = {};
4531
- const envTables = {};
4532
- for (const section of sections) {
4533
- if (section.serverName == null) {
4534
- continue;
4535
- }
4536
- if (section.subPath === "env") {
4537
- const envTable = {};
4538
- for (const statement of readStatements(section.body)) {
4539
- const value = parseStatementValue(statement);
4540
- if (typeof value === "string") {
4541
- envTable[statement.key] = value;
4542
- }
4543
- }
4544
- envTables[section.serverName] = envTable;
4545
- continue;
4546
- }
4547
- if (section.subPath !== "") {
4548
- continue;
4549
- }
4550
- let command = null;
4551
- let args = [];
4552
- const env = {};
4553
- for (const statement of readStatements(section.body)) {
4554
- const value = parseStatementValue(statement);
4555
- if (statement.key === "command" && typeof value === "string") {
4556
- command = value;
4557
- } else if (statement.key === "args" && Array.isArray(value) == true) {
4558
- args = value.filter((item) => typeof item === "string");
4559
- } else if (statement.key === "env" && value != null && typeof value === "object" && Array.isArray(value) == false) {
4560
- for (const [key, item] of Object.entries(value)) {
4561
- if (typeof item === "string") {
4562
- env[key] = item;
4563
- }
4564
- }
4565
- }
4566
- }
4567
- if (command != null) {
4568
- entries[section.serverName] = { command, args, env };
4569
- }
4570
- }
4571
- for (const [name, envTable] of Object.entries(envTables)) {
4572
- if (entries[name] != null) {
4573
- Object.assign(entries[name].env, envTable);
4574
- }
4575
- }
4576
- return entries;
4572
+ return readTomlEntries(text);
4577
4573
  }
4578
4574
  function quoteTomlString(value) {
4579
- const escaped = value.replace(/\\/g, "\\\\").replace(/"/g, '\\"').replace(/\n/g, "\\n").replace(/\r/g, "\\r").replace(/\t/g, "\\t");
4575
+ 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")}`);
4580
4576
  return `"${escaped}"`;
4581
4577
  }
4582
4578
  function formatTomlKey(key) {
@@ -4620,6 +4616,11 @@ function joinSections(sections, eol) {
4620
4616
  return lines.join(eol);
4621
4617
  }
4622
4618
  function writeMcpServerToml(text, name, entry) {
4619
+ parseConfigToml(text);
4620
+ const updated = editMcpServerToml(text, name, entry);
4621
+ return verifyTomlEdit(text, updated, name, entry);
4622
+ }
4623
+ function editMcpServerToml(text, name, entry) {
4623
4624
  let eol = "\n";
4624
4625
  if (text.includes("\r\n") == true) {
4625
4626
  eol = "\r\n";
@@ -4692,34 +4693,36 @@ function writeMcpServerToml(text, name, entry) {
4692
4693
  var CODEX_CONFIG_PATH = path__default.default.join(os5__default.default.homedir(), ".codex", "config.toml");
4693
4694
  function createCodexTarget() {
4694
4695
  function readText() {
4695
- if (fs6__default.default.existsSync(CODEX_CONFIG_PATH) == false) {
4696
+ if (fs7__default.default.existsSync(CODEX_CONFIG_PATH) == false) {
4696
4697
  return "";
4697
4698
  }
4698
- return fs6__default.default.readFileSync(CODEX_CONFIG_PATH, "utf8");
4699
- }
4700
- function writeText(text) {
4701
- fs6__default.default.mkdirSync(path__default.default.dirname(CODEX_CONFIG_PATH), { recursive: true });
4702
- fs6__default.default.writeFileSync(CODEX_CONFIG_PATH, text, "utf8");
4703
- return { status: "written", detail: CODEX_CONFIG_PATH };
4699
+ return fs7__default.default.readFileSync(CODEX_CONFIG_PATH, "utf8");
4704
4700
  }
4705
4701
  return {
4706
4702
  id: "codex",
4707
4703
  label: "Codex CLI",
4708
4704
  location: CODEX_CONFIG_PATH,
4709
- detected: findExecutable("codex") != null || fs6__default.default.existsSync(CODEX_CONFIG_PATH) == true,
4705
+ detected: findExecutable("codex") != null || fs7__default.default.existsSync(CODEX_CONFIG_PATH) == true,
4710
4706
  readEntries() {
4711
4707
  const text = readText();
4712
4708
  return parseMcpServersToml(text);
4713
4709
  },
4714
- writeEntry(name, entry) {
4715
- const text = readText();
4716
- const updated = writeMcpServerToml(text, name, entry);
4717
- return writeText(updated);
4710
+ hasEntry(name) {
4711
+ return hasMcpServerToml(readText(), name);
4712
+ },
4713
+ writeEntry(name, entry, options) {
4714
+ updateConfigFile(CODEX_CONFIG_PATH, (original) => {
4715
+ let text = original;
4716
+ if (options?.replace) {
4717
+ text = writeMcpServerToml(text, name, null);
4718
+ }
4719
+ return writeMcpServerToml(text, name, entry);
4720
+ });
4721
+ return { status: "written", detail: CODEX_CONFIG_PATH };
4718
4722
  },
4719
4723
  removeEntry(name) {
4720
- const text = readText();
4721
- const updated = writeMcpServerToml(text, name, null);
4722
- return writeText(updated);
4724
+ updateConfigFile(CODEX_CONFIG_PATH, (text) => writeMcpServerToml(text, name, null));
4725
+ return { status: "written", detail: CODEX_CONFIG_PATH };
4723
4726
  }
4724
4727
  };
4725
4728
  }
@@ -4927,13 +4930,14 @@ function buildHostItems(names, entries) {
4927
4930
  };
4928
4931
  });
4929
4932
  }
4930
- async function manageHost(prompter, output, target, entries, name) {
4933
+ async function manageHost(prompter, output, target, targets, entries, name) {
4931
4934
  const action = await prompter.select(
4932
4935
  name,
4933
4936
  [
4934
4937
  { value: "edit", key: "e", label: "Edit", hint: "open its settings" },
4935
4938
  { value: "check", key: "c", label: "Check the connection", hint: "connect now and offer to pin the host key" },
4936
4939
  { value: "print", key: "p", label: "Print the entry", hint: "JSON, Codex TOML and a claude command, ready to paste" },
4940
+ ...targets.some((candidate) => candidate.id !== target.id) ? [{ value: "copy", key: "o", label: "Copy to another client", hint: "keep this entry and copy its settings" }] : [],
4937
4941
  { value: "remove", key: "r", label: "Remove", hint: `from ${target.label}` },
4938
4942
  { separator: true },
4939
4943
  { value: "back", key: "b", label: "Back" }
@@ -4952,6 +4956,10 @@ async function manageHost(prompter, output, target, entries, name) {
4952
4956
  printHost(prompter, name, entries[name]);
4953
4957
  return;
4954
4958
  }
4959
+ if (action === "copy") {
4960
+ await copyEntryToClient(prompter, target, targets, name, entries[name]);
4961
+ return;
4962
+ }
4955
4963
  if (action === "remove") {
4956
4964
  const remove = await prompter.askYesNo(`Remove ${name} from ${target.location}?`, false);
4957
4965
  if (remove == true) {
@@ -5053,7 +5061,7 @@ ${paint.bold(target.label)} ${paint.dim(target.location)}
5053
5061
  continue;
5054
5062
  }
5055
5063
  if (picked.startsWith(HOST_VALUE_PREFIX) == true) {
5056
- await manageHost(prompter, output, target, entries, picked.slice(HOST_VALUE_PREFIX.length));
5064
+ await manageHost(prompter, output, target, targets, entries, picked.slice(HOST_VALUE_PREFIX.length));
5057
5065
  }
5058
5066
  } catch (ex) {
5059
5067
  if (ex instanceof PromptClosedError) {
@@ -5506,7 +5514,7 @@ ${file.content}`);
5506
5514
  const { profile, session: openSession } = resolveProfile(context, { session });
5507
5515
  const resolvedLocalPath = expandHomePath(localPath);
5508
5516
  requirePermittedLocalPath(resolvedLocalPath, "read");
5509
- if (fs6__default.default.existsSync(resolvedLocalPath) == false) {
5517
+ if (fs7__default.default.existsSync(resolvedLocalPath) == false) {
5510
5518
  return textResult(`local file not found: ${resolvedLocalPath}`);
5511
5519
  }
5512
5520
  const transferredBytes = await withHostConnection(profile, openSession, async (connection) => {
@@ -5546,7 +5554,7 @@ ${file.content}`);
5546
5554
  const { profile, session: openSession } = resolveProfile(context, { session });
5547
5555
  const resolvedLocalPath = expandHomePath(localPath);
5548
5556
  requirePermittedLocalPath(resolvedLocalPath, "write");
5549
- fs6__default.default.mkdirSync(path__default.default.dirname(resolvedLocalPath), { recursive: true });
5557
+ fs7__default.default.mkdirSync(path__default.default.dirname(resolvedLocalPath), { recursive: true });
5550
5558
  const transferredBytes = await withHostConnection(profile, openSession, async (connection) => {
5551
5559
  return await downloadFile(connection, remotePath, resolvedLocalPath);
5552
5560
  });