@akms/mcp-ssh 0.1.7 → 0.1.8
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +242 -205
- package/README.md +299 -287
- package/lib/cli.cjs +332 -326
- package/lib/cli.cjs.map +1 -1
- package/lib/cli.js +332 -326
- package/lib/cli.js.map +1 -1
- package/lib/index.cjs +1 -1
- package/lib/index.cjs.map +1 -1
- package/lib/index.d.cts +1 -1
- package/lib/index.d.ts +1 -1
- package/lib/index.js +1 -1
- package/lib/index.js.map +1 -1
- package/package.json +2 -1
package/lib/cli.js
CHANGED
|
@@ -1,14 +1,16 @@
|
|
|
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
10
|
import ssh2 from 'ssh2';
|
|
11
11
|
import { setInterval as setInterval$1, clearInterval as clearInterval$1 } from 'timers';
|
|
12
|
+
import { isDeepStrictEqual } from 'util';
|
|
13
|
+
import { parse, TomlError } from 'smol-toml';
|
|
12
14
|
import { McpServer } from '@modelcontextprotocol/sdk/server/mcp.js';
|
|
13
15
|
import { z } from 'zod';
|
|
14
16
|
|
|
@@ -27,7 +29,7 @@ var CWD_MARKER = "__AKMS_SSH_CWD__";
|
|
|
27
29
|
var MAX_DIRECTORY_ENTRIES = 1e3;
|
|
28
30
|
var SERVER_NAME = "akms-mcp-ssh";
|
|
29
31
|
var PACKAGE_NAME = "@akms/mcp-ssh";
|
|
30
|
-
var SERVER_VERSION = "0.1.
|
|
32
|
+
var SERVER_VERSION = "0.1.8";
|
|
31
33
|
|
|
32
34
|
// src/_defs/command-rules.ts
|
|
33
35
|
var CRITICAL_PATH_PATTERN = new RegExp(
|
|
@@ -613,7 +615,7 @@ function findExecutable(name) {
|
|
|
613
615
|
for (const directory of directories) {
|
|
614
616
|
for (const suffix of suffixes) {
|
|
615
617
|
const candidate = path.join(directory, `${name}${suffix}`);
|
|
616
|
-
if (
|
|
618
|
+
if (fs7.existsSync(candidate) == true && fs7.statSync(candidate).isFile() == true) {
|
|
617
619
|
return candidate;
|
|
618
620
|
}
|
|
619
621
|
}
|
|
@@ -754,7 +756,7 @@ function loadHostProfile(env = process.env) {
|
|
|
754
756
|
if (keyPath != null) {
|
|
755
757
|
const expandedKeyPath = expandHomePath(keyPath);
|
|
756
758
|
profile.privateKeyPath = expandedKeyPath;
|
|
757
|
-
if (
|
|
759
|
+
if (fs7.existsSync(expandedKeyPath) == false) {
|
|
758
760
|
_logger.warn(`${SINGLE_HOST_ENV.KEY} does not exist; connections will fail`, {
|
|
759
761
|
private_key_path: expandedKeyPath
|
|
760
762
|
});
|
|
@@ -786,7 +788,7 @@ function loadHostProfile(env = process.env) {
|
|
|
786
788
|
if (jumpKeyPath != null) {
|
|
787
789
|
profile.jump.privateKeyPath = expandHomePath(jumpKeyPath);
|
|
788
790
|
profile.jump.passphrase = readEnv(env, SINGLE_HOST_ENV.JUMP_PASSPHRASE);
|
|
789
|
-
if (
|
|
791
|
+
if (fs7.existsSync(profile.jump.privateKeyPath) == false) {
|
|
790
792
|
_logger.warn(`${SINGLE_HOST_ENV.JUMP_KEY} does not exist; connections will fail`, {
|
|
791
793
|
private_key_path: profile.jump.privateKeyPath
|
|
792
794
|
});
|
|
@@ -1272,14 +1274,14 @@ function inspectRemotePath(remotePath, policy, access) {
|
|
|
1272
1274
|
function toRealPath(inputPath) {
|
|
1273
1275
|
const _logger = $logger.child({ context: "toRealPath" });
|
|
1274
1276
|
try {
|
|
1275
|
-
return
|
|
1277
|
+
return fs7.realpathSync.native(inputPath);
|
|
1276
1278
|
} catch (ex) {
|
|
1277
1279
|
_logger.debug("path does not resolve, falling back to its parent directory", {
|
|
1278
1280
|
reason: ex instanceof Error ? ex.name : "unknown"
|
|
1279
1281
|
});
|
|
1280
1282
|
}
|
|
1281
1283
|
try {
|
|
1282
|
-
const parentRealPath =
|
|
1284
|
+
const parentRealPath = fs7.realpathSync.native(path.dirname(inputPath));
|
|
1283
1285
|
return path.join(parentRealPath, path.basename(inputPath));
|
|
1284
1286
|
} catch (ex) {
|
|
1285
1287
|
_logger.debug("parent directory does not resolve either, comparing lexically", {
|
|
@@ -1371,7 +1373,7 @@ function readPrivateKey(label, keyPath) {
|
|
|
1371
1373
|
return cached;
|
|
1372
1374
|
}
|
|
1373
1375
|
try {
|
|
1374
|
-
const contents =
|
|
1376
|
+
const contents = fs7.readFileSync(keyPath);
|
|
1375
1377
|
PRIVATE_KEY_CACHE.set(keyPath, contents);
|
|
1376
1378
|
return contents;
|
|
1377
1379
|
} catch (ex) {
|
|
@@ -2326,7 +2328,7 @@ async function writeRemoteFile(connection, args) {
|
|
|
2326
2328
|
}
|
|
2327
2329
|
async function uploadFile(connection, localPath, remotePath) {
|
|
2328
2330
|
requirePermittedPath(connection, remotePath, "write");
|
|
2329
|
-
const localStats =
|
|
2331
|
+
const localStats = fs7.statSync(localPath);
|
|
2330
2332
|
if (localStats.isFile() == false) {
|
|
2331
2333
|
throw new Error(`local path is not a file: ${localPath}`);
|
|
2332
2334
|
}
|
|
@@ -2363,7 +2365,7 @@ async function downloadFile(connection, remotePath, localPath) {
|
|
|
2363
2365
|
});
|
|
2364
2366
|
});
|
|
2365
2367
|
});
|
|
2366
|
-
const localStats =
|
|
2368
|
+
const localStats = fs7.statSync(localPath);
|
|
2367
2369
|
const _logger = $logger.child({
|
|
2368
2370
|
context: "downloadFile",
|
|
2369
2371
|
profile: connection.profile.name,
|
|
@@ -3193,6 +3195,61 @@ async function runConnectionCheck(profile, output) {
|
|
|
3193
3195
|
};
|
|
3194
3196
|
}
|
|
3195
3197
|
|
|
3198
|
+
// src/modules/manage/copy-entry.ts
|
|
3199
|
+
async function copyEntryToClient(prompter, source, targets, name, entry) {
|
|
3200
|
+
const destinations = targets.filter((target) => target.id !== source.id);
|
|
3201
|
+
if (destinations.length === 0) {
|
|
3202
|
+
return;
|
|
3203
|
+
}
|
|
3204
|
+
for (; ; ) {
|
|
3205
|
+
const items = destinations.map((target) => ({
|
|
3206
|
+
value: target.id,
|
|
3207
|
+
key: target.id,
|
|
3208
|
+
label: target.label,
|
|
3209
|
+
hint: target.detected ? target.location : `${target.location} \xB7 not found on this machine`
|
|
3210
|
+
}));
|
|
3211
|
+
items.push({ separator: true }, { value: "back", key: "b", label: "Back" });
|
|
3212
|
+
const picked = await prompter.select(`Copy ${name} to which client?`, items, { cancelValue: "back" });
|
|
3213
|
+
const destination = destinations.find((target) => target.id === picked);
|
|
3214
|
+
if (destination == null) {
|
|
3215
|
+
return;
|
|
3216
|
+
}
|
|
3217
|
+
const exists = destination.hasEntry?.(name) ?? Object.hasOwn(destination.readEntries(), name);
|
|
3218
|
+
if (exists) {
|
|
3219
|
+
try {
|
|
3220
|
+
const overwrite = await prompter.askYesNo(
|
|
3221
|
+
`${name} already exists in ${destination.label} (${destination.location}). Overwrite it?`,
|
|
3222
|
+
false
|
|
3223
|
+
);
|
|
3224
|
+
if (overwrite == false) {
|
|
3225
|
+
prompter.write(" Not copied; the destination entry was kept.\n\n");
|
|
3226
|
+
return;
|
|
3227
|
+
}
|
|
3228
|
+
} catch (ex) {
|
|
3229
|
+
if (ex instanceof PromptBackError == false) {
|
|
3230
|
+
throw ex;
|
|
3231
|
+
}
|
|
3232
|
+
continue;
|
|
3233
|
+
}
|
|
3234
|
+
}
|
|
3235
|
+
const result = destination.writeEntry(name, {
|
|
3236
|
+
command: entry.command,
|
|
3237
|
+
args: [...entry.args],
|
|
3238
|
+
env: { ...entry.env }
|
|
3239
|
+
}, { replace: true });
|
|
3240
|
+
if (result.status === "written") {
|
|
3241
|
+
prompter.write(` ${prompter.paint.success(`Copied ${name} to ${destination.label}`)} ${prompter.paint.dim(result.detail)}
|
|
3242
|
+
`);
|
|
3243
|
+
prompter.write(prompter.paint.dim(" Restart the destination client (or reconnect it from its MCP menu) to pick up the change.\n\n"));
|
|
3244
|
+
} else {
|
|
3245
|
+
prompter.write(` Not copied: ${result.detail}
|
|
3246
|
+
|
|
3247
|
+
`);
|
|
3248
|
+
}
|
|
3249
|
+
return;
|
|
3250
|
+
}
|
|
3251
|
+
}
|
|
3252
|
+
|
|
3196
3253
|
// src/modules/manage/entry.ts
|
|
3197
3254
|
var MANAGED_ENV_NAMES = [
|
|
3198
3255
|
SINGLE_HOST_ENV.HOST,
|
|
@@ -3423,7 +3480,7 @@ function findPrivateKeys() {
|
|
|
3423
3480
|
const sshDirectory = path.join(os5.homedir(), ".ssh");
|
|
3424
3481
|
let names = [];
|
|
3425
3482
|
try {
|
|
3426
|
-
names =
|
|
3483
|
+
names = fs7.readdirSync(sshDirectory);
|
|
3427
3484
|
} catch {
|
|
3428
3485
|
return [];
|
|
3429
3486
|
}
|
|
@@ -3435,14 +3492,14 @@ function findPrivateKeys() {
|
|
|
3435
3492
|
const fullPath = path.join(sshDirectory, name);
|
|
3436
3493
|
let head = "";
|
|
3437
3494
|
try {
|
|
3438
|
-
const stat =
|
|
3495
|
+
const stat = fs7.statSync(fullPath);
|
|
3439
3496
|
if (stat.isFile() == false || stat.size > MAX_KEY_FILE_BYTES) {
|
|
3440
3497
|
continue;
|
|
3441
3498
|
}
|
|
3442
|
-
const descriptor =
|
|
3499
|
+
const descriptor = fs7.openSync(fullPath, "r");
|
|
3443
3500
|
const buffer = Buffer.alloc(64);
|
|
3444
|
-
const bytesRead =
|
|
3445
|
-
|
|
3501
|
+
const bytesRead = fs7.readSync(descriptor, buffer, 0, buffer.length, 0);
|
|
3502
|
+
fs7.closeSync(descriptor);
|
|
3446
3503
|
head = buffer.subarray(0, bytesRead).toString("utf8");
|
|
3447
3504
|
} catch {
|
|
3448
3505
|
continue;
|
|
@@ -3454,7 +3511,7 @@ function findPrivateKeys() {
|
|
|
3454
3511
|
}
|
|
3455
3512
|
let hint = "";
|
|
3456
3513
|
try {
|
|
3457
|
-
const publicKey =
|
|
3514
|
+
const publicKey = fs7.readFileSync(`${fullPath}.pub`, "utf8").trim().split(/\s+/);
|
|
3458
3515
|
hint = publicKey[0].replace(/^ssh-/, "");
|
|
3459
3516
|
if (publicKey.length > 2) {
|
|
3460
3517
|
hint += ` \xB7 ${publicKey.slice(2).join(" ")}`;
|
|
@@ -3523,7 +3580,7 @@ async function askKeyPath(prompter, current) {
|
|
|
3523
3580
|
}
|
|
3524
3581
|
for (; ; ) {
|
|
3525
3582
|
const typed = await prompter.askRequired("Private key path");
|
|
3526
|
-
if (
|
|
3583
|
+
if (fs7.existsSync(expandHomePath(typed)) == true) {
|
|
3527
3584
|
return typed;
|
|
3528
3585
|
}
|
|
3529
3586
|
prompter.write(` ${expandHomePath(typed)} does not exist.
|
|
@@ -4057,6 +4114,65 @@ async function runHostForm(prompter, output, options) {
|
|
|
4057
4114
|
prompter.transient = previousTransient;
|
|
4058
4115
|
}
|
|
4059
4116
|
}
|
|
4117
|
+
function readExisting(filePath) {
|
|
4118
|
+
try {
|
|
4119
|
+
return fs7.readFileSync(filePath, "utf8");
|
|
4120
|
+
} catch (error) {
|
|
4121
|
+
if (error.code === "ENOENT") {
|
|
4122
|
+
return null;
|
|
4123
|
+
}
|
|
4124
|
+
throw error;
|
|
4125
|
+
}
|
|
4126
|
+
}
|
|
4127
|
+
function updateConfigFile(filePath, update) {
|
|
4128
|
+
let destination = filePath;
|
|
4129
|
+
let symbolicLink = false;
|
|
4130
|
+
try {
|
|
4131
|
+
symbolicLink = fs7.lstatSync(filePath).isSymbolicLink();
|
|
4132
|
+
} catch (error) {
|
|
4133
|
+
if (error.code !== "ENOENT") {
|
|
4134
|
+
throw error;
|
|
4135
|
+
}
|
|
4136
|
+
}
|
|
4137
|
+
if (symbolicLink) {
|
|
4138
|
+
destination = fs7.realpathSync(filePath);
|
|
4139
|
+
}
|
|
4140
|
+
const before = readExisting(destination);
|
|
4141
|
+
const updated = update(before ?? "");
|
|
4142
|
+
if (before != null) {
|
|
4143
|
+
fs7.accessSync(destination, fs7.constants.W_OK);
|
|
4144
|
+
}
|
|
4145
|
+
fs7.mkdirSync(path.dirname(destination), { recursive: true });
|
|
4146
|
+
const temporary = path.join(path.dirname(destination), `.${path.basename(destination)}.${randomUUID()}.tmp`);
|
|
4147
|
+
const mode = before == null ? 384 : fs7.statSync(destination).mode & 511;
|
|
4148
|
+
let descriptor;
|
|
4149
|
+
let ownsTemporary = false;
|
|
4150
|
+
try {
|
|
4151
|
+
descriptor = fs7.openSync(temporary, "wx", mode);
|
|
4152
|
+
ownsTemporary = true;
|
|
4153
|
+
fs7.writeFileSync(descriptor, updated, "utf8");
|
|
4154
|
+
fs7.fsyncSync(descriptor);
|
|
4155
|
+
fs7.closeSync(descriptor);
|
|
4156
|
+
descriptor = void 0;
|
|
4157
|
+
if (readExisting(destination) !== before) {
|
|
4158
|
+
throw new Error("The config changed while saving; reload it and try again.");
|
|
4159
|
+
}
|
|
4160
|
+
fs7.renameSync(temporary, destination);
|
|
4161
|
+
ownsTemporary = false;
|
|
4162
|
+
} finally {
|
|
4163
|
+
try {
|
|
4164
|
+
if (descriptor != null) {
|
|
4165
|
+
fs7.closeSync(descriptor);
|
|
4166
|
+
}
|
|
4167
|
+
} finally {
|
|
4168
|
+
if (ownsTemporary) {
|
|
4169
|
+
fs7.unlinkSync(temporary);
|
|
4170
|
+
}
|
|
4171
|
+
}
|
|
4172
|
+
}
|
|
4173
|
+
}
|
|
4174
|
+
|
|
4175
|
+
// src/modules/manage/targets/json-file.ts
|
|
4060
4176
|
function toMcpServerEntry(value) {
|
|
4061
4177
|
if (typeof value !== "object" || value == null) {
|
|
4062
4178
|
return null;
|
|
@@ -4069,7 +4185,7 @@ function toMcpServerEntry(value) {
|
|
|
4069
4185
|
if (Array.isArray(record.args) == true) {
|
|
4070
4186
|
args = record.args.filter((item) => typeof item === "string");
|
|
4071
4187
|
}
|
|
4072
|
-
const env =
|
|
4188
|
+
const env = /* @__PURE__ */ Object.create(null);
|
|
4073
4189
|
if (typeof record.env === "object" && record.env != null) {
|
|
4074
4190
|
for (const [key, item] of Object.entries(record.env)) {
|
|
4075
4191
|
if (typeof item === "string") {
|
|
@@ -4079,16 +4195,22 @@ function toMcpServerEntry(value) {
|
|
|
4079
4195
|
}
|
|
4080
4196
|
return { command: record.command, args, env };
|
|
4081
4197
|
}
|
|
4082
|
-
function
|
|
4083
|
-
if (typeof document !== "object" || document == null || Array.isArray(document)
|
|
4198
|
+
function readRawMcpServersJson(document) {
|
|
4199
|
+
if (typeof document !== "object" || document == null || Array.isArray(document)) {
|
|
4084
4200
|
throw new Error("the config file is not a JSON object");
|
|
4085
4201
|
}
|
|
4086
|
-
const entries = {};
|
|
4087
4202
|
const servers = document.mcpServers;
|
|
4088
|
-
if (
|
|
4089
|
-
return
|
|
4203
|
+
if (servers === void 0) {
|
|
4204
|
+
return {};
|
|
4205
|
+
}
|
|
4206
|
+
if (typeof servers !== "object" || servers == null || Array.isArray(servers)) {
|
|
4207
|
+
throw new Error("mcpServers is not a JSON object");
|
|
4090
4208
|
}
|
|
4091
|
-
|
|
4209
|
+
return servers;
|
|
4210
|
+
}
|
|
4211
|
+
function readMcpServersJson(document) {
|
|
4212
|
+
const entries = /* @__PURE__ */ Object.create(null);
|
|
4213
|
+
for (const [name, value] of Object.entries(readRawMcpServersJson(document))) {
|
|
4092
4214
|
const entry = toMcpServerEntry(value);
|
|
4093
4215
|
if (entry != null) {
|
|
4094
4216
|
entries[name] = entry;
|
|
@@ -4096,11 +4218,11 @@ function readMcpServersJson(document) {
|
|
|
4096
4218
|
}
|
|
4097
4219
|
return entries;
|
|
4098
4220
|
}
|
|
4099
|
-
function readDocument(filePath) {
|
|
4100
|
-
if (
|
|
4221
|
+
function readDocument(filePath, contents) {
|
|
4222
|
+
if (contents == null && fs7.existsSync(filePath) == false) {
|
|
4101
4223
|
return {};
|
|
4102
4224
|
}
|
|
4103
|
-
const text =
|
|
4225
|
+
const text = contents ?? fs7.readFileSync(filePath, "utf8");
|
|
4104
4226
|
if (text.trim() === "") {
|
|
4105
4227
|
return {};
|
|
4106
4228
|
}
|
|
@@ -4121,39 +4243,35 @@ function readDocument(filePath) {
|
|
|
4121
4243
|
}
|
|
4122
4244
|
function createJsonFileTarget(options) {
|
|
4123
4245
|
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
4246
|
return {
|
|
4130
4247
|
id,
|
|
4131
4248
|
label,
|
|
4132
4249
|
location: filePath,
|
|
4133
|
-
detected:
|
|
4250
|
+
detected: fs7.existsSync(filePath),
|
|
4134
4251
|
readEntries() {
|
|
4135
4252
|
const document = readDocument(filePath);
|
|
4136
4253
|
return readMcpServersJson(document);
|
|
4137
4254
|
},
|
|
4255
|
+
hasEntry(name) {
|
|
4256
|
+
return Object.hasOwn(readRawMcpServersJson(readDocument(filePath)), name);
|
|
4257
|
+
},
|
|
4138
4258
|
writeEntry(name, entry) {
|
|
4139
|
-
|
|
4140
|
-
|
|
4141
|
-
|
|
4142
|
-
servers =
|
|
4143
|
-
|
|
4144
|
-
|
|
4145
|
-
|
|
4146
|
-
writeDocument(document);
|
|
4259
|
+
updateConfigFile(filePath, (contents) => {
|
|
4260
|
+
const document = readDocument(filePath, contents);
|
|
4261
|
+
const servers = Object.assign(/* @__PURE__ */ Object.create(null), readRawMcpServersJson(document));
|
|
4262
|
+
servers[name] = entry;
|
|
4263
|
+
document.mcpServers = servers;
|
|
4264
|
+
return JSON.stringify(document, null, 4) + "\n";
|
|
4265
|
+
});
|
|
4147
4266
|
return { status: "written", detail: filePath };
|
|
4148
4267
|
},
|
|
4149
4268
|
removeEntry(name) {
|
|
4150
|
-
|
|
4151
|
-
|
|
4152
|
-
|
|
4153
|
-
|
|
4154
|
-
|
|
4155
|
-
|
|
4156
|
-
writeDocument(document);
|
|
4269
|
+
updateConfigFile(filePath, (contents) => {
|
|
4270
|
+
const document = readDocument(filePath, contents);
|
|
4271
|
+
const servers = readRawMcpServersJson(document);
|
|
4272
|
+
delete servers[name];
|
|
4273
|
+
return JSON.stringify(document, null, 4) + "\n";
|
|
4274
|
+
});
|
|
4157
4275
|
return { status: "written", detail: filePath };
|
|
4158
4276
|
}
|
|
4159
4277
|
};
|
|
@@ -4205,11 +4323,11 @@ function runClaude(executable, args) {
|
|
|
4205
4323
|
}
|
|
4206
4324
|
return { status: "written", detail: CLAUDE_CODE_CONFIG_PATH };
|
|
4207
4325
|
}
|
|
4208
|
-
function
|
|
4209
|
-
if (
|
|
4326
|
+
function readRawClaudeEntries() {
|
|
4327
|
+
if (fs7.existsSync(CLAUDE_CODE_CONFIG_PATH) == false) {
|
|
4210
4328
|
return {};
|
|
4211
4329
|
}
|
|
4212
|
-
const text =
|
|
4330
|
+
const text = fs7.readFileSync(CLAUDE_CODE_CONFIG_PATH, "utf8");
|
|
4213
4331
|
let document;
|
|
4214
4332
|
try {
|
|
4215
4333
|
document = JSON.parse(text);
|
|
@@ -4220,7 +4338,7 @@ function readClaudeEntries() {
|
|
|
4220
4338
|
}
|
|
4221
4339
|
throw new Error(`${CLAUDE_CODE_CONFIG_PATH} is not valid JSON: ${detail}`);
|
|
4222
4340
|
}
|
|
4223
|
-
return
|
|
4341
|
+
return readRawMcpServersJson(document);
|
|
4224
4342
|
}
|
|
4225
4343
|
function createClaudeCodeTarget() {
|
|
4226
4344
|
const executable = findExecutable("claude");
|
|
@@ -4228,28 +4346,35 @@ function createClaudeCodeTarget() {
|
|
|
4228
4346
|
id: "claude-code",
|
|
4229
4347
|
label: "Claude Code (user scope)",
|
|
4230
4348
|
location: CLAUDE_CODE_CONFIG_PATH,
|
|
4231
|
-
detected: executable != null ||
|
|
4232
|
-
readEntries
|
|
4349
|
+
detected: executable != null || fs7.existsSync(CLAUDE_CODE_CONFIG_PATH) == true,
|
|
4350
|
+
readEntries() {
|
|
4351
|
+
return readMcpServersJson({ mcpServers: readRawClaudeEntries() });
|
|
4352
|
+
},
|
|
4353
|
+
hasEntry(name) {
|
|
4354
|
+
return Object.hasOwn(readRawClaudeEntries(), name);
|
|
4355
|
+
},
|
|
4233
4356
|
writeEntry(name, entry) {
|
|
4234
4357
|
if (executable == null) {
|
|
4235
4358
|
return {
|
|
4236
4359
|
status: "unavailable",
|
|
4237
|
-
detail:
|
|
4238
|
-
${formatClaudeAddCommand(name, entry)}`
|
|
4360
|
+
detail: "the claude CLI is not on PATH \u2014 install it, or use Print to get a command to run yourself"
|
|
4239
4361
|
};
|
|
4240
4362
|
}
|
|
4241
|
-
const
|
|
4242
|
-
|
|
4363
|
+
const previousEntries = readRawClaudeEntries();
|
|
4364
|
+
const hadPrevious = Object.hasOwn(previousEntries, name);
|
|
4365
|
+
const previous = previousEntries[name];
|
|
4366
|
+
if (hadPrevious) {
|
|
4243
4367
|
const removed = runClaude(executable, ["mcp", "remove", name, "-s", "user"]);
|
|
4244
4368
|
if (removed.status === "unavailable") {
|
|
4245
4369
|
return removed;
|
|
4246
4370
|
}
|
|
4247
4371
|
}
|
|
4248
4372
|
const added = runClaude(executable, ["mcp", "add-json", name, toClaudeAddJsonPayload(entry), "-s", "user"]);
|
|
4249
|
-
if (added.status === "unavailable" &&
|
|
4250
|
-
runClaude(executable, ["mcp", "add-json", name,
|
|
4373
|
+
if (added.status === "unavailable" && hadPrevious) {
|
|
4374
|
+
const restored = runClaude(executable, ["mcp", "add-json", name, JSON.stringify(previous), "-s", "user"]);
|
|
4375
|
+
const recovery = restored.status === "written" ? "the previous entry was restored" : `could not restore the previous entry: ${restored.detail}`;
|
|
4251
4376
|
return { status: "unavailable", detail: `${added.detail}
|
|
4252
|
-
|
|
4377
|
+
${recovery}` };
|
|
4253
4378
|
}
|
|
4254
4379
|
return added;
|
|
4255
4380
|
},
|
|
@@ -4266,194 +4391,121 @@ function createClaudeCodeTarget() {
|
|
|
4266
4391
|
};
|
|
4267
4392
|
}
|
|
4268
4393
|
|
|
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;
|
|
4394
|
+
// src/modules/manage/targets/toml-lines.ts
|
|
4395
|
+
function lineState() {
|
|
4396
|
+
return { quote: null, multiline: false, depth: 0 };
|
|
4397
|
+
}
|
|
4398
|
+
function scanTomlLine(line, state) {
|
|
4399
|
+
for (let index = 0; index < line.length; index += 1) {
|
|
4400
|
+
const character = line[index];
|
|
4401
|
+
if (state.quote != null) {
|
|
4402
|
+
if (character === "\\" && state.quote === '"') {
|
|
4403
|
+
index += 1;
|
|
4404
|
+
} else if (character === state.quote) {
|
|
4405
|
+
if (!state.multiline) {
|
|
4406
|
+
state.quote = null;
|
|
4407
|
+
} else if (line.slice(index, index + 3) === state.quote.repeat(3)) {
|
|
4408
|
+
while (line[index + 1] === character) {
|
|
4409
|
+
index += 1;
|
|
4410
|
+
}
|
|
4411
|
+
state.quote = null;
|
|
4412
|
+
state.multiline = false;
|
|
4332
4413
|
}
|
|
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
4414
|
}
|
|
4415
|
+
continue;
|
|
4339
4416
|
}
|
|
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");
|
|
4417
|
+
if (character === "#") {
|
|
4418
|
+
break;
|
|
4346
4419
|
}
|
|
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;
|
|
4420
|
+
if (character === '"' || character === "'") {
|
|
4421
|
+
state.quote = character;
|
|
4422
|
+
state.multiline = line.slice(index, index + 3) === character.repeat(3);
|
|
4423
|
+
if (state.multiline) {
|
|
4424
|
+
index += 2;
|
|
4367
4425
|
}
|
|
4426
|
+
} else if (character === "[" || character === "{") {
|
|
4427
|
+
state.depth += 1;
|
|
4428
|
+
} else if (character === "]" || character === "}") {
|
|
4429
|
+
state.depth -= 1;
|
|
4368
4430
|
}
|
|
4369
4431
|
}
|
|
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
|
-
}
|
|
4432
|
+
}
|
|
4433
|
+
function isTable(value) {
|
|
4434
|
+
return value != null && typeof value === "object" && !Array.isArray(value) && !(value instanceof Date);
|
|
4435
|
+
}
|
|
4436
|
+
function parseConfigToml(text) {
|
|
4437
|
+
let document;
|
|
4438
|
+
try {
|
|
4439
|
+
document = parse(text, { integersAsBigInt: true });
|
|
4440
|
+
} catch (error) {
|
|
4441
|
+
const location = error instanceof TomlError ? ` at line ${error.line}, column ${error.column}` : "";
|
|
4442
|
+
throw new Error(`Invalid TOML${location}; fix the config before changing it.`);
|
|
4404
4443
|
}
|
|
4405
|
-
|
|
4406
|
-
|
|
4407
|
-
if (rawKey.startsWith('"') == false) {
|
|
4408
|
-
return rawKey;
|
|
4444
|
+
if (Object.hasOwn(document, "mcp_servers") && !isTable(document.mcp_servers)) {
|
|
4445
|
+
throw new Error("mcp_servers must be a TOML table.");
|
|
4409
4446
|
}
|
|
4410
|
-
|
|
4411
|
-
return parsed;
|
|
4447
|
+
return document;
|
|
4412
4448
|
}
|
|
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;
|
|
4449
|
+
function readTomlEntries(text) {
|
|
4450
|
+
return readMcpServersJson({ mcpServers: parseConfigToml(text).mcp_servers ?? {} });
|
|
4451
|
+
}
|
|
4452
|
+
function hasTomlEntry(text, name) {
|
|
4453
|
+
return Object.hasOwn(parseConfigToml(text).mcp_servers ?? {}, name);
|
|
4454
|
+
}
|
|
4455
|
+
function normalize(document) {
|
|
4456
|
+
const servers = document.mcp_servers;
|
|
4457
|
+
if (servers == null) {
|
|
4458
|
+
return;
|
|
4459
|
+
}
|
|
4460
|
+
for (const server of Object.values(servers)) {
|
|
4461
|
+
if (isTable(server) && isTable(server.env) && Object.keys(server.env).length === 0) {
|
|
4462
|
+
delete server.env;
|
|
4434
4463
|
}
|
|
4435
4464
|
}
|
|
4436
|
-
|
|
4465
|
+
if (Object.keys(servers).length === 0) {
|
|
4466
|
+
delete document.mcp_servers;
|
|
4467
|
+
}
|
|
4437
4468
|
}
|
|
4438
|
-
function
|
|
4439
|
-
|
|
4440
|
-
|
|
4441
|
-
|
|
4442
|
-
|
|
4443
|
-
|
|
4444
|
-
|
|
4445
|
-
|
|
4446
|
-
|
|
4447
|
-
|
|
4448
|
-
|
|
4449
|
-
|
|
4450
|
-
|
|
4451
|
-
|
|
4452
|
-
|
|
4453
|
-
|
|
4454
|
-
|
|
4469
|
+
function verifyTomlEdit(before, after, name, entry) {
|
|
4470
|
+
const expected = parseConfigToml(before);
|
|
4471
|
+
const servers = expected.mcp_servers ?? /* @__PURE__ */ Object.create(null);
|
|
4472
|
+
if (entry == null) {
|
|
4473
|
+
delete servers[name];
|
|
4474
|
+
} else {
|
|
4475
|
+
const previous = Object.hasOwn(servers, name) && isTable(servers[name]) ? servers[name] : {};
|
|
4476
|
+
Object.defineProperty(servers, name, {
|
|
4477
|
+
value: Object.assign(/* @__PURE__ */ Object.create(null), previous, { command: entry.command, args: [...entry.args], env: Object.assign(/* @__PURE__ */ Object.create(null), entry.env) }),
|
|
4478
|
+
configurable: true,
|
|
4479
|
+
enumerable: true,
|
|
4480
|
+
writable: true
|
|
4481
|
+
});
|
|
4482
|
+
}
|
|
4483
|
+
expected.mcp_servers = servers;
|
|
4484
|
+
let actual;
|
|
4485
|
+
try {
|
|
4486
|
+
actual = parseConfigToml(after);
|
|
4487
|
+
} catch {
|
|
4488
|
+
throw new Error("Cannot safely edit this TOML layout. Use [mcp_servers.<name>] tables; nothing was written.");
|
|
4455
4489
|
}
|
|
4456
|
-
|
|
4490
|
+
normalize(expected);
|
|
4491
|
+
normalize(actual);
|
|
4492
|
+
if (!isDeepStrictEqual(expected, actual)) {
|
|
4493
|
+
throw new Error("Cannot safely edit this TOML layout without changing other settings; nothing was written.");
|
|
4494
|
+
}
|
|
4495
|
+
return after;
|
|
4496
|
+
}
|
|
4497
|
+
|
|
4498
|
+
// src/modules/manage/targets/toml-subset.ts
|
|
4499
|
+
var BARE_KEY_PATTERN = /^[A-Za-z0-9_-]+$/;
|
|
4500
|
+
var ANY_HEADER_PATTERN = /^\s*\[/;
|
|
4501
|
+
var SERVER_HEADER_PATTERN = /^\s*\[\s*(?:mcp_servers|"mcp_servers"|'mcp_servers')\s*\.\s*([A-Za-z0-9_-]+|"(?:[^"\\]|\\.)*"|'[^']*')\s*((?:\.[^\]]*)?)\]\s*(?:#.*)?$/;
|
|
4502
|
+
var KEY_VALUE_PATTERN = /^\s*([A-Za-z0-9_-]+|"(?:[^"\\]|\\.)*"|'[^']*')\s*=\s*(.*)$/;
|
|
4503
|
+
var OWNED_TABLE_KEYS = ["command", "args", "env"];
|
|
4504
|
+
function unquoteKey(rawKey) {
|
|
4505
|
+
if (rawKey.startsWith('"') || rawKey.startsWith("'")) {
|
|
4506
|
+
return parseConfigToml(`value = ${rawKey}`).value;
|
|
4507
|
+
}
|
|
4508
|
+
return rawKey;
|
|
4457
4509
|
}
|
|
4458
4510
|
function readStatements(body) {
|
|
4459
4511
|
const statements = [];
|
|
@@ -4465,28 +4517,25 @@ function readStatements(body) {
|
|
|
4465
4517
|
continue;
|
|
4466
4518
|
}
|
|
4467
4519
|
const start = index;
|
|
4468
|
-
|
|
4469
|
-
|
|
4520
|
+
const state = lineState();
|
|
4521
|
+
scanTomlLine(match[2], state);
|
|
4470
4522
|
index += 1;
|
|
4471
|
-
while (
|
|
4472
|
-
|
|
4473
|
-
rawValue += `
|
|
4474
|
-
${continuation}`;
|
|
4475
|
-
balance += bracketBalance(continuation);
|
|
4523
|
+
while ((state.quote != null || state.depth > 0) && index < body.length) {
|
|
4524
|
+
scanTomlLine(body[index], state);
|
|
4476
4525
|
index += 1;
|
|
4477
4526
|
}
|
|
4478
|
-
statements.push({ key: unquoteKey(match[1]),
|
|
4527
|
+
statements.push({ key: unquoteKey(match[1]), start, end: index });
|
|
4479
4528
|
}
|
|
4480
4529
|
return statements;
|
|
4481
4530
|
}
|
|
4482
4531
|
function splitSections(lines) {
|
|
4483
4532
|
const sections = [{ header: null, serverName: null, subPath: "", body: [] }];
|
|
4484
|
-
|
|
4533
|
+
const state = lineState();
|
|
4485
4534
|
for (const line of lines) {
|
|
4486
4535
|
const current = sections[sections.length - 1];
|
|
4487
|
-
if (
|
|
4536
|
+
if (state.quote != null || state.depth > 0) {
|
|
4488
4537
|
current.body.push(line);
|
|
4489
|
-
|
|
4538
|
+
scanTomlLine(line, state);
|
|
4490
4539
|
continue;
|
|
4491
4540
|
}
|
|
4492
4541
|
if (ANY_HEADER_PATTERN.test(line) == true) {
|
|
@@ -4501,73 +4550,18 @@ function splitSections(lines) {
|
|
|
4501
4550
|
continue;
|
|
4502
4551
|
}
|
|
4503
4552
|
current.body.push(line);
|
|
4504
|
-
|
|
4505
|
-
if (keyValueMatch != null) {
|
|
4506
|
-
balance = bracketBalance(stripComment(keyValueMatch[2]));
|
|
4507
|
-
}
|
|
4553
|
+
scanTomlLine(line, state);
|
|
4508
4554
|
}
|
|
4509
4555
|
return sections;
|
|
4510
4556
|
}
|
|
4511
|
-
function
|
|
4512
|
-
|
|
4513
|
-
const value = new TomlValueParser(statement.rawValue).parse();
|
|
4514
|
-
return value;
|
|
4515
|
-
} catch {
|
|
4516
|
-
return null;
|
|
4517
|
-
}
|
|
4557
|
+
function hasMcpServerToml(text, name) {
|
|
4558
|
+
return hasTomlEntry(text, name);
|
|
4518
4559
|
}
|
|
4519
4560
|
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;
|
|
4561
|
+
return readTomlEntries(text);
|
|
4568
4562
|
}
|
|
4569
4563
|
function quoteTomlString(value) {
|
|
4570
|
-
const escaped = value.replace(/\\/g, "\\\\").replace(/"/g, '\\"').replace(/\n/g, "\\n").replace(/\r/g, "\\r").replace(/\t/g, "\\t");
|
|
4564
|
+
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
4565
|
return `"${escaped}"`;
|
|
4572
4566
|
}
|
|
4573
4567
|
function formatTomlKey(key) {
|
|
@@ -4611,6 +4605,11 @@ function joinSections(sections, eol) {
|
|
|
4611
4605
|
return lines.join(eol);
|
|
4612
4606
|
}
|
|
4613
4607
|
function writeMcpServerToml(text, name, entry) {
|
|
4608
|
+
parseConfigToml(text);
|
|
4609
|
+
const updated = editMcpServerToml(text, name, entry);
|
|
4610
|
+
return verifyTomlEdit(text, updated, name, entry);
|
|
4611
|
+
}
|
|
4612
|
+
function editMcpServerToml(text, name, entry) {
|
|
4614
4613
|
let eol = "\n";
|
|
4615
4614
|
if (text.includes("\r\n") == true) {
|
|
4616
4615
|
eol = "\r\n";
|
|
@@ -4683,34 +4682,36 @@ function writeMcpServerToml(text, name, entry) {
|
|
|
4683
4682
|
var CODEX_CONFIG_PATH = path.join(os5.homedir(), ".codex", "config.toml");
|
|
4684
4683
|
function createCodexTarget() {
|
|
4685
4684
|
function readText() {
|
|
4686
|
-
if (
|
|
4685
|
+
if (fs7.existsSync(CODEX_CONFIG_PATH) == false) {
|
|
4687
4686
|
return "";
|
|
4688
4687
|
}
|
|
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 };
|
|
4688
|
+
return fs7.readFileSync(CODEX_CONFIG_PATH, "utf8");
|
|
4695
4689
|
}
|
|
4696
4690
|
return {
|
|
4697
4691
|
id: "codex",
|
|
4698
4692
|
label: "Codex CLI",
|
|
4699
4693
|
location: CODEX_CONFIG_PATH,
|
|
4700
|
-
detected: findExecutable("codex") != null ||
|
|
4694
|
+
detected: findExecutable("codex") != null || fs7.existsSync(CODEX_CONFIG_PATH) == true,
|
|
4701
4695
|
readEntries() {
|
|
4702
4696
|
const text = readText();
|
|
4703
4697
|
return parseMcpServersToml(text);
|
|
4704
4698
|
},
|
|
4705
|
-
|
|
4706
|
-
|
|
4707
|
-
|
|
4708
|
-
|
|
4699
|
+
hasEntry(name) {
|
|
4700
|
+
return hasMcpServerToml(readText(), name);
|
|
4701
|
+
},
|
|
4702
|
+
writeEntry(name, entry, options) {
|
|
4703
|
+
updateConfigFile(CODEX_CONFIG_PATH, (original) => {
|
|
4704
|
+
let text = original;
|
|
4705
|
+
if (options?.replace) {
|
|
4706
|
+
text = writeMcpServerToml(text, name, null);
|
|
4707
|
+
}
|
|
4708
|
+
return writeMcpServerToml(text, name, entry);
|
|
4709
|
+
});
|
|
4710
|
+
return { status: "written", detail: CODEX_CONFIG_PATH };
|
|
4709
4711
|
},
|
|
4710
4712
|
removeEntry(name) {
|
|
4711
|
-
|
|
4712
|
-
|
|
4713
|
-
return writeText(updated);
|
|
4713
|
+
updateConfigFile(CODEX_CONFIG_PATH, (text) => writeMcpServerToml(text, name, null));
|
|
4714
|
+
return { status: "written", detail: CODEX_CONFIG_PATH };
|
|
4714
4715
|
}
|
|
4715
4716
|
};
|
|
4716
4717
|
}
|
|
@@ -4918,13 +4919,14 @@ function buildHostItems(names, entries) {
|
|
|
4918
4919
|
};
|
|
4919
4920
|
});
|
|
4920
4921
|
}
|
|
4921
|
-
async function manageHost(prompter, output, target, entries, name) {
|
|
4922
|
+
async function manageHost(prompter, output, target, targets, entries, name) {
|
|
4922
4923
|
const action = await prompter.select(
|
|
4923
4924
|
name,
|
|
4924
4925
|
[
|
|
4925
4926
|
{ value: "edit", key: "e", label: "Edit", hint: "open its settings" },
|
|
4926
4927
|
{ value: "check", key: "c", label: "Check the connection", hint: "connect now and offer to pin the host key" },
|
|
4927
4928
|
{ value: "print", key: "p", label: "Print the entry", hint: "JSON, Codex TOML and a claude command, ready to paste" },
|
|
4929
|
+
...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
4930
|
{ value: "remove", key: "r", label: "Remove", hint: `from ${target.label}` },
|
|
4929
4931
|
{ separator: true },
|
|
4930
4932
|
{ value: "back", key: "b", label: "Back" }
|
|
@@ -4943,6 +4945,10 @@ async function manageHost(prompter, output, target, entries, name) {
|
|
|
4943
4945
|
printHost(prompter, name, entries[name]);
|
|
4944
4946
|
return;
|
|
4945
4947
|
}
|
|
4948
|
+
if (action === "copy") {
|
|
4949
|
+
await copyEntryToClient(prompter, target, targets, name, entries[name]);
|
|
4950
|
+
return;
|
|
4951
|
+
}
|
|
4946
4952
|
if (action === "remove") {
|
|
4947
4953
|
const remove = await prompter.askYesNo(`Remove ${name} from ${target.location}?`, false);
|
|
4948
4954
|
if (remove == true) {
|
|
@@ -5044,7 +5050,7 @@ ${paint.bold(target.label)} ${paint.dim(target.location)}
|
|
|
5044
5050
|
continue;
|
|
5045
5051
|
}
|
|
5046
5052
|
if (picked.startsWith(HOST_VALUE_PREFIX) == true) {
|
|
5047
|
-
await manageHost(prompter, output, target, entries, picked.slice(HOST_VALUE_PREFIX.length));
|
|
5053
|
+
await manageHost(prompter, output, target, targets, entries, picked.slice(HOST_VALUE_PREFIX.length));
|
|
5048
5054
|
}
|
|
5049
5055
|
} catch (ex) {
|
|
5050
5056
|
if (ex instanceof PromptClosedError) {
|
|
@@ -5497,7 +5503,7 @@ ${file.content}`);
|
|
|
5497
5503
|
const { profile, session: openSession } = resolveProfile(context, { session });
|
|
5498
5504
|
const resolvedLocalPath = expandHomePath(localPath);
|
|
5499
5505
|
requirePermittedLocalPath(resolvedLocalPath, "read");
|
|
5500
|
-
if (
|
|
5506
|
+
if (fs7.existsSync(resolvedLocalPath) == false) {
|
|
5501
5507
|
return textResult(`local file not found: ${resolvedLocalPath}`);
|
|
5502
5508
|
}
|
|
5503
5509
|
const transferredBytes = await withHostConnection(profile, openSession, async (connection) => {
|
|
@@ -5537,7 +5543,7 @@ ${file.content}`);
|
|
|
5537
5543
|
const { profile, session: openSession } = resolveProfile(context, { session });
|
|
5538
5544
|
const resolvedLocalPath = expandHomePath(localPath);
|
|
5539
5545
|
requirePermittedLocalPath(resolvedLocalPath, "write");
|
|
5540
|
-
|
|
5546
|
+
fs7.mkdirSync(path.dirname(resolvedLocalPath), { recursive: true });
|
|
5541
5547
|
const transferredBytes = await withHostConnection(profile, openSession, async (connection) => {
|
|
5542
5548
|
return await downloadFile(connection, remotePath, resolvedLocalPath);
|
|
5543
5549
|
});
|