@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/CHANGELOG.md +250 -205
- package/README.md +307 -287
- package/lib/cli.cjs +344 -336
- package/lib/cli.cjs.map +1 -1
- package/lib/cli.js +344 -335
- package/lib/cli.js.map +1 -1
- package/lib/index.cjs +13 -11
- package/lib/index.cjs.map +1 -1
- package/lib/index.d.cts +2 -1
- package/lib/index.d.ts +2 -1
- package/lib/index.js +13 -10
- package/lib/index.js.map +1 -1
- package/package.json +2 -1
package/lib/cli.js
CHANGED
|
@@ -1,14 +1,15 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
2
|
import { StdioServerTransport } from '@modelcontextprotocol/sdk/server/stdio.js';
|
|
3
|
-
import
|
|
3
|
+
import fs7 from 'fs';
|
|
4
4
|
import os5 from 'os';
|
|
5
5
|
import path from 'path';
|
|
6
6
|
import { spawnSync, spawn } from 'child_process';
|
|
7
|
-
import { createHash } from 'crypto';
|
|
7
|
+
import { randomUUID, createHash } from 'crypto';
|
|
8
8
|
import { Duplex } from 'stream';
|
|
9
9
|
import { StringDecoder } from 'string_decoder';
|
|
10
|
-
import ssh2 from 'ssh2';
|
|
11
10
|
import { setInterval as setInterval$1, clearInterval as clearInterval$1 } from 'timers';
|
|
11
|
+
import { isDeepStrictEqual } from 'util';
|
|
12
|
+
import { parse, TomlError } from 'smol-toml';
|
|
12
13
|
import { McpServer } from '@modelcontextprotocol/sdk/server/mcp.js';
|
|
13
14
|
import { z } from 'zod';
|
|
14
15
|
|
|
@@ -27,7 +28,7 @@ var CWD_MARKER = "__AKMS_SSH_CWD__";
|
|
|
27
28
|
var MAX_DIRECTORY_ENTRIES = 1e3;
|
|
28
29
|
var SERVER_NAME = "akms-mcp-ssh";
|
|
29
30
|
var PACKAGE_NAME = "@akms/mcp-ssh";
|
|
30
|
-
var SERVER_VERSION = "0.1.
|
|
31
|
+
var SERVER_VERSION = "0.1.9";
|
|
31
32
|
|
|
32
33
|
// src/_defs/command-rules.ts
|
|
33
34
|
var CRITICAL_PATH_PATTERN = new RegExp(
|
|
@@ -613,7 +614,7 @@ function findExecutable(name) {
|
|
|
613
614
|
for (const directory of directories) {
|
|
614
615
|
for (const suffix of suffixes) {
|
|
615
616
|
const candidate = path.join(directory, `${name}${suffix}`);
|
|
616
|
-
if (
|
|
617
|
+
if (fs7.existsSync(candidate) == true && fs7.statSync(candidate).isFile() == true) {
|
|
617
618
|
return candidate;
|
|
618
619
|
}
|
|
619
620
|
}
|
|
@@ -754,7 +755,7 @@ function loadHostProfile(env = process.env) {
|
|
|
754
755
|
if (keyPath != null) {
|
|
755
756
|
const expandedKeyPath = expandHomePath(keyPath);
|
|
756
757
|
profile.privateKeyPath = expandedKeyPath;
|
|
757
|
-
if (
|
|
758
|
+
if (fs7.existsSync(expandedKeyPath) == false) {
|
|
758
759
|
_logger.warn(`${SINGLE_HOST_ENV.KEY} does not exist; connections will fail`, {
|
|
759
760
|
private_key_path: expandedKeyPath
|
|
760
761
|
});
|
|
@@ -786,7 +787,7 @@ function loadHostProfile(env = process.env) {
|
|
|
786
787
|
if (jumpKeyPath != null) {
|
|
787
788
|
profile.jump.privateKeyPath = expandHomePath(jumpKeyPath);
|
|
788
789
|
profile.jump.passphrase = readEnv(env, SINGLE_HOST_ENV.JUMP_PASSPHRASE);
|
|
789
|
-
if (
|
|
790
|
+
if (fs7.existsSync(profile.jump.privateKeyPath) == false) {
|
|
790
791
|
_logger.warn(`${SINGLE_HOST_ENV.JUMP_KEY} does not exist; connections will fail`, {
|
|
791
792
|
private_key_path: profile.jump.privateKeyPath
|
|
792
793
|
});
|
|
@@ -1272,14 +1273,14 @@ function inspectRemotePath(remotePath, policy, access) {
|
|
|
1272
1273
|
function toRealPath(inputPath) {
|
|
1273
1274
|
const _logger = $logger.child({ context: "toRealPath" });
|
|
1274
1275
|
try {
|
|
1275
|
-
return
|
|
1276
|
+
return fs7.realpathSync.native(inputPath);
|
|
1276
1277
|
} catch (ex) {
|
|
1277
1278
|
_logger.debug("path does not resolve, falling back to its parent directory", {
|
|
1278
1279
|
reason: ex instanceof Error ? ex.name : "unknown"
|
|
1279
1280
|
});
|
|
1280
1281
|
}
|
|
1281
1282
|
try {
|
|
1282
|
-
const parentRealPath =
|
|
1283
|
+
const parentRealPath = fs7.realpathSync.native(path.dirname(inputPath));
|
|
1283
1284
|
return path.join(parentRealPath, path.basename(inputPath));
|
|
1284
1285
|
} catch (ex) {
|
|
1285
1286
|
_logger.debug("parent directory does not resolve either, comparing lexically", {
|
|
@@ -1351,7 +1352,6 @@ var GuardRejectionError = class extends Error {
|
|
|
1351
1352
|
};
|
|
1352
1353
|
|
|
1353
1354
|
// src/modules/ssh/ssh-connection.ts
|
|
1354
|
-
var { Client } = ssh2;
|
|
1355
1355
|
var OUTPUT_HARD_CAP_MULTIPLIER = 4;
|
|
1356
1356
|
var PRIVATE_KEY_CACHE = /* @__PURE__ */ new Map();
|
|
1357
1357
|
var WINDOWS_AGENT_PIPE = "\\\\.\\pipe\\openssh-ssh-agent";
|
|
@@ -1371,7 +1371,7 @@ function readPrivateKey(label, keyPath) {
|
|
|
1371
1371
|
return cached;
|
|
1372
1372
|
}
|
|
1373
1373
|
try {
|
|
1374
|
-
const contents =
|
|
1374
|
+
const contents = fs7.readFileSync(keyPath);
|
|
1375
1375
|
PRIVATE_KEY_CACHE.set(keyPath, contents);
|
|
1376
1376
|
return contents;
|
|
1377
1377
|
} catch (ex) {
|
|
@@ -1493,7 +1493,7 @@ function openProxyCommand(profile, hop) {
|
|
|
1493
1493
|
}
|
|
1494
1494
|
};
|
|
1495
1495
|
}
|
|
1496
|
-
function openJumpTransport(profile, jump, proxy, onHostKeySeen) {
|
|
1496
|
+
function openJumpTransport(profile, jump, proxy, onHostKeySeen, Client) {
|
|
1497
1497
|
const label = `${profile.name} jump host`;
|
|
1498
1498
|
const endpoint = `${jump.username}@${jump.host}:${jump.port}`;
|
|
1499
1499
|
const _logger = $logger.child({ context: "openJumpTransport", profile: profile.name, jump_host: jump.host });
|
|
@@ -1558,7 +1558,7 @@ function openJumpTransport(profile, jump, proxy, onHostKeySeen) {
|
|
|
1558
1558
|
jumpClient.connect(jumpConfig);
|
|
1559
1559
|
});
|
|
1560
1560
|
}
|
|
1561
|
-
async function openTransport(profile, onJumpHostKeySeen) {
|
|
1561
|
+
async function openTransport(profile, onJumpHostKeySeen, Client) {
|
|
1562
1562
|
let proxy = null;
|
|
1563
1563
|
if (profile.proxyCommand != null) {
|
|
1564
1564
|
proxy = openProxyCommand(profile, profile.jump ?? profile);
|
|
@@ -1567,7 +1567,7 @@ async function openTransport(profile, onJumpHostKeySeen) {
|
|
|
1567
1567
|
return proxy;
|
|
1568
1568
|
}
|
|
1569
1569
|
try {
|
|
1570
|
-
const jumpTransport = await openJumpTransport(profile, profile.jump, proxy, onJumpHostKeySeen);
|
|
1570
|
+
const jumpTransport = await openJumpTransport(profile, profile.jump, proxy, onJumpHostKeySeen, Client);
|
|
1571
1571
|
return jumpTransport;
|
|
1572
1572
|
} catch (ex) {
|
|
1573
1573
|
proxy?.close();
|
|
@@ -1631,6 +1631,11 @@ var SshConnection = class {
|
|
|
1631
1631
|
get wasDropped() {
|
|
1632
1632
|
return this.droppedAfterReady;
|
|
1633
1633
|
}
|
|
1634
|
+
assertNotDisposed() {
|
|
1635
|
+
if (this.disposed == true) {
|
|
1636
|
+
throw new Error(`ssh connection for '${this.profile.name}' has been closed; open a new session`);
|
|
1637
|
+
}
|
|
1638
|
+
}
|
|
1634
1639
|
/**
|
|
1635
1640
|
* Opens the connection, or joins the in-flight attempt when one is already running.
|
|
1636
1641
|
*
|
|
@@ -1641,9 +1646,7 @@ var SshConnection = class {
|
|
|
1641
1646
|
* unreachable, the handshake times out, or the carrier cannot be opened.
|
|
1642
1647
|
*/
|
|
1643
1648
|
async connect(onProgress) {
|
|
1644
|
-
|
|
1645
|
-
throw new Error(`ssh connection for '${this.profile.name}' has been closed; open a new session`);
|
|
1646
|
-
}
|
|
1649
|
+
this.assertNotDisposed();
|
|
1647
1650
|
if (this.client != null) {
|
|
1648
1651
|
return;
|
|
1649
1652
|
}
|
|
@@ -1662,12 +1665,14 @@ var SshConnection = class {
|
|
|
1662
1665
|
this.observedFingerprint = fingerprint;
|
|
1663
1666
|
});
|
|
1664
1667
|
this.connecting = (async () => {
|
|
1668
|
+
const { default: { Client } } = await import('ssh2');
|
|
1669
|
+
this.assertNotDisposed();
|
|
1665
1670
|
if (this.profile.jump != null || this.profile.proxyCommand != null) {
|
|
1666
1671
|
onProgress?.("transport");
|
|
1667
1672
|
}
|
|
1668
1673
|
const transport = await openTransport(this.profile, (fingerprint) => {
|
|
1669
1674
|
this.observedJumpFingerprint = fingerprint;
|
|
1670
|
-
});
|
|
1675
|
+
}, Client);
|
|
1671
1676
|
if (this.disposed == true) {
|
|
1672
1677
|
transport?.close();
|
|
1673
1678
|
throw new Error(`ssh connection for '${this.profile.name}' was closed during the handshake`);
|
|
@@ -2326,7 +2331,7 @@ async function writeRemoteFile(connection, args) {
|
|
|
2326
2331
|
}
|
|
2327
2332
|
async function uploadFile(connection, localPath, remotePath) {
|
|
2328
2333
|
requirePermittedPath(connection, remotePath, "write");
|
|
2329
|
-
const localStats =
|
|
2334
|
+
const localStats = fs7.statSync(localPath);
|
|
2330
2335
|
if (localStats.isFile() == false) {
|
|
2331
2336
|
throw new Error(`local path is not a file: ${localPath}`);
|
|
2332
2337
|
}
|
|
@@ -2363,7 +2368,7 @@ async function downloadFile(connection, remotePath, localPath) {
|
|
|
2363
2368
|
});
|
|
2364
2369
|
});
|
|
2365
2370
|
});
|
|
2366
|
-
const localStats =
|
|
2371
|
+
const localStats = fs7.statSync(localPath);
|
|
2367
2372
|
const _logger = $logger.child({
|
|
2368
2373
|
context: "downloadFile",
|
|
2369
2374
|
profile: connection.profile.name,
|
|
@@ -3193,6 +3198,61 @@ async function runConnectionCheck(profile, output) {
|
|
|
3193
3198
|
};
|
|
3194
3199
|
}
|
|
3195
3200
|
|
|
3201
|
+
// src/modules/manage/copy-entry.ts
|
|
3202
|
+
async function copyEntryToClient(prompter, source, targets, name, entry) {
|
|
3203
|
+
const destinations = targets.filter((target) => target.id !== source.id);
|
|
3204
|
+
if (destinations.length === 0) {
|
|
3205
|
+
return;
|
|
3206
|
+
}
|
|
3207
|
+
for (; ; ) {
|
|
3208
|
+
const items = destinations.map((target) => ({
|
|
3209
|
+
value: target.id,
|
|
3210
|
+
key: target.id,
|
|
3211
|
+
label: target.label,
|
|
3212
|
+
hint: target.detected ? target.location : `${target.location} \xB7 not found on this machine`
|
|
3213
|
+
}));
|
|
3214
|
+
items.push({ separator: true }, { value: "back", key: "b", label: "Back" });
|
|
3215
|
+
const picked = await prompter.select(`Copy ${name} to which client?`, items, { cancelValue: "back" });
|
|
3216
|
+
const destination = destinations.find((target) => target.id === picked);
|
|
3217
|
+
if (destination == null) {
|
|
3218
|
+
return;
|
|
3219
|
+
}
|
|
3220
|
+
const exists = destination.hasEntry?.(name) ?? Object.hasOwn(destination.readEntries(), name);
|
|
3221
|
+
if (exists) {
|
|
3222
|
+
try {
|
|
3223
|
+
const overwrite = await prompter.askYesNo(
|
|
3224
|
+
`${name} already exists in ${destination.label} (${destination.location}). Overwrite it?`,
|
|
3225
|
+
false
|
|
3226
|
+
);
|
|
3227
|
+
if (overwrite == false) {
|
|
3228
|
+
prompter.write(" Not copied; the destination entry was kept.\n\n");
|
|
3229
|
+
return;
|
|
3230
|
+
}
|
|
3231
|
+
} catch (ex) {
|
|
3232
|
+
if (ex instanceof PromptBackError == false) {
|
|
3233
|
+
throw ex;
|
|
3234
|
+
}
|
|
3235
|
+
continue;
|
|
3236
|
+
}
|
|
3237
|
+
}
|
|
3238
|
+
const result = destination.writeEntry(name, {
|
|
3239
|
+
command: entry.command,
|
|
3240
|
+
args: [...entry.args],
|
|
3241
|
+
env: { ...entry.env }
|
|
3242
|
+
}, { replace: true });
|
|
3243
|
+
if (result.status === "written") {
|
|
3244
|
+
prompter.write(` ${prompter.paint.success(`Copied ${name} to ${destination.label}`)} ${prompter.paint.dim(result.detail)}
|
|
3245
|
+
`);
|
|
3246
|
+
prompter.write(prompter.paint.dim(" Restart the destination client (or reconnect it from its MCP menu) to pick up the change.\n\n"));
|
|
3247
|
+
} else {
|
|
3248
|
+
prompter.write(` Not copied: ${result.detail}
|
|
3249
|
+
|
|
3250
|
+
`);
|
|
3251
|
+
}
|
|
3252
|
+
return;
|
|
3253
|
+
}
|
|
3254
|
+
}
|
|
3255
|
+
|
|
3196
3256
|
// src/modules/manage/entry.ts
|
|
3197
3257
|
var MANAGED_ENV_NAMES = [
|
|
3198
3258
|
SINGLE_HOST_ENV.HOST,
|
|
@@ -3423,7 +3483,7 @@ function findPrivateKeys() {
|
|
|
3423
3483
|
const sshDirectory = path.join(os5.homedir(), ".ssh");
|
|
3424
3484
|
let names = [];
|
|
3425
3485
|
try {
|
|
3426
|
-
names =
|
|
3486
|
+
names = fs7.readdirSync(sshDirectory);
|
|
3427
3487
|
} catch {
|
|
3428
3488
|
return [];
|
|
3429
3489
|
}
|
|
@@ -3435,14 +3495,14 @@ function findPrivateKeys() {
|
|
|
3435
3495
|
const fullPath = path.join(sshDirectory, name);
|
|
3436
3496
|
let head = "";
|
|
3437
3497
|
try {
|
|
3438
|
-
const stat =
|
|
3498
|
+
const stat = fs7.statSync(fullPath);
|
|
3439
3499
|
if (stat.isFile() == false || stat.size > MAX_KEY_FILE_BYTES) {
|
|
3440
3500
|
continue;
|
|
3441
3501
|
}
|
|
3442
|
-
const descriptor =
|
|
3502
|
+
const descriptor = fs7.openSync(fullPath, "r");
|
|
3443
3503
|
const buffer = Buffer.alloc(64);
|
|
3444
|
-
const bytesRead =
|
|
3445
|
-
|
|
3504
|
+
const bytesRead = fs7.readSync(descriptor, buffer, 0, buffer.length, 0);
|
|
3505
|
+
fs7.closeSync(descriptor);
|
|
3446
3506
|
head = buffer.subarray(0, bytesRead).toString("utf8");
|
|
3447
3507
|
} catch {
|
|
3448
3508
|
continue;
|
|
@@ -3454,7 +3514,7 @@ function findPrivateKeys() {
|
|
|
3454
3514
|
}
|
|
3455
3515
|
let hint = "";
|
|
3456
3516
|
try {
|
|
3457
|
-
const publicKey =
|
|
3517
|
+
const publicKey = fs7.readFileSync(`${fullPath}.pub`, "utf8").trim().split(/\s+/);
|
|
3458
3518
|
hint = publicKey[0].replace(/^ssh-/, "");
|
|
3459
3519
|
if (publicKey.length > 2) {
|
|
3460
3520
|
hint += ` \xB7 ${publicKey.slice(2).join(" ")}`;
|
|
@@ -3523,7 +3583,7 @@ async function askKeyPath(prompter, current) {
|
|
|
3523
3583
|
}
|
|
3524
3584
|
for (; ; ) {
|
|
3525
3585
|
const typed = await prompter.askRequired("Private key path");
|
|
3526
|
-
if (
|
|
3586
|
+
if (fs7.existsSync(expandHomePath(typed)) == true) {
|
|
3527
3587
|
return typed;
|
|
3528
3588
|
}
|
|
3529
3589
|
prompter.write(` ${expandHomePath(typed)} does not exist.
|
|
@@ -4057,6 +4117,65 @@ async function runHostForm(prompter, output, options) {
|
|
|
4057
4117
|
prompter.transient = previousTransient;
|
|
4058
4118
|
}
|
|
4059
4119
|
}
|
|
4120
|
+
function readExisting(filePath) {
|
|
4121
|
+
try {
|
|
4122
|
+
return fs7.readFileSync(filePath, "utf8");
|
|
4123
|
+
} catch (error) {
|
|
4124
|
+
if (error.code === "ENOENT") {
|
|
4125
|
+
return null;
|
|
4126
|
+
}
|
|
4127
|
+
throw error;
|
|
4128
|
+
}
|
|
4129
|
+
}
|
|
4130
|
+
function updateConfigFile(filePath, update) {
|
|
4131
|
+
let destination = filePath;
|
|
4132
|
+
let symbolicLink = false;
|
|
4133
|
+
try {
|
|
4134
|
+
symbolicLink = fs7.lstatSync(filePath).isSymbolicLink();
|
|
4135
|
+
} catch (error) {
|
|
4136
|
+
if (error.code !== "ENOENT") {
|
|
4137
|
+
throw error;
|
|
4138
|
+
}
|
|
4139
|
+
}
|
|
4140
|
+
if (symbolicLink) {
|
|
4141
|
+
destination = fs7.realpathSync(filePath);
|
|
4142
|
+
}
|
|
4143
|
+
const before = readExisting(destination);
|
|
4144
|
+
const updated = update(before ?? "");
|
|
4145
|
+
if (before != null) {
|
|
4146
|
+
fs7.accessSync(destination, fs7.constants.W_OK);
|
|
4147
|
+
}
|
|
4148
|
+
fs7.mkdirSync(path.dirname(destination), { recursive: true });
|
|
4149
|
+
const temporary = path.join(path.dirname(destination), `.${path.basename(destination)}.${randomUUID()}.tmp`);
|
|
4150
|
+
const mode = before == null ? 384 : fs7.statSync(destination).mode & 511;
|
|
4151
|
+
let descriptor;
|
|
4152
|
+
let ownsTemporary = false;
|
|
4153
|
+
try {
|
|
4154
|
+
descriptor = fs7.openSync(temporary, "wx", mode);
|
|
4155
|
+
ownsTemporary = true;
|
|
4156
|
+
fs7.writeFileSync(descriptor, updated, "utf8");
|
|
4157
|
+
fs7.fsyncSync(descriptor);
|
|
4158
|
+
fs7.closeSync(descriptor);
|
|
4159
|
+
descriptor = void 0;
|
|
4160
|
+
if (readExisting(destination) !== before) {
|
|
4161
|
+
throw new Error("The config changed while saving; reload it and try again.");
|
|
4162
|
+
}
|
|
4163
|
+
fs7.renameSync(temporary, destination);
|
|
4164
|
+
ownsTemporary = false;
|
|
4165
|
+
} finally {
|
|
4166
|
+
try {
|
|
4167
|
+
if (descriptor != null) {
|
|
4168
|
+
fs7.closeSync(descriptor);
|
|
4169
|
+
}
|
|
4170
|
+
} finally {
|
|
4171
|
+
if (ownsTemporary) {
|
|
4172
|
+
fs7.unlinkSync(temporary);
|
|
4173
|
+
}
|
|
4174
|
+
}
|
|
4175
|
+
}
|
|
4176
|
+
}
|
|
4177
|
+
|
|
4178
|
+
// src/modules/manage/targets/json-file.ts
|
|
4060
4179
|
function toMcpServerEntry(value) {
|
|
4061
4180
|
if (typeof value !== "object" || value == null) {
|
|
4062
4181
|
return null;
|
|
@@ -4069,7 +4188,7 @@ function toMcpServerEntry(value) {
|
|
|
4069
4188
|
if (Array.isArray(record.args) == true) {
|
|
4070
4189
|
args = record.args.filter((item) => typeof item === "string");
|
|
4071
4190
|
}
|
|
4072
|
-
const env =
|
|
4191
|
+
const env = /* @__PURE__ */ Object.create(null);
|
|
4073
4192
|
if (typeof record.env === "object" && record.env != null) {
|
|
4074
4193
|
for (const [key, item] of Object.entries(record.env)) {
|
|
4075
4194
|
if (typeof item === "string") {
|
|
@@ -4079,16 +4198,22 @@ function toMcpServerEntry(value) {
|
|
|
4079
4198
|
}
|
|
4080
4199
|
return { command: record.command, args, env };
|
|
4081
4200
|
}
|
|
4082
|
-
function
|
|
4083
|
-
if (typeof document !== "object" || document == null || Array.isArray(document)
|
|
4201
|
+
function readRawMcpServersJson(document) {
|
|
4202
|
+
if (typeof document !== "object" || document == null || Array.isArray(document)) {
|
|
4084
4203
|
throw new Error("the config file is not a JSON object");
|
|
4085
4204
|
}
|
|
4086
|
-
const entries = {};
|
|
4087
4205
|
const servers = document.mcpServers;
|
|
4088
|
-
if (
|
|
4089
|
-
return
|
|
4206
|
+
if (servers === void 0) {
|
|
4207
|
+
return {};
|
|
4208
|
+
}
|
|
4209
|
+
if (typeof servers !== "object" || servers == null || Array.isArray(servers)) {
|
|
4210
|
+
throw new Error("mcpServers is not a JSON object");
|
|
4090
4211
|
}
|
|
4091
|
-
|
|
4212
|
+
return servers;
|
|
4213
|
+
}
|
|
4214
|
+
function readMcpServersJson(document) {
|
|
4215
|
+
const entries = /* @__PURE__ */ Object.create(null);
|
|
4216
|
+
for (const [name, value] of Object.entries(readRawMcpServersJson(document))) {
|
|
4092
4217
|
const entry = toMcpServerEntry(value);
|
|
4093
4218
|
if (entry != null) {
|
|
4094
4219
|
entries[name] = entry;
|
|
@@ -4096,11 +4221,11 @@ function readMcpServersJson(document) {
|
|
|
4096
4221
|
}
|
|
4097
4222
|
return entries;
|
|
4098
4223
|
}
|
|
4099
|
-
function readDocument(filePath) {
|
|
4100
|
-
if (
|
|
4224
|
+
function readDocument(filePath, contents) {
|
|
4225
|
+
if (contents == null && fs7.existsSync(filePath) == false) {
|
|
4101
4226
|
return {};
|
|
4102
4227
|
}
|
|
4103
|
-
const text =
|
|
4228
|
+
const text = contents ?? fs7.readFileSync(filePath, "utf8");
|
|
4104
4229
|
if (text.trim() === "") {
|
|
4105
4230
|
return {};
|
|
4106
4231
|
}
|
|
@@ -4121,39 +4246,35 @@ function readDocument(filePath) {
|
|
|
4121
4246
|
}
|
|
4122
4247
|
function createJsonFileTarget(options) {
|
|
4123
4248
|
const { id, label, filePath } = options;
|
|
4124
|
-
function writeDocument(document) {
|
|
4125
|
-
fs6.mkdirSync(path.dirname(filePath), { recursive: true });
|
|
4126
|
-
fs6.writeFileSync(filePath, `${JSON.stringify(document, null, 4)}
|
|
4127
|
-
`, "utf8");
|
|
4128
|
-
}
|
|
4129
4249
|
return {
|
|
4130
4250
|
id,
|
|
4131
4251
|
label,
|
|
4132
4252
|
location: filePath,
|
|
4133
|
-
detected:
|
|
4253
|
+
detected: fs7.existsSync(filePath),
|
|
4134
4254
|
readEntries() {
|
|
4135
4255
|
const document = readDocument(filePath);
|
|
4136
4256
|
return readMcpServersJson(document);
|
|
4137
4257
|
},
|
|
4258
|
+
hasEntry(name) {
|
|
4259
|
+
return Object.hasOwn(readRawMcpServersJson(readDocument(filePath)), name);
|
|
4260
|
+
},
|
|
4138
4261
|
writeEntry(name, entry) {
|
|
4139
|
-
|
|
4140
|
-
|
|
4141
|
-
|
|
4142
|
-
servers =
|
|
4143
|
-
|
|
4144
|
-
|
|
4145
|
-
|
|
4146
|
-
writeDocument(document);
|
|
4262
|
+
updateConfigFile(filePath, (contents) => {
|
|
4263
|
+
const document = readDocument(filePath, contents);
|
|
4264
|
+
const servers = Object.assign(/* @__PURE__ */ Object.create(null), readRawMcpServersJson(document));
|
|
4265
|
+
servers[name] = entry;
|
|
4266
|
+
document.mcpServers = servers;
|
|
4267
|
+
return JSON.stringify(document, null, 4) + "\n";
|
|
4268
|
+
});
|
|
4147
4269
|
return { status: "written", detail: filePath };
|
|
4148
4270
|
},
|
|
4149
4271
|
removeEntry(name) {
|
|
4150
|
-
|
|
4151
|
-
|
|
4152
|
-
|
|
4153
|
-
|
|
4154
|
-
|
|
4155
|
-
|
|
4156
|
-
writeDocument(document);
|
|
4272
|
+
updateConfigFile(filePath, (contents) => {
|
|
4273
|
+
const document = readDocument(filePath, contents);
|
|
4274
|
+
const servers = readRawMcpServersJson(document);
|
|
4275
|
+
delete servers[name];
|
|
4276
|
+
return JSON.stringify(document, null, 4) + "\n";
|
|
4277
|
+
});
|
|
4157
4278
|
return { status: "written", detail: filePath };
|
|
4158
4279
|
}
|
|
4159
4280
|
};
|
|
@@ -4205,11 +4326,11 @@ function runClaude(executable, args) {
|
|
|
4205
4326
|
}
|
|
4206
4327
|
return { status: "written", detail: CLAUDE_CODE_CONFIG_PATH };
|
|
4207
4328
|
}
|
|
4208
|
-
function
|
|
4209
|
-
if (
|
|
4329
|
+
function readRawClaudeEntries() {
|
|
4330
|
+
if (fs7.existsSync(CLAUDE_CODE_CONFIG_PATH) == false) {
|
|
4210
4331
|
return {};
|
|
4211
4332
|
}
|
|
4212
|
-
const text =
|
|
4333
|
+
const text = fs7.readFileSync(CLAUDE_CODE_CONFIG_PATH, "utf8");
|
|
4213
4334
|
let document;
|
|
4214
4335
|
try {
|
|
4215
4336
|
document = JSON.parse(text);
|
|
@@ -4220,7 +4341,7 @@ function readClaudeEntries() {
|
|
|
4220
4341
|
}
|
|
4221
4342
|
throw new Error(`${CLAUDE_CODE_CONFIG_PATH} is not valid JSON: ${detail}`);
|
|
4222
4343
|
}
|
|
4223
|
-
return
|
|
4344
|
+
return readRawMcpServersJson(document);
|
|
4224
4345
|
}
|
|
4225
4346
|
function createClaudeCodeTarget() {
|
|
4226
4347
|
const executable = findExecutable("claude");
|
|
@@ -4228,28 +4349,35 @@ function createClaudeCodeTarget() {
|
|
|
4228
4349
|
id: "claude-code",
|
|
4229
4350
|
label: "Claude Code (user scope)",
|
|
4230
4351
|
location: CLAUDE_CODE_CONFIG_PATH,
|
|
4231
|
-
detected: executable != null ||
|
|
4232
|
-
readEntries
|
|
4352
|
+
detected: executable != null || fs7.existsSync(CLAUDE_CODE_CONFIG_PATH) == true,
|
|
4353
|
+
readEntries() {
|
|
4354
|
+
return readMcpServersJson({ mcpServers: readRawClaudeEntries() });
|
|
4355
|
+
},
|
|
4356
|
+
hasEntry(name) {
|
|
4357
|
+
return Object.hasOwn(readRawClaudeEntries(), name);
|
|
4358
|
+
},
|
|
4233
4359
|
writeEntry(name, entry) {
|
|
4234
4360
|
if (executable == null) {
|
|
4235
4361
|
return {
|
|
4236
4362
|
status: "unavailable",
|
|
4237
|
-
detail:
|
|
4238
|
-
${formatClaudeAddCommand(name, entry)}`
|
|
4363
|
+
detail: "the claude CLI is not on PATH \u2014 install it, or use Print to get a command to run yourself"
|
|
4239
4364
|
};
|
|
4240
4365
|
}
|
|
4241
|
-
const
|
|
4242
|
-
|
|
4366
|
+
const previousEntries = readRawClaudeEntries();
|
|
4367
|
+
const hadPrevious = Object.hasOwn(previousEntries, name);
|
|
4368
|
+
const previous = previousEntries[name];
|
|
4369
|
+
if (hadPrevious) {
|
|
4243
4370
|
const removed = runClaude(executable, ["mcp", "remove", name, "-s", "user"]);
|
|
4244
4371
|
if (removed.status === "unavailable") {
|
|
4245
4372
|
return removed;
|
|
4246
4373
|
}
|
|
4247
4374
|
}
|
|
4248
4375
|
const added = runClaude(executable, ["mcp", "add-json", name, toClaudeAddJsonPayload(entry), "-s", "user"]);
|
|
4249
|
-
if (added.status === "unavailable" &&
|
|
4250
|
-
runClaude(executable, ["mcp", "add-json", name,
|
|
4376
|
+
if (added.status === "unavailable" && hadPrevious) {
|
|
4377
|
+
const restored = runClaude(executable, ["mcp", "add-json", name, JSON.stringify(previous), "-s", "user"]);
|
|
4378
|
+
const recovery = restored.status === "written" ? "the previous entry was restored" : `could not restore the previous entry: ${restored.detail}`;
|
|
4251
4379
|
return { status: "unavailable", detail: `${added.detail}
|
|
4252
|
-
|
|
4380
|
+
${recovery}` };
|
|
4253
4381
|
}
|
|
4254
4382
|
return added;
|
|
4255
4383
|
},
|
|
@@ -4266,194 +4394,121 @@ function createClaudeCodeTarget() {
|
|
|
4266
4394
|
};
|
|
4267
4395
|
}
|
|
4268
4396
|
|
|
4269
|
-
// src/modules/manage/targets/toml-
|
|
4270
|
-
|
|
4271
|
-
|
|
4272
|
-
|
|
4273
|
-
|
|
4274
|
-
|
|
4275
|
-
|
|
4276
|
-
|
|
4277
|
-
|
|
4278
|
-
|
|
4279
|
-
|
|
4280
|
-
|
|
4281
|
-
|
|
4282
|
-
|
|
4283
|
-
|
|
4284
|
-
|
|
4285
|
-
|
|
4286
|
-
|
|
4287
|
-
|
|
4288
|
-
}
|
|
4289
|
-
parseValue() {
|
|
4290
|
-
this.skipWhitespace();
|
|
4291
|
-
const current = this.text[this.position];
|
|
4292
|
-
if (current === '"') {
|
|
4293
|
-
return this.parseBasicString();
|
|
4294
|
-
}
|
|
4295
|
-
if (current === "'") {
|
|
4296
|
-
return this.parseLiteralString();
|
|
4297
|
-
}
|
|
4298
|
-
if (current === "[") {
|
|
4299
|
-
return this.parseArray();
|
|
4300
|
-
}
|
|
4301
|
-
if (current === "{") {
|
|
4302
|
-
return this.parseInlineTable();
|
|
4303
|
-
}
|
|
4304
|
-
throw new Error(`unsupported toml value at offset ${this.position}`);
|
|
4305
|
-
}
|
|
4306
|
-
parseBasicString() {
|
|
4307
|
-
this.position += 1;
|
|
4308
|
-
let result = "";
|
|
4309
|
-
while (this.position < this.text.length) {
|
|
4310
|
-
const current = this.text[this.position];
|
|
4311
|
-
if (current === '"') {
|
|
4312
|
-
this.position += 1;
|
|
4313
|
-
return result;
|
|
4314
|
-
}
|
|
4315
|
-
if (current !== "\\") {
|
|
4316
|
-
result += current;
|
|
4317
|
-
this.position += 1;
|
|
4318
|
-
continue;
|
|
4319
|
-
}
|
|
4320
|
-
const escaped = this.text[this.position + 1];
|
|
4321
|
-
this.position += 2;
|
|
4322
|
-
if (escaped === "n") {
|
|
4323
|
-
result += "\n";
|
|
4324
|
-
} else if (escaped === "t") {
|
|
4325
|
-
result += " ";
|
|
4326
|
-
} else if (escaped === "r") {
|
|
4327
|
-
result += "\r";
|
|
4328
|
-
} else if (escaped === "u" || escaped === "U") {
|
|
4329
|
-
let length = 4;
|
|
4330
|
-
if (escaped === "U") {
|
|
4331
|
-
length = 8;
|
|
4397
|
+
// src/modules/manage/targets/toml-lines.ts
|
|
4398
|
+
function lineState() {
|
|
4399
|
+
return { quote: null, multiline: false, depth: 0 };
|
|
4400
|
+
}
|
|
4401
|
+
function scanTomlLine(line, state) {
|
|
4402
|
+
for (let index = 0; index < line.length; index += 1) {
|
|
4403
|
+
const character = line[index];
|
|
4404
|
+
if (state.quote != null) {
|
|
4405
|
+
if (character === "\\" && state.quote === '"') {
|
|
4406
|
+
index += 1;
|
|
4407
|
+
} else if (character === state.quote) {
|
|
4408
|
+
if (!state.multiline) {
|
|
4409
|
+
state.quote = null;
|
|
4410
|
+
} else if (line.slice(index, index + 3) === state.quote.repeat(3)) {
|
|
4411
|
+
while (line[index + 1] === character) {
|
|
4412
|
+
index += 1;
|
|
4413
|
+
}
|
|
4414
|
+
state.quote = null;
|
|
4415
|
+
state.multiline = false;
|
|
4332
4416
|
}
|
|
4333
|
-
const hex = this.text.slice(this.position, this.position + length);
|
|
4334
|
-
result += String.fromCodePoint(Number.parseInt(hex, 16));
|
|
4335
|
-
this.position += length;
|
|
4336
|
-
} else {
|
|
4337
|
-
result += escaped;
|
|
4338
4417
|
}
|
|
4418
|
+
continue;
|
|
4339
4419
|
}
|
|
4340
|
-
|
|
4341
|
-
|
|
4342
|
-
parseLiteralString() {
|
|
4343
|
-
const end = this.text.indexOf("'", this.position + 1);
|
|
4344
|
-
if (end < 0) {
|
|
4345
|
-
throw new Error("unterminated toml literal string");
|
|
4420
|
+
if (character === "#") {
|
|
4421
|
+
break;
|
|
4346
4422
|
}
|
|
4347
|
-
|
|
4348
|
-
|
|
4349
|
-
|
|
4350
|
-
|
|
4351
|
-
|
|
4352
|
-
this.position += 1;
|
|
4353
|
-
const items = [];
|
|
4354
|
-
for (; ; ) {
|
|
4355
|
-
this.skipWhitespace();
|
|
4356
|
-
if (this.position >= this.text.length) {
|
|
4357
|
-
throw new Error("unterminated toml array");
|
|
4358
|
-
}
|
|
4359
|
-
if (this.text[this.position] === "]") {
|
|
4360
|
-
this.position += 1;
|
|
4361
|
-
return items;
|
|
4362
|
-
}
|
|
4363
|
-
items.push(this.parseValue());
|
|
4364
|
-
this.skipWhitespace();
|
|
4365
|
-
if (this.text[this.position] === ",") {
|
|
4366
|
-
this.position += 1;
|
|
4423
|
+
if (character === '"' || character === "'") {
|
|
4424
|
+
state.quote = character;
|
|
4425
|
+
state.multiline = line.slice(index, index + 3) === character.repeat(3);
|
|
4426
|
+
if (state.multiline) {
|
|
4427
|
+
index += 2;
|
|
4367
4428
|
}
|
|
4429
|
+
} else if (character === "[" || character === "{") {
|
|
4430
|
+
state.depth += 1;
|
|
4431
|
+
} else if (character === "]" || character === "}") {
|
|
4432
|
+
state.depth -= 1;
|
|
4368
4433
|
}
|
|
4369
4434
|
}
|
|
4370
|
-
|
|
4371
|
-
|
|
4372
|
-
|
|
4373
|
-
|
|
4374
|
-
|
|
4375
|
-
|
|
4376
|
-
|
|
4377
|
-
|
|
4378
|
-
|
|
4379
|
-
|
|
4380
|
-
|
|
4381
|
-
}
|
|
4382
|
-
let key;
|
|
4383
|
-
if (this.text[this.position] === '"') {
|
|
4384
|
-
key = this.parseBasicString();
|
|
4385
|
-
} else {
|
|
4386
|
-
const match = /^[A-Za-z0-9_-]+/.exec(this.text.slice(this.position));
|
|
4387
|
-
if (match == null) {
|
|
4388
|
-
throw new Error(`invalid inline table key at offset ${this.position}`);
|
|
4389
|
-
}
|
|
4390
|
-
key = match[0];
|
|
4391
|
-
this.position += key.length;
|
|
4392
|
-
}
|
|
4393
|
-
this.skipWhitespace();
|
|
4394
|
-
if (this.text[this.position] !== "=") {
|
|
4395
|
-
throw new Error(`expected '=' at offset ${this.position}`);
|
|
4396
|
-
}
|
|
4397
|
-
this.position += 1;
|
|
4398
|
-
table[key] = this.parseValue();
|
|
4399
|
-
this.skipWhitespace();
|
|
4400
|
-
if (this.text[this.position] === ",") {
|
|
4401
|
-
this.position += 1;
|
|
4402
|
-
}
|
|
4403
|
-
}
|
|
4435
|
+
}
|
|
4436
|
+
function isTable(value) {
|
|
4437
|
+
return value != null && typeof value === "object" && !Array.isArray(value) && !(value instanceof Date);
|
|
4438
|
+
}
|
|
4439
|
+
function parseConfigToml(text) {
|
|
4440
|
+
let document;
|
|
4441
|
+
try {
|
|
4442
|
+
document = parse(text, { integersAsBigInt: true });
|
|
4443
|
+
} catch (error) {
|
|
4444
|
+
const location = error instanceof TomlError ? ` at line ${error.line}, column ${error.column}` : "";
|
|
4445
|
+
throw new Error(`Invalid TOML${location}; fix the config before changing it.`);
|
|
4404
4446
|
}
|
|
4405
|
-
|
|
4406
|
-
|
|
4407
|
-
if (rawKey.startsWith('"') == false) {
|
|
4408
|
-
return rawKey;
|
|
4447
|
+
if (Object.hasOwn(document, "mcp_servers") && !isTable(document.mcp_servers)) {
|
|
4448
|
+
throw new Error("mcp_servers must be a TOML table.");
|
|
4409
4449
|
}
|
|
4410
|
-
|
|
4411
|
-
return parsed;
|
|
4450
|
+
return document;
|
|
4412
4451
|
}
|
|
4413
|
-
function
|
|
4414
|
-
|
|
4415
|
-
|
|
4416
|
-
|
|
4417
|
-
|
|
4418
|
-
|
|
4419
|
-
|
|
4420
|
-
|
|
4421
|
-
|
|
4422
|
-
|
|
4423
|
-
|
|
4424
|
-
|
|
4425
|
-
|
|
4426
|
-
|
|
4427
|
-
quote = current;
|
|
4428
|
-
} else if (current === "#") {
|
|
4429
|
-
break;
|
|
4430
|
-
} else if (current === "[" || current === "{") {
|
|
4431
|
-
balance += 1;
|
|
4432
|
-
} else if (current === "]" || current === "}") {
|
|
4433
|
-
balance -= 1;
|
|
4452
|
+
function readTomlEntries(text) {
|
|
4453
|
+
return readMcpServersJson({ mcpServers: parseConfigToml(text).mcp_servers ?? {} });
|
|
4454
|
+
}
|
|
4455
|
+
function hasTomlEntry(text, name) {
|
|
4456
|
+
return Object.hasOwn(parseConfigToml(text).mcp_servers ?? {}, name);
|
|
4457
|
+
}
|
|
4458
|
+
function normalize(document) {
|
|
4459
|
+
const servers = document.mcp_servers;
|
|
4460
|
+
if (servers == null) {
|
|
4461
|
+
return;
|
|
4462
|
+
}
|
|
4463
|
+
for (const server of Object.values(servers)) {
|
|
4464
|
+
if (isTable(server) && isTable(server.env) && Object.keys(server.env).length === 0) {
|
|
4465
|
+
delete server.env;
|
|
4434
4466
|
}
|
|
4435
4467
|
}
|
|
4436
|
-
|
|
4468
|
+
if (Object.keys(servers).length === 0) {
|
|
4469
|
+
delete document.mcp_servers;
|
|
4470
|
+
}
|
|
4437
4471
|
}
|
|
4438
|
-
function
|
|
4439
|
-
|
|
4440
|
-
|
|
4441
|
-
|
|
4442
|
-
|
|
4443
|
-
|
|
4444
|
-
|
|
4445
|
-
|
|
4446
|
-
|
|
4447
|
-
|
|
4448
|
-
|
|
4449
|
-
|
|
4450
|
-
|
|
4451
|
-
|
|
4452
|
-
|
|
4453
|
-
|
|
4454
|
-
|
|
4472
|
+
function verifyTomlEdit(before, after, name, entry) {
|
|
4473
|
+
const expected = parseConfigToml(before);
|
|
4474
|
+
const servers = expected.mcp_servers ?? /* @__PURE__ */ Object.create(null);
|
|
4475
|
+
if (entry == null) {
|
|
4476
|
+
delete servers[name];
|
|
4477
|
+
} else {
|
|
4478
|
+
const previous = Object.hasOwn(servers, name) && isTable(servers[name]) ? servers[name] : {};
|
|
4479
|
+
Object.defineProperty(servers, name, {
|
|
4480
|
+
value: Object.assign(/* @__PURE__ */ Object.create(null), previous, { command: entry.command, args: [...entry.args], env: Object.assign(/* @__PURE__ */ Object.create(null), entry.env) }),
|
|
4481
|
+
configurable: true,
|
|
4482
|
+
enumerable: true,
|
|
4483
|
+
writable: true
|
|
4484
|
+
});
|
|
4485
|
+
}
|
|
4486
|
+
expected.mcp_servers = servers;
|
|
4487
|
+
let actual;
|
|
4488
|
+
try {
|
|
4489
|
+
actual = parseConfigToml(after);
|
|
4490
|
+
} catch {
|
|
4491
|
+
throw new Error("Cannot safely edit this TOML layout. Use [mcp_servers.<name>] tables; nothing was written.");
|
|
4492
|
+
}
|
|
4493
|
+
normalize(expected);
|
|
4494
|
+
normalize(actual);
|
|
4495
|
+
if (!isDeepStrictEqual(expected, actual)) {
|
|
4496
|
+
throw new Error("Cannot safely edit this TOML layout without changing other settings; nothing was written.");
|
|
4497
|
+
}
|
|
4498
|
+
return after;
|
|
4499
|
+
}
|
|
4500
|
+
|
|
4501
|
+
// src/modules/manage/targets/toml-subset.ts
|
|
4502
|
+
var BARE_KEY_PATTERN = /^[A-Za-z0-9_-]+$/;
|
|
4503
|
+
var ANY_HEADER_PATTERN = /^\s*\[/;
|
|
4504
|
+
var SERVER_HEADER_PATTERN = /^\s*\[\s*(?:mcp_servers|"mcp_servers"|'mcp_servers')\s*\.\s*([A-Za-z0-9_-]+|"(?:[^"\\]|\\.)*"|'[^']*')\s*((?:\.[^\]]*)?)\]\s*(?:#.*)?$/;
|
|
4505
|
+
var KEY_VALUE_PATTERN = /^\s*([A-Za-z0-9_-]+|"(?:[^"\\]|\\.)*"|'[^']*')\s*=\s*(.*)$/;
|
|
4506
|
+
var OWNED_TABLE_KEYS = ["command", "args", "env"];
|
|
4507
|
+
function unquoteKey(rawKey) {
|
|
4508
|
+
if (rawKey.startsWith('"') || rawKey.startsWith("'")) {
|
|
4509
|
+
return parseConfigToml(`value = ${rawKey}`).value;
|
|
4455
4510
|
}
|
|
4456
|
-
return
|
|
4511
|
+
return rawKey;
|
|
4457
4512
|
}
|
|
4458
4513
|
function readStatements(body) {
|
|
4459
4514
|
const statements = [];
|
|
@@ -4465,28 +4520,25 @@ function readStatements(body) {
|
|
|
4465
4520
|
continue;
|
|
4466
4521
|
}
|
|
4467
4522
|
const start = index;
|
|
4468
|
-
|
|
4469
|
-
|
|
4523
|
+
const state = lineState();
|
|
4524
|
+
scanTomlLine(match[2], state);
|
|
4470
4525
|
index += 1;
|
|
4471
|
-
while (
|
|
4472
|
-
|
|
4473
|
-
rawValue += `
|
|
4474
|
-
${continuation}`;
|
|
4475
|
-
balance += bracketBalance(continuation);
|
|
4526
|
+
while ((state.quote != null || state.depth > 0) && index < body.length) {
|
|
4527
|
+
scanTomlLine(body[index], state);
|
|
4476
4528
|
index += 1;
|
|
4477
4529
|
}
|
|
4478
|
-
statements.push({ key: unquoteKey(match[1]),
|
|
4530
|
+
statements.push({ key: unquoteKey(match[1]), start, end: index });
|
|
4479
4531
|
}
|
|
4480
4532
|
return statements;
|
|
4481
4533
|
}
|
|
4482
4534
|
function splitSections(lines) {
|
|
4483
4535
|
const sections = [{ header: null, serverName: null, subPath: "", body: [] }];
|
|
4484
|
-
|
|
4536
|
+
const state = lineState();
|
|
4485
4537
|
for (const line of lines) {
|
|
4486
4538
|
const current = sections[sections.length - 1];
|
|
4487
|
-
if (
|
|
4539
|
+
if (state.quote != null || state.depth > 0) {
|
|
4488
4540
|
current.body.push(line);
|
|
4489
|
-
|
|
4541
|
+
scanTomlLine(line, state);
|
|
4490
4542
|
continue;
|
|
4491
4543
|
}
|
|
4492
4544
|
if (ANY_HEADER_PATTERN.test(line) == true) {
|
|
@@ -4501,73 +4553,18 @@ function splitSections(lines) {
|
|
|
4501
4553
|
continue;
|
|
4502
4554
|
}
|
|
4503
4555
|
current.body.push(line);
|
|
4504
|
-
|
|
4505
|
-
if (keyValueMatch != null) {
|
|
4506
|
-
balance = bracketBalance(stripComment(keyValueMatch[2]));
|
|
4507
|
-
}
|
|
4556
|
+
scanTomlLine(line, state);
|
|
4508
4557
|
}
|
|
4509
4558
|
return sections;
|
|
4510
4559
|
}
|
|
4511
|
-
function
|
|
4512
|
-
|
|
4513
|
-
const value = new TomlValueParser(statement.rawValue).parse();
|
|
4514
|
-
return value;
|
|
4515
|
-
} catch {
|
|
4516
|
-
return null;
|
|
4517
|
-
}
|
|
4560
|
+
function hasMcpServerToml(text, name) {
|
|
4561
|
+
return hasTomlEntry(text, name);
|
|
4518
4562
|
}
|
|
4519
4563
|
function parseMcpServersToml(text) {
|
|
4520
|
-
|
|
4521
|
-
const entries = {};
|
|
4522
|
-
const envTables = {};
|
|
4523
|
-
for (const section of sections) {
|
|
4524
|
-
if (section.serverName == null) {
|
|
4525
|
-
continue;
|
|
4526
|
-
}
|
|
4527
|
-
if (section.subPath === "env") {
|
|
4528
|
-
const envTable = {};
|
|
4529
|
-
for (const statement of readStatements(section.body)) {
|
|
4530
|
-
const value = parseStatementValue(statement);
|
|
4531
|
-
if (typeof value === "string") {
|
|
4532
|
-
envTable[statement.key] = value;
|
|
4533
|
-
}
|
|
4534
|
-
}
|
|
4535
|
-
envTables[section.serverName] = envTable;
|
|
4536
|
-
continue;
|
|
4537
|
-
}
|
|
4538
|
-
if (section.subPath !== "") {
|
|
4539
|
-
continue;
|
|
4540
|
-
}
|
|
4541
|
-
let command = null;
|
|
4542
|
-
let args = [];
|
|
4543
|
-
const env = {};
|
|
4544
|
-
for (const statement of readStatements(section.body)) {
|
|
4545
|
-
const value = parseStatementValue(statement);
|
|
4546
|
-
if (statement.key === "command" && typeof value === "string") {
|
|
4547
|
-
command = value;
|
|
4548
|
-
} else if (statement.key === "args" && Array.isArray(value) == true) {
|
|
4549
|
-
args = value.filter((item) => typeof item === "string");
|
|
4550
|
-
} else if (statement.key === "env" && value != null && typeof value === "object" && Array.isArray(value) == false) {
|
|
4551
|
-
for (const [key, item] of Object.entries(value)) {
|
|
4552
|
-
if (typeof item === "string") {
|
|
4553
|
-
env[key] = item;
|
|
4554
|
-
}
|
|
4555
|
-
}
|
|
4556
|
-
}
|
|
4557
|
-
}
|
|
4558
|
-
if (command != null) {
|
|
4559
|
-
entries[section.serverName] = { command, args, env };
|
|
4560
|
-
}
|
|
4561
|
-
}
|
|
4562
|
-
for (const [name, envTable] of Object.entries(envTables)) {
|
|
4563
|
-
if (entries[name] != null) {
|
|
4564
|
-
Object.assign(entries[name].env, envTable);
|
|
4565
|
-
}
|
|
4566
|
-
}
|
|
4567
|
-
return entries;
|
|
4564
|
+
return readTomlEntries(text);
|
|
4568
4565
|
}
|
|
4569
4566
|
function quoteTomlString(value) {
|
|
4570
|
-
const escaped = value.replace(/\\/g, "\\\\").replace(/"/g, '\\"').replace(/\n/g, "\\n").replace(/\r/g, "\\r").replace(/\t/g, "\\t");
|
|
4567
|
+
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")}`);
|
|
4571
4568
|
return `"${escaped}"`;
|
|
4572
4569
|
}
|
|
4573
4570
|
function formatTomlKey(key) {
|
|
@@ -4611,6 +4608,11 @@ function joinSections(sections, eol) {
|
|
|
4611
4608
|
return lines.join(eol);
|
|
4612
4609
|
}
|
|
4613
4610
|
function writeMcpServerToml(text, name, entry) {
|
|
4611
|
+
parseConfigToml(text);
|
|
4612
|
+
const updated = editMcpServerToml(text, name, entry);
|
|
4613
|
+
return verifyTomlEdit(text, updated, name, entry);
|
|
4614
|
+
}
|
|
4615
|
+
function editMcpServerToml(text, name, entry) {
|
|
4614
4616
|
let eol = "\n";
|
|
4615
4617
|
if (text.includes("\r\n") == true) {
|
|
4616
4618
|
eol = "\r\n";
|
|
@@ -4683,34 +4685,36 @@ function writeMcpServerToml(text, name, entry) {
|
|
|
4683
4685
|
var CODEX_CONFIG_PATH = path.join(os5.homedir(), ".codex", "config.toml");
|
|
4684
4686
|
function createCodexTarget() {
|
|
4685
4687
|
function readText() {
|
|
4686
|
-
if (
|
|
4688
|
+
if (fs7.existsSync(CODEX_CONFIG_PATH) == false) {
|
|
4687
4689
|
return "";
|
|
4688
4690
|
}
|
|
4689
|
-
return
|
|
4690
|
-
}
|
|
4691
|
-
function writeText(text) {
|
|
4692
|
-
fs6.mkdirSync(path.dirname(CODEX_CONFIG_PATH), { recursive: true });
|
|
4693
|
-
fs6.writeFileSync(CODEX_CONFIG_PATH, text, "utf8");
|
|
4694
|
-
return { status: "written", detail: CODEX_CONFIG_PATH };
|
|
4691
|
+
return fs7.readFileSync(CODEX_CONFIG_PATH, "utf8");
|
|
4695
4692
|
}
|
|
4696
4693
|
return {
|
|
4697
4694
|
id: "codex",
|
|
4698
4695
|
label: "Codex CLI",
|
|
4699
4696
|
location: CODEX_CONFIG_PATH,
|
|
4700
|
-
detected: findExecutable("codex") != null ||
|
|
4697
|
+
detected: findExecutable("codex") != null || fs7.existsSync(CODEX_CONFIG_PATH) == true,
|
|
4701
4698
|
readEntries() {
|
|
4702
4699
|
const text = readText();
|
|
4703
4700
|
return parseMcpServersToml(text);
|
|
4704
4701
|
},
|
|
4705
|
-
|
|
4706
|
-
|
|
4707
|
-
|
|
4708
|
-
|
|
4702
|
+
hasEntry(name) {
|
|
4703
|
+
return hasMcpServerToml(readText(), name);
|
|
4704
|
+
},
|
|
4705
|
+
writeEntry(name, entry, options) {
|
|
4706
|
+
updateConfigFile(CODEX_CONFIG_PATH, (original) => {
|
|
4707
|
+
let text = original;
|
|
4708
|
+
if (options?.replace) {
|
|
4709
|
+
text = writeMcpServerToml(text, name, null);
|
|
4710
|
+
}
|
|
4711
|
+
return writeMcpServerToml(text, name, entry);
|
|
4712
|
+
});
|
|
4713
|
+
return { status: "written", detail: CODEX_CONFIG_PATH };
|
|
4709
4714
|
},
|
|
4710
4715
|
removeEntry(name) {
|
|
4711
|
-
|
|
4712
|
-
|
|
4713
|
-
return writeText(updated);
|
|
4716
|
+
updateConfigFile(CODEX_CONFIG_PATH, (text) => writeMcpServerToml(text, name, null));
|
|
4717
|
+
return { status: "written", detail: CODEX_CONFIG_PATH };
|
|
4714
4718
|
}
|
|
4715
4719
|
};
|
|
4716
4720
|
}
|
|
@@ -4918,13 +4922,14 @@ function buildHostItems(names, entries) {
|
|
|
4918
4922
|
};
|
|
4919
4923
|
});
|
|
4920
4924
|
}
|
|
4921
|
-
async function manageHost(prompter, output, target, entries, name) {
|
|
4925
|
+
async function manageHost(prompter, output, target, targets, entries, name) {
|
|
4922
4926
|
const action = await prompter.select(
|
|
4923
4927
|
name,
|
|
4924
4928
|
[
|
|
4925
4929
|
{ value: "edit", key: "e", label: "Edit", hint: "open its settings" },
|
|
4926
4930
|
{ value: "check", key: "c", label: "Check the connection", hint: "connect now and offer to pin the host key" },
|
|
4927
4931
|
{ value: "print", key: "p", label: "Print the entry", hint: "JSON, Codex TOML and a claude command, ready to paste" },
|
|
4932
|
+
...targets.some((candidate) => candidate.id !== target.id) ? [{ value: "copy", key: "o", label: "Copy to another client", hint: "keep this entry and copy its settings" }] : [],
|
|
4928
4933
|
{ value: "remove", key: "r", label: "Remove", hint: `from ${target.label}` },
|
|
4929
4934
|
{ separator: true },
|
|
4930
4935
|
{ value: "back", key: "b", label: "Back" }
|
|
@@ -4943,6 +4948,10 @@ async function manageHost(prompter, output, target, entries, name) {
|
|
|
4943
4948
|
printHost(prompter, name, entries[name]);
|
|
4944
4949
|
return;
|
|
4945
4950
|
}
|
|
4951
|
+
if (action === "copy") {
|
|
4952
|
+
await copyEntryToClient(prompter, target, targets, name, entries[name]);
|
|
4953
|
+
return;
|
|
4954
|
+
}
|
|
4946
4955
|
if (action === "remove") {
|
|
4947
4956
|
const remove = await prompter.askYesNo(`Remove ${name} from ${target.location}?`, false);
|
|
4948
4957
|
if (remove == true) {
|
|
@@ -5044,7 +5053,7 @@ ${paint.bold(target.label)} ${paint.dim(target.location)}
|
|
|
5044
5053
|
continue;
|
|
5045
5054
|
}
|
|
5046
5055
|
if (picked.startsWith(HOST_VALUE_PREFIX) == true) {
|
|
5047
|
-
await manageHost(prompter, output, target, entries, picked.slice(HOST_VALUE_PREFIX.length));
|
|
5056
|
+
await manageHost(prompter, output, target, targets, entries, picked.slice(HOST_VALUE_PREFIX.length));
|
|
5048
5057
|
}
|
|
5049
5058
|
} catch (ex) {
|
|
5050
5059
|
if (ex instanceof PromptClosedError) {
|
|
@@ -5497,7 +5506,7 @@ ${file.content}`);
|
|
|
5497
5506
|
const { profile, session: openSession } = resolveProfile(context, { session });
|
|
5498
5507
|
const resolvedLocalPath = expandHomePath(localPath);
|
|
5499
5508
|
requirePermittedLocalPath(resolvedLocalPath, "read");
|
|
5500
|
-
if (
|
|
5509
|
+
if (fs7.existsSync(resolvedLocalPath) == false) {
|
|
5501
5510
|
return textResult(`local file not found: ${resolvedLocalPath}`);
|
|
5502
5511
|
}
|
|
5503
5512
|
const transferredBytes = await withHostConnection(profile, openSession, async (connection) => {
|
|
@@ -5537,7 +5546,7 @@ ${file.content}`);
|
|
|
5537
5546
|
const { profile, session: openSession } = resolveProfile(context, { session });
|
|
5538
5547
|
const resolvedLocalPath = expandHomePath(localPath);
|
|
5539
5548
|
requirePermittedLocalPath(resolvedLocalPath, "write");
|
|
5540
|
-
|
|
5549
|
+
fs7.mkdirSync(path.dirname(resolvedLocalPath), { recursive: true });
|
|
5541
5550
|
const transferredBytes = await withHostConnection(profile, openSession, async (connection) => {
|
|
5542
5551
|
return await downloadFile(connection, remotePath, resolvedLocalPath);
|
|
5543
5552
|
});
|