@akms/mcp 0.3.1 → 0.3.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/CHANGELOG.md CHANGED
@@ -2,6 +2,16 @@
2
2
 
3
3
  All notable changes to `@akms/mcp`.
4
4
 
5
+ ## [0.3.2] - 2026-10-26
6
+
7
+ ### Changed
8
+
9
+ - Point Sync review instructions to its current menu and name the services retaining old connection entries, grouping only identical retained lists.
10
+ - Separate runtime waits from failed changes in Register and Sync. Group retained connection names, count saved connections separately from service entries, and show semantic colors only on interactive terminals without NO_COLOR.
11
+ - Offer unimported connections directly from Sync and compare both saved and external settings before resolving an individual entry during session migration. Wide terminals show versions side by side; narrow terminals keep wrapped values.
12
+ - Preserve existing terminal symbols and keep status words visible when colors are disabled.
13
+ - Document the upgrade path from 0.2.x, including pending runtimes, differing imported settings, reconnecting clients, rollback and registry migration.
14
+
5
15
  ## [0.3.1] - 2026-10-26
6
16
 
7
17
  ### Verification
package/README.md CHANGED
@@ -48,6 +48,29 @@ akms-mcp sync
48
48
 
49
49
  Use `--help` for the command reference and `--version` for the launcher's version.
50
50
 
51
+ ## Upgrading from 0.2.x
52
+
53
+ For clients with individual npx entries and no shared registry, start the current setup launcher:
54
+
55
+ ```sh
56
+ npx -y --prefer-online @akms/mcp@latest
57
+ ```
58
+
59
+ 1. Open **Runtime**, select each MCP kind you use, and choose **Install AKMS runtime** or **Update runtime installation**.
60
+ 2. Open **Register**, select your AI services, then choose **Apply registration** and confirm the review. Matching existing connections are imported into the shared catalog, and their individual client entries are replaced by one `akms_<kind>` entry per service.
61
+ 3. Answer **Yes** to **Compare existing settings and choose which to share?** when versions differ, even if only their descriptions differ. Select the connection, choose **Use 1** or **Use 2**, keep the shared connection name to replace its old entries, and confirm **Import ... and apply it?**. If you defer this choice, use **Import existing connections** or reopen Register and Apply to return to the comparison.
62
+ 4. Run **Sync** if any saved changes remain unfinished.
63
+ 5. Restart or reconnect your AI clients to load the new entries. In Codex, start a new thread and archive the old threads whose servers you want to retire: each thread owns its MCP processes, and archiving stopped those processes in the isolated Codex 0.164.0 check.
64
+ 6. Call `db_catalog_status`, `ssh_catalog_status`, `wsl_catalog_status` or `s3_catalog_status` for each kind you use and check its loaded and skipped connections. Tools now include the connection prefix, such as `pg_alpha__db_query` or `ssh_dev__ssh_exec`.
65
+
66
+ **If you registered first:** a missing-runtime result is a normal pending state. Updated setup labels this **Waiting for runtime**. Existing client entries stay in place while the runtime is missing. Install it from **Runtime**, then run **Sync** to finish applying the session entries and removing the old entries; installing the runtime alone does not finish that conversion.
67
+
68
+ **Finish importing differences:** in 0.3.1, connections still awaiting import did not appear in Sync, even when only their descriptions differed. Finish the Register comparison or use **Import existing connections** to review them. Updated Sync screens show **Not yet imported** and offer **Import existing connections** on the same screen. Complete that import review even when the saved connections are already up to date.
69
+
70
+ **Existing development registry:** if setup shows **Registry update available**, open **Migrate registry**, review the proposed changes and confirm them. See [Migrate an older registry](#migrate-an-older-registry).
71
+
72
+ **Return to individual entries:** open **Runtime**, select the kind, choose **Switch back to per-connection entries**, then review and confirm. This restores the saved individual launch commands and credentials and removes the session entry after the replacements are verified; the registry stays version 2. Restart or reconnect clients after changing the layout.
73
+
51
74
  ## Shared registration
52
75
 
53
76
  ### Startup checks and updates
package/lib/cli.js CHANGED
@@ -373,6 +373,8 @@ function createStyle(enabled) {
373
373
  return {
374
374
  accent: paint(36, 39),
375
375
  success: paint(32, 39),
376
+ warning: paint(33, 39),
377
+ error: paint(31, 39),
376
378
  dim: paint(2, 22),
377
379
  bold: paint(1, 22)
378
380
  };
@@ -789,7 +791,7 @@ var fieldName = (field) => {
789
791
  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
792
  return Object.hasOwn(names, key) ? names[key] : plainText(key);
791
793
  };
792
- function writeWrapped(prompter, text, indent = "", changed) {
794
+ function writeWrapped(prompter, text, indent = "", changed, style) {
793
795
  const characters = Array.from(plainText(text));
794
796
  let line = indent;
795
797
  let width = displayWidth(indent);
@@ -804,7 +806,7 @@ function writeWrapped(prompter, text, indent = "", changed) {
804
806
  const size = displayWidth(character);
805
807
  if (width + size > prompter.width) {
806
808
  flush();
807
- prompter.write(line + "\n");
809
+ prompter.write((style ? style(line) : line) + "\n");
808
810
  line = indent;
809
811
  width = displayWidth(indent);
810
812
  }
@@ -817,7 +819,7 @@ function writeWrapped(prompter, text, indent = "", changed) {
817
819
  width += size;
818
820
  }
819
821
  flush();
820
- prompter.write(line + "\n");
822
+ prompter.write((style ? style(line) : line) + "\n");
821
823
  }
822
824
  function changedRange(values, index) {
823
825
  if (values.length !== 2 || values[0] === values[1]) return void 0;
@@ -839,6 +841,37 @@ function compareEntries(prompter, versions) {
839
841
  const key = field.replace(/^env\./, "");
840
842
  return versions.map(({ entry }) => field.startsWith("env.") && visibleField.test(key) ? Object.hasOwn(entry.env, key) ? plainText(entry.env[key]) || "(empty)" : "(not set)" : "(hidden)");
841
843
  });
844
+ if (versions.length === 2 && prompter.width >= 90) {
845
+ const width = Math.floor((prompter.width - 3) / 2);
846
+ const wrap = (value, changed) => {
847
+ const lines = [{ text: "", width: 0 }];
848
+ for (const [index, character] of Array.from(value).entries()) {
849
+ const size = displayWidth(character);
850
+ if (lines[lines.length - 1].width + size > width) lines.push({ text: "", width: 0 });
851
+ const line = lines[lines.length - 1];
852
+ line.text += changed && index >= changed[0] && index < changed[1] ? prompter.paint.accent(prompter.paint.bold(character)) : character;
853
+ line.width += size;
854
+ }
855
+ return lines;
856
+ };
857
+ const row = (left, right) => {
858
+ for (let line = 0; line < Math.max(left.length, right.length); line++) {
859
+ prompter.write(`${left[line]?.text ?? ""}${" ".repeat(width - (left[line]?.width ?? 0))} | ${right[line]?.text ?? ""}
860
+ `);
861
+ }
862
+ };
863
+ prompter.write("\n");
864
+ row(wrap(`[1] ${plainText(versions[0].label)}`), wrap(`[2] ${plainText(versions[1].label)}`));
865
+ for (let field = 0; field < fields.length; field++) {
866
+ prompter.write(`
867
+ ${prompter.paint.dim(fieldName(fields[field]))}
868
+ `);
869
+ row(wrap(values[field][0], changedRange(values[field], 0)), wrap(values[field][1], changedRange(values[field], 1)));
870
+ }
871
+ if (values.some((row2) => row2.includes("(hidden)"))) prompter.write(prompter.paint.dim("\nSecret values are hidden.\n"));
872
+ prompter.write("\n");
873
+ return;
874
+ }
842
875
  for (let index = 0; index < versions.length; index++) {
843
876
  prompter.write("\n");
844
877
  writeWrapped(prompter, `[${index + 1}] ${versions[index].label}`);
@@ -1045,10 +1078,10 @@ async function checkConnections(prompter, entries, check = checkStartup) {
1045
1078
  const missing = prefixes?.filter((prefix) => !covered.has(prefix)) ?? [];
1046
1079
  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
1080
  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."}`));
1081
+ 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."}`), "", void 0, result.ok ? prompter.paint.success : prompter.paint.error);
1049
1082
  if (!result.ok) {
1050
1083
  failed = true;
1051
- for (const line of result.stderr) writeWrapped(prompter, safe(line), " ");
1084
+ for (const line of result.stderr) writeWrapped(prompter, safe(line), " ", void 0, prompter.paint.dim);
1052
1085
  }
1053
1086
  }
1054
1087
  if (failed) prompter.write("Settings remain saved. Fix failed startup checks before restarting or reconnecting your AI clients.\n");
@@ -1478,11 +1511,26 @@ var Registry = class {
1478
1511
  inspect() {
1479
1512
  return this.reconcile(this.read(), false);
1480
1513
  }
1514
+ formatResults(results, prefix = "", paint, reviewHint) {
1515
+ return formatResults(results, prefix, {
1516
+ paint,
1517
+ reviewHint,
1518
+ connectionCount: Object.values(this.read().connections).filter((item) => item.entry).length
1519
+ });
1520
+ }
1481
1521
  reconcile(state, apply, kinds) {
1482
1522
  const results = [];
1483
1523
  const active = (kind) => Object.entries(state.connections).filter(([, item]) => item.kind === kind && item.entry);
1484
1524
  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." });
1525
+ for (const client of state.clients) results.push({
1526
+ client,
1527
+ name: sessionName(kind),
1528
+ kind,
1529
+ connections: active(kind).map(([name]) => name),
1530
+ status: "pending",
1531
+ waitingFor: "runtime",
1532
+ detail: "Install it from Runtime, then run Sync. Your existing entries keep working."
1533
+ });
1486
1534
  }
1487
1535
  const work = [
1488
1536
  ...Object.entries(state.connections).filter(([, connection]) => !kinds || kinds.includes(connection.kind)).map(([name, connection]) => ({ name, connection, bundle: false })),
@@ -1498,7 +1546,15 @@ var Registry = class {
1498
1546
  const previous = connection.applied[client];
1499
1547
  if (!desired && !previous) continue;
1500
1548
  const target = this.targets.find((candidate) => candidate.id === client);
1501
- const report = (status, detail) => results.push({ client, name, status, detail });
1549
+ const report = (status, detail, waitingFor) => results.push({
1550
+ client,
1551
+ name,
1552
+ status,
1553
+ detail,
1554
+ kind: connection.kind,
1555
+ connections: bundle ? active(connection.kind).map(([name2]) => name2) : [name],
1556
+ ...waitingFor ? { waitingFor } : {}
1557
+ });
1502
1558
  if (!target) {
1503
1559
  report("error", "Service adapter unavailable; use a newer package to retry.");
1504
1560
  continue;
@@ -1516,20 +1572,18 @@ var Registry = class {
1516
1572
  report("removed", "Removed");
1517
1573
  } else if (exists && (!actual || !previous || hash !== previous)) {
1518
1574
  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
1575
  } else {
1522
1576
  if (!desired && state.clients.includes(client)) {
1523
1577
  if (!bundle && this.isSessionMode(connection.kind, state) && active(connection.kind).length) {
1524
1578
  const session = state.sessions?.[connection.kind];
1525
1579
  const replacement2 = entries[sessionName(connection.kind)];
1526
1580
  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.");
1581
+ report("pending", "Connection entry kept until the session server is applied.", "session");
1528
1582
  continue;
1529
1583
  }
1530
1584
  }
1531
1585
  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.");
1586
+ report("pending", "Session entry kept until all connection entries are applied.", "connections");
1533
1587
  continue;
1534
1588
  }
1535
1589
  }
@@ -1540,7 +1594,11 @@ var Registry = class {
1540
1594
  replacement = state.connections[replacement.replacement];
1541
1595
  }
1542
1596
  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.");
1597
+ report("pending", "Old name kept until the new name is applied.", "rename");
1598
+ continue;
1599
+ }
1600
+ if (!apply) {
1601
+ report("pending", desired ? "Needs applying" : "Needs removal");
1544
1602
  continue;
1545
1603
  }
1546
1604
  const result = desired ? target.writeEntry(name, desired) : target.removeEntry(name);
@@ -1631,7 +1689,7 @@ var Registry = class {
1631
1689
  const registry = this;
1632
1690
  let snapshot;
1633
1691
  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.") });
1692
+ const saved = (results) => ({ status: "written", detail: registry.formatResults(results, "Saved in the shared AKMS catalog.") });
1635
1693
  const prepare = (name, entry, oldName) => {
1636
1694
  if (!registry.read().clients.length) throw new RegistryError("Register at least one AI service before saving a connection.");
1637
1695
  const prior = snapshot && own(snapshot, oldName ?? name) ? snapshot[oldName ?? name] : void 0;
@@ -1660,30 +1718,58 @@ var Registry = class {
1660
1718
  };
1661
1719
  }
1662
1720
  };
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`);
1721
+ function formatResults(results, prefix = "", options = {}) {
1722
+ const plain = (text) => text;
1723
+ const paint = options.paint ?? { success: plain, warning: plain, error: plain, dim: plain, bold: plain };
1724
+ const safe = (text) => text.replace(/[\x00-\x1f\x7f]/g, " ");
1725
+ const lines = prefix ? [paint.bold(prefix)] : [];
1726
+ const total = options.connectionCount ?? new Set(results.flatMap((item) => item.connections ?? [item.name])).size;
1727
+ lines.push(` Connections: ${total}`);
1670
1728
  const updated = results.filter((item) => item.status === "synced" && item.detail === "Applied").length;
1671
1729
  const unchanged = results.filter((item) => item.status === "synced" && item.detail !== "Applied").length;
1672
1730
  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")]
1731
+ const failed = results.filter((item) => item.status === "error");
1732
+ lines.push(` Service entries: ${paint.success(`${updated} updated \xB7 ${unchanged} already up to date`)}${removed ? ` \xB7 ${paint.success(`${removed} removed`)}` : ""} \xB7 ${(failed.length ? paint.error : paint.dim)(`${failed.length} failed`)}`);
1733
+ if (!results.length) lines.push(paint.dim(" No shared connections need applying."));
1734
+ if (results.length && results.every((item) => item.status === "synced" || item.status === "removed")) lines.push(paint.success(" OK: all service entries are up to date."));
1735
+ const waiting = results.filter((item) => item.status === "pending");
1736
+ for (const kind of KINDS) {
1737
+ const runtime = waiting.filter((item) => item.kind === kind && item.waitingFor === "runtime");
1738
+ const kept = waiting.filter((item) => item.kind === kind && item.waitingFor === "session");
1739
+ if (!runtime.length && !kept.length) continue;
1740
+ lines.push(`
1741
+ ${paint.warning(runtime.length ? `Waiting for runtime - ${kind.toUpperCase()}` : `Waiting for ${sessionName(kind)}`)}`);
1742
+ if (runtime.length) lines.push(" Install it from Runtime, then run Sync. Your existing entries keep working.");
1743
+ else lines.push(" Resolve this session entry in Sync. Existing connection entries stay until it is ready.");
1744
+ const retained = /* @__PURE__ */ new Map();
1745
+ for (const client of new Set(kept.map((item) => item.client))) {
1746
+ const names = [...new Set(kept.filter((item) => item.client === client).map((item) => item.name))];
1747
+ const key = JSON.stringify([...names].sort());
1748
+ const group = retained.get(key);
1749
+ if (group) group.clients.push(client);
1750
+ else retained.set(key, { clients: [client], names });
1751
+ }
1752
+ for (const group of retained.values()) {
1753
+ const clients = group.clients.map((client) => CLIENT_LABELS2[client]).join(" \xB7 ");
1754
+ lines.push(paint.dim(` Kept in ${clients} until ${sessionName(kind)} is applied: ${group.names.map(safe).join(", ")}`));
1755
+ }
1756
+ }
1757
+ for (const [heading, rows, color] of [
1758
+ ["Needs review (existing settings kept)", results.filter((item) => item.status === "conflict"), paint.warning],
1759
+ ["Waiting for Sync", waiting.filter((item) => !item.waitingFor || !["runtime", "session"].includes(item.waitingFor)), paint.warning],
1760
+ ["Could not finish", failed, paint.error]
1677
1761
  ]) {
1678
1762
  if (!rows.length) continue;
1679
1763
  lines.push(`
1680
- ${heading}:`);
1764
+ ${color(`${heading}:`)}`);
1681
1765
  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}`);
1766
+ lines.push(` ${safe(name)}`);
1767
+ for (const row of rows.filter((item) => item.name === name)) lines.push(` ${row.status === "error" ? paint.error("FAILED ") : ""}${CLIENT_LABELS2[row.client]}: ${safe(row.detail)}`);
1684
1768
  }
1685
1769
  }
1686
- if (attention.length) lines.push("\nNext: open Sync to compare settings or retry unfinished changes.");
1770
+ if (results.some((item) => item.status === "conflict")) lines.push(paint.warning(`
1771
+ ${options.reviewHint ?? "Next: open Sync to compare settings and choose which version to use."}`));
1772
+ if (failed.length) lines.push(paint.error("\nNext: fix the errors above, then run Sync again."));
1687
1773
  return lines.join("\n");
1688
1774
  }
1689
1775
 
@@ -1728,7 +1814,7 @@ async function runtimeMenu(prompter, registry, requestedKind) {
1728
1814
  prompter.write(`New kinds will use ${mode === "catalog" ? "session servers" : "individual entries"}. Existing kinds keep their settings.
1729
1815
  `);
1730
1816
  if (await prompter.askYesNo("Change the default for new kinds?", false)) {
1731
- prompter.write(`${formatResults(registry.setDefaultMode(mode), "Default for new kinds saved.")}
1817
+ prompter.write(`${registry.formatResults(registry.setDefaultMode(mode), "Default for new kinds saved.", prompter.paint)}
1732
1818
  `);
1733
1819
  await prompter.acknowledge();
1734
1820
  }
@@ -1757,7 +1843,7 @@ async function runtimeMenu(prompter, registry, requestedKind) {
1757
1843
  if (!await prompter.askYesNo("Apply this configuration mode?", false)) continue;
1758
1844
  const results2 = registry.applySession(plan2);
1759
1845
  prompter.clearScreen();
1760
- prompter.write(`${formatResults(results2, "Configuration mode saved.")}
1846
+ prompter.write(`${registry.formatResults(results2, "Configuration mode saved.", prompter.paint)}
1761
1847
  `);
1762
1848
  await registry.checkSaved(prompter, plan2.names);
1763
1849
  prompter.write("Restart or reconnect your AI clients to load the new server layout. Fix startup failures first.\n");
@@ -1793,7 +1879,7 @@ Convert ${plan.names.length} connection(s):
1793
1879
  if (!await prompter.askYesNo("Apply these launch commands to registered services?", true)) continue;
1794
1880
  const results = registry.applyRuntime(plan);
1795
1881
  prompter.clearScreen();
1796
- prompter.write(`${formatResults(results, "Runtime settings saved.")}
1882
+ prompter.write(`${registry.formatResults(results, "Runtime settings saved.", prompter.paint)}
1797
1883
 
1798
1884
  Restart or reconnect AI clients to replace running npx processes.
1799
1885
  `);
@@ -1915,7 +2001,7 @@ async function migrationMenu(prompter, registry) {
1915
2001
  if (!await prompter.askYesNo("Migrate registry and apply these changes?", false)) return;
1916
2002
  const results = registry.applyMigration(plan);
1917
2003
  prompter.clearScreen();
1918
- prompter.write(`${formatResults(results, "Registry migration saved.")}
2004
+ prompter.write(`${registry.formatResults(results, "Registry migration saved.", prompter.paint)}
1919
2005
  `);
1920
2006
  await registry.checkSaved(prompter, plan.kinds.filter((item) => item.status !== "individual").flatMap((item) => item.names));
1921
2007
  prompter.write("Reconnect affected clients after changes to their server layout. Fix startup failures first.\n");
@@ -1930,6 +2016,12 @@ async function migrationMenu(prompter, registry) {
1930
2016
  }
1931
2017
 
1932
2018
  // src/modules/registration/menu.ts
2019
+ function pendingImports(registry, kind) {
2020
+ const state = registry.read();
2021
+ const entries = registry.candidates(kind).entries.filter((candidate) => !Object.hasOwn(state.connections, candidate.name));
2022
+ const names = [...new Set(entries.map((candidate) => candidate.name))];
2023
+ return { names, differing: names.filter((name) => new Set(entries.filter((item) => item.name === name).map((item) => entryHash(item.entry))).size > 1).length };
2024
+ }
1933
2025
  function preview(prompter, entry) {
1934
2026
  if (!entry) {
1935
2027
  prompter.write(" Remove this connection from the service.\n");
@@ -2005,8 +2097,8 @@ Remove only AKMS-managed entries from these services; keep the shared catalog.
2005
2097
  const results = registry.register([...selected], initial.clients, plan.imports);
2006
2098
  prompter.clearScreen();
2007
2099
  prompter.write(`
2008
- ${formatResults(results, `Registration saved.
2009
- AI services: ${[...selected].map((id) => CLIENT_LABELS2[id]).join(" \xB7 ") || "none"}`)}
2100
+ ${registry.formatResults(results, `Registration saved.
2101
+ AI services: ${[...selected].map((id) => CLIENT_LABELS2[id]).join(" \xB7 ") || "none"}`, prompter.paint)}
2010
2102
  `);
2011
2103
  if (importNames.length) prompter.write(`Imported ${importNames.length} existing connection(s) into the shared list.
2012
2104
  `);
@@ -2079,7 +2171,7 @@ Import existing connections:
2079
2171
  if (await prompter.askYesNo("Import matching connections and apply them?", true)) {
2080
2172
  const results2 = registry.register(state.clients, state.clients, matching.imports);
2081
2173
  prompter.clearScreen();
2082
- prompter.write(`${formatResults(results2, "Import complete")}
2174
+ prompter.write(`${registry.formatResults(results2, "Import complete", prompter.paint)}
2083
2175
  `);
2084
2176
  await prompter.acknowledge();
2085
2177
  }
@@ -2107,7 +2199,7 @@ ${plainText(name)} \xB7 Choose the settings to use on all registered services.
2107
2199
  const results = registry.importEntry(destination, chosen, candidates);
2108
2200
  prompter.clearScreen();
2109
2201
  prompter.write(`
2110
- ${formatResults(results, "Import complete")}
2202
+ ${registry.formatResults(results, "Import complete", prompter.paint)}
2111
2203
  `);
2112
2204
  if (destination !== name) prompter.write(" The original entries were kept under their old name.\n");
2113
2205
  await prompter.acknowledge();
@@ -2127,19 +2219,28 @@ async function syncMenu(prompter, registry) {
2127
2219
  const results = pendingResults ?? registry.sync();
2128
2220
  pendingResults = void 0;
2129
2221
  prompter.write(`
2130
- ${formatResults(results, "Sync results")}
2222
+ ${registry.formatResults(results, "Sync results", prompter.paint, "Next: choose an item below to compare settings and pick a version.")}
2131
2223
  `);
2224
+ const imports = pendingImports(registry);
2225
+ if (imports.names.length) prompter.write(prompter.paint.warning(`
2226
+ Not yet imported: ${imports.names.length} connection(s)${imports.differing === imports.names.length ? " with differing settings" : ""} - open Import existing connections.
2227
+ `));
2132
2228
  const conflicts = results.filter((result) => result.status === "conflict");
2133
- if (!conflicts.length) {
2229
+ if (!conflicts.length && !imports.names.length) {
2134
2230
  await prompter.acknowledge();
2135
2231
  return;
2136
2232
  }
2137
2233
  const picked = await prompter.select("Review differences", [
2138
2234
  ...conflicts.map((item, index) => ({ value: String(index), label: `${CLIENT_LABELS2[item.client]} \xB7 ${item.name}` })),
2235
+ ...imports.names.length ? [{ value: "import", key: "import", label: "Import existing connections", hint: `${imports.names.length} connection(s) to review` }] : [],
2139
2236
  { value: "back", key: "b", label: "Later \u2014 keep existing settings" }
2140
2237
  ], { cancelValue: "back" });
2141
2238
  if (picked === "back") return;
2142
2239
  try {
2240
+ if (picked === "import") {
2241
+ await importMenu(prompter, registry);
2242
+ continue;
2243
+ }
2143
2244
  prompter.clearScreen();
2144
2245
  const conflict = conflicts[Number(picked)];
2145
2246
  const state = registry.read();
@@ -2173,12 +2274,17 @@ ${formatResults(results, "Sync results")}
2173
2274
  const client = CLIENT_LABELS2[conflict.client];
2174
2275
  const registered = state.clients.includes(conflict.client);
2175
2276
  const sessionMode = registry.isSessionMode(connection.kind, state);
2176
- const removing = !connection.entry || !registered || sessionMode;
2277
+ const removing = !connection.entry || !registered;
2278
+ const retiring = sessionMode && !removing;
2177
2279
  const subject = connection.entry && descriptionOnly([connection.entry, actual]) ? "description" : "settings";
2178
2280
  writeWrapped(prompter, `Sync / ${plainText(conflict.name)}`);
2179
2281
  writeWrapped(prompter, `Registered services: ${state.clients.map((id) => CLIENT_LABELS2[id]).join(" \xB7 ") || "none"}`);
2180
2282
  prompter.write("\n");
2181
2283
  if (!removing && connection.entry) {
2284
+ if (retiring) {
2285
+ writeWrapped(prompter, `Choose settings for ${sessionName(connection.kind)} before removing the old ${client} entry.`);
2286
+ writeWrapped(prompter, "The old entry stays until the session server is ready.");
2287
+ }
2182
2288
  compareEntries(prompter, [{ label: "Saved in AKMS", entry: connection.entry }, { label: `Currently in ${client}`, entry: actual }]);
2183
2289
  } else {
2184
2290
  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.");
@@ -2190,8 +2296,8 @@ Currently in ${client}
2190
2296
  prompter.write("\n");
2191
2297
  }
2192
2298
  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" }] : [],
2299
+ { value: "shared", label: removing ? `Remove from ${client}` : retiring ? "Use 1: keep AKMS settings" : `Use 1: AKMS -> ${client}`, hint: removing ? "finish the pending removal" : retiring ? `use version 1 in ${sessionName(connection.kind)}` : `replace its ${subject} with version 1` },
2300
+ ...registered ? [{ value: "client", label: removing ? `Restore from ${client}` : `Use 2: ${client} -> all ${state.clients.length} services`, hint: retiring ? `save version 2 in AKMS for ${sessionName(connection.kind)}` : "save in AKMS and sync registered services" }] : [],
2195
2301
  { value: "back", key: "b", label: "Back without changes" }
2196
2302
  ];
2197
2303
  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" });
@@ -2200,8 +2306,9 @@ Currently in ${client}
2200
2306
  if (choice === "client") {
2201
2307
  writeWrapped(prompter, `Save the ${client} version in AKMS.`);
2202
2308
  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?`;
2309
+ } else writeWrapped(prompter, removing ? `Remove ${plainText(conflict.name)} from ${client}.` : retiring ? `Keep the saved AKMS ${subject}.` : `Copy the saved AKMS ${subject} to ${client}.`);
2310
+ if (retiring) writeWrapped(prompter, `Keep the old ${client} entry until ${sessionName(connection.kind)} is ready, then remove it.`);
2311
+ const confirmation = choice === "client" ? `Save in AKMS and sync ${state.clients.length} services?` : removing ? `Remove from ${client}?` : retiring ? "Keep version 1 and finish Sync?" : `Update ${client} with version 1?`;
2205
2312
  if (await prompter.askYesNo(confirmation, false)) {
2206
2313
  pendingResults = registry.resolve(conflict.client, conflict.name, actual, choice === "client");
2207
2314
  }
@@ -2920,7 +3027,7 @@ ${failure}
2920
3027
  const state = registry.read();
2921
3028
  const clients = state.clients;
2922
3029
  const migration = registry.needsMigration(state);
2923
- if (migration) prompter.write("Registry update available. Choose Migrate registry to review the changes.\n");
3030
+ if (migration) prompter.write(prompter.paint.warning("Registry update available. Choose Migrate registry to review the changes.\n"));
2924
3031
  const picked = await prompter.select("AKMS MCP \xB7 Connection setup", [
2925
3032
  { value: "db", label: "Database", hint: "PostgreSQL connections" },
2926
3033
  { value: "ssh", label: "SSH", hint: "Remote hosts" },
@@ -3208,7 +3315,7 @@ async function main() {
3208
3315
  if (argumentsList.showVersion == true) {
3209
3316
  let version = "development";
3210
3317
  if (true) {
3211
- version = "0.3.1";
3318
+ version = "0.3.2";
3212
3319
  }
3213
3320
  process.stdout.write(`${version}
3214
3321
  `);
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@akms/mcp",
3
- "version": "0.3.1",
3
+ "version": "0.3.2",
4
4
  "description": "Interactive launcher for configuring installed AKMS database, SSH, WSL and S3 MCP runtimes.",
5
5
  "keywords": [
6
6
  "mcp",