@akms/mcp 0.2.1 → 0.3.1

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.
Files changed (4) hide show
  1. package/CHANGELOG.md +40 -0
  2. package/README.md +76 -15
  3. package/lib/cli.js +2600 -133
  4. package/package.json +13 -3
package/lib/cli.js CHANGED
@@ -44,7 +44,12 @@ function parseArguments(argv) {
44
44
  commandSeen = true;
45
45
  continue;
46
46
  }
47
- if (Object.hasOwn(MCP_PACKAGES, argument) == true && parsed.mcpId == null) {
47
+ if ((argument === "register" || argument === "sync") && !commandSeen && !parsed.mcpId) {
48
+ commandSeen = true;
49
+ parsed.managementMode = argument;
50
+ continue;
51
+ }
52
+ if (Object.hasOwn(MCP_PACKAGES, argument) == true && parsed.mcpId == null && !parsed.managementMode) {
48
53
  parsed.mcpId = argument;
49
54
  continue;
50
55
  }
@@ -53,6 +58,217 @@ function parseArguments(argv) {
53
58
  return parsed;
54
59
  }
55
60
 
61
+ // src/modules/registration/registry.ts
62
+ import fs4 from "fs";
63
+ import path5 from "path";
64
+ import { createHash } from "crypto";
65
+
66
+ // src/modules/registration/config-file.ts
67
+ import fs from "fs";
68
+ import path from "path";
69
+ import { randomUUID } from "crypto";
70
+ function readExisting(filePath) {
71
+ try {
72
+ return fs.readFileSync(filePath, "utf8");
73
+ } catch (error) {
74
+ if (error.code === "ENOENT") {
75
+ return null;
76
+ }
77
+ throw error;
78
+ }
79
+ }
80
+ function updateConfigFile(filePath, update, options = {}) {
81
+ let destination = filePath;
82
+ let symbolicLink = false;
83
+ try {
84
+ symbolicLink = fs.lstatSync(filePath).isSymbolicLink();
85
+ } catch (error) {
86
+ if (error.code !== "ENOENT") {
87
+ throw error;
88
+ }
89
+ }
90
+ if (symbolicLink) {
91
+ destination = fs.realpathSync(filePath);
92
+ }
93
+ const before = readExisting(destination);
94
+ const updated = update(before ?? "");
95
+ if (before === updated) return;
96
+ if (before != null) {
97
+ fs.accessSync(destination, fs.constants.W_OK);
98
+ }
99
+ fs.mkdirSync(path.dirname(destination), { recursive: true });
100
+ const temporary = path.join(path.dirname(destination), `.${path.basename(destination)}.${randomUUID()}.tmp`);
101
+ const mode = options.mode ?? (before == null ? 384 : fs.statSync(destination).mode & 511);
102
+ let descriptor;
103
+ let ownsTemporary = false;
104
+ try {
105
+ descriptor = fs.openSync(temporary, "wx", mode);
106
+ ownsTemporary = true;
107
+ fs.writeFileSync(descriptor, updated, "utf8");
108
+ fs.fsyncSync(descriptor);
109
+ fs.closeSync(descriptor);
110
+ descriptor = void 0;
111
+ if (readExisting(destination) !== before) {
112
+ throw new Error("The config changed while saving; reload it and try again.");
113
+ }
114
+ options.beforeCommit?.(before);
115
+ if (readExisting(destination) !== before) throw new Error("The config changed while saving; reload it and try again.");
116
+ fs.renameSync(temporary, destination);
117
+ ownsTemporary = false;
118
+ } finally {
119
+ try {
120
+ if (descriptor != null) {
121
+ fs.closeSync(descriptor);
122
+ }
123
+ } finally {
124
+ if (ownsTemporary) {
125
+ fs.unlinkSync(temporary);
126
+ }
127
+ }
128
+ }
129
+ }
130
+
131
+ // src/modules/registration/types.ts
132
+ var CLIENT_LABELS2 = {
133
+ "claude-code": "Claude Code",
134
+ codex: "Codex",
135
+ "claude-desktop": "Claude Desktop",
136
+ cursor: "Cursor"
137
+ };
138
+ var KINDS = ["db", "ssh", "wsl", "s3"];
139
+
140
+ // src/modules/registration/runtime.ts
141
+ import fs2 from "fs";
142
+ import os from "os";
143
+ import path2 from "path";
144
+ import { execFile } from "child_process";
145
+ import { promisify, stripVTControlCharacters } from "util";
146
+ var packageName = (kind) => `@akms/mcp-${kind}`;
147
+ var binary = (command) => command.replace(/\\/g, "/").split("/").pop().toLowerCase();
148
+ function runtimePrefix() {
149
+ return path2.join(process.env.AKMS_MCP_HOME ? path2.resolve(process.env.AKMS_MCP_HOME) : path2.join(os.homedir(), ".akms", "mcp"), "runtime");
150
+ }
151
+ function runtimeRoot(prefix = runtimePrefix(), platform = process.platform) {
152
+ return path2.join(prefix, ...platform === "win32" ? [] : ["lib"], "node_modules");
153
+ }
154
+ var runtimeInstallCommand = (kind) => `npm install --global --prefix "${runtimePrefix()}" ${packageName(kind)}@latest`;
155
+ async function latestRuntimeVersion(kind, timeoutMs = 6e3) {
156
+ try {
157
+ const { stdout } = await promisify(execFile)(process.execPath, [npmCli(), "view", `${packageName(kind)}@latest`, "version", "--json", "--prefer-online", "--fetch-retries=0", "--fetch-timeout=4000"], {
158
+ windowsHide: true,
159
+ timeout: timeoutMs,
160
+ maxBuffer: 65536
161
+ });
162
+ const version = JSON.parse(stdout);
163
+ return typeof version === "string" && /^\d+\.\d+\.\d+(?:-[0-9A-Za-z.-]+)?(?:\+[0-9A-Za-z.-]+)?$/.test(version) ? version : null;
164
+ } catch {
165
+ return null;
166
+ }
167
+ }
168
+ function runtimeIsOlder(installed, current) {
169
+ const parse2 = (value) => /^(\d+)\.(\d+)\.(\d+)(?:-([0-9A-Za-z.-]+))?(?:\+[0-9A-Za-z.-]+)?$/.exec(value);
170
+ const left = parse2(installed), right = parse2(current);
171
+ if (!left || !right) return false;
172
+ for (let index = 1; index <= 3; index++) {
173
+ if (BigInt(left[index]) !== BigInt(right[index])) return BigInt(left[index]) < BigInt(right[index]);
174
+ }
175
+ if (!left[4] || !right[4]) return Boolean(left[4] && !right[4]);
176
+ const a = left[4].split("."), b = right[4].split(".");
177
+ for (let index = 0; index < Math.max(a.length, b.length); index++) {
178
+ if (a[index] === b[index]) continue;
179
+ if (a[index] === void 0 || b[index] === void 0) return a[index] === void 0;
180
+ const numericA = /^\d+$/.test(a[index]), numericB = /^\d+$/.test(b[index]);
181
+ if (numericA && numericB) return BigInt(a[index]) < BigInt(b[index]);
182
+ if (numericA !== numericB) return numericA;
183
+ return a[index] < b[index];
184
+ }
185
+ return false;
186
+ }
187
+ function launchKind(entry) {
188
+ for (const kind of KINDS) {
189
+ if (new RegExp(`^akms-mcp-${kind}(?:\\.cmd|\\.exe)?$`).test(binary(entry.command)) || entry.args.some((arg) => new RegExp(`^@akms/mcp-${kind}(?:@[^\\s]+)?$`).test(arg))) return kind;
190
+ if (/^(node|nodejs)(\.exe)?$/.test(binary(entry.command)) && entry.args[0]?.replace(/\\/g, "/").endsWith(`/node_modules/@akms/mcp-${kind}/lib/cli.js`)) return kind;
191
+ }
192
+ return null;
193
+ }
194
+ function standardLaunch(entry, kind) {
195
+ if (launchKind(entry) !== kind) return false;
196
+ const command = binary(entry.command);
197
+ if (/^(node|nodejs)(\.exe)?$/.test(command)) return entry.args.length === 1 && entry.args[0].replace(/\\/g, "/").endsWith(`/node_modules/@akms/mcp-${kind}/lib/cli.js`);
198
+ if (new RegExp(`^akms-mcp-${kind}(?:\\.cmd|\\.exe)?$`).test(command)) return entry.args.length === 0;
199
+ let args = entry.args;
200
+ if (command === "cmd" || command === "cmd.exe") {
201
+ if (args[0]?.toLowerCase() !== "/c" || !/^npx(?:\.cmd)?$/i.test(args[1] ?? "")) return false;
202
+ args = args.slice(2);
203
+ } else if (!/^npx(?:\.cmd)?$/.test(command)) return false;
204
+ return args.length === 2 && ["-y", "--yes"].includes(args[0]) && new RegExp(`^@akms/mcp-${kind}(?:@[^\\s]+)?$`).test(args[1]);
205
+ }
206
+ function catalogLaunch(entry, kind = launchKind(entry)) {
207
+ return !!kind && entry.args.length === 2 && entry.args[1] === "--catalog" && standardLaunch({ ...entry, args: entry.args.slice(0, 1) }, kind);
208
+ }
209
+ function npmCli() {
210
+ const candidates = [
211
+ path2.join(path2.dirname(process.execPath), "node_modules", "npm", "bin", "npm-cli.js"),
212
+ path2.join(path2.dirname(process.execPath), "..", "lib", "node_modules", "npm", "bin", "npm-cli.js")
213
+ ];
214
+ if (process.env.npm_execpath && /^(npm|npx)-cli\.js$/.test(path2.basename(process.env.npm_execpath))) {
215
+ candidates.unshift(path2.join(path2.dirname(process.env.npm_execpath), "npm-cli.js"));
216
+ }
217
+ for (const folder of (process.env.PATH ?? "").split(path2.delimiter).filter(Boolean)) {
218
+ candidates.push(process.platform === "win32" ? path2.join(folder, "node_modules", "npm", "bin", "npm-cli.js") : path2.join(folder, "npm"));
219
+ }
220
+ for (const file of candidates) {
221
+ try {
222
+ const resolved = fs2.realpathSync(file);
223
+ if (resolved.endsWith(".js") && fs2.statSync(resolved).isFile()) return resolved;
224
+ } catch {
225
+ }
226
+ }
227
+ throw new Error("npm was not found. Install Node.js with npm, then reopen configuration.");
228
+ }
229
+ function findGlobalRuntime(kind, root) {
230
+ try {
231
+ const directory = path2.join(root ?? runtimeRoot(), "@akms", `mcp-${kind}`);
232
+ const metadata = JSON.parse(fs2.readFileSync(path2.join(directory, "package.json"), "utf8"));
233
+ const bin = typeof metadata.bin === "string" ? metadata.bin : metadata.bin?.[`akms-mcp-${kind}`];
234
+ const script = path2.join(directory, "lib", "cli.js");
235
+ if (metadata.name !== packageName(kind) || !["lib/cli.js", "./lib/cli.js"].includes(bin) || typeof metadata.version !== "string" || !fs2.statSync(script).isFile()) return null;
236
+ return { command: process.execPath, args: [script], version: metadata.version, linked: fs2.lstatSync(directory).isSymbolicLink() };
237
+ } catch {
238
+ return null;
239
+ }
240
+ }
241
+ async function installGlobalRuntime(kind) {
242
+ const cancellation = new AbortController();
243
+ const cancel = () => cancellation.abort();
244
+ process.on("SIGINT", cancel);
245
+ process.on("SIGTERM", cancel);
246
+ try {
247
+ await promisify(execFile)(process.execPath, [npmCli(), "install", "--global", "--prefix", runtimePrefix(), "--prefer-online", "--no-fund", "--no-audit", `${packageName(kind)}@latest`], {
248
+ windowsHide: true,
249
+ timeout: 18e4,
250
+ maxBuffer: 1024 * 1024,
251
+ signal: cancellation.signal,
252
+ cwd: os.homedir()
253
+ });
254
+ } catch (error) {
255
+ if (cancellation.signal.aborted) throw new Error("AKMS runtime installation cancelled.");
256
+ const stderr = error.stderr;
257
+ const detail = stripVTControlCharacters(typeof stderr === "string" ? stderr : error instanceof Error ? error.message : "").replace(/[\x00-\x08\x0b-\x1f\x7f]/g, "").trim().split(/\r?\n/).slice(-6).map((line) => line.slice(0, 500)).join("\n");
258
+ throw new Error(`AKMS runtime installation failed. Run ${runtimeInstallCommand(kind)} in your terminal and retry.${detail ? `
259
+
260
+ ${detail}` : ""}`);
261
+ } finally {
262
+ process.off("SIGINT", cancel);
263
+ process.off("SIGTERM", cancel);
264
+ }
265
+ }
266
+
267
+ // src/modules/registration/startup-check.ts
268
+ import { spawn, execFile as execFile2 } from "child_process";
269
+ import path3 from "path";
270
+ import { stripVTControlCharacters as stripVTControlCharacters2 } from "util";
271
+
56
272
  // src/modules/launcher/prompter.ts
57
273
  var PromptClosedError = class extends Error {
58
274
  constructor() {
@@ -162,7 +378,7 @@ function createStyle(enabled) {
162
378
  };
163
379
  }
164
380
  var Prompter = class {
165
- constructor(input, output) {
381
+ constructor(input, output, initialInput = "") {
166
382
  this.input = input;
167
383
  this.output = output;
168
384
  const ttyInput = input;
@@ -178,6 +394,7 @@ var Prompter = class {
178
394
  input.setEncoding("utf8");
179
395
  input.on("data", this.onInputData);
180
396
  input.on("end", this.onInputEnd);
397
+ if (initialInput) this.onData(initialInput);
181
398
  }
182
399
  input;
183
400
  output;
@@ -220,6 +437,79 @@ var Prompter = class {
220
437
  this.lineBuffer = "";
221
438
  return text;
222
439
  }
440
+ get paint() {
441
+ return this.style;
442
+ }
443
+ /** Available content columns, leaving room for the terminal cursor. */
444
+ get width() {
445
+ return this.columns() - 1;
446
+ }
447
+ /** Start a fresh terminal page without erasing scrollback or emitting ANSI to pipes. */
448
+ clearScreen() {
449
+ if (this.interactive) this.output.write(`${ESC}[2J${ESC}[H`);
450
+ }
451
+ /** Keep results visible before navigating; Esc acknowledges them like Enter. */
452
+ async acknowledge() {
453
+ try {
454
+ await this.pause();
455
+ } catch (error) {
456
+ if (!(error instanceof PromptBackError)) throw error;
457
+ }
458
+ }
459
+ async pause() {
460
+ if (this.interactive) await this.readText("Press Enter to continue ");
461
+ }
462
+ write(text) {
463
+ this.output.write(text);
464
+ }
465
+ async askRequired(question, defaultValue) {
466
+ for (; ; ) {
467
+ const answer = (await this.readText(`${question}${defaultValue ? ` (${defaultValue})` : ""}: `)).trim() || defaultValue;
468
+ if (answer) return answer;
469
+ this.write(" A value is required.\n");
470
+ }
471
+ }
472
+ async askYesNo(question, defaultValue) {
473
+ for (; ; ) {
474
+ const answer = (await this.readText(`${question} (${defaultValue ? "Y/n" : "y/N"}): `)).trim().toLowerCase();
475
+ if (!answer) return defaultValue;
476
+ if (answer === "y" || answer === "yes") return true;
477
+ if (answer === "n" || answer === "no") return false;
478
+ this.write(" Enter y or n.\n");
479
+ }
480
+ }
481
+ async readText(prompt) {
482
+ this.input.resume();
483
+ this.output.write(prompt);
484
+ if (!this.interactive) {
485
+ const line = await this.readLine();
486
+ if (line == null) throw new PromptClosedError();
487
+ if (line === "ESC" || line === ESC) throw new PromptBackError();
488
+ return line;
489
+ }
490
+ if (this.inputEnded) throw new PromptClosedError();
491
+ this.input.setRawMode?.(true);
492
+ this.output.write(`${ESC}[?25h`);
493
+ return new Promise((resolve, reject) => {
494
+ let text = "";
495
+ const finish = (error) => {
496
+ this.keyHandler = null;
497
+ this.input.setRawMode?.(false);
498
+ this.output.write("\n");
499
+ if (error) reject(error);
500
+ else resolve(text);
501
+ };
502
+ this.keyHandler = (key) => {
503
+ if (key === KEY_CTRL_C || key === KEY_CTRL_D) return finish(new PromptClosedError());
504
+ if (key === ESC) return finish(new PromptBackError());
505
+ if (key === "\r" || key === "\n") return finish();
506
+ if (key === "\b" || key === "\x7F") text = Array.from(text).slice(0, -1).join("");
507
+ else if (key === "") text = "";
508
+ else if (!/[\x00-\x1f\x7f]/.test(key)) text += key;
509
+ this.output.write(`\r${ESC}[K${prompt}${text}`);
510
+ };
511
+ });
512
+ }
223
513
  /**
224
514
  * Picks one of `items`. On a TTY: ↑/↓ move, 1–9 jump, Enter picks, Esc goes back. Off a
225
515
  * TTY: a numbered list, answered by number, key or label.
@@ -486,146 +776,2313 @@ var Prompter = class {
486
776
  }
487
777
  };
488
778
 
489
- // src/modules/launcher/menu.ts
490
- async function runMenu(options) {
491
- let targetId = options.targetId;
492
- let defaultValue = "db";
493
- for (; ; ) {
494
- const prompter = new Prompter(options.input, options.output);
495
- let selected = "quit";
496
- try {
497
- for (; ; ) {
498
- let clientLabel = "Automatically selected by each MCP";
499
- if (targetId != null) {
500
- clientLabel = CLIENT_LABELS[targetId];
779
+ // src/modules/registration/comparison.ts
780
+ var visibleField = /^(DB_DRIVER|DB_HOST|DB_PORT|DB_NAME|DB_USER|DB_SSL|DB_DESCRIPTION|SSH_HOST|SSH_PORT|SSH_USER|SSH_DESCRIPTION|WSL_DISTRO|WSL_USER|WSL_DESCRIPTION|S3_BACKEND|S3_BUCKET|S3_REGION|S3_PREFIX|S3_AUTH|S3_DESCRIPTION)$/;
781
+ var plainText = (value) => value.replace(/[\x00-\x1f\x7f]/g, " ");
782
+ function descriptionOnly(entries) {
783
+ const fields = differingFields(entries);
784
+ return fields.length > 0 && fields.every((field) => /^env\.(DB|SSH|WSL|S3)_DESCRIPTION$/.test(field));
785
+ }
786
+ var fieldName = (field) => {
787
+ const key = field.replace(/^env\./, "");
788
+ if (/^(DB|SSH|WSL|S3)_DESCRIPTION$/.test(key)) return "Description";
789
+ const names = { command: "Launch command", args: "Launch arguments", DB_HOST: "Host", SSH_HOST: "Host", DB_PORT: "Port", SSH_PORT: "Port", DB_NAME: "Database", DB_USER: "User", SSH_USER: "User", WSL_USER: "User", WSL_DISTRO: "Linux distribution", S3_BUCKET: "Bucket", S3_REGION: "Region", S3_PREFIX: "Key prefix" };
790
+ return Object.hasOwn(names, key) ? names[key] : plainText(key);
791
+ };
792
+ function writeWrapped(prompter, text, indent = "", changed) {
793
+ const characters = Array.from(plainText(text));
794
+ let line = indent;
795
+ let width = displayWidth(indent);
796
+ let segment = "";
797
+ let highlighted = false;
798
+ const flush = () => {
799
+ line += highlighted ? prompter.paint.accent(prompter.paint.bold(segment)) : segment;
800
+ segment = "";
801
+ };
802
+ for (let index = 0; index < characters.length; index++) {
803
+ const character = characters[index];
804
+ const size = displayWidth(character);
805
+ if (width + size > prompter.width) {
806
+ flush();
807
+ prompter.write(line + "\n");
808
+ line = indent;
809
+ width = displayWidth(indent);
810
+ }
811
+ const highlight = !!changed && index >= changed[0] && index < changed[1];
812
+ if (highlight !== highlighted) {
813
+ flush();
814
+ highlighted = highlight;
815
+ }
816
+ segment += character;
817
+ width += size;
818
+ }
819
+ flush();
820
+ prompter.write(line + "\n");
821
+ }
822
+ function changedRange(values, index) {
823
+ if (values.length !== 2 || values[0] === values[1]) return void 0;
824
+ const current = Array.from(values[index]);
825
+ const other = Array.from(values[1 - index]);
826
+ let start = 0, end = current.length, otherEnd = other.length;
827
+ while (start < end && start < otherEnd && current[start] === other[start]) start++;
828
+ while (end > start && otherEnd > start && current[end - 1] === other[otherEnd - 1]) {
829
+ end--;
830
+ otherEnd--;
831
+ }
832
+ return [start, end];
833
+ }
834
+ function compareEntries(prompter, versions) {
835
+ const entries = versions.map((version) => version.entry);
836
+ const fields = differingFields(entries);
837
+ writeWrapped(prompter, descriptionOnly(entries) ? "Description differs. Connection settings match." : `${fields.length} setting(s) differ.`);
838
+ const values = fields.map((field) => {
839
+ const key = field.replace(/^env\./, "");
840
+ return versions.map(({ entry }) => field.startsWith("env.") && visibleField.test(key) ? Object.hasOwn(entry.env, key) ? plainText(entry.env[key]) || "(empty)" : "(not set)" : "(hidden)");
841
+ });
842
+ for (let index = 0; index < versions.length; index++) {
843
+ prompter.write("\n");
844
+ writeWrapped(prompter, `[${index + 1}] ${versions[index].label}`);
845
+ for (let field = 0; field < fields.length; field++) {
846
+ writeWrapped(prompter, fieldName(fields[field]), " ");
847
+ writeWrapped(prompter, values[field][index], " ", changedRange(values[field], index));
848
+ }
849
+ }
850
+ if (values.some((row) => row.includes("(hidden)"))) prompter.write("\nSecret values are hidden.\n");
851
+ prompter.write("\n");
852
+ }
853
+
854
+ // src/modules/registration/startup-check.ts
855
+ function visibleRuntimePath(entry) {
856
+ const kind = launchKind(entry);
857
+ return kind && standardLaunch({ ...entry, args: entry.args.slice(0, 1) }, kind) && (entry.args.length === 1 || entry.args.length === 2 && entry.args[1] === "--catalog") ? entry.args[0] : void 0;
858
+ }
859
+ function redactStartupText(text, entry) {
860
+ const normalize2 = (value) => stripVTControlCharacters2(value).replace(/[\x00-\x08\x0b-\x1f\x7f]/g, "");
861
+ const direct = visibleRuntimePath(entry);
862
+ const privateValues = [...Object.entries(entry.env).filter(([key]) => !visibleField.test(key)).map(([, value]) => value), ...entry.args.slice(direct ? 1 : 0)];
863
+ const secrets = /* @__PURE__ */ new Set();
864
+ for (const value of privateValues) {
865
+ if (!value) continue;
866
+ secrets.add(value);
867
+ const values = [value];
868
+ if (value.includes("=")) values.push(value.slice(value.indexOf("=") + 1));
869
+ for (const candidate of values) {
870
+ secrets.add(candidate);
871
+ try {
872
+ const url = new URL(candidate);
873
+ if (url.password) {
874
+ secrets.add(url.password);
875
+ secrets.add(decodeURIComponent(url.password));
501
876
  }
502
- const picked = await prompter.select("AKMS MCP \xB7 Connection setup", [
503
- { value: "db", label: "Database", hint: "PostgreSQL connections" },
504
- { value: "ssh", label: "SSH", hint: "Remote hosts" },
505
- { value: "wsl", label: "WSL", hint: "Local Linux distros \xB7 Windows" },
506
- { value: "s3", label: "S3", hint: "AWS S3 and S3-compatible buckets" },
507
- { separator: true },
508
- { value: "client", label: "Client", hint: clientLabel },
509
- { value: "quit", label: "Quit" }
510
- ], { defaultValue, cancelValue: "quit" });
511
- if (picked !== "client") {
512
- selected = picked;
513
- break;
877
+ } catch {
878
+ }
879
+ }
880
+ }
881
+ for (const value of [...secrets]) {
882
+ if (!value) continue;
883
+ secrets.add(JSON.stringify(value).slice(1, -1));
884
+ secrets.add(encodeURIComponent(value));
885
+ }
886
+ for (const value of [...secrets]) secrets.add(normalize2(value));
887
+ let result = normalize2(text);
888
+ for (const value of [...secrets].filter(Boolean).sort((a, b) => b.length - a.length)) result = result.split(value).join("(hidden)");
889
+ return result;
890
+ }
891
+ async function checkStartup(entry, timeoutMs = 1e4) {
892
+ return new Promise((resolve) => {
893
+ const inherited = process.platform === "win32" ? ["APPDATA", "LOCALAPPDATA", "HOMEDRIVE", "HOMEPATH", "PATH", "SYSTEMROOT", "SYSTEMDRIVE", "TEMP", "TMP", "USERPROFILE", "PROGRAMFILES"] : ["HOME", "LOGNAME", "PATH", "SHELL", "TERM", "USER", "TMPDIR"];
894
+ const env = Object.fromEntries(inherited.filter((key) => process.env[key] !== void 0).map((key) => [key, process.env[key]]));
895
+ let stderr = "", stderrOmitted = false, output = "", bytes = 0, done = false, closing = false, detail;
896
+ let server, version, tools = 0, expected = 1, pages = 0;
897
+ const toolNames = [];
898
+ let timer, killTimer, endTimer;
899
+ let child;
900
+ const result = (code, signal) => {
901
+ if (done) return;
902
+ done = true;
903
+ clearTimeout(timer);
904
+ clearTimeout(killTimer);
905
+ clearTimeout(endTimer);
906
+ const clean = (value) => redactStartupText(value, entry);
907
+ const ok = !detail && closing && code === 0;
908
+ const runtime = visibleRuntimePath(entry);
909
+ const failure = detail ?? (code !== 0 ? `Exited ${code ?? signal ?? "without an exit code"}.` : "Startup check did not finish.");
910
+ resolve({
911
+ ok,
912
+ server: server && clean(server).slice(0, 200),
913
+ version: version && clean(version).slice(0, 100),
914
+ tools,
915
+ toolNames: toolNames.map(clean),
916
+ exitCode: code,
917
+ detail: ok ? void 0 : clean(`${failure}${runtime ? ` Runtime: ${runtime}` : ""}`).slice(0, 2e3),
918
+ stderr: clean(stderr).trim().split(/\r?\n/).filter(Boolean).slice(-6).map((line) => line.slice(0, 500))
919
+ });
920
+ };
921
+ const stop = (reason) => {
922
+ if (closing || done) return;
923
+ closing = true;
924
+ detail = reason;
925
+ clearTimeout(timer);
926
+ child.stdin?.end();
927
+ killTimer = setTimeout(() => {
928
+ if (process.platform === "win32" && child.pid) {
929
+ execFile2(path3.join(process.env.SYSTEMROOT ?? "C:\\Windows", "System32", "taskkill.exe"), ["/PID", String(child.pid), "/T", "/F"], { windowsHide: true, timeout: 4e3 }, () => {
930
+ });
931
+ } else if (child.pid) {
932
+ try {
933
+ process.kill(-child.pid, "SIGKILL");
934
+ } catch {
935
+ child.kill("SIGKILL");
936
+ }
514
937
  }
515
- const clients = [
516
- { value: "auto", label: "Automatic", hint: "Let each MCP detect the client" },
517
- { value: "claude-code", label: CLIENT_LABELS["claude-code"] },
518
- { value: "codex", label: CLIENT_LABELS.codex },
519
- { value: "claude-desktop", label: CLIENT_LABELS["claude-desktop"] },
520
- { value: "cursor", label: CLIENT_LABELS.cursor },
521
- { separator: true },
522
- { value: "back", label: "Back" }
523
- ];
524
- const client = await prompter.select("Client", clients, { defaultValue: targetId ?? "auto", cancelValue: "back" });
525
- if (client === "auto") {
526
- targetId = void 0;
527
- } else if (client !== "back") {
528
- targetId = client;
938
+ endTimer = setTimeout(() => {
939
+ child.kill("SIGKILL");
940
+ child.stdout?.destroy();
941
+ child.stderr?.destroy();
942
+ child.unref();
943
+ result(null);
944
+ }, 4500);
945
+ }, 500);
946
+ };
947
+ try {
948
+ child = spawn(entry.command, entry.args, { env: { ...env, ...entry.env }, shell: false, windowsHide: true, detached: process.platform !== "win32", stdio: "pipe" });
949
+ } catch (error) {
950
+ detail = error instanceof Error ? error.message : "Unable to start process.";
951
+ result(null);
952
+ return;
953
+ }
954
+ timer = setTimeout(() => stop(`Timed out after ${timeoutMs} ms.`), timeoutMs);
955
+ const send = (message) => child.stdin?.write(JSON.stringify(message) + "\n");
956
+ child.on("error", (error) => {
957
+ detail = `Unable to start ${entry.command} (${error.code ?? "error"}).`;
958
+ closing = true;
959
+ result(null);
960
+ });
961
+ child.on("close", (code, signal) => {
962
+ if (!closing) detail ??= "Process exited before initialize and tools/list completed.";
963
+ result(code, signal);
964
+ });
965
+ child.stdin?.on("error", () => {
966
+ if (!closing) stop("Server closed its input before the startup check completed.");
967
+ });
968
+ child.stderr?.setEncoding("utf8");
969
+ child.stderr?.on("data", (chunk) => {
970
+ if (stderrOmitted) return;
971
+ stderr += chunk;
972
+ if (stderr.length > 65536) {
973
+ stderrOmitted = true;
974
+ stderr = "[stderr exceeded the diagnostic limit; output omitted]";
975
+ stop("Too much stderr output.");
976
+ }
977
+ });
978
+ child.stdout?.setEncoding("utf8");
979
+ child.stdout?.on("data", (chunk) => {
980
+ if (closing || done) return;
981
+ bytes += Buffer.byteLength(chunk);
982
+ if (bytes > 2 * 1024 * 1024) {
983
+ stop("MCP response exceeded the startup check limit.");
984
+ return;
985
+ }
986
+ output += chunk;
987
+ let newline;
988
+ while (!closing && (newline = output.indexOf("\n")) >= 0) {
989
+ const line = output.slice(0, newline);
990
+ output = output.slice(newline + 1);
991
+ try {
992
+ const message = JSON.parse(line);
993
+ if (message?.jsonrpc !== "2.0") throw new Error();
994
+ if (message.method) {
995
+ if (message.id !== void 0) send(message.method === "ping" ? { jsonrpc: "2.0", id: message.id, result: {} } : { jsonrpc: "2.0", id: message.id, error: { code: -32601, message: "Not supported by startup check" } });
996
+ continue;
997
+ }
998
+ if (message.id !== expected) continue;
999
+ if (message.error) {
1000
+ stop(`MCP request failed: ${typeof message.error.message === "string" ? message.error.message : "server error"}`);
1001
+ break;
1002
+ }
1003
+ if (expected === 1) {
1004
+ if (typeof message.result?.protocolVersion !== "string" || typeof message.result?.serverInfo?.name !== "string" || typeof message.result?.serverInfo?.version !== "string") throw new Error();
1005
+ server = message.result.serverInfo.name;
1006
+ version = message.result.serverInfo.version;
1007
+ send({ jsonrpc: "2.0", method: "notifications/initialized" });
1008
+ send({ jsonrpc: "2.0", id: ++expected, method: "tools/list", params: {} });
1009
+ } else {
1010
+ if (!Array.isArray(message.result?.tools) || !message.result.tools.every((tool) => typeof tool?.name === "string")) throw new Error();
1011
+ tools += message.result.tools.length;
1012
+ toolNames.push(...message.result.tools.map((tool) => tool.name));
1013
+ if (message.result.nextCursor !== void 0) {
1014
+ if (typeof message.result.nextCursor !== "string" || ++pages > 100) throw new Error();
1015
+ send({ jsonrpc: "2.0", id: ++expected, method: "tools/list", params: { cursor: message.result.nextCursor } });
1016
+ } else stop();
1017
+ }
1018
+ } catch {
1019
+ stop("Invalid MCP response on stdout.");
529
1020
  }
530
1021
  }
531
- } catch (error) {
532
- if (error instanceof PromptClosedError) {
533
- return 0;
1022
+ });
1023
+ child.once("spawn", () => send({ jsonrpc: "2.0", id: 1, method: "initialize", params: { protocolVersion: "2025-06-18", capabilities: {}, clientInfo: { name: "akms-startup-check", version: "1.0.0" } } }));
1024
+ });
1025
+ }
1026
+ async function checkConnections(prompter, entries, check = checkStartup) {
1027
+ if (!entries.length) return;
1028
+ prompter.write("\nStartup check (initialize and tools/list only)\n");
1029
+ const checked = /* @__PURE__ */ new Map();
1030
+ let failed = false;
1031
+ for (const { name, entry, prefixes, maskEntries } of entries) {
1032
+ const key = JSON.stringify([entry.command, entry.args, Object.entries(entry.env).sort(([a], [b]) => a.localeCompare(b))]);
1033
+ let result = checked.get(key);
1034
+ if (!result) {
1035
+ try {
1036
+ result = await check(entry);
1037
+ } catch {
1038
+ result = { ok: false, exitCode: null, detail: "Unable to complete the startup check.", stderr: [] };
534
1039
  }
535
- throw error;
536
- } finally {
537
- prompter.close();
1040
+ checked.set(key, result);
538
1041
  }
539
- if (selected === "quit") {
540
- return 0;
1042
+ const listedNames = result.toolNames ?? [];
1043
+ const longestFirst = [...prefixes ?? []].sort((a, b) => b.length - a.length);
1044
+ const covered = new Set(listedNames.map((tool) => longestFirst.find((prefix) => tool.startsWith(prefix))));
1045
+ const missing = prefixes?.filter((prefix) => !covered.has(prefix)) ?? [];
1046
+ if (result.ok && missing.length) result = { ...result, ok: false, detail: `Missing connection tools: ${missing.map((prefix) => prefix.slice(0, -2)).join(", ")}. Review catalog status before reconnecting.` };
1047
+ const safe = (value) => [entry, ...maskEntries ?? []].reduce((text, source) => redactStartupText(text, source), value);
1048
+ writeWrapped(prompter, safe(result.ok ? `OK ${name}: ${result.server}@${result.version}, ${result.tools} tools` : `FAILED ${name}: exit ${result.exitCode ?? "unavailable"}. ${result.detail ?? "Server failed to start."}`));
1049
+ if (!result.ok) {
1050
+ failed = true;
1051
+ for (const line of result.stderr) writeWrapped(prompter, safe(line), " ");
541
1052
  }
542
- defaultValue = selected;
543
- try {
544
- const code = await options.launch({ mcpId: selected, targetId, pendingInput: prompter.takePendingInput() });
545
- if (code === 130) {
546
- return 0;
547
- }
548
- if (code !== 0) {
549
- options.output.write(`
550
- Setup exited with code ${code}.
1053
+ }
1054
+ if (failed) prompter.write("Settings remain saved. Fix failed startup checks before restarting or reconnecting your AI clients.\n");
1055
+ }
551
1056
 
552
- `);
553
- }
554
- } catch (error) {
555
- options.output.write(`
556
- ${String(error)}
1057
+ // src/modules/registration/catalog.ts
1058
+ import fs3 from "fs";
1059
+ import os2 from "os";
1060
+ import path4 from "path";
1061
+ var LONGEST_TOOL_NAME_LENGTH = { db: 13, ssh: 17, wsl: 17, s3: 11 };
1062
+ var maxConnectionNameLength = (kind) => 64 - `mcp__akms_${kind}__`.length - 2 - LONGEST_TOOL_NAME_LENGTH[kind];
1063
+ var defaultCatalogPath = () => path4.join(process.env.AKMS_MCP_HOME ? path4.resolve(process.env.AKMS_MCP_HOME) : path4.join(os2.homedir(), ".akms", "mcp"), "registry.json");
557
1064
 
558
- `);
559
- }
1065
+ // src/modules/registration/registry.ts
1066
+ var sessionName = (kind) => `akms_${kind}`;
1067
+ var MIGRATION_STEPS = [
1068
+ { id: "v1_to_v2", label: "Upgrade registry version 1 to version 2", needed: (state) => state.version === 1 },
1069
+ { id: "catalog_default", label: "Record catalog mode as the default for new kinds", needed: (state) => state.defaultMode === void 0 }
1070
+ ];
1071
+ var own = (value, key) => Object.hasOwn(value, key);
1072
+ var record = (value) => value != null && typeof value === "object" && !Array.isArray(value);
1073
+ var validEntry = (value) => record(value) && typeof value.command === "string" && Array.isArray(value.args) && value.args.every((arg) => typeof arg === "string") && record(value.env) && Object.values(value.env).every((item) => typeof item === "string");
1074
+ var RegistryError = class extends Error {
1075
+ };
1076
+ function entryHash(entry) {
1077
+ return createHash("sha256").update(JSON.stringify([entry.command, entry.args, Object.entries(entry.env).sort(([a], [b]) => a.localeCompare(b))])).digest("hex");
1078
+ }
1079
+ function differingFields(entries) {
1080
+ if (entries.length < 2) return [];
1081
+ const fields = [];
1082
+ if (new Set(entries.map((entry) => entry.command)).size > 1) fields.push("command");
1083
+ if (new Set(entries.map((entry) => JSON.stringify(entry.args))).size > 1) fields.push("args");
1084
+ for (const key of new Set(entries.flatMap((entry) => Object.keys(entry.env)))) {
1085
+ if (new Set(entries.map((entry) => own(entry.env, key) ? entry.env[key] : void 0)).size > 1) fields.push(`env.${key}`);
560
1086
  }
1087
+ return fields;
561
1088
  }
562
-
563
- // src/modules/launcher/npm.ts
564
- import { execFile, spawn } from "child_process";
565
- import { existsSync } from "fs";
566
- import { mkdtemp, realpath, rm } from "fs/promises";
567
- import { tmpdir } from "os";
568
- import { basename, delimiter, dirname, join } from "path";
569
- import { promisify } from "util";
570
- var execFileAsync = promisify(execFile);
571
- async function findNpmCli() {
572
- const candidates = [];
573
- const npmExecPath = process.env.npm_execpath;
574
- if (npmExecPath != null && ["npm-cli.js", "npx-cli.js"].includes(basename(npmExecPath)) == true) {
575
- candidates.push(join(dirname(npmExecPath), "npm-cli.js"));
1089
+ function describeDifferences(entries) {
1090
+ const fields = differingFields(entries);
1091
+ if (fields.length && fields.every((field) => /^env\.(DB|SSH|WSL|S3)_DESCRIPTION$/.test(field))) return "Only the description differs; connection settings match.";
1092
+ return `Different fields: ${fields.map((field) => field.replace(/^env\./, "").replace(/[\x00-\x1f\x7f]/g, " ")).join(", ")}.`;
1093
+ }
1094
+ function entryKind(entry) {
1095
+ return launchKind(entry);
1096
+ }
1097
+ function defaultRegistryPath() {
1098
+ return defaultCatalogPath();
1099
+ }
1100
+ var Registry = class {
1101
+ constructor(targets, file = defaultRegistryPath(), runtimeOptions = {}) {
1102
+ this.targets = targets;
1103
+ this.runtimeOptions = runtimeOptions;
1104
+ this.file = file;
576
1105
  }
577
- candidates.push(join(dirname(process.execPath), "node_modules", "npm", "bin", "npm-cli.js"));
578
- for (const directory of (process.env.PATH ?? "").split(delimiter)) {
579
- if (directory === "") {
580
- continue;
1106
+ targets;
1107
+ runtimeOptions;
1108
+ file;
1109
+ observedText;
1110
+ backupCaptured = false;
1111
+ globalRuntime(kind) {
1112
+ return (this.runtimeOptions.find ?? findGlobalRuntime)(kind);
1113
+ }
1114
+ latestRuntime(kind) {
1115
+ return (this.runtimeOptions.latest ?? latestRuntimeVersion)(kind);
1116
+ }
1117
+ isSessionMode(kind, state = this.read()) {
1118
+ return state.version === 2 && (state.sessions?.[kind]?.mode ?? state.defaultMode) === "catalog";
1119
+ }
1120
+ needsMigration(state = this.read()) {
1121
+ return MIGRATION_STEPS.some((step) => step.needed(state));
1122
+ }
1123
+ planMigration() {
1124
+ const state = this.read();
1125
+ const steps = MIGRATION_STEPS.filter((step) => step.needed(state)).map(({ id, label }) => ({ id, label }));
1126
+ const kinds = steps.length ? KINDS.map((kind) => {
1127
+ const names = Object.entries(state.connections).filter(([, item]) => item.kind === kind && item.entry).map(([name]) => name);
1128
+ if (state.sessions?.[kind]?.mode === "individual") return { kind, names, status: "individual" };
1129
+ if (this.isSessionMode(kind, state)) return { kind, names, status: "catalog" };
1130
+ if (!names.length) return { kind, names, status: "empty" };
1131
+ try {
1132
+ for (const name of names) this.validateName(kind, name);
1133
+ if (!this.globalRuntime(kind)) {
1134
+ const changes = state.clients.map((client) => {
1135
+ const remove = names.filter((name) => state.connections[name].applied[client]);
1136
+ const review = [];
1137
+ try {
1138
+ const target = this.targets.find((item) => item.id === client);
1139
+ if (!target) throw new Error();
1140
+ const entries = target.readEntries();
1141
+ if (target.hasEntry?.(sessionName(kind)) ?? own(entries, sessionName(kind))) review.push(sessionName(kind));
1142
+ for (const name of remove) if ((target.hasEntry?.(name) ?? own(entries, name)) && (!entries[name] || entryHash(entries[name]) !== state.connections[name].applied[client])) review.push(name);
1143
+ } catch {
1144
+ review.push("Cannot inspect this service; Sync will retry.");
1145
+ }
1146
+ return { client, add: [sessionName(kind)], remove, review };
1147
+ });
1148
+ return { kind, names, status: "pending-runtime", changes, detail: "Install from Runtime, then run Sync. Existing entries stay until the session is verified." };
1149
+ }
1150
+ return { kind, names, status: "convert", session: this.sessionPlan(kind, "catalog", state) };
1151
+ } catch (error) {
1152
+ return { kind, names, status: "blocked", detail: error instanceof RegistryError ? error.message : "Cannot preview this kind. Check its runtime and catalog settings." };
1153
+ }
1154
+ }) : [];
1155
+ return { expected: JSON.stringify(state), steps, kinds };
1156
+ }
1157
+ applyMigration(plan) {
1158
+ if (!plan.steps.length) return this.locked(() => {
1159
+ if (JSON.stringify(this.read()) !== plan.expected) throw new RegistryError("The catalog changed since review. Reopen Migrate registry.");
1160
+ return [];
1161
+ });
1162
+ return this.change((state) => {
1163
+ if (JSON.stringify(state) !== plan.expected) throw new RegistryError("The catalog changed since review. Reopen Migrate registry.");
1164
+ if (plan.kinds.some((item) => item.status === "blocked")) throw new RegistryError("Resolve blocked kinds before migrating the registry.");
1165
+ for (const item of plan.kinds) {
1166
+ if (item.status === "pending-runtime" && this.globalRuntime(item.kind)) throw new RegistryError("Runtime availability changed since review. Reopen Migrate registry.");
1167
+ if (item.session) this.applySessionState(state, item.session, false);
1168
+ }
1169
+ for (const step of plan.steps) {
1170
+ if (step.id === "v1_to_v2") {
1171
+ state.version = 2;
1172
+ state.sessions ??= {};
1173
+ } else if (step.id === "catalog_default") state.defaultMode = "catalog";
1174
+ else throw new RegistryError("Unknown registry migration step. Reopen Migrate registry.");
1175
+ }
1176
+ }, plan.kinds.filter((item) => item.status === "convert" || item.status === "pending-runtime").map((item) => item.kind));
1177
+ }
1178
+ setDefaultMode(mode) {
1179
+ return this.change((state) => {
1180
+ if (state.version !== 2 || state.defaultMode === void 0) throw new RegistryError("Migrate registry before changing the default for new kinds.");
1181
+ if (state.defaultMode === mode) return;
1182
+ for (const kind of KINDS) if (!state.sessions?.[kind] && Object.values(state.connections).some((item) => item.kind === kind && item.entry)) {
1183
+ if (!this.globalRuntime(kind)) throw new RegistryError("Set up the pending runtimes and run Sync before changing the default for new kinds.");
1184
+ state.sessions ??= {};
1185
+ state.sessions[kind] = { mode: this.isSessionMode(kind, state) ? "catalog" : "individual", entry: this.sessionEntry(kind), applied: {} };
1186
+ }
1187
+ state.defaultMode = mode;
1188
+ }, []);
1189
+ }
1190
+ kindForName(name, state = this.read()) {
1191
+ return own(state.connections, name) ? state.connections[name].kind : KINDS.find((kind) => sessionName(kind) === name && (state.sessions?.[kind] || this.isSessionMode(kind, state)));
1192
+ }
1193
+ validateName(kind, name) {
1194
+ if (!/^[A-Za-z0-9_-]+$/.test(name) || name.length > maxConnectionNameLength(kind)) throw new RegistryError(`Connection names must use 1-${maxConnectionNameLength(kind)} ASCII letters, digits, underscores or hyphens for ${kind.toUpperCase()}. Rename this connection before saving.`);
1195
+ if (KINDS.some((value) => sessionName(value) === name)) throw new RegistryError("Names akms_db, akms_ssh, akms_wsl and akms_s3 are reserved for session servers. Choose another name.");
1196
+ }
1197
+ sessionEntry(kind) {
1198
+ if (path5.basename(this.file) !== "registry.json") throw new RegistryError("Session mode requires a catalog named registry.json in the AKMS catalog directory.");
1199
+ const runtime = this.requireRuntime(kind);
1200
+ const env = {};
1201
+ if (process.env.AKMS_MCP_HOME || path5.resolve(this.file) !== path5.resolve(defaultCatalogPath())) env.AKMS_MCP_HOME = path5.dirname(path5.resolve(this.file));
1202
+ return { command: runtime.command, args: [...runtime.args, "--catalog"], env };
1203
+ }
1204
+ planSession(kind, mode) {
1205
+ return this.sessionPlan(kind, mode, this.read());
1206
+ }
1207
+ sessionPlan(kind, mode, state) {
1208
+ const names = Object.entries(state.connections).filter(([, item]) => item.kind === kind && item.entry).map(([name]) => name);
1209
+ for (const name of names) this.validateName(kind, name);
1210
+ if (own(state.connections, sessionName(kind))) throw new RegistryError("Rename the connection using the reserved session server name before switching modes.");
1211
+ const entry = mode === "catalog" ? this.sessionEntry(kind) : state.sessions?.[kind]?.entry ?? (this.isSessionMode(kind, state) && this.globalRuntime(kind) ? this.sessionEntry(kind) : void 0);
1212
+ if (!entry) throw new RegistryError("This kind has no session server to switch back from.");
1213
+ const changes = state.clients.map((client) => {
1214
+ const target = this.targets.find((item) => item.id === client);
1215
+ const add = mode === "catalog" ? names.length ? [sessionName(kind)] : [] : names;
1216
+ const remove = mode === "catalog" ? Object.keys(state.connections).filter((name) => state.connections[name].kind === kind && state.connections[name].applied[client]) : [sessionName(kind)];
1217
+ const review = [];
1218
+ try {
1219
+ if (!target) throw new Error();
1220
+ const actual = target.readEntries();
1221
+ for (const name of [.../* @__PURE__ */ new Set([...add, ...remove])]) {
1222
+ const prior = name === sessionName(kind) ? state.sessions?.[kind]?.applied[client] : state.connections[name]?.applied[client];
1223
+ const wanted = mode === "catalog" && name === sessionName(kind) ? entry : mode === "individual" ? state.connections[name]?.entry : null;
1224
+ if ((target.hasEntry?.(name) ?? own(actual, name)) && (!actual[name] || entryHash(actual[name]) !== prior && (!wanted || entryHash(actual[name]) !== entryHash(wanted)))) review.push(name);
1225
+ }
1226
+ } catch {
1227
+ review.push("Cannot inspect this service; Sync will retry.");
1228
+ }
1229
+ return { client, add, remove, review };
1230
+ });
1231
+ return { kind, mode, entry, expected: JSON.stringify(state), names, changes, custom: names.filter((name) => !standardLaunch(state.connections[name].entry, kind)) };
1232
+ }
1233
+ applySession(plan) {
1234
+ return this.change((state) => this.applySessionState(state, plan));
1235
+ }
1236
+ applySessionState(state, plan, checkSnapshot = true) {
1237
+ if (checkSnapshot && JSON.stringify(state) !== plan.expected) throw new RegistryError("The catalog changed since review. Reopen Runtime before switching modes.");
1238
+ if (plan.mode === "catalog" && entryHash(this.sessionEntry(plan.kind)) !== entryHash(plan.entry)) throw new RegistryError("The runtime changed since review. Reopen Runtime before switching modes.");
1239
+ state.version = 2;
1240
+ state.sessions ??= {};
1241
+ state.sessions[plan.kind] = { mode: plan.mode, entry: plan.entry, applied: state.sessions[plan.kind]?.applied ?? {} };
1242
+ }
1243
+ async checkSaved(prompter, names) {
1244
+ const state = this.read();
1245
+ const entries = [];
1246
+ const sessions = /* @__PURE__ */ new Set();
1247
+ for (const name of new Set(names)) {
1248
+ const connection = state.connections[name];
1249
+ if (!connection?.entry) continue;
1250
+ if (this.isSessionMode(connection.kind, state)) sessions.add(connection.kind);
1251
+ else entries.push({ name, entry: connection.entry });
581
1252
  }
582
- if (process.platform === "win32") {
583
- candidates.push(join(directory, "node_modules", "npm", "bin", "npm-cli.js"));
584
- } else {
585
- candidates.push(join(directory, "npm"));
1253
+ for (const kind of sessions) {
1254
+ const entry = state.sessions?.[kind]?.entry;
1255
+ if (!entry) {
1256
+ prompter.write(`Startup check pending for ${sessionName(kind)}: install its runtime from Runtime, then run Sync.
1257
+ `);
1258
+ continue;
1259
+ }
1260
+ entries.push({
1261
+ name: sessionName(kind),
1262
+ entry,
1263
+ prefixes: Object.entries(state.connections).filter(([, item]) => item.kind === kind && item.entry).map(([name]) => `${name}__`),
1264
+ maskEntries: Object.values(state.connections).filter((item) => item.kind === kind && item.entry).map((item) => item.entry)
1265
+ });
586
1266
  }
1267
+ await checkConnections(prompter, entries, this.runtimeOptions.check);
587
1268
  }
588
- for (const candidate of candidates) {
589
- if (existsSync(candidate) == true) {
590
- const resolved = await realpath(candidate);
591
- return resolved;
592
- }
1269
+ async installRuntime(kind) {
1270
+ if (this.globalRuntime(kind)?.linked) throw new RegistryError("This is a linked development installation. Build or update its source checkout instead.");
1271
+ await (this.runtimeOptions.install ?? installGlobalRuntime)(kind);
1272
+ return this.requireRuntime(kind);
593
1273
  }
594
- throw new Error("npm could not be found. Install Node.js with npm, then reopen your terminal.");
595
- }
596
- async function resolveLatestVersion(options) {
597
- const metadataCache = await mkdtemp(join(tmpdir(), "akms-mcp-latest-"));
598
- try {
599
- const result = await execFileAsync(process.execPath, [
600
- options.npmCli,
601
- "view",
602
- `${options.packageName}@latest`,
603
- "version",
604
- "--json",
605
- "--prefer-online",
606
- "--prefer-offline=false",
607
- "--offline=false",
608
- "--fetch-retries=0",
609
- "--fetch-timeout=15000",
610
- "--update-notifier=false",
611
- "--cache",
612
- metadataCache
613
- ], { signal: options.signal, timeout: 2e4, windowsHide: true });
614
- const version = JSON.parse(result.stdout);
615
- if (typeof version !== "string" || /^\d+\.\d+\.\d+(?:-[0-9A-Za-z.-]+)?(?:\+[0-9A-Za-z.-]+)?$/.test(version) == false) {
616
- throw new Error("npm returned an invalid latest version.");
1274
+ requireRuntime(kind) {
1275
+ const runtime = this.globalRuntime(kind);
1276
+ if (!runtime) throw new RegistryError(`Install the AKMS runtime first: ${runtimeInstallCommand(kind)}. Then reopen Runtime.`);
1277
+ return runtime;
1278
+ }
1279
+ runtimeSnapshot(state, kind) {
1280
+ return JSON.stringify([state.clients, Object.entries(state.connections).filter(([, item]) => item.kind === kind).map(([name, item]) => [name, item.entry ? entryHash(item.entry) : null]).sort(([a], [b]) => String(a).localeCompare(String(b)))]);
1281
+ }
1282
+ planRuntime(kind) {
1283
+ const state = this.read();
1284
+ const runtime = this.requireRuntime(kind);
1285
+ const names = [], skipped = [];
1286
+ for (const [name, item] of Object.entries(state.connections)) {
1287
+ if (item.kind !== kind || !item.entry) continue;
1288
+ if (!standardLaunch(item.entry, kind)) {
1289
+ skipped.push(name);
1290
+ continue;
1291
+ }
1292
+ if (item.entry.command !== runtime.command || JSON.stringify(item.entry.args) !== JSON.stringify(runtime.args)) names.push(name);
617
1293
  }
618
- return version;
619
- } finally {
620
- await rm(metadataCache, { recursive: true, force: true });
1294
+ return { kind, runtime, names, skipped, expected: this.runtimeSnapshot(state, kind) };
621
1295
  }
622
- }
623
- async function launchMcp(options) {
624
- const selected = MCP_PACKAGES[options.mcpId];
625
- if (options.mcpId === "wsl" && process.platform !== "win32") {
626
- throw new Error("WSL setup must run from Windows.");
1296
+ applyRuntime(plan) {
1297
+ return this.change((state) => {
1298
+ if (this.runtimeSnapshot(state, plan.kind) !== plan.expected) throw new RegistryError("Connections or registration changed since review. Reopen Runtime before applying.");
1299
+ const runtime = this.requireRuntime(plan.kind);
1300
+ if (JSON.stringify(runtime) !== JSON.stringify(plan.runtime)) throw new RegistryError("The global installation changed since review. Reopen Runtime before applying.");
1301
+ for (const name of plan.names) {
1302
+ const item = state.connections[name];
1303
+ if (!item?.entry || item.kind !== plan.kind || !standardLaunch(item.entry, plan.kind)) throw new RegistryError("This connection has a custom launch command. Review it individually.");
1304
+ item.entry = { ...item.entry, command: runtime.command, args: [...runtime.args] };
1305
+ }
1306
+ });
627
1307
  }
628
- const npmCli = await findNpmCli();
1308
+ read() {
1309
+ let text;
1310
+ try {
1311
+ text = fs4.readFileSync(this.file, "utf8");
1312
+ this.observedText = text;
1313
+ } catch (error) {
1314
+ if (error.code === "ENOENT") {
1315
+ this.observedText = null;
1316
+ return { version: 2, defaultMode: "catalog", clients: [], connections: {}, sessions: {} };
1317
+ }
1318
+ throw new RegistryError("Cannot read the AKMS registry. Check its permissions.");
1319
+ }
1320
+ let value;
1321
+ try {
1322
+ value = JSON.parse(text);
1323
+ } catch {
1324
+ throw new RegistryError("Invalid AKMS registry JSON; the file was not changed.");
1325
+ }
1326
+ if (!record(value) || ![1, 2].includes(value.version)) throw new RegistryError("Unsupported AKMS registry version; update this MCP package before changing it.");
1327
+ if (!Array.isArray(value.clients) || !value.clients.every((id) => typeof id === "string" && own(CLIENT_LABELS2, id)) || new Set(value.clients).size !== value.clients.length || !record(value.connections)) throw new RegistryError("Invalid AKMS registry; the file was not changed.");
1328
+ for (const [name, connection] of Object.entries(value.connections)) {
1329
+ if (!name.trim() || !record(connection) || !KINDS.includes(connection.kind) || !(connection.entry === null || validEntry(connection.entry)) || !record(connection.applied) || Object.entries(connection.applied).some(([id, hash]) => !own(CLIENT_LABELS2, id) || typeof hash !== "string" || !/^[a-f0-9]{64}$/.test(hash)) || connection.replacement != null && typeof connection.replacement !== "string") throw new RegistryError("Invalid AKMS registry entry; the file was not changed.");
1330
+ }
1331
+ if (value.version === 2) {
1332
+ if (!record(value.sessions) || value.defaultMode !== void 0 && !["catalog", "individual"].includes(value.defaultMode)) throw new RegistryError("Invalid session server state; the file was not changed.");
1333
+ for (const [kind, session] of Object.entries(value.sessions)) {
1334
+ if (!KINDS.includes(kind) || !record(session) || !["catalog", "individual"].includes(session.mode) || !validEntry(session.entry) || !catalogLaunch(session.entry, kind) || Object.keys(session.entry.env).some((key) => key !== "AKMS_MCP_HOME") || !record(session.applied) || Object.entries(session.applied).some(([id, hash]) => !own(CLIENT_LABELS2, id) || typeof hash !== "string" || !/^[a-f0-9]{64}$/.test(hash))) throw new RegistryError("Invalid session server state; the file was not changed.");
1335
+ if (own(value.connections, sessionName(kind))) throw new RegistryError("A connection uses a reserved session server name; the file was not changed.");
1336
+ }
1337
+ } else if (value.sessions !== void 0 || value.defaultMode !== void 0) throw new RegistryError("Session server state requires registry version 2; the file was not changed.");
1338
+ return value;
1339
+ }
1340
+ save(state) {
1341
+ const after = JSON.stringify(state, null, 2) + "\n";
1342
+ updateConfigFile(this.file, (before) => {
1343
+ if (this.observedText !== void 0 && before !== (this.observedText ?? "")) throw new RegistryError("The AKMS catalog changed outside this command. Reload it and retry Sync.");
1344
+ return after;
1345
+ }, { beforeCommit: (before) => {
1346
+ if (before !== null && !this.backupCaptured) {
1347
+ updateConfigFile(path5.join(path5.dirname(this.file), "registry.previous.json"), () => before, { mode: fs4.statSync(this.file).mode & 511 });
1348
+ this.backupCaptured = true;
1349
+ }
1350
+ } });
1351
+ this.observedText = after;
1352
+ }
1353
+ /** Serializes standalone editors across packages, including their config-file writes. */
1354
+ locked(action) {
1355
+ fs4.mkdirSync(path5.dirname(this.file), { recursive: true, mode: 448 });
1356
+ const lock = this.file + ".lock";
1357
+ let descriptor;
1358
+ try {
1359
+ descriptor = fs4.openSync(lock, "wx", 384);
1360
+ } catch (error) {
1361
+ if (error.code !== "EEXIST") throw error;
1362
+ let abandoned = false;
1363
+ try {
1364
+ const pid = Number(fs4.readFileSync(lock, "utf8"));
1365
+ if (Number.isSafeInteger(pid) && pid > 0) {
1366
+ try {
1367
+ process.kill(pid, 0);
1368
+ } catch (probe) {
1369
+ abandoned = probe.code === "ESRCH";
1370
+ }
1371
+ }
1372
+ } catch {
1373
+ }
1374
+ if (!abandoned) throw new RegistryError("Another AKMS configuration command is running. Finish it and retry.");
1375
+ fs4.unlinkSync(lock);
1376
+ descriptor = fs4.openSync(lock, "wx", 384);
1377
+ }
1378
+ this.backupCaptured = false;
1379
+ try {
1380
+ fs4.writeFileSync(descriptor, String(process.pid));
1381
+ return action();
1382
+ } finally {
1383
+ this.backupCaptured = false;
1384
+ fs4.closeSync(descriptor);
1385
+ fs4.unlinkSync(lock);
1386
+ }
1387
+ }
1388
+ change(action, kinds) {
1389
+ return this.locked(() => {
1390
+ const state = this.read();
1391
+ action(state);
1392
+ for (const kind of KINDS) {
1393
+ if (this.isSessionMode(kind, state) && !state.sessions?.[kind] && Object.values(state.connections).some((item) => item.kind === kind && item.entry) && this.globalRuntime(kind)) {
1394
+ state.sessions ??= {};
1395
+ state.sessions[kind] = { mode: "catalog", entry: this.sessionEntry(kind), applied: {} };
1396
+ }
1397
+ }
1398
+ this.save(state);
1399
+ return this.reconcile(state, true, kinds);
1400
+ });
1401
+ }
1402
+ register(clients, expected, imports = []) {
1403
+ if (clients.some((id) => !own(CLIENT_LABELS2, id))) throw new RegistryError("Unknown AI service.");
1404
+ return this.change((state) => {
1405
+ if (expected && JSON.stringify(state.clients) !== JSON.stringify(expected)) throw new RegistryError("Registration changed in another command. Reopen Register before applying.");
1406
+ const grouped = /* @__PURE__ */ new Map();
1407
+ for (const candidate of imports) grouped.set(candidate.name, [...grouped.get(candidate.name) ?? [], candidate]);
1408
+ for (const [name, candidates] of grouped) {
1409
+ if (own(state.connections, name)) throw new RegistryError("The shared catalog changed since review. Reopen Register or Import before applying.");
1410
+ const chosen = candidates[0];
1411
+ this.validateName(chosen.kind, name);
1412
+ const applied = {};
1413
+ for (const candidate of candidates) {
1414
+ if (!clients.includes(candidate.client) || candidate.kind !== chosen.kind || entryKind(candidate.entry) !== chosen.kind || entryHash(candidate.entry) !== entryHash(chosen.entry)) throw new RegistryError("Different existing settings need an explicit choice before import.");
1415
+ const actual = this.targets.find((target) => target.id === candidate.client)?.readEntries()[name];
1416
+ if (!actual || entryHash(actual) !== entryHash(candidate.entry)) throw new RegistryError("Existing settings changed since review. Reopen Register or Import before applying.");
1417
+ applied[candidate.client] = entryHash(actual);
1418
+ }
1419
+ Object.defineProperty(state.connections, name, { value: { kind: chosen.kind, entry: chosen.entry, applied }, enumerable: true, configurable: true, writable: true });
1420
+ }
1421
+ state.clients = [...new Set(clients)];
1422
+ });
1423
+ }
1424
+ /** Discover without writing. Only unambiguous, previously unmanaged names are batched. */
1425
+ planRegistration(clients, kind) {
1426
+ const state = this.read();
1427
+ const { entries, errors } = this.candidates(kind, clients);
1428
+ const groups = /* @__PURE__ */ new Map();
1429
+ for (const entry of entries) groups.set(entry.name, [...groups.get(entry.name) ?? [], entry]);
1430
+ const imports = [];
1431
+ const conflicts = [];
1432
+ for (const [name, candidates] of groups) {
1433
+ if (own(state.connections, name)) continue;
1434
+ try {
1435
+ this.validateName(candidates[0].kind, name);
1436
+ } catch {
1437
+ conflicts.push(name);
1438
+ continue;
1439
+ }
1440
+ if (new Set(candidates.map((candidate) => entryHash(candidate.entry))).size === 1) imports.push(...candidates);
1441
+ else conflicts.push(name);
1442
+ }
1443
+ return { imports, conflicts, errors };
1444
+ }
1445
+ write(kind, name, entry, oldName, expected) {
1446
+ this.validateName(kind, name);
1447
+ if (!validEntry(entry) || entryKind(entry) !== kind || catalogLaunch(entry)) throw new RegistryError("Invalid connection name or MCP entry.");
1448
+ return this.change((state) => {
1449
+ if (!state.clients.length) throw new RegistryError("Register at least one AI service before saving a connection.");
1450
+ const observedName = oldName ?? name;
1451
+ const observed = own(state.connections, observedName) ? state.connections[observedName].entry : null;
1452
+ if (expected !== void 0 && (observed ? entryHash(observed) : null) !== expected) throw new RegistryError("This connection changed while the form was open. Reload it before saving.");
1453
+ const previous = own(state.connections, name) ? state.connections[name] : void 0;
1454
+ if (previous && previous.kind !== kind) throw new RegistryError(`The name ${name} is already used by another MCP. Choose another name.`);
1455
+ if (oldName && oldName !== name && previous) throw new RegistryError(`The name ${name} already exists. Choose another name.`);
1456
+ if (oldName && oldName !== name) {
1457
+ const old = own(state.connections, oldName) ? state.connections[oldName] : void 0;
1458
+ if (!old || old.kind !== kind || !old.entry) throw new RegistryError("The connection changed; reload before renaming it.");
1459
+ old.entry = null;
1460
+ old.replacement = name;
1461
+ }
1462
+ Object.defineProperty(state.connections, name, { value: { kind, entry, applied: previous?.applied ?? {} }, enumerable: true, configurable: true, writable: true });
1463
+ });
1464
+ }
1465
+ remove(kind, name, expected) {
1466
+ return this.change((state) => {
1467
+ const observed = own(state.connections, name) ? state.connections[name].entry : null;
1468
+ if (expected !== void 0 && (observed ? entryHash(observed) : null) !== expected) throw new RegistryError("This connection changed since it was displayed. Reload it before removing it.");
1469
+ if (!own(state.connections, name)) return;
1470
+ if (state.connections[name].kind !== kind) throw new RegistryError("This connection belongs to another MCP.");
1471
+ state.connections[name].entry = null;
1472
+ });
1473
+ }
1474
+ sync() {
1475
+ return this.change(() => {
1476
+ });
1477
+ }
1478
+ inspect() {
1479
+ return this.reconcile(this.read(), false);
1480
+ }
1481
+ reconcile(state, apply, kinds) {
1482
+ const results = [];
1483
+ const active = (kind) => Object.entries(state.connections).filter(([, item]) => item.kind === kind && item.entry);
1484
+ for (const kind of kinds ?? KINDS) if (this.isSessionMode(kind, state) && active(kind).length && !state.sessions?.[kind]) {
1485
+ for (const client of state.clients) results.push({ client, name: sessionName(kind), status: "pending", detail: "Install this kind's runtime from Runtime, then run Sync. Existing client entries are kept." });
1486
+ }
1487
+ const work = [
1488
+ ...Object.entries(state.connections).filter(([, connection]) => !kinds || kinds.includes(connection.kind)).map(([name, connection]) => ({ name, connection, bundle: false })),
1489
+ ...(kinds ?? KINDS).flatMap((kind) => state.sessions?.[kind] ? [{ name: sessionName(kind), bundle: true, connection: { kind, entry: state.sessions[kind].entry, applied: state.sessions[kind].applied } }] : [])
1490
+ ];
1491
+ const wanted = (item) => item.bundle ? this.isSessionMode(item.connection.kind, state) && active(item.connection.kind).length ? item.connection.entry : null : this.isSessionMode(item.connection.kind, state) ? null : item.connection.entry;
1492
+ work.sort((a, b) => (wanted(a) ? 0 : a.bundle ? 2 : 1) - (wanted(b) ? 0 : b.bundle ? 2 : 1));
1493
+ for (const item of work) {
1494
+ const { name, connection, bundle } = item;
1495
+ const clients = /* @__PURE__ */ new Set([...state.clients, ...Object.keys(connection.applied)]);
1496
+ for (const client of clients) {
1497
+ const desired = state.clients.includes(client) ? wanted(item) : null;
1498
+ const previous = connection.applied[client];
1499
+ if (!desired && !previous) continue;
1500
+ const target = this.targets.find((candidate) => candidate.id === client);
1501
+ const report = (status, detail) => results.push({ client, name, status, detail });
1502
+ if (!target) {
1503
+ report("error", "Service adapter unavailable; use a newer package to retry.");
1504
+ continue;
1505
+ }
1506
+ try {
1507
+ const entries = target.readEntries();
1508
+ const actual = own(entries, name) ? entries[name] : void 0;
1509
+ const exists = target.hasEntry?.(name) ?? actual != null;
1510
+ const hash = actual ? entryHash(actual) : void 0;
1511
+ if (desired && hash === entryHash(desired)) {
1512
+ if (apply) connection.applied[client] = hash;
1513
+ report("synced", "Up to date");
1514
+ } else if (!desired && !exists) {
1515
+ if (apply) delete connection.applied[client];
1516
+ report("removed", "Removed");
1517
+ } else if (exists && (!actual || !previous || hash !== previous)) {
1518
+ report("conflict", actual && desired ? describeDifferences([desired, actual]) : actual ? "Changed outside AKMS; review before removing this entry." : "This name is used by another server type; choose another name.");
1519
+ } else if (!apply) {
1520
+ report("pending", desired ? "Needs applying" : "Needs removal");
1521
+ } else {
1522
+ if (!desired && state.clients.includes(client)) {
1523
+ if (!bundle && this.isSessionMode(connection.kind, state) && active(connection.kind).length) {
1524
+ const session = state.sessions?.[connection.kind];
1525
+ const replacement2 = entries[sessionName(connection.kind)];
1526
+ if (!session || !replacement2 || entryHash(replacement2) !== entryHash(session.entry) || session.applied[client] !== entryHash(session.entry)) {
1527
+ report("pending", "Connection entry kept until the session server is applied.");
1528
+ continue;
1529
+ }
1530
+ }
1531
+ if (bundle && !this.isSessionMode(connection.kind, state) && active(connection.kind).some(([connectionName, value]) => !entries[connectionName] || entryHash(entries[connectionName]) !== entryHash(value.entry))) {
1532
+ report("pending", "Session entry kept until all connection entries are applied.");
1533
+ continue;
1534
+ }
1535
+ }
1536
+ let replacement = connection.replacement && state.connections[connection.replacement];
1537
+ const visited = /* @__PURE__ */ new Set();
1538
+ while (replacement && !replacement.entry && replacement.replacement && !visited.has(replacement.replacement)) {
1539
+ visited.add(replacement.replacement);
1540
+ replacement = state.connections[replacement.replacement];
1541
+ }
1542
+ if (!desired && state.clients.includes(client) && !this.isSessionMode(connection.kind, state) && replacement && replacement.entry && replacement.applied[client] !== entryHash(replacement.entry)) {
1543
+ report("pending", "Old name kept until the new name is applied.");
1544
+ continue;
1545
+ }
1546
+ const result = desired ? target.writeEntry(name, desired) : target.removeEntry(name);
1547
+ if (result.status !== "written") {
1548
+ report("error", "Could not write this service's settings. Check its CLI, syntax and permissions.");
1549
+ continue;
1550
+ }
1551
+ const after = target.readEntries()[name];
1552
+ if (desired ? !after || entryHash(after) !== entryHash(desired) : target.hasEntry?.(name) ?? after != null) {
1553
+ report("error", "Settings could not be verified; retry Sync.");
1554
+ continue;
1555
+ }
1556
+ if (desired) connection.applied[client] = entryHash(desired);
1557
+ else delete connection.applied[client];
1558
+ report(desired ? "synced" : "removed", desired ? "Applied" : "Removed");
1559
+ }
1560
+ } catch {
1561
+ report("error", "Cannot read or update settings. Check the configuration syntax, CLI and file permissions.");
1562
+ }
1563
+ if (apply) this.save(state);
1564
+ }
1565
+ if (apply && !bundle && !connection.entry && Object.keys(connection.applied).length === 0) delete state.connections[name];
1566
+ }
1567
+ if (apply) this.save(state);
1568
+ return results;
1569
+ }
1570
+ candidates(kind, clients = this.read().clients) {
1571
+ const entries = [];
1572
+ const errors = [];
1573
+ for (const client of clients) {
1574
+ const target = this.targets.find((item) => item.id === client);
1575
+ try {
1576
+ if (!target) throw new RegistryError();
1577
+ for (const [name, entry] of Object.entries(target.readEntries())) {
1578
+ const found = entryKind(entry);
1579
+ if (found && !catalogLaunch(entry) && (!kind || found === kind)) entries.push({ client, name, kind: found, entry });
1580
+ }
1581
+ } catch {
1582
+ errors.push(`${CLIENT_LABELS2[client]}: cannot inspect existing entries.`);
1583
+ }
1584
+ }
1585
+ return { entries, errors };
1586
+ }
1587
+ importEntry(name, chosen, reviewed) {
1588
+ this.validateName(chosen.kind, name);
1589
+ if (catalogLaunch(chosen.entry)) throw new RegistryError("Session server entries are not individual connections.");
1590
+ return this.change((state) => {
1591
+ if (!name.trim()) throw new RegistryError("A connection name is required.");
1592
+ const previous = own(state.connections, name) ? state.connections[name] : void 0;
1593
+ if (name !== chosen.name && previous) throw new RegistryError("That shared name already exists. Choose a new name to keep both connections.");
1594
+ if (previous && previous.kind !== chosen.kind) throw new RegistryError("This name belongs to another MCP. Choose another name.");
1595
+ const applied = { ...previous?.applied };
1596
+ for (const candidate of reviewed) {
1597
+ const target = this.targets.find((item) => item.id === candidate.client);
1598
+ const actual = target?.readEntries()[candidate.name];
1599
+ if (!actual || entryHash(actual) !== entryHash(candidate.entry)) throw new RegistryError("Settings changed since review; import again.");
1600
+ if (candidate.name === name && candidate.kind === chosen.kind) applied[candidate.client] = entryHash(actual);
1601
+ }
1602
+ Object.defineProperty(state.connections, name, { value: { kind: chosen.kind, entry: chosen.entry, applied }, enumerable: true, configurable: true, writable: true });
1603
+ });
1604
+ }
1605
+ resolve(client, name, expected, useClient) {
1606
+ return this.change((state) => {
1607
+ const sessionKind = KINDS.find((kind) => sessionName(kind) === name && state.sessions?.[kind]);
1608
+ if (sessionKind) {
1609
+ const actual2 = this.targets.find((item) => item.id === client)?.readEntries()[name];
1610
+ if (!actual2 || entryHash(actual2) !== entryHash(expected)) throw new RegistryError("Settings changed since review; run Sync again.");
1611
+ if (useClient || entryKind(actual2) !== sessionKind) throw new RegistryError("Session launches can only be restored from AKMS. Keep external settings or rename the conflicting entry manually.");
1612
+ state.sessions[sessionKind].applied[client] = entryHash(actual2);
1613
+ return;
1614
+ }
1615
+ const connection = own(state.connections, name) ? state.connections[name] : void 0;
1616
+ const target = this.targets.find((item) => item.id === client);
1617
+ const actual = target?.readEntries()[name];
1618
+ if (!connection || !actual || entryHash(actual) !== entryHash(expected)) throw new RegistryError("Settings changed since review; run Sync again.");
1619
+ if (entryKind(actual) !== connection.kind) throw new RegistryError("This entry runs a different server. Rename it manually before retrying.");
1620
+ connection.applied[client] = entryHash(actual);
1621
+ if (useClient) {
1622
+ if (!state.clients.includes(client)) throw new RegistryError("Register this service before importing its changes.");
1623
+ this.validateName(connection.kind, name);
1624
+ if (catalogLaunch(actual)) throw new RegistryError("A session server cannot be imported as an individual connection.");
1625
+ connection.entry = actual;
1626
+ delete connection.replacement;
1627
+ }
1628
+ });
1629
+ }
1630
+ target(kind) {
1631
+ const registry = this;
1632
+ let snapshot;
1633
+ const expected = (name) => snapshot ? own(snapshot, name) ? entryHash(snapshot[name]) : null : void 0;
1634
+ const saved = (results) => ({ status: "written", detail: formatResults(results, "Saved in the shared AKMS catalog.") });
1635
+ const prepare = (name, entry, oldName) => {
1636
+ if (!registry.read().clients.length) throw new RegistryError("Register at least one AI service before saving a connection.");
1637
+ const prior = snapshot && own(snapshot, oldName ?? name) ? snapshot[oldName ?? name] : void 0;
1638
+ if (prior && !standardLaunch(prior, kind)) return { ...entry, command: prior.command, args: [...prior.args] };
1639
+ const runtime = registry.requireRuntime(kind);
1640
+ return { ...entry, command: runtime.command, args: [...runtime.args] };
1641
+ };
1642
+ return {
1643
+ id: "registered",
1644
+ detected: true,
1645
+ get label() {
1646
+ const clients = registry.read().clients;
1647
+ return clients.length ? clients.map((id) => CLIENT_LABELS2[id]).join(" \xB7 ") : "No services registered";
1648
+ },
1649
+ location: this.file,
1650
+ readEntries: () => {
1651
+ const entries = Object.fromEntries(Object.entries(registry.read().connections).filter(([, value]) => value.kind === kind && value.entry).map(([name, value]) => [name, value.entry]));
1652
+ snapshot = structuredClone(entries);
1653
+ return entries;
1654
+ },
1655
+ hasEntry: (name) => own(registry.read().connections, name),
1656
+ prepareEntry: prepare,
1657
+ writeEntry: (name, entry) => saved(registry.write(kind, name, prepare(name, entry), void 0, expected(name))),
1658
+ renameEntry: (oldName, name, entry) => saved(registry.write(kind, name, prepare(name, entry, oldName), oldName, expected(oldName))),
1659
+ removeEntry: (name) => saved(registry.remove(kind, name, expected(name)))
1660
+ };
1661
+ }
1662
+ };
1663
+ function formatResults(results, prefix = "") {
1664
+ const lines = prefix ? [prefix] : [];
1665
+ if (!results.length) return [...lines, " No shared connections need applying."].join("\n");
1666
+ const attention = results.filter((item) => !["synced", "removed"].includes(item.status));
1667
+ const total = new Set(results.map((item) => item.name)).size;
1668
+ const unresolved = new Set(attention.map((item) => item.name)).size;
1669
+ lines.push(` Connections: ${total - unresolved} complete \xB7 ${unresolved} need attention`);
1670
+ const updated = results.filter((item) => item.status === "synced" && item.detail === "Applied").length;
1671
+ const unchanged = results.filter((item) => item.status === "synced" && item.detail !== "Applied").length;
1672
+ const removed = results.filter((item) => item.status === "removed").length;
1673
+ lines.push(` Service entries: ${updated} updated \xB7 ${unchanged} already up to date${removed ? ` \xB7 ${removed} removed` : ""}`);
1674
+ for (const [heading, rows] of [
1675
+ ["Needs review (existing settings kept)", attention.filter((item) => item.status === "conflict")],
1676
+ ["Could not finish", attention.filter((item) => item.status !== "conflict")]
1677
+ ]) {
1678
+ if (!rows.length) continue;
1679
+ lines.push(`
1680
+ ${heading}:`);
1681
+ for (const name of new Set(rows.map((item) => item.name))) {
1682
+ lines.push(` ${name.replace(/[\x00-\x1f\x7f]/g, " ")}`);
1683
+ for (const row of rows.filter((item) => item.name === name)) lines.push(` ${CLIENT_LABELS2[row.client]}: ${row.detail}`);
1684
+ }
1685
+ }
1686
+ if (attention.length) lines.push("\nNext: open Sync to compare settings or retry unfinished changes.");
1687
+ return lines.join("\n");
1688
+ }
1689
+
1690
+ // src/modules/registration/runtime-menu.ts
1691
+ async function runtimeMenu(prompter, registry, requestedKind) {
1692
+ let kind = requestedKind;
1693
+ for (; ; ) {
1694
+ prompter.clearScreen();
1695
+ if (!kind) {
1696
+ const selected = await prompter.select("Runtime / choose MCP", [
1697
+ ...KINDS.map((value) => ({ value, label: packageName(value) })),
1698
+ { value: "back", key: "b", label: "Back" }
1699
+ ], { cancelValue: "back" });
1700
+ if (selected === "back") return;
1701
+ kind = selected;
1702
+ }
1703
+ try {
1704
+ const installed = registry.globalRuntime(kind);
1705
+ const sessionMode = registry.isSessionMode(kind);
1706
+ writeWrapped(prompter, `Runtime / ${packageName(kind)}`);
1707
+ writeWrapped(prompter, installed ? `Installed: ${installed.version}${installed.linked ? " (linked development checkout)" : ""}` : "Not installed in the AKMS runtime directory.");
1708
+ writeWrapped(prompter, `Runtime directory: ${runtimePrefix()}`);
1709
+ prompter.write("Connections start Node directly, without a resident npm/npx process.\n");
1710
+ prompter.write(`Configuration: ${sessionMode ? "one session server for this kind" : "one entry per connection"}.
1711
+ `);
1712
+ if (installed) writeWrapped(prompter, installed.args[0]);
1713
+ const picked = await prompter.select("AKMS runtime", [
1714
+ ...installed && !sessionMode ? [{ value: "apply", label: "Use for existing connections", hint: "convert npx and old runtime paths together" }] : [],
1715
+ ...installed ? [{ value: "catalog", key: "catalog", label: sessionMode ? "Reapply session launch command" : "Switch to session server", hint: `one ${sessionName(kind)} entry per registered service` }] : [],
1716
+ ...sessionMode && (installed || registry.read().sessions?.[kind]) ? [{ value: "individual", key: "individual", label: "Switch back to per-connection entries", hint: "restore connection settings from the catalog" }] : [],
1717
+ ...!installed?.linked ? [{ value: "install", label: installed ? "Update runtime installation" : "Install AKMS runtime", hint: runtimeInstallCommand(kind) }] : [],
1718
+ ...!registry.needsMigration() ? [{ value: "default", key: "default", label: registry.read().defaultMode === "catalog" ? "Use individual entries for new kinds" : "Start new kinds as session servers", hint: "keep existing kinds and rollback choices" }] : [],
1719
+ { value: "back", key: "b", label: "Back" }
1720
+ ], { cancelValue: "back" });
1721
+ if (picked === "back") {
1722
+ if (requestedKind) return;
1723
+ kind = void 0;
1724
+ continue;
1725
+ }
1726
+ if (picked === "default") {
1727
+ const mode = registry.read().defaultMode === "catalog" ? "individual" : "catalog";
1728
+ prompter.write(`New kinds will use ${mode === "catalog" ? "session servers" : "individual entries"}. Existing kinds keep their settings.
1729
+ `);
1730
+ if (await prompter.askYesNo("Change the default for new kinds?", false)) {
1731
+ prompter.write(`${formatResults(registry.setDefaultMode(mode), "Default for new kinds saved.")}
1732
+ `);
1733
+ await prompter.acknowledge();
1734
+ }
1735
+ continue;
1736
+ }
1737
+ if (picked === "catalog" || picked === "individual") {
1738
+ const plan2 = registry.planSession(kind, picked);
1739
+ prompter.clearScreen();
1740
+ writeWrapped(prompter, picked === "catalog" ? `Switch to session server: ${sessionName(kind)}` : "Switch back to per-connection entries");
1741
+ prompter.write("Registry version 2 prevents older AKMS setup packages from changing these settings.\nThe previous catalog revision is kept as registry.previous.json.\n\n");
1742
+ for (const change of plan2.changes) {
1743
+ writeWrapped(prompter, CLIENT_LABELS2[change.client]);
1744
+ writeWrapped(prompter, `Apply: ${change.add.join(", ") || "none"}`, " ");
1745
+ writeWrapped(prompter, `Remove after replacements are applied: ${change.remove.join(", ") || "none"}`, " ");
1746
+ if (change.review.length) writeWrapped(prompter, `Keep for Sync review: ${change.review.join(", ")}`, " ");
1747
+ }
1748
+ if (picked === "catalog") {
1749
+ prompter.write("\nPasswords stay in the catalog and its previous-revision copy. Session entries contain no connection credentials.\n");
1750
+ writeWrapped(prompter, `Command: ${plan2.entry.command}`);
1751
+ writeWrapped(prompter, `Runtime: ${plan2.entry.args[0]} --catalog`);
1752
+ if (plan2.custom.length) {
1753
+ prompter.write("Custom commands will not run in catalog mode; they are retained for switching back:\n");
1754
+ for (const name of plan2.custom) writeWrapped(prompter, plainText(name), " ");
1755
+ }
1756
+ } else prompter.write("\nConnection credentials will be copied back into registered client settings. The registry stays version 2.\n");
1757
+ if (!await prompter.askYesNo("Apply this configuration mode?", false)) continue;
1758
+ const results2 = registry.applySession(plan2);
1759
+ prompter.clearScreen();
1760
+ prompter.write(`${formatResults(results2, "Configuration mode saved.")}
1761
+ `);
1762
+ await registry.checkSaved(prompter, plan2.names);
1763
+ prompter.write("Restart or reconnect your AI clients to load the new server layout. Fix startup failures first.\n");
1764
+ await prompter.acknowledge();
1765
+ continue;
1766
+ }
1767
+ if (picked === "install") {
1768
+ prompter.clearScreen();
1769
+ writeWrapped(prompter, `Installing ${packageName(kind)}@latest in ${runtimePrefix()}...`);
1770
+ const runtime = await registry.installRuntime(kind);
1771
+ prompter.write(`Installed ${runtime.version}. Reconnect your AI clients to load the update.
1772
+ `);
1773
+ await prompter.acknowledge();
1774
+ continue;
1775
+ }
1776
+ const plan = registry.planRuntime(kind);
1777
+ prompter.clearScreen();
1778
+ writeWrapped(prompter, `Use AKMS runtime ${packageName(kind)} ${plan.runtime.version}`);
1779
+ prompter.write(`
1780
+ Convert ${plan.names.length} connection(s):
1781
+ `);
1782
+ for (const name of plan.names) writeWrapped(prompter, plainText(name), " ");
1783
+ if (plan.skipped.length) {
1784
+ prompter.write("Custom commands kept for individual review:\n");
1785
+ for (const name of plan.skipped) writeWrapped(prompter, plainText(name), " ");
1786
+ }
1787
+ prompter.write("\nConnection values and credentials stay the same.\nVersion-pinned npx entries will use the installed version shown above.\n");
1788
+ if (!plan.names.length) {
1789
+ prompter.write("No saved launch commands need converting.\nIf a legacy client entry is missing here, import it from Register or Import existing connections first.\n");
1790
+ await prompter.acknowledge();
1791
+ continue;
1792
+ }
1793
+ if (!await prompter.askYesNo("Apply these launch commands to registered services?", true)) continue;
1794
+ const results = registry.applyRuntime(plan);
1795
+ prompter.clearScreen();
1796
+ prompter.write(`${formatResults(results, "Runtime settings saved.")}
1797
+
1798
+ Restart or reconnect AI clients to replace running npx processes.
1799
+ `);
1800
+ await registry.checkSaved(prompter, plan.names);
1801
+ await prompter.acknowledge();
1802
+ } catch (error) {
1803
+ if (error instanceof PromptClosedError) throw error;
1804
+ if (error instanceof PromptBackError) continue;
1805
+ prompter.write(`${error instanceof Error ? error.message : "Could not configure the AKMS runtime."}
1806
+ `);
1807
+ await prompter.acknowledge();
1808
+ }
1809
+ }
1810
+ }
1811
+
1812
+ // src/modules/registration/updates-menu.ts
1813
+ async function updatesMenu(prompter, registry) {
1814
+ const kinds = KINDS.filter((kind) => kind !== "wsl" || process.platform === "win32");
1815
+ const latest = /* @__PURE__ */ new Map();
1816
+ const refresh = async () => {
1817
+ prompter.clearScreen();
1818
+ prompter.write("Updates / checking published runtime versions...\n");
1819
+ await Promise.all(kinds.map(async (kind) => {
1820
+ try {
1821
+ latest.set(kind, await registry.latestRuntime(kind));
1822
+ } catch {
1823
+ latest.set(kind, null);
1824
+ }
1825
+ }));
1826
+ };
1827
+ await refresh();
1828
+ for (; ; ) {
1829
+ try {
1830
+ prompter.clearScreen();
1831
+ prompter.write("Updates / AKMS runtimes\n\n");
1832
+ const rows = kinds.map((kind) => {
1833
+ const installed = registry.globalRuntime(kind), published = latest.get(kind);
1834
+ const update = !!installed && !installed.linked && !!published && runtimeIsOlder(installed.version, published);
1835
+ const status = installed?.linked ? "Development link" : !published ? "Unavailable" : !installed ? "Not installed" : update ? "Update available" : "Up to date";
1836
+ writeWrapped(prompter, `${packageName(kind)} | installed: ${installed?.version ?? "not installed"} | latest: ${published ?? "unavailable"} | ${status}`);
1837
+ return { kind, installed, published, update };
1838
+ });
1839
+ const updates = rows.filter((row) => row.update);
1840
+ const available = rows.filter((row) => row.published && !row.installed?.linked && (!row.installed || row.update));
1841
+ prompter.write("\nDevelopment links are managed in their source checkouts.\n");
1842
+ const picked = await prompter.select("Updates", [
1843
+ ...available.map((row) => ({ value: row.kind, key: row.kind, label: `${row.installed ? "Update" : "Install"} ${packageName(row.kind)}` })),
1844
+ ...updates.length ? [{ value: "all", key: "a", label: `Update all available (${updates.length})` }] : [],
1845
+ { value: "refresh", key: "r", label: "Refresh versions" },
1846
+ { value: "back", key: "b", label: "Back" }
1847
+ ], { cancelValue: "back" });
1848
+ if (picked === "back") return;
1849
+ if (picked === "refresh") {
1850
+ await refresh();
1851
+ continue;
1852
+ }
1853
+ const selected = picked === "all" ? updates : available.filter((row) => row.kind === picked);
1854
+ if (!selected.length) continue;
1855
+ if (!await prompter.askYesNo(`Install latest published releases for ${selected.map((row) => packageName(row.kind)).join(", ")}?`, true)) continue;
1856
+ prompter.clearScreen();
1857
+ for (const row of selected) {
1858
+ writeWrapped(prompter, `Updating ${packageName(row.kind)}...`);
1859
+ try {
1860
+ const installed = await registry.installRuntime(row.kind);
1861
+ writeWrapped(prompter, `Installed ${packageName(row.kind)} ${installed.version}.`);
1862
+ const names = Object.entries(registry.read().connections).filter(([, item]) => item.kind === row.kind && item.entry).map(([name]) => name);
1863
+ await registry.checkSaved(prompter, names);
1864
+ } catch (error) {
1865
+ let detail = error instanceof Error ? error.message : "Installation failed.";
1866
+ for (const item of Object.values(registry.read().connections)) if (item.entry) detail = redactStartupText(detail, item.entry);
1867
+ writeWrapped(prompter, `FAILED ${packageName(row.kind)}: ${detail}`);
1868
+ if (/\b(?:EBUSY|EPERM|EACCES)\b/i.test(detail)) prompter.write("Close your AI clients, then retry the update. A running runtime may have locked its files.\n");
1869
+ }
1870
+ }
1871
+ prompter.write("Reconnect or restart your AI clients to load updated runtimes. Fix any failed startup checks first.\n");
1872
+ await prompter.acknowledge();
1873
+ } catch (error) {
1874
+ if (error instanceof PromptClosedError) throw error;
1875
+ if (error instanceof PromptBackError) continue;
1876
+ throw error;
1877
+ }
1878
+ }
1879
+ }
1880
+
1881
+ // src/modules/registration/migration-menu.ts
1882
+ async function migrationMenu(prompter, registry) {
1883
+ try {
1884
+ prompter.clearScreen();
1885
+ const plan = registry.planMigration();
1886
+ prompter.write("Migrate registry\n\n");
1887
+ if (!plan.steps.length) {
1888
+ prompter.write("The registry is already in the current format.\n");
1889
+ await prompter.acknowledge();
1890
+ return;
1891
+ }
1892
+ prompter.write("Steps:\n");
1893
+ plan.steps.forEach((step, index) => writeWrapped(prompter, `${index + 1}. ${step.label}`, " "));
1894
+ prompter.write('\nThe registry will record defaultMode: "catalog" for new kinds.\nA kind explicitly switched back to individual entries keeps that choice.\n');
1895
+ for (const item of plan.kinds) {
1896
+ prompter.write("\n");
1897
+ const target = sessionName(item.kind);
1898
+ const action = item.status === "catalog" ? `already ${target}; keep` : item.status === "individual" ? "keep explicit per-connection choice (rollback)" : item.status === "empty" ? `no connections; future connections use ${target}` : item.status === "blocked" ? "blocked" : `per-connection entries -> ${target}${item.status === "pending-runtime" ? " after runtime setup" : ""}`;
1899
+ writeWrapped(prompter, `${item.kind.toUpperCase()}: ${item.names.length} connection(s), ${action}`);
1900
+ if (item.detail) writeWrapped(prompter, plainText(item.detail), " ");
1901
+ for (const change of item.session?.changes ?? item.changes ?? []) {
1902
+ writeWrapped(prompter, `${CLIENT_LABELS2[change.client]}${item.status === "pending-runtime" ? " (after Runtime + Sync)" : ""}`, " ");
1903
+ writeWrapped(prompter, `Apply: ${change.add.join(", ") || "none"}`, " ");
1904
+ writeWrapped(prompter, `Remove after verification: ${change.remove.join(", ") || "none"}`, " ");
1905
+ if (change.review.length) writeWrapped(prompter, `Keep for Sync review: ${change.review.map(plainText).join(", ")}`, " ");
1906
+ }
1907
+ if (item.session?.custom.length) writeWrapped(prompter, `Custom commands retained for rollback: ${item.session.custom.map(plainText).join(", ")}`, " ");
1908
+ }
1909
+ prompter.write("\nThe previous catalog revision is kept in registry.previous.json.\nSession entries contain no connection credentials; those stay in the catalog and its previous revision.\n");
1910
+ if (plan.kinds.some((item) => item.status === "blocked")) {
1911
+ prompter.write("Resolve blocked kinds before migrating. No migration was applied.\n");
1912
+ await prompter.acknowledge();
1913
+ return;
1914
+ }
1915
+ if (!await prompter.askYesNo("Migrate registry and apply these changes?", false)) return;
1916
+ const results = registry.applyMigration(plan);
1917
+ prompter.clearScreen();
1918
+ prompter.write(`${formatResults(results, "Registry migration saved.")}
1919
+ `);
1920
+ await registry.checkSaved(prompter, plan.kinds.filter((item) => item.status !== "individual").flatMap((item) => item.names));
1921
+ prompter.write("Reconnect affected clients after changes to their server layout. Fix startup failures first.\n");
1922
+ await prompter.acknowledge();
1923
+ } catch (error) {
1924
+ if (error instanceof PromptClosedError) throw error;
1925
+ if (error instanceof PromptBackError) return;
1926
+ prompter.write(`${error instanceof Error ? error.message : "Could not migrate the registry."}
1927
+ `);
1928
+ await prompter.acknowledge();
1929
+ }
1930
+ }
1931
+
1932
+ // src/modules/registration/menu.ts
1933
+ function preview(prompter, entry) {
1934
+ if (!entry) {
1935
+ prompter.write(" Remove this connection from the service.\n");
1936
+ return;
1937
+ }
1938
+ for (const [key, value] of Object.entries(entry.env)) prompter.write(` ${plainText(key)}: ${visibleField.test(key) ? plainText(value) : "(hidden)"}
1939
+ `);
1940
+ }
1941
+ async function registrationMenu(prompter, registry, initialClient) {
1942
+ const initial = registry.read();
1943
+ const selected = new Set(initial.clients);
1944
+ if (initialClient) selected.add(initialClient);
1945
+ const discovered = registry.candidates(void 0, registry.targets.filter((target) => target.id !== "registered").map((target) => target.id));
1946
+ let cursor = initialClient ?? registry.targets[0]?.id;
1947
+ let privacyNoticeShown = false;
1948
+ for (; ; ) {
1949
+ try {
1950
+ prompter.clearScreen();
1951
+ prompter.write("\nRegister \xB7 AI services\nShared by Database, SSH, WSL and S3.\nEnter toggles a service; Apply saves the selection.\nMatching existing connections are imported on Apply; differing settings stay unchanged.\n\n");
1952
+ for (const error of discovered.errors) prompter.write(` ${error}
1953
+ `);
1954
+ const picked = await prompter.select("Register", [
1955
+ ...registry.targets.filter((target) => target.id !== "registered").map((target) => ({
1956
+ value: target.id,
1957
+ label: `[${selected.has(target.id) ? "x" : " "}] ${target.label}`,
1958
+ hint: `${initial.clients.includes(target.id) ? "Registered" : "Not registered"} \xB7 ${discovered.entries.filter((entry) => entry.client === target.id).length} existing AKMS \xB7 ${target.detected ? "Found" : "Not detected"} \xB7 ${target.location}`
1959
+ })),
1960
+ { separator: true },
1961
+ { value: "apply", key: "a", label: "Apply registration" },
1962
+ { value: "back", key: "q", label: "Back" }
1963
+ ], { defaultValue: cursor, cancelValue: "back" });
1964
+ if (picked === "back") return false;
1965
+ if (picked !== "apply") {
1966
+ const client = picked;
1967
+ if (selected.has(client)) selected.delete(client);
1968
+ else selected.add(client);
1969
+ cursor = client;
1970
+ continue;
1971
+ }
1972
+ const removed = initial.clients.filter((id) => !selected.has(id));
1973
+ const count = Object.values(registry.read().connections).filter((item) => item.entry).length;
1974
+ const plan = registry.planRegistration([...selected]);
1975
+ const importNames = [...new Set(plan.imports.map((entry) => entry.name))];
1976
+ prompter.clearScreen();
1977
+ prompter.write("Review registration\n");
1978
+ prompter.write(`
1979
+ Selected: ${[...selected].map((id) => CLIENT_LABELS2[id]).join(" \xB7 ") || "none"}
1980
+ Apply ${count + importNames.length} shared connection(s) to these services.
1981
+ `);
1982
+ if (importNames.length) prompter.write(`Import ${importNames.length} existing connection(s); identical copies become one shared item:
1983
+ ${importNames.join("\n ")}
1984
+ `);
1985
+ if (plan.conflicts.length) prompter.write(`Keep ${plan.conflicts.length} differing connection(s) unchanged until you choose a version:
1986
+ ${plan.conflicts.join("\n ")}
1987
+ `);
1988
+ for (const error of plan.errors) prompter.write(` ${error}
1989
+ `);
1990
+ if (initial.defaultMode === "catalog") {
1991
+ prompter.write("Services use one session entry per kind. Imported individual entries are removed after that session entry is verified.\n");
1992
+ if (plan.imports.some((item) => !registry.globalRuntime(item.kind))) prompter.write("Install missing runtimes from Runtime, then run Sync. Existing entries stay until then.\n");
1993
+ if (plan.imports.some((item) => !standardLaunch(item.entry, item.kind))) prompter.write("Custom launch commands are retained for rollback; session mode uses each connection's saved environment.\n");
1994
+ }
1995
+ if (removed.length) prompter.write(`Unregister: ${removed.map((id) => CLIENT_LABELS2[id]).join(" \xB7 ")}
1996
+ Remove only AKMS-managed entries from these services; keep the shared catalog.
1997
+ `);
1998
+ if (!privacyNoticeShown) {
1999
+ writeWrapped(prompter, `Connection settings, including passwords, are stored in ${plainText(registry.file)}.`);
2000
+ prompter.write("Keep this file private: do not share, sync or commit it.\n");
2001
+ prompter.write("Its previous revision, registry.previous.json, also contains saved credentials.\n");
2002
+ privacyNoticeShown = true;
2003
+ }
2004
+ if (!await prompter.askYesNo("Apply this registration?", removed.length === 0)) continue;
2005
+ const results = registry.register([...selected], initial.clients, plan.imports);
2006
+ prompter.clearScreen();
2007
+ prompter.write(`
2008
+ ${formatResults(results, `Registration saved.
2009
+ AI services: ${[...selected].map((id) => CLIENT_LABELS2[id]).join(" \xB7 ") || "none"}`)}
2010
+ `);
2011
+ if (importNames.length) prompter.write(`Imported ${importNames.length} existing connection(s) into the shared list.
2012
+ `);
2013
+ try {
2014
+ const conflicts = new Set(results.filter((result) => result.status === "conflict").map((result) => result.name));
2015
+ if (conflicts.size && await prompter.askYesNo(`Compare settings for ${conflicts.size} connection(s) now?`, true)) await syncMenu(prompter, registry);
2016
+ const current = registry.read();
2017
+ const remaining = registry.candidates().entries.filter((candidate) => !Object.hasOwn(current.connections, candidate.name));
2018
+ const names = [...new Set(remaining.map((candidate) => candidate.name))];
2019
+ if (names.length) {
2020
+ prompter.write(`
2021
+ ${names.length} existing connection(s) still need a choice before joining the shared list:
2022
+ `);
2023
+ for (const name of names) {
2024
+ const copies = remaining.filter((candidate) => candidate.name === name);
2025
+ prompter.write(` ${plainText(name)} \xB7 ${copies.map((copy) => CLIENT_LABELS2[copy.client]).join(" \xB7 ")}
2026
+ `);
2027
+ if (new Set(copies.map((copy) => entryHash(copy.entry))).size > 1) prompter.write(` ${describeDifferences(copies.map((copy) => copy.entry))}
2028
+ `);
2029
+ }
2030
+ prompter.write("Their existing settings are unchanged. Compare them now, or use Import existing connections later.\n");
2031
+ if (await prompter.askYesNo("Compare existing settings and choose which to share?", true)) await importMenu(prompter, registry);
2032
+ }
2033
+ if (!conflicts.size && !names.length) await prompter.acknowledge();
2034
+ } catch (error) {
2035
+ if (!(error instanceof PromptBackError)) throw error;
2036
+ }
2037
+ return true;
2038
+ } catch (error) {
2039
+ if (error instanceof PromptClosedError) throw error;
2040
+ if (error instanceof PromptBackError) continue;
2041
+ prompter.write(` ${error instanceof Error ? error.message : "Registration could not be saved."}
2042
+ `);
2043
+ await prompter.acknowledge();
2044
+ }
2045
+ }
2046
+ }
2047
+ async function importMenu(prompter, registry, kind) {
2048
+ for (; ; ) {
2049
+ prompter.clearScreen();
2050
+ prompter.write("Import existing connections\n\n");
2051
+ const { entries, errors } = registry.candidates(kind);
2052
+ for (const error of errors) prompter.write(` ${error}
2053
+ `);
2054
+ const state = registry.read();
2055
+ const matching = registry.planRegistration(state.clients, kind);
2056
+ const matchingNames = [...new Set(matching.imports.map((entry) => entry.name))];
2057
+ const names = [...new Set(entries.filter((candidate) => {
2058
+ const current = Object.hasOwn(state.connections, candidate.name) ? state.connections[candidate.name] : void 0;
2059
+ return !current?.entry || current.applied[candidate.client] !== entryHash(candidate.entry) || entryHash(current.entry) !== entryHash(candidate.entry);
2060
+ }).map((entry) => entry.name))];
2061
+ if (!names.length) {
2062
+ prompter.write(" No existing connections need importing.\n");
2063
+ await prompter.acknowledge();
2064
+ return;
2065
+ }
2066
+ const picked = await prompter.select("Import existing connections", [
2067
+ ...names.map((name) => ({ value: `entry:${name}`, key: name, label: name, hint: [...new Set(entries.filter((entry) => entry.name === name).map((entry) => CLIENT_LABELS2[entry.client]))].join(" \xB7 ") })),
2068
+ ...matchingNames.length ? [{ value: "matching", key: "all", label: "Import all matching connections", hint: `${matchingNames.length} connections; differing versions stay unchanged` }] : [],
2069
+ { value: "back", key: "b", label: "Back" }
2070
+ ], { cancelValue: "back" });
2071
+ if (picked === "back") return;
2072
+ try {
2073
+ prompter.clearScreen();
2074
+ if (picked === "matching") {
2075
+ prompter.write(`
2076
+ Import existing connections:
2077
+ ${matchingNames.join("\n ")}
2078
+ `);
2079
+ if (await prompter.askYesNo("Import matching connections and apply them?", true)) {
2080
+ const results2 = registry.register(state.clients, state.clients, matching.imports);
2081
+ prompter.clearScreen();
2082
+ prompter.write(`${formatResults(results2, "Import complete")}
2083
+ `);
2084
+ await prompter.acknowledge();
2085
+ }
2086
+ continue;
2087
+ }
2088
+ const name = picked.slice(6);
2089
+ const candidates = entries.filter((entry) => entry.name === name);
2090
+ let chosen = candidates[0];
2091
+ if (new Set(candidates.map((candidate) => entryHash(candidate.entry))).size > 1) {
2092
+ prompter.write(`
2093
+ ${plainText(name)} \xB7 Choose the settings to use on all registered services.
2094
+ `);
2095
+ compareEntries(prompter, candidates.map((candidate) => ({ label: CLIENT_LABELS2[candidate.client], entry: candidate.entry })));
2096
+ const choice = await prompter.select("Use settings from", [
2097
+ ...candidates.map((candidate, index) => ({ value: String(index), label: `Use ${index + 1}: ${CLIENT_LABELS2[candidate.client]}` })),
2098
+ { value: "back", label: "Back" }
2099
+ ], { cancelValue: "back" });
2100
+ if (choice === "back") continue;
2101
+ chosen = candidates[Number(choice)];
2102
+ }
2103
+ preview(prompter, chosen.entry);
2104
+ const destination = await prompter.askRequired("Shared connection name (change it to keep both)", name);
2105
+ prompter.write("The selected version will be used by every registered service.\n");
2106
+ if (!await prompter.askYesNo(`Import ${destination} and apply it?`, true)) continue;
2107
+ const results = registry.importEntry(destination, chosen, candidates);
2108
+ prompter.clearScreen();
2109
+ prompter.write(`
2110
+ ${formatResults(results, "Import complete")}
2111
+ `);
2112
+ if (destination !== name) prompter.write(" The original entries were kept under their old name.\n");
2113
+ await prompter.acknowledge();
2114
+ } catch (error) {
2115
+ if (error instanceof PromptClosedError) throw error;
2116
+ if (error instanceof PromptBackError) continue;
2117
+ prompter.write(` ${error instanceof Error ? error.message : "Import failed."}
2118
+ `);
2119
+ await prompter.acknowledge();
2120
+ }
2121
+ }
2122
+ }
2123
+ async function syncMenu(prompter, registry) {
2124
+ let pendingResults;
2125
+ for (; ; ) {
2126
+ prompter.clearScreen();
2127
+ const results = pendingResults ?? registry.sync();
2128
+ pendingResults = void 0;
2129
+ prompter.write(`
2130
+ ${formatResults(results, "Sync results")}
2131
+ `);
2132
+ const conflicts = results.filter((result) => result.status === "conflict");
2133
+ if (!conflicts.length) {
2134
+ await prompter.acknowledge();
2135
+ return;
2136
+ }
2137
+ const picked = await prompter.select("Review differences", [
2138
+ ...conflicts.map((item, index) => ({ value: String(index), label: `${CLIENT_LABELS2[item.client]} \xB7 ${item.name}` })),
2139
+ { value: "back", key: "b", label: "Later \u2014 keep existing settings" }
2140
+ ], { cancelValue: "back" });
2141
+ if (picked === "back") return;
2142
+ try {
2143
+ prompter.clearScreen();
2144
+ const conflict = conflicts[Number(picked)];
2145
+ const state = registry.read();
2146
+ const sessionKind = Object.keys(state.sessions ?? {}).find((kind) => sessionName(kind) === conflict.name);
2147
+ if (sessionKind) {
2148
+ const session = state.sessions[sessionKind];
2149
+ const actual2 = registry.targets.find((target) => target.id === conflict.client)?.readEntries()[conflict.name];
2150
+ if (!actual2 || entryKind(actual2) !== sessionKind) {
2151
+ prompter.write("This session name belongs to an unsupported or different server. Rename the external entry manually; it was kept.\n");
2152
+ await prompter.acknowledge();
2153
+ continue;
2154
+ }
2155
+ const removing2 = !registry.isSessionMode(sessionKind, state) || !state.clients.includes(conflict.client) || !Object.values(state.connections).some((item) => item.kind === sessionKind && item.entry);
2156
+ writeWrapped(prompter, `Session server / ${conflict.name} / ${CLIENT_LABELS2[conflict.client]}`);
2157
+ compareEntries(prompter, [{ label: "AKMS session launch", entry: session.entry }, { label: "External settings", entry: actual2 }]);
2158
+ const choice2 = await prompter.select("Resolve session launch", [
2159
+ { value: "apply", label: removing2 ? "Remove the old session entry" : "Restore the AKMS session launch" },
2160
+ { value: "back", key: "b", label: "Keep external settings for now" }
2161
+ ], { cancelValue: "back" });
2162
+ if (choice2 === "apply" && await prompter.askYesNo(removing2 ? "Remove this session entry?" : "Replace these external launch settings?", false)) pendingResults = registry.resolve(conflict.client, conflict.name, actual2, false);
2163
+ continue;
2164
+ }
2165
+ const connection = state.connections[conflict.name];
2166
+ if (!connection) throw new Error("This connection changed. Reopen Sync to review its current settings.");
2167
+ const actual = registry.targets.find((target) => target.id === conflict.client)?.readEntries()[conflict.name];
2168
+ if (!actual || entryKind(actual) !== connection.kind) {
2169
+ prompter.write(" This name belongs to a different or unsupported server. Rename that entry manually, or rename the shared connection.\n");
2170
+ await prompter.acknowledge();
2171
+ continue;
2172
+ }
2173
+ const client = CLIENT_LABELS2[conflict.client];
2174
+ const registered = state.clients.includes(conflict.client);
2175
+ const sessionMode = registry.isSessionMode(connection.kind, state);
2176
+ const removing = !connection.entry || !registered || sessionMode;
2177
+ const subject = connection.entry && descriptionOnly([connection.entry, actual]) ? "description" : "settings";
2178
+ writeWrapped(prompter, `Sync / ${plainText(conflict.name)}`);
2179
+ writeWrapped(prompter, `Registered services: ${state.clients.map((id) => CLIENT_LABELS2[id]).join(" \xB7 ") || "none"}`);
2180
+ prompter.write("\n");
2181
+ if (!removing && connection.entry) {
2182
+ compareEntries(prompter, [{ label: "Saved in AKMS", entry: connection.entry }, { label: `Currently in ${client}`, entry: actual }]);
2183
+ } else {
2184
+ writeWrapped(prompter, !registered ? `${client} is no longer registered.` : sessionMode ? `This kind now uses ${sessionName(connection.kind)}. The individual client entry is no longer needed.` : "This connection was removed from AKMS.");
2185
+ writeWrapped(prompter, `Its settings in ${client} changed before removal could finish.`);
2186
+ prompter.write(`
2187
+ Currently in ${client}
2188
+ `);
2189
+ preview(prompter, actual);
2190
+ prompter.write("\n");
2191
+ }
2192
+ const choices = [
2193
+ { value: "shared", label: removing ? `Remove from ${client}` : `Use 1: AKMS -> ${client}`, hint: removing ? "finish the pending removal" : `replace its ${subject} with version 1` },
2194
+ ...registered ? [{ value: "client", label: sessionMode ? `Import ${client} settings into the catalog, then remove this entry` : removing ? `Restore from ${client}` : `Use 2: ${client} -> all ${state.clients.length} services`, hint: "save in AKMS and sync registered services" }] : [],
2195
+ { value: "back", key: "b", label: "Back without changes" }
2196
+ ];
2197
+ const choice = await prompter.select(removing ? registered ? "Remove or restore this connection?" : `Remove this connection from ${client}?` : `Which ${subject} should be used?`, choices, { cancelValue: "back" });
2198
+ if (choice === "back") continue;
2199
+ prompter.write("\n");
2200
+ if (choice === "client") {
2201
+ writeWrapped(prompter, `Save the ${client} version in AKMS.`);
2202
+ writeWrapped(prompter, `Sync to: ${state.clients.map((id) => CLIENT_LABELS2[id]).join(" \xB7 ")}`);
2203
+ } else writeWrapped(prompter, removing ? `Remove ${plainText(conflict.name)} from ${client}.` : `Copy the saved AKMS ${subject} to ${client}.`);
2204
+ const confirmation = choice === "client" ? `Save in AKMS and sync ${state.clients.length} services?` : removing ? `Remove from ${client}?` : `Update ${client} with version 1?`;
2205
+ if (await prompter.askYesNo(confirmation, false)) {
2206
+ pendingResults = registry.resolve(conflict.client, conflict.name, actual, choice === "client");
2207
+ }
2208
+ } catch (error) {
2209
+ if (error instanceof PromptClosedError) throw error;
2210
+ if (error instanceof PromptBackError) continue;
2211
+ prompter.write(` ${error instanceof Error ? error.message : "Could not resolve the difference."}
2212
+ `);
2213
+ await prompter.acknowledge();
2214
+ }
2215
+ }
2216
+ }
2217
+
2218
+ // src/modules/registration/targets/index.ts
2219
+ import os5 from "os";
2220
+ import path10 from "path";
2221
+
2222
+ // src/modules/registration/targets/claude-code.ts
2223
+ import { spawnSync } from "child_process";
2224
+ import fs8 from "fs";
2225
+ import os3 from "os";
2226
+ import path8 from "path";
2227
+
2228
+ // src/modules/registration/paths.ts
2229
+ import fs5 from "fs";
2230
+ import path6 from "path";
2231
+ function findExecutable(name) {
2232
+ const searchPath = process.env.PATH ?? "";
2233
+ const directories = searchPath.split(path6.delimiter).filter((directory) => directory !== "");
2234
+ let suffixes = [""];
2235
+ if (process.platform === "win32") {
2236
+ const pathExt = process.env.PATHEXT ?? ".COM;.EXE;.BAT;.CMD";
2237
+ suffixes = pathExt.split(";").filter((suffix) => suffix !== "");
2238
+ }
2239
+ for (const directory of directories) {
2240
+ for (const suffix of suffixes) {
2241
+ const candidate = path6.join(directory, `${name}${suffix}`);
2242
+ if (fs5.existsSync(candidate) == true && fs5.statSync(candidate).isFile() == true) {
2243
+ return candidate;
2244
+ }
2245
+ }
2246
+ }
2247
+ return null;
2248
+ }
2249
+
2250
+ // src/modules/registration/targets/json-file.ts
2251
+ import fs7 from "fs";
2252
+
2253
+ // src/modules/registration/targets/config-file.ts
2254
+ import fs6 from "fs";
2255
+ import path7 from "path";
2256
+ import { randomUUID as randomUUID2 } from "crypto";
2257
+ function readExisting2(filePath) {
2258
+ try {
2259
+ return fs6.readFileSync(filePath, "utf8");
2260
+ } catch (error) {
2261
+ if (error.code === "ENOENT") {
2262
+ return null;
2263
+ }
2264
+ throw error;
2265
+ }
2266
+ }
2267
+ function updateConfigFile2(filePath, update) {
2268
+ let destination = filePath;
2269
+ let symbolicLink = false;
2270
+ try {
2271
+ symbolicLink = fs6.lstatSync(filePath).isSymbolicLink();
2272
+ } catch (error) {
2273
+ if (error.code !== "ENOENT") {
2274
+ throw error;
2275
+ }
2276
+ }
2277
+ if (symbolicLink) {
2278
+ destination = fs6.realpathSync(filePath);
2279
+ }
2280
+ const before = readExisting2(destination);
2281
+ const updated = update(before ?? "");
2282
+ if (before != null) {
2283
+ fs6.accessSync(destination, fs6.constants.W_OK);
2284
+ }
2285
+ fs6.mkdirSync(path7.dirname(destination), { recursive: true });
2286
+ const temporary = path7.join(path7.dirname(destination), `.${path7.basename(destination)}.${randomUUID2()}.tmp`);
2287
+ const mode = before == null ? 384 : fs6.statSync(destination).mode & 511;
2288
+ let descriptor;
2289
+ let ownsTemporary = false;
2290
+ try {
2291
+ descriptor = fs6.openSync(temporary, "wx", mode);
2292
+ ownsTemporary = true;
2293
+ fs6.writeFileSync(descriptor, updated, "utf8");
2294
+ fs6.fsyncSync(descriptor);
2295
+ fs6.closeSync(descriptor);
2296
+ descriptor = void 0;
2297
+ if (readExisting2(destination) !== before) {
2298
+ throw new Error("The config changed while saving; reload it and try again.");
2299
+ }
2300
+ fs6.renameSync(temporary, destination);
2301
+ ownsTemporary = false;
2302
+ } finally {
2303
+ try {
2304
+ if (descriptor != null) {
2305
+ fs6.closeSync(descriptor);
2306
+ }
2307
+ } finally {
2308
+ if (ownsTemporary) {
2309
+ fs6.unlinkSync(temporary);
2310
+ }
2311
+ }
2312
+ }
2313
+ }
2314
+
2315
+ // src/modules/registration/targets/json-file.ts
2316
+ function toMcpServerEntry(value) {
2317
+ if (typeof value !== "object" || value == null) {
2318
+ return null;
2319
+ }
2320
+ const record2 = value;
2321
+ if (typeof record2.command !== "string") {
2322
+ return null;
2323
+ }
2324
+ let args = [];
2325
+ if (Array.isArray(record2.args) == true) {
2326
+ args = record2.args.filter((item) => typeof item === "string");
2327
+ }
2328
+ const env = /* @__PURE__ */ Object.create(null);
2329
+ if (typeof record2.env === "object" && record2.env != null) {
2330
+ for (const [key, item] of Object.entries(record2.env)) {
2331
+ if (typeof item === "string") {
2332
+ env[key] = item;
2333
+ }
2334
+ }
2335
+ }
2336
+ return { command: record2.command, args, env };
2337
+ }
2338
+ function readRawMcpServersJson(document) {
2339
+ if (typeof document !== "object" || document == null || Array.isArray(document)) {
2340
+ throw new Error("the config file is not a JSON object");
2341
+ }
2342
+ const servers = document.mcpServers;
2343
+ if (servers === void 0) {
2344
+ return {};
2345
+ }
2346
+ if (typeof servers !== "object" || servers == null || Array.isArray(servers)) {
2347
+ throw new Error("mcpServers is not a JSON object");
2348
+ }
2349
+ return servers;
2350
+ }
2351
+ function readMcpServersJson(document) {
2352
+ const entries = /* @__PURE__ */ Object.create(null);
2353
+ for (const [name, value] of Object.entries(readRawMcpServersJson(document))) {
2354
+ const entry = toMcpServerEntry(value);
2355
+ if (entry != null) {
2356
+ entries[name] = entry;
2357
+ }
2358
+ }
2359
+ return entries;
2360
+ }
2361
+ function readDocument(filePath, contents) {
2362
+ if (contents == null && fs7.existsSync(filePath) == false) {
2363
+ return {};
2364
+ }
2365
+ const text = contents ?? fs7.readFileSync(filePath, "utf8");
2366
+ if (text.trim() === "") {
2367
+ return {};
2368
+ }
2369
+ let document;
2370
+ try {
2371
+ document = JSON.parse(text);
2372
+ } catch (ex) {
2373
+ let detail = String(ex);
2374
+ if (ex instanceof Error) {
2375
+ detail = ex.message;
2376
+ }
2377
+ throw new Error(`${filePath} is not valid JSON: ${detail}`);
2378
+ }
2379
+ if (typeof document !== "object" || document == null || Array.isArray(document) == true) {
2380
+ throw new Error(`${filePath} is not a JSON object`);
2381
+ }
2382
+ return document;
2383
+ }
2384
+ function createJsonFileTarget(options) {
2385
+ const { id, label, filePath } = options;
2386
+ return {
2387
+ id,
2388
+ label,
2389
+ location: filePath,
2390
+ detected: fs7.existsSync(filePath),
2391
+ readEntries() {
2392
+ const document = readDocument(filePath);
2393
+ return readMcpServersJson(document);
2394
+ },
2395
+ hasEntry(name) {
2396
+ return Object.hasOwn(readRawMcpServersJson(readDocument(filePath)), name);
2397
+ },
2398
+ writeEntry(name, entry) {
2399
+ updateConfigFile2(filePath, (contents) => {
2400
+ const document = readDocument(filePath, contents);
2401
+ const servers = Object.assign(/* @__PURE__ */ Object.create(null), readRawMcpServersJson(document));
2402
+ servers[name] = entry;
2403
+ document.mcpServers = servers;
2404
+ return JSON.stringify(document, null, 4) + "\n";
2405
+ });
2406
+ return { status: "written", detail: filePath };
2407
+ },
2408
+ removeEntry(name) {
2409
+ updateConfigFile2(filePath, (contents) => {
2410
+ const document = readDocument(filePath, contents);
2411
+ const servers = readRawMcpServersJson(document);
2412
+ delete servers[name];
2413
+ return JSON.stringify(document, null, 4) + "\n";
2414
+ });
2415
+ return { status: "written", detail: filePath };
2416
+ }
2417
+ };
2418
+ }
2419
+
2420
+ // src/modules/registration/targets/claude-code.ts
2421
+ var CLAUDE_CODE_CONFIG_PATH = path8.join(os3.homedir(), ".claude.json");
2422
+ var CMD_META_CHARACTER_PATTERN = /([()\][%!^"`<>&|;, *?])/g;
2423
+ function toClaudeAddJsonPayload(entry) {
2424
+ return JSON.stringify({ type: "stdio", command: entry.command, args: entry.args, env: entry.env });
2425
+ }
2426
+ function escapeCmdArgument(argument) {
2427
+ let escaped = argument.replace(/(\\*)"/g, '$1$1\\"');
2428
+ escaped = escaped.replace(/(\\*)$/, "$1$1");
2429
+ escaped = `"${escaped}"`;
2430
+ escaped = escaped.replace(CMD_META_CHARACTER_PATTERN, "^$1");
2431
+ escaped = escaped.replace(CMD_META_CHARACTER_PATTERN, "^$1");
2432
+ return escaped;
2433
+ }
2434
+ function runCliCommand(executable, args) {
2435
+ const isBatchShim = /\.(cmd|bat)$/i.test(executable) == true;
2436
+ if (process.platform !== "win32" || isBatchShim == false) {
2437
+ const result2 = spawnSync(executable, args, { encoding: "utf8", windowsHide: true });
2438
+ return result2;
2439
+ }
2440
+ const commandLine = [executable.replace(CMD_META_CHARACTER_PATTERN, "^$1"), ...args.map(escapeCmdArgument)].join(" ");
2441
+ const result = spawnSync(process.env.ComSpec ?? "cmd.exe", ["/d", "/s", "/c", `"${commandLine}"`], {
2442
+ encoding: "utf8",
2443
+ windowsHide: true,
2444
+ windowsVerbatimArguments: true
2445
+ });
2446
+ return result;
2447
+ }
2448
+ function runClaude(executable, args) {
2449
+ const result = runCliCommand(executable, args);
2450
+ if (result.error != null) {
2451
+ return { status: "unavailable", detail: `could not run claude: ${result.error.message}` };
2452
+ }
2453
+ if (result.status !== 0) {
2454
+ let output = (result.stderr ?? "").trim();
2455
+ if (output === "") {
2456
+ output = (result.stdout ?? "").trim();
2457
+ }
2458
+ return { status: "unavailable", detail: `claude exited with ${result.status}: ${output}` };
2459
+ }
2460
+ return { status: "written", detail: CLAUDE_CODE_CONFIG_PATH };
2461
+ }
2462
+ function readRawClaudeEntries() {
2463
+ if (fs8.existsSync(CLAUDE_CODE_CONFIG_PATH) == false) {
2464
+ return {};
2465
+ }
2466
+ const text = fs8.readFileSync(CLAUDE_CODE_CONFIG_PATH, "utf8");
2467
+ let document;
2468
+ try {
2469
+ document = JSON.parse(text);
2470
+ } catch (ex) {
2471
+ let detail = String(ex);
2472
+ if (ex instanceof Error) {
2473
+ detail = ex.message;
2474
+ }
2475
+ throw new Error(`${CLAUDE_CODE_CONFIG_PATH} is not valid JSON: ${detail}`);
2476
+ }
2477
+ return readRawMcpServersJson(document);
2478
+ }
2479
+ function createClaudeCodeTarget() {
2480
+ const executable = findExecutable("claude");
2481
+ return {
2482
+ id: "claude-code",
2483
+ label: "Claude Code (user scope)",
2484
+ location: CLAUDE_CODE_CONFIG_PATH,
2485
+ detected: executable != null || fs8.existsSync(CLAUDE_CODE_CONFIG_PATH) == true,
2486
+ readEntries() {
2487
+ return readMcpServersJson({ mcpServers: readRawClaudeEntries() });
2488
+ },
2489
+ hasEntry(name) {
2490
+ return Object.hasOwn(readRawClaudeEntries(), name);
2491
+ },
2492
+ writeEntry(name, entry) {
2493
+ if (executable == null) {
2494
+ return {
2495
+ status: "unavailable",
2496
+ detail: "the claude CLI is not on PATH \u2014 install it, or use Print to get a command to run yourself"
2497
+ };
2498
+ }
2499
+ const previousEntries = readRawClaudeEntries();
2500
+ const hadPrevious = Object.hasOwn(previousEntries, name);
2501
+ const previous = previousEntries[name];
2502
+ if (hadPrevious) {
2503
+ const removed = runClaude(executable, ["mcp", "remove", name, "-s", "user"]);
2504
+ if (removed.status === "unavailable") {
2505
+ return removed;
2506
+ }
2507
+ }
2508
+ const added = runClaude(executable, ["mcp", "add-json", name, toClaudeAddJsonPayload(entry), "-s", "user"]);
2509
+ if (added.status === "unavailable" && hadPrevious) {
2510
+ const restored = runClaude(executable, ["mcp", "add-json", name, JSON.stringify(previous), "-s", "user"]);
2511
+ const recovery = restored.status === "written" ? "the previous entry was restored" : `could not restore the previous entry: ${restored.detail}`;
2512
+ return { status: "unavailable", detail: `${added.detail}
2513
+ ${recovery}` };
2514
+ }
2515
+ return added;
2516
+ },
2517
+ removeEntry(name) {
2518
+ if (executable == null) {
2519
+ return {
2520
+ status: "unavailable",
2521
+ detail: `the claude CLI is not on PATH \u2014 run this yourself:
2522
+ claude mcp remove ${name} -s user`
2523
+ };
2524
+ }
2525
+ return runClaude(executable, ["mcp", "remove", name, "-s", "user"]);
2526
+ }
2527
+ };
2528
+ }
2529
+
2530
+ // src/modules/registration/targets/codex.ts
2531
+ import fs9 from "fs";
2532
+ import os4 from "os";
2533
+ import path9 from "path";
2534
+
2535
+ // src/modules/registration/targets/toml-lines.ts
2536
+ function lineState() {
2537
+ return { quote: null, multiline: false, depth: 0 };
2538
+ }
2539
+ function scanTomlLine(line, state) {
2540
+ for (let index = 0; index < line.length; index += 1) {
2541
+ const character = line[index];
2542
+ if (state.quote != null) {
2543
+ if (character === "\\" && state.quote === '"') {
2544
+ index += 1;
2545
+ } else if (character === state.quote) {
2546
+ if (!state.multiline) {
2547
+ state.quote = null;
2548
+ } else if (line.slice(index, index + 3) === state.quote.repeat(3)) {
2549
+ while (line[index + 1] === character) {
2550
+ index += 1;
2551
+ }
2552
+ state.quote = null;
2553
+ state.multiline = false;
2554
+ }
2555
+ }
2556
+ continue;
2557
+ }
2558
+ if (character === "#") {
2559
+ break;
2560
+ }
2561
+ if (character === '"' || character === "'") {
2562
+ state.quote = character;
2563
+ state.multiline = line.slice(index, index + 3) === character.repeat(3);
2564
+ if (state.multiline) {
2565
+ index += 2;
2566
+ }
2567
+ } else if (character === "[" || character === "{") {
2568
+ state.depth += 1;
2569
+ } else if (character === "]" || character === "}") {
2570
+ state.depth -= 1;
2571
+ }
2572
+ }
2573
+ }
2574
+
2575
+ // src/modules/registration/targets/toml-validation.ts
2576
+ import { isDeepStrictEqual } from "util";
2577
+ import { parse, TomlError } from "smol-toml";
2578
+ function isTable(value) {
2579
+ return value != null && typeof value === "object" && !Array.isArray(value) && !(value instanceof Date);
2580
+ }
2581
+ function parseConfigToml(text) {
2582
+ let document;
2583
+ try {
2584
+ document = parse(text, { integersAsBigInt: true });
2585
+ } catch (error) {
2586
+ const location = error instanceof TomlError ? ` at line ${error.line}, column ${error.column}` : "";
2587
+ throw new Error(`Invalid TOML${location}; fix the config before changing it.`);
2588
+ }
2589
+ if (Object.hasOwn(document, "mcp_servers") && !isTable(document.mcp_servers)) {
2590
+ throw new Error("mcp_servers must be a TOML table.");
2591
+ }
2592
+ return document;
2593
+ }
2594
+ function readTomlEntries(text) {
2595
+ return readMcpServersJson({ mcpServers: parseConfigToml(text).mcp_servers ?? {} });
2596
+ }
2597
+ function hasTomlEntry(text, name) {
2598
+ return Object.hasOwn(parseConfigToml(text).mcp_servers ?? {}, name);
2599
+ }
2600
+ function normalize(document) {
2601
+ const servers = document.mcp_servers;
2602
+ if (servers == null) {
2603
+ return;
2604
+ }
2605
+ for (const server of Object.values(servers)) {
2606
+ if (isTable(server) && isTable(server.env) && Object.keys(server.env).length === 0) {
2607
+ delete server.env;
2608
+ }
2609
+ }
2610
+ if (Object.keys(servers).length === 0) {
2611
+ delete document.mcp_servers;
2612
+ }
2613
+ }
2614
+ function verifyTomlEdit(before, after, name, entry) {
2615
+ const expected = parseConfigToml(before);
2616
+ const servers = expected.mcp_servers ?? /* @__PURE__ */ Object.create(null);
2617
+ if (entry == null) {
2618
+ delete servers[name];
2619
+ } else {
2620
+ const previous = Object.hasOwn(servers, name) && isTable(servers[name]) ? servers[name] : {};
2621
+ Object.defineProperty(servers, name, {
2622
+ value: Object.assign(/* @__PURE__ */ Object.create(null), previous, { command: entry.command, args: [...entry.args], env: Object.assign(/* @__PURE__ */ Object.create(null), entry.env) }),
2623
+ configurable: true,
2624
+ enumerable: true,
2625
+ writable: true
2626
+ });
2627
+ }
2628
+ expected.mcp_servers = servers;
2629
+ let actual;
2630
+ try {
2631
+ actual = parseConfigToml(after);
2632
+ } catch {
2633
+ throw new Error("Cannot safely edit this TOML layout. Use [mcp_servers.<name>] tables; nothing was written.");
2634
+ }
2635
+ normalize(expected);
2636
+ normalize(actual);
2637
+ if (!isDeepStrictEqual(expected, actual)) {
2638
+ throw new Error("Cannot safely edit this TOML layout without changing other settings; nothing was written.");
2639
+ }
2640
+ return after;
2641
+ }
2642
+
2643
+ // src/modules/registration/targets/toml-subset.ts
2644
+ var BARE_KEY_PATTERN = /^[A-Za-z0-9_-]+$/;
2645
+ var ANY_HEADER_PATTERN = /^\s*\[/;
2646
+ var SERVER_HEADER_PATTERN = /^\s*\[\s*(?:mcp_servers|"mcp_servers"|'mcp_servers')\s*\.\s*([A-Za-z0-9_-]+|"(?:[^"\\]|\\.)*"|'[^']*')\s*((?:\.[^\]]*)?)\]\s*(?:#.*)?$/;
2647
+ var KEY_VALUE_PATTERN = /^\s*([A-Za-z0-9_-]+|"(?:[^"\\]|\\.)*"|'[^']*')\s*=\s*(.*)$/;
2648
+ var OWNED_TABLE_KEYS = ["command", "args", "env"];
2649
+ function unquoteKey(rawKey) {
2650
+ if (rawKey.startsWith('"') || rawKey.startsWith("'")) {
2651
+ return parseConfigToml(`value = ${rawKey}`).value;
2652
+ }
2653
+ return rawKey;
2654
+ }
2655
+ function readStatements(body) {
2656
+ const statements = [];
2657
+ let index = 0;
2658
+ while (index < body.length) {
2659
+ const match = KEY_VALUE_PATTERN.exec(body[index]);
2660
+ if (match == null) {
2661
+ index += 1;
2662
+ continue;
2663
+ }
2664
+ const start = index;
2665
+ const state = lineState();
2666
+ scanTomlLine(match[2], state);
2667
+ index += 1;
2668
+ while ((state.quote != null || state.depth > 0) && index < body.length) {
2669
+ scanTomlLine(body[index], state);
2670
+ index += 1;
2671
+ }
2672
+ statements.push({ key: unquoteKey(match[1]), start, end: index });
2673
+ }
2674
+ return statements;
2675
+ }
2676
+ function splitSections(lines) {
2677
+ const sections = [{ header: null, serverName: null, subPath: "", body: [] }];
2678
+ const state = lineState();
2679
+ for (const line of lines) {
2680
+ const current = sections[sections.length - 1];
2681
+ if (state.quote != null || state.depth > 0) {
2682
+ current.body.push(line);
2683
+ scanTomlLine(line, state);
2684
+ continue;
2685
+ }
2686
+ if (ANY_HEADER_PATTERN.test(line) == true) {
2687
+ const serverMatch = SERVER_HEADER_PATTERN.exec(line);
2688
+ let serverName = null;
2689
+ let subPath = "";
2690
+ if (serverMatch != null) {
2691
+ serverName = unquoteKey(serverMatch[1]);
2692
+ subPath = serverMatch[2].replace(/^\./, "").replace(/\s+/g, "");
2693
+ }
2694
+ sections.push({ header: line, serverName, subPath, body: [] });
2695
+ continue;
2696
+ }
2697
+ current.body.push(line);
2698
+ scanTomlLine(line, state);
2699
+ }
2700
+ return sections;
2701
+ }
2702
+ function hasMcpServerToml(text, name) {
2703
+ return hasTomlEntry(text, name);
2704
+ }
2705
+ function parseMcpServersToml(text) {
2706
+ return readTomlEntries(text);
2707
+ }
2708
+ function quoteTomlString(value) {
2709
+ 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")}`);
2710
+ return `"${escaped}"`;
2711
+ }
2712
+ function formatTomlKey(key) {
2713
+ if (BARE_KEY_PATTERN.test(key) == true) {
2714
+ return key;
2715
+ }
2716
+ return quoteTomlString(key);
2717
+ }
2718
+ function renderOwnedLines(entry) {
2719
+ return [
2720
+ `command = ${quoteTomlString(entry.command)}`,
2721
+ `args = [${entry.args.map((argument) => quoteTomlString(argument)).join(", ")}]`
2722
+ ];
2723
+ }
2724
+ function renderEnvLines(entry) {
2725
+ return Object.entries(entry.env).map(([key, value]) => `${formatTomlKey(key)} = ${quoteTomlString(value)}`);
2726
+ }
2727
+ function renderMcpServerTable(name, entry) {
2728
+ const tableKey = formatTomlKey(name);
2729
+ const lines = [`[mcp_servers.${tableKey}]`, ...renderOwnedLines(entry)];
2730
+ const envLines = renderEnvLines(entry);
2731
+ if (envLines.length > 0) {
2732
+ lines.push("");
2733
+ lines.push(`[mcp_servers.${tableKey}.env]`);
2734
+ lines.push(...envLines);
2735
+ }
2736
+ return `${lines.join("\n")}
2737
+ `;
2738
+ }
2739
+ function isBlank(line) {
2740
+ return line.trim() === "";
2741
+ }
2742
+ function joinSections(sections, eol) {
2743
+ const lines = [];
2744
+ for (const section of sections) {
2745
+ if (section.header != null) {
2746
+ lines.push(section.header);
2747
+ }
2748
+ lines.push(...section.body);
2749
+ }
2750
+ return lines.join(eol);
2751
+ }
2752
+ function writeMcpServerToml(text, name, entry) {
2753
+ parseConfigToml(text);
2754
+ const updated = editMcpServerToml(text, name, entry);
2755
+ return verifyTomlEdit(text, updated, name, entry);
2756
+ }
2757
+ function editMcpServerToml(text, name, entry) {
2758
+ let eol = "\n";
2759
+ if (text.includes("\r\n") == true) {
2760
+ eol = "\r\n";
2761
+ }
2762
+ const sections = splitSections(text.split(/\r?\n/));
2763
+ const isOwn = (section) => section.serverName === name;
2764
+ if (entry == null) {
2765
+ const kept = sections.filter((section) => isOwn(section) == false);
2766
+ const removedText = joinSections(kept, eol);
2767
+ return removedText;
2768
+ }
2769
+ const tableKey = formatTomlKey(name);
2770
+ const envLines = renderEnvLines(entry);
2771
+ const tableIndex = sections.findIndex((section) => isOwn(section) == true && section.subPath === "");
2772
+ if (tableIndex < 0) {
2773
+ const withoutStrayEnv = sections.filter((section) => (isOwn(section) == true && section.subPath === "env") == false);
2774
+ let prefix = joinSections(withoutStrayEnv, eol);
2775
+ if (prefix.trim() === "") {
2776
+ prefix = "";
2777
+ } else {
2778
+ prefix = prefix.replace(/(\r?\n)*$/, "");
2779
+ prefix = `${prefix}${eol}${eol}`;
2780
+ }
2781
+ const block = renderMcpServerTable(name, entry).replace(/\n/g, eol);
2782
+ return `${prefix}${block}`;
2783
+ }
2784
+ const table = sections[tableIndex];
2785
+ const ownedLineIndexes = /* @__PURE__ */ new Set();
2786
+ for (const statement of readStatements(table.body)) {
2787
+ if (OWNED_TABLE_KEYS.includes(statement.key) == true) {
2788
+ for (let index = statement.start; index < statement.end; index += 1) {
2789
+ ownedLineIndexes.add(index);
2790
+ }
2791
+ }
2792
+ }
2793
+ const userLines = table.body.filter((_line, index) => ownedLineIndexes.has(index) == false);
2794
+ let trailingBlankCount = 0;
2795
+ while (trailingBlankCount < userLines.length && isBlank(userLines[userLines.length - 1 - trailingBlankCount]) == true) {
2796
+ trailingBlankCount += 1;
2797
+ }
2798
+ const trailingBlanks = userLines.slice(userLines.length - trailingBlankCount);
2799
+ const userContent = userLines.slice(0, userLines.length - trailingBlankCount);
2800
+ const rebuilt = [];
2801
+ for (let index = 0; index < sections.length; index += 1) {
2802
+ const section = sections[index];
2803
+ if (isOwn(section) == true && section.subPath === "env") {
2804
+ continue;
2805
+ }
2806
+ if (index !== tableIndex) {
2807
+ rebuilt.push(section);
2808
+ continue;
2809
+ }
2810
+ if (envLines.length === 0) {
2811
+ rebuilt.push({ ...section, body: [...renderOwnedLines(entry), ...userContent, ...trailingBlanks] });
2812
+ continue;
2813
+ }
2814
+ rebuilt.push({ ...section, body: [...renderOwnedLines(entry), ...userContent, ""] });
2815
+ rebuilt.push({
2816
+ header: `[mcp_servers.${tableKey}.env]`,
2817
+ serverName: name,
2818
+ subPath: "env",
2819
+ body: [...envLines, ...trailingBlanks]
2820
+ });
2821
+ }
2822
+ const rebuiltText = joinSections(rebuilt, eol);
2823
+ return rebuiltText;
2824
+ }
2825
+
2826
+ // src/modules/registration/targets/codex.ts
2827
+ function createCodexTarget() {
2828
+ const CODEX_CONFIG_PATH = path9.join(process.env.CODEX_HOME ? path9.resolve(process.env.CODEX_HOME) : path9.join(os4.homedir(), ".codex"), "config.toml");
2829
+ function readText() {
2830
+ if (fs9.existsSync(CODEX_CONFIG_PATH) == false) {
2831
+ return "";
2832
+ }
2833
+ return fs9.readFileSync(CODEX_CONFIG_PATH, "utf8");
2834
+ }
2835
+ return {
2836
+ id: "codex",
2837
+ label: "Codex CLI",
2838
+ location: CODEX_CONFIG_PATH,
2839
+ detected: findExecutable("codex") != null || fs9.existsSync(CODEX_CONFIG_PATH) == true,
2840
+ readEntries() {
2841
+ const text = readText();
2842
+ return parseMcpServersToml(text);
2843
+ },
2844
+ hasEntry(name) {
2845
+ return hasMcpServerToml(readText(), name);
2846
+ },
2847
+ writeEntry(name, entry, options) {
2848
+ updateConfigFile2(CODEX_CONFIG_PATH, (original) => {
2849
+ let text = original;
2850
+ if (options?.replace) {
2851
+ text = writeMcpServerToml(text, name, null);
2852
+ }
2853
+ return writeMcpServerToml(text, name, entry);
2854
+ });
2855
+ return { status: "written", detail: CODEX_CONFIG_PATH };
2856
+ },
2857
+ removeEntry(name) {
2858
+ updateConfigFile2(CODEX_CONFIG_PATH, (text) => writeMcpServerToml(text, name, null));
2859
+ return { status: "written", detail: CODEX_CONFIG_PATH };
2860
+ }
2861
+ };
2862
+ }
2863
+
2864
+ // src/modules/registration/targets/index.ts
2865
+ function claudeDesktopConfigPath() {
2866
+ if (process.platform === "win32") {
2867
+ const appData = process.env.APPDATA ?? path10.join(os5.homedir(), "AppData", "Roaming");
2868
+ return path10.join(appData, "Claude", "claude_desktop_config.json");
2869
+ }
2870
+ if (process.platform === "darwin") {
2871
+ return path10.join(os5.homedir(), "Library", "Application Support", "Claude", "claude_desktop_config.json");
2872
+ }
2873
+ return path10.join(os5.homedir(), ".config", "Claude", "claude_desktop_config.json");
2874
+ }
2875
+ function listConfigTargets() {
2876
+ const targets = [
2877
+ createClaudeCodeTarget(),
2878
+ createCodexTarget(),
2879
+ createJsonFileTarget({ id: "claude-desktop", label: "Claude Desktop", filePath: claudeDesktopConfigPath() }),
2880
+ createJsonFileTarget({ id: "cursor", label: "Cursor", filePath: path10.join(os5.homedir(), ".cursor", "mcp.json") })
2881
+ ];
2882
+ const detected = targets.filter((target) => target.detected == true);
2883
+ const others = targets.filter((target) => target.detected == false);
2884
+ return [...detected, ...others];
2885
+ }
2886
+
2887
+ // src/modules/launcher/menu.ts
2888
+ async function runRegistrationCommand(options) {
2889
+ const prompter = new Prompter(options.input, options.output);
2890
+ const registry = new Registry(options.targets ?? listConfigTargets(), options.registryPath, options.runtimeOptions);
2891
+ try {
2892
+ if (options.mode === "register") await registrationMenu(prompter, registry, options.targetId);
2893
+ else await syncMenu(prompter, registry);
2894
+ return 0;
2895
+ } catch (error) {
2896
+ if (error instanceof PromptClosedError || error instanceof PromptBackError) return 0;
2897
+ throw error;
2898
+ } finally {
2899
+ prompter.close();
2900
+ }
2901
+ }
2902
+ async function runMenu(options) {
2903
+ const registry = new Registry(options.targets ?? listConfigTargets(), options.registryPath, options.runtimeOptions);
2904
+ let defaultValue = "db";
2905
+ let failure;
2906
+ for (; ; ) {
2907
+ const prompter = new Prompter(options.input, options.output);
2908
+ let selected = "quit";
2909
+ try {
2910
+ if (failure) {
2911
+ prompter.write(`
2912
+ ${failure}
2913
+
2914
+ `);
2915
+ await prompter.acknowledge();
2916
+ failure = void 0;
2917
+ }
2918
+ for (; ; ) {
2919
+ prompter.clearScreen();
2920
+ const state = registry.read();
2921
+ const clients = state.clients;
2922
+ const migration = registry.needsMigration(state);
2923
+ if (migration) prompter.write("Registry update available. Choose Migrate registry to review the changes.\n");
2924
+ const picked = await prompter.select("AKMS MCP \xB7 Connection setup", [
2925
+ { value: "db", label: "Database", hint: "PostgreSQL connections" },
2926
+ { value: "ssh", label: "SSH", hint: "Remote hosts" },
2927
+ { value: "wsl", label: "WSL", hint: "Local Linux distros \xB7 Windows" },
2928
+ { value: "s3", label: "S3", hint: "AWS S3 and S3-compatible buckets" },
2929
+ { separator: true },
2930
+ { value: "register", label: "Register", hint: clients.length ? clients.map((id) => CLIENT_LABELS2[id]).join(" \xB7 ") : "Choose AI services once" },
2931
+ { value: "sync", label: "Sync", hint: "Review and retry shared settings" },
2932
+ { value: "runtime", label: "Runtime", hint: "AKMS installation / replace npx launches" },
2933
+ { value: "updates", label: "Updates", hint: "Check installed MCP releases" },
2934
+ ...migration ? [{ value: "migrate", key: "migrate", label: "Migrate registry", hint: "Review older registry format and session layout" }] : [],
2935
+ { value: "quit", label: "Quit" }
2936
+ ], { defaultValue, cancelValue: "quit" });
2937
+ if (picked !== "register" && picked !== "sync" && picked !== "runtime" && picked !== "updates" && picked !== "migrate") {
2938
+ selected = picked;
2939
+ break;
2940
+ }
2941
+ try {
2942
+ if (picked === "register") await registrationMenu(prompter, registry, options.targetId);
2943
+ else if (picked === "runtime") await runtimeMenu(prompter, registry);
2944
+ else if (picked === "updates") await updatesMenu(prompter, registry);
2945
+ else if (picked === "migrate") await migrationMenu(prompter, registry);
2946
+ else await syncMenu(prompter, registry);
2947
+ } catch (error) {
2948
+ if (error instanceof PromptClosedError) throw error;
2949
+ if (!(error instanceof PromptBackError)) {
2950
+ options.output.write(`
2951
+ ${error instanceof Error ? error.message : "Could not update settings."}
2952
+ `);
2953
+ await prompter.acknowledge();
2954
+ }
2955
+ }
2956
+ }
2957
+ } catch (error) {
2958
+ if (error instanceof PromptClosedError) return 0;
2959
+ throw error;
2960
+ } finally {
2961
+ prompter.close();
2962
+ }
2963
+ if (selected === "quit") return 0;
2964
+ defaultValue = selected;
2965
+ try {
2966
+ const code = await options.launch({ mcpId: selected, targetId: options.targetId, pendingInput: prompter.takePendingInput() });
2967
+ if (code === 130) return 0;
2968
+ if (code !== 0) failure = `Setup exited with code ${code}.`;
2969
+ } catch (error) {
2970
+ failure = String(error);
2971
+ }
2972
+ }
2973
+ }
2974
+
2975
+ // src/modules/launcher/npm.ts
2976
+ import { execFile as execFile3, spawn as spawn2 } from "child_process";
2977
+ import { existsSync } from "fs";
2978
+ import { mkdtemp, realpath, rm } from "fs/promises";
2979
+ import { tmpdir } from "os";
2980
+ import { basename, delimiter, dirname, join } from "path";
2981
+ import { promisify as promisify2 } from "util";
2982
+ var execFileAsync = promisify2(execFile3);
2983
+ async function findNpmCli() {
2984
+ const candidates = [];
2985
+ const npmExecPath = process.env.npm_execpath;
2986
+ if (npmExecPath != null && ["npm-cli.js", "npx-cli.js"].includes(basename(npmExecPath)) == true) {
2987
+ candidates.push(join(dirname(npmExecPath), "npm-cli.js"));
2988
+ }
2989
+ candidates.push(join(dirname(process.execPath), "node_modules", "npm", "bin", "npm-cli.js"));
2990
+ candidates.push(join(dirname(process.execPath), "..", "lib", "node_modules", "npm", "bin", "npm-cli.js"));
2991
+ for (const directory of (process.env.PATH ?? "").split(delimiter)) {
2992
+ if (directory === "") {
2993
+ continue;
2994
+ }
2995
+ if (process.platform === "win32") {
2996
+ candidates.push(join(directory, "node_modules", "npm", "bin", "npm-cli.js"));
2997
+ } else {
2998
+ candidates.push(join(directory, "npm"));
2999
+ }
3000
+ }
3001
+ for (const candidate of candidates) {
3002
+ if (existsSync(candidate) == true) {
3003
+ const resolved = await realpath(candidate);
3004
+ if (resolved.endsWith(".js")) return resolved;
3005
+ }
3006
+ }
3007
+ throw new Error("npm could not be found. Install Node.js with npm, then reopen your terminal.");
3008
+ }
3009
+ async function resolveLatestVersion(options) {
3010
+ const metadataCache = await mkdtemp(join(tmpdir(), "akms-mcp-latest-"));
3011
+ try {
3012
+ const result = await execFileAsync(process.execPath, [
3013
+ options.npmCli,
3014
+ "view",
3015
+ `${options.packageName}@latest`,
3016
+ "version",
3017
+ "--json",
3018
+ "--prefer-online",
3019
+ "--prefer-offline=false",
3020
+ "--offline=false",
3021
+ "--fetch-retries=0",
3022
+ "--fetch-timeout=15000",
3023
+ "--update-notifier=false",
3024
+ "--cache",
3025
+ metadataCache
3026
+ ], { signal: options.signal, timeout: 2e4, windowsHide: true });
3027
+ const version = JSON.parse(result.stdout);
3028
+ if (typeof version !== "string" || /^\d+\.\d+\.\d+(?:-[0-9A-Za-z.-]+)?(?:\+[0-9A-Za-z.-]+)?$/.test(version) == false) {
3029
+ throw new Error("npm returned an invalid latest version.");
3030
+ }
3031
+ return version;
3032
+ } finally {
3033
+ await rm(metadataCache, { recursive: true, force: true });
3034
+ }
3035
+ }
3036
+ async function launchMcp(options, dependencies = {}) {
3037
+ const selected = MCP_PACKAGES[options.mcpId];
3038
+ if (options.mcpId === "wsl" && process.platform !== "win32") {
3039
+ throw new Error("WSL setup must run from Windows.");
3040
+ }
3041
+ const findRuntime = dependencies.findRuntime ?? findGlobalRuntime;
3042
+ const output = dependencies.output ?? process.stdout;
3043
+ let runtime = findRuntime(options.mcpId);
3044
+ let pendingInput = options.pendingInput;
3045
+ if (!runtime) {
3046
+ const prompter = new Prompter(dependencies.input ?? process.stdin, output, pendingInput);
3047
+ try {
3048
+ prompter.write(`
3049
+ ${selected.packageName} is not installed in the AKMS runtime directory.
3050
+ `);
3051
+ if (!await prompter.askYesNo("Install the AKMS runtime before opening setup?", true)) return 0;
3052
+ try {
3053
+ prompter.write(`Installing ${selected.packageName}...
3054
+ `);
3055
+ await (dependencies.installRuntime ?? installGlobalRuntime)(options.mcpId);
3056
+ runtime = findRuntime(options.mcpId);
3057
+ if (!runtime) throw new Error("Installation completed but the AKMS runtime could not be found.");
3058
+ } catch (error) {
3059
+ if (error instanceof Error && error.message === "AKMS runtime installation cancelled.") return 130;
3060
+ prompter.write(`${error instanceof Error ? error.message : "AKMS runtime installation failed."}
3061
+ `);
3062
+ if (!await prompter.askYesNo("Open the latest setup with npm exec instead?", false)) return 1;
3063
+ }
3064
+ } catch (error) {
3065
+ if (error instanceof PromptClosedError || error instanceof PromptBackError) return 0;
3066
+ throw error;
3067
+ } finally {
3068
+ prompter.close();
3069
+ pendingInput = prompter.takePendingInput();
3070
+ }
3071
+ }
3072
+ if (runtime) {
3073
+ const argumentsList = [...runtime.args, selected.setupCommand];
3074
+ if (options.targetId) argumentsList.push("--target", options.targetId);
3075
+ output.write(`Opening ${selected.packageName} ${runtime.version}${runtime.linked ? " (linked development checkout)" : ""}
3076
+
3077
+ `);
3078
+ return (dependencies.runChild ?? runChild)(runtime.command, argumentsList, pendingInput);
3079
+ }
3080
+ return launchLatestSetup(options, dependencies, pendingInput);
3081
+ }
3082
+ async function launchLatestSetup(options, dependencies, pendingInput) {
3083
+ const selected = MCP_PACKAGES[options.mcpId];
3084
+ const output = dependencies.output ?? process.stdout;
3085
+ const npmCli2 = await (dependencies.findNpmCli ?? findNpmCli)();
629
3086
  const cancellation = new AbortController();
630
3087
  const cancelLookup = () => {
631
3088
  cancellation.abort();
@@ -634,10 +3091,10 @@ async function launchMcp(options) {
634
3091
  process.on("SIGTERM", cancelLookup);
635
3092
  let version;
636
3093
  try {
637
- process.stdout.write(`
3094
+ output.write(`
638
3095
  Checking the latest ${selected.packageName} release...
639
3096
  `);
640
- version = await resolveLatestVersion({ npmCli, packageName: selected.packageName, signal: cancellation.signal });
3097
+ version = await (dependencies.resolveLatestVersion ?? resolveLatestVersion)({ npmCli: npmCli2, packageName: selected.packageName, signal: cancellation.signal });
641
3098
  } catch (error) {
642
3099
  if (cancellation.signal.aborted == true) {
643
3100
  return 130;
@@ -649,7 +3106,7 @@ ${String(error)}`, { cause: error });
649
3106
  process.off("SIGTERM", cancelLookup);
650
3107
  }
651
3108
  const argumentsList = [
652
- npmCli,
3109
+ npmCli2,
653
3110
  "exec",
654
3111
  "--yes",
655
3112
  "--prefer-online",
@@ -663,16 +3120,19 @@ ${String(error)}`, { cause: error });
663
3120
  if (options.targetId != null) {
664
3121
  argumentsList.push("--target", options.targetId);
665
3122
  }
666
- process.stdout.write(`Opening ${selected.packageName} ${version}
3123
+ output.write(`Opening ${selected.packageName} ${version} with npm exec
667
3124
 
668
3125
  `);
3126
+ return (dependencies.runChild ?? runChild)(process.execPath, argumentsList, pendingInput);
3127
+ }
3128
+ async function runChild(command, argumentsList, pendingInput) {
669
3129
  return new Promise((resolve, reject) => {
670
3130
  const interactiveInput = process.stdin.isTTY === true;
671
3131
  let inputMode = "pipe";
672
3132
  if (interactiveInput == true) {
673
3133
  inputMode = "inherit";
674
3134
  }
675
- const child = spawn(process.execPath, argumentsList, {
3135
+ const child = spawn2(command, argumentsList, {
676
3136
  stdio: [inputMode, "inherit", "inherit"],
677
3137
  windowsHide: true
678
3138
  });
@@ -690,8 +3150,8 @@ ${String(error)}`, { cause: error });
690
3150
  reject(error);
691
3151
  }
692
3152
  });
693
- if (options.pendingInput != null) {
694
- child.stdin.write(options.pendingInput);
3153
+ if (pendingInput != null) {
3154
+ child.stdin.write(pendingInput);
695
3155
  }
696
3156
  process.stdin.pipe(child.stdin);
697
3157
  }
@@ -726,14 +3186,17 @@ var HELP_TEXT = [
726
3186
  " akms-mcp Open the setup menu",
727
3187
  " akms-mcp [setup|config] Open the same menu",
728
3188
  " akms-mcp [setup|config] <mcp> Open db, ssh, wsl or s3 setup directly",
3189
+ " akms-mcp register Register AI services for all AKMS connections",
3190
+ " akms-mcp sync Review differences and retry pending changes",
729
3191
  "",
730
3192
  "Options:",
731
3193
  " --target <client> claude-code | codex | claude-desktop | cursor",
732
3194
  " -h, --help Print this help",
733
3195
  " -v, --version Print the launcher version",
734
3196
  "",
735
- "Each launch checks npm for the selected package's latest release.",
736
- "Node.js 22+ and npm are required. WSL setup runs on Windows."
3197
+ "Opens the installed runtime's setup. Missing runtimes can be installed with confirmation.",
3198
+ "Node.js 22+ and npm are required. WSL setup runs on Windows.",
3199
+ "Catalog: ~/.akms/mcp/registry.json (or $AKMS_MCP_HOME/registry.json) stores settings and passwords in plaintext."
737
3200
  ].join("\n");
738
3201
  async function main() {
739
3202
  const argumentsList = parseArguments(process.argv.slice(2));
@@ -745,7 +3208,7 @@ async function main() {
745
3208
  if (argumentsList.showVersion == true) {
746
3209
  let version = "development";
747
3210
  if (true) {
748
- version = "0.2.1";
3211
+ version = "0.3.1";
749
3212
  }
750
3213
  process.stdout.write(`${version}
751
3214
  `);
@@ -755,6 +3218,10 @@ async function main() {
755
3218
  process.exitCode = await launchMcp({ mcpId: argumentsList.mcpId, targetId: argumentsList.targetId });
756
3219
  return;
757
3220
  }
3221
+ if (argumentsList.managementMode) {
3222
+ process.exitCode = await runRegistrationCommand({ input: process.stdin, output: process.stdout, mode: argumentsList.managementMode, targetId: argumentsList.targetId });
3223
+ return;
3224
+ }
758
3225
  process.exitCode = await runMenu({
759
3226
  input: process.stdin,
760
3227
  output: process.stdout,