@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.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
|
|
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
|
|
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.
|
|
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 (
|
|
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 (
|
|
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 (
|
|
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
|
|
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 =
|
|
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 =
|
|
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
|
-
|
|
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 =
|
|
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 =
|
|
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 =
|
|
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 =
|
|
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 =
|
|
3510
|
+
const descriptor = fs7__default.default.openSync(fullPath, "r");
|
|
3452
3511
|
const buffer = Buffer.alloc(64);
|
|
3453
|
-
const bytesRead =
|
|
3454
|
-
|
|
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 =
|
|
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 (
|
|
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
|
|
4092
|
-
if (typeof document !== "object" || document == null || Array.isArray(document)
|
|
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 (
|
|
4098
|
-
return
|
|
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
|
-
|
|
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 (
|
|
4232
|
+
function readDocument(filePath, contents) {
|
|
4233
|
+
if (contents == null && fs7__default.default.existsSync(filePath) == false) {
|
|
4110
4234
|
return {};
|
|
4111
4235
|
}
|
|
4112
|
-
const text =
|
|
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:
|
|
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
|
-
|
|
4149
|
-
|
|
4150
|
-
|
|
4151
|
-
servers =
|
|
4152
|
-
|
|
4153
|
-
|
|
4154
|
-
|
|
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
|
-
|
|
4160
|
-
|
|
4161
|
-
|
|
4162
|
-
|
|
4163
|
-
|
|
4164
|
-
|
|
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
|
|
4218
|
-
if (
|
|
4337
|
+
function readRawClaudeEntries() {
|
|
4338
|
+
if (fs7__default.default.existsSync(CLAUDE_CODE_CONFIG_PATH) == false) {
|
|
4219
4339
|
return {};
|
|
4220
4340
|
}
|
|
4221
|
-
const text =
|
|
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
|
|
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 ||
|
|
4241
|
-
readEntries
|
|
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:
|
|
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
|
|
4251
|
-
|
|
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" &&
|
|
4259
|
-
runClaude(executable, ["mcp", "add-json", name,
|
|
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
|
-
|
|
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-
|
|
4279
|
-
|
|
4280
|
-
|
|
4281
|
-
|
|
4282
|
-
|
|
4283
|
-
|
|
4284
|
-
|
|
4285
|
-
|
|
4286
|
-
|
|
4287
|
-
|
|
4288
|
-
|
|
4289
|
-
|
|
4290
|
-
|
|
4291
|
-
|
|
4292
|
-
|
|
4293
|
-
|
|
4294
|
-
|
|
4295
|
-
|
|
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
|
-
|
|
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
|
-
|
|
4357
|
-
|
|
4358
|
-
|
|
4359
|
-
|
|
4360
|
-
|
|
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
|
-
|
|
4380
|
-
|
|
4381
|
-
|
|
4382
|
-
|
|
4383
|
-
|
|
4384
|
-
|
|
4385
|
-
|
|
4386
|
-
|
|
4387
|
-
|
|
4388
|
-
|
|
4389
|
-
|
|
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
|
-
|
|
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
|
-
|
|
4420
|
-
return parsed;
|
|
4458
|
+
return document;
|
|
4421
4459
|
}
|
|
4422
|
-
function
|
|
4423
|
-
|
|
4424
|
-
|
|
4425
|
-
|
|
4426
|
-
|
|
4427
|
-
|
|
4428
|
-
|
|
4429
|
-
|
|
4430
|
-
|
|
4431
|
-
|
|
4432
|
-
|
|
4433
|
-
|
|
4434
|
-
|
|
4435
|
-
|
|
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
|
-
|
|
4476
|
+
if (Object.keys(servers).length === 0) {
|
|
4477
|
+
delete document.mcp_servers;
|
|
4478
|
+
}
|
|
4446
4479
|
}
|
|
4447
|
-
function
|
|
4448
|
-
|
|
4449
|
-
|
|
4450
|
-
|
|
4451
|
-
|
|
4452
|
-
|
|
4453
|
-
|
|
4454
|
-
|
|
4455
|
-
|
|
4456
|
-
|
|
4457
|
-
|
|
4458
|
-
|
|
4459
|
-
|
|
4460
|
-
|
|
4461
|
-
|
|
4462
|
-
|
|
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
|
|
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
|
-
|
|
4478
|
-
|
|
4531
|
+
const state = lineState();
|
|
4532
|
+
scanTomlLine(match[2], state);
|
|
4479
4533
|
index += 1;
|
|
4480
|
-
while (
|
|
4481
|
-
|
|
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]),
|
|
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
|
-
|
|
4544
|
+
const state = lineState();
|
|
4494
4545
|
for (const line of lines) {
|
|
4495
4546
|
const current = sections[sections.length - 1];
|
|
4496
|
-
if (
|
|
4547
|
+
if (state.quote != null || state.depth > 0) {
|
|
4497
4548
|
current.body.push(line);
|
|
4498
|
-
|
|
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
|
-
|
|
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
|
|
4521
|
-
|
|
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
|
-
|
|
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 (
|
|
4696
|
+
if (fs7__default.default.existsSync(CODEX_CONFIG_PATH) == false) {
|
|
4696
4697
|
return "";
|
|
4697
4698
|
}
|
|
4698
|
-
return
|
|
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 ||
|
|
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
|
-
|
|
4715
|
-
|
|
4716
|
-
|
|
4717
|
-
|
|
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
|
-
|
|
4721
|
-
|
|
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 (
|
|
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
|
-
|
|
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
|
});
|