@yawlabs/mcph 0.53.0 → 0.55.0

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 (2) hide show
  1. package/dist/index.js +1785 -1169
  2. package/package.json +1 -1
package/dist/index.js CHANGED
@@ -245,9 +245,9 @@ async function migrateLegacyConfigPaths(opts) {
245
245
  }
246
246
  }
247
247
  async function findLegacyProjectRoot(cwd, home) {
248
- const { resolve: resolve4, dirname: dirname2 } = await import("path");
249
- const homeResolved = resolve4(home);
250
- let dir = resolve4(cwd);
248
+ const { resolve: resolve5, dirname: dirname2 } = await import("path");
249
+ const homeResolved = resolve5(home);
250
+ let dir = resolve5(cwd);
251
251
  let prev = "";
252
252
  while (dir !== prev) {
253
253
  if (dir === homeResolved) return null;
@@ -921,7 +921,7 @@ Delete token (save this): ${result.deleteToken}
921
921
  return 0;
922
922
  }
923
923
  function runTest(args) {
924
- return new Promise((resolve4) => {
924
+ return new Promise((resolve5) => {
925
925
  const child = spawn("npx", ["-y", "@yawlabs/mcp-compliance", "test", "--format", "json", ...args], {
926
926
  stdio: ["ignore", "pipe", "inherit"],
927
927
  shell: process.platform === "win32"
@@ -934,7 +934,7 @@ function runTest(args) {
934
934
  process.stderr.write(`
935
935
  Failed to launch mcp-compliance: ${err.message}
936
936
  `);
937
- resolve4(null);
937
+ resolve5(null);
938
938
  });
939
939
  child.on("close", (code) => {
940
940
  try {
@@ -943,15 +943,15 @@ Failed to launch mcp-compliance: ${err.message}
943
943
  process.stderr.write(`
944
944
  mcp-compliance returned unexpected JSON (exit ${code}).
945
945
  `);
946
- resolve4(null);
946
+ resolve5(null);
947
947
  return;
948
948
  }
949
- resolve4(parsed);
949
+ resolve5(parsed);
950
950
  } catch {
951
951
  process.stderr.write(`
952
952
  mcp-compliance exited ${code} without valid JSON output.
953
953
  `);
954
- resolve4(null);
954
+ resolve5(null);
955
955
  }
956
956
  });
957
957
  });
@@ -990,10 +990,10 @@ Publish failed: ${err?.message ?? String(err)}
990
990
  }
991
991
 
992
992
  // src/doctor-cmd.ts
993
- import { existsSync, readFileSync, statSync } from "fs";
994
- import { readFile as readFile3 } from "fs/promises";
995
- import { homedir as homedir4 } from "os";
996
- import { join as join4 } from "path";
993
+ import { existsSync as existsSync3, readFileSync, statSync } from "fs";
994
+ import { readFile as readFile5 } from "fs/promises";
995
+ import { homedir as homedir6 } from "os";
996
+ import { join as join6 } from "path";
997
997
 
998
998
  // src/analytics.ts
999
999
  import { request as request3 } from "undici";
@@ -1453,6 +1453,17 @@ function pathFor(client, scope, os, base) {
1453
1453
  throw new Error(`Unhandled client: ${client}`);
1454
1454
  }
1455
1455
  function buildLaunchEntry(opts) {
1456
+ if (opts.upstream) {
1457
+ const { command, args, env } = opts.upstream;
1458
+ if (opts.os === "windows") {
1459
+ const wrapped = { command: "cmd", args: ["/c", command, ...args] };
1460
+ if (env && Object.keys(env).length > 0) wrapped.env = { ...env };
1461
+ return wrapped;
1462
+ }
1463
+ const entry2 = { command, args: [...args] };
1464
+ if (env && Object.keys(env).length > 0) entry2.env = { ...env };
1465
+ return entry2;
1466
+ }
1456
1467
  const pkg = opts.pkg ?? "@yawlabs/mcph@latest";
1457
1468
  const entry = opts.os === "windows" ? { command: "cmd", args: ["/c", "npx", "-y", pkg] } : { command: "npx", args: ["-y", pkg] };
1458
1469
  if (opts.token) entry.env = { MCPH_TOKEN: opts.token };
@@ -1609,1236 +1620,1817 @@ async function reportTools(serverId, tools) {
1609
1620
  }
1610
1621
  }
1611
1622
 
1612
- // src/usage-hints.ts
1613
- var MAX_PEERS = 3;
1614
- var MIN_SUCCESS_TO_SHOW = 1;
1615
- var RELIABILITY_MIN_OBSERVATIONS = 3;
1616
- var RELIABILITY_THRESHOLD = 0.8;
1617
- function buildCoUsageMap(packs) {
1618
- const result = /* @__PURE__ */ new Map();
1619
- for (const pack of packs) {
1620
- for (const ns of pack.namespaces) {
1621
- const bucket = result.get(ns) ?? /* @__PURE__ */ new Set();
1622
- for (const peer of pack.namespaces) {
1623
- if (peer !== ns) bucket.add(peer);
1624
- }
1625
- result.set(ns, bucket);
1626
- }
1623
+ // src/try-cmd.ts
1624
+ import { createHash } from "crypto";
1625
+ import { existsSync as existsSync2 } from "fs";
1626
+ import { chmod as chmod2, mkdir as mkdir3, readFile as readFile4, readdir, unlink as unlink2 } from "fs/promises";
1627
+ import { homedir as homedir5, hostname, userInfo } from "os";
1628
+ import { join as join5, resolve as resolve3 } from "path";
1629
+ import { request as request5 } from "undici";
1630
+
1631
+ // src/install-cmd.ts
1632
+ import { existsSync } from "fs";
1633
+ import { chmod, readFile as readFile3 } from "fs/promises";
1634
+ import { homedir as homedir4 } from "os";
1635
+ import { join as join4, resolve as resolve2 } from "path";
1636
+ import { createInterface } from "readline/promises";
1637
+ var USAGE = "Usage: mcph install <claude-code|claude-desktop|cursor|vscode> [--scope user|project|local]\n [--token <mcp_pat_\u2026>] [--project-dir <path>] [--os macos|linux|windows]\n [--force | --skip] [--dry-run] [--no-mcph-config]\n mcph install --list (detect clients; no writes)\n mcph install --all [--token <mcp_pat_\u2026>] (install into every detected client)";
1638
+ async function runInstall(opts) {
1639
+ const stdout = opts.io?.stdout ?? process.stdout;
1640
+ const stderr = opts.io?.stderr ?? process.stderr;
1641
+ const messages = [];
1642
+ const log2 = (s) => {
1643
+ messages.push(s);
1644
+ stdout.write(`${s}
1645
+ `);
1646
+ };
1647
+ const err = (s) => {
1648
+ messages.push(s);
1649
+ stderr.write(`${s}
1650
+ `);
1651
+ };
1652
+ if (opts.listOnly && opts.all) {
1653
+ err("mcph install: --list and --all are mutually exclusive");
1654
+ return { written: [], wouldWrite: [], messages, exitCode: 2 };
1627
1655
  }
1628
- const sorted = /* @__PURE__ */ new Map();
1629
- for (const [ns, peers] of result) {
1630
- sorted.set(ns, Array.from(peers).sort());
1656
+ if (opts.listOnly) return runInstallList(opts, log2);
1657
+ if (opts.all) return runInstallAll(opts, log2, err);
1658
+ if (opts.force && opts.skip) {
1659
+ err("mcph install: --force and --skip are mutually exclusive");
1660
+ return { written: [], wouldWrite: [], messages, exitCode: 2 };
1631
1661
  }
1632
- return sorted;
1633
- }
1634
- function formatUsageHint(usage, coUsedWith) {
1635
- const parts = [];
1636
- if (usage && usage.succeeded >= MIN_SUCCESS_TO_SHOW) {
1637
- parts.push(`used ${usage.succeeded}x`);
1662
+ if (!opts.clientId) {
1663
+ err(`mcph install: client argument required
1664
+ ${USAGE}`);
1665
+ return { written: [], wouldWrite: [], messages, exitCode: 2 };
1638
1666
  }
1639
- if (coUsedWith.length > 0) {
1640
- const shown = coUsedWith.slice(0, MAX_PEERS);
1641
- const more = coUsedWith.length - shown.length;
1642
- const names = shown.map((n) => `"${n}"`).join(", ");
1643
- const tail = more > 0 ? ` +${more} more` : "";
1644
- parts.push(`often loaded with ${names}${tail}`);
1667
+ const target = INSTALL_TARGETS.find((t) => t.clientId === opts.clientId);
1668
+ if (!target) {
1669
+ err(`mcph install: unknown client ${opts.clientId}
1670
+ ${USAGE}`);
1671
+ return { written: [], wouldWrite: [], messages, exitCode: 2 };
1645
1672
  }
1646
- if (parts.length === 0) return null;
1647
- return `usage: ${parts.join("; ")}`;
1648
- }
1649
- function formatReliabilityWarning(usage) {
1650
- if (!usage || usage.dispatched < RELIABILITY_MIN_OBSERVATIONS) return null;
1651
- const rate = usage.succeeded / usage.dispatched;
1652
- if (rate >= RELIABILITY_THRESHOLD) return null;
1653
- const pct = Math.round(rate * 100);
1654
- return `reliability: ${pct}% success across ${usage.dispatched} past calls`;
1655
- }
1656
- function selectFlakyNamespaces(entries, limit) {
1657
- if (limit <= 0) return [];
1658
- return Array.from(entries).filter(({ usage }) => {
1659
- if (usage.dispatched < RELIABILITY_MIN_OBSERVATIONS) return false;
1660
- return usage.succeeded / usage.dispatched < RELIABILITY_THRESHOLD;
1661
- }).sort((a, b) => {
1662
- const aRate = a.usage.succeeded / a.usage.dispatched;
1663
- const bRate = b.usage.succeeded / b.usage.dispatched;
1664
- if (aRate !== bRate) return aRate - bRate;
1665
- if (a.usage.dispatched !== b.usage.dispatched) return b.usage.dispatched - a.usage.dispatched;
1666
- return a.namespace.localeCompare(b.namespace);
1667
- }).slice(0, limit);
1668
- }
1669
-
1670
- // src/doctor-cmd.ts
1671
- var VERSION = true ? "0.53.0" : "dev";
1672
- async function runDoctor(opts = {}) {
1673
- if (opts.json) return runDoctorJson(opts);
1674
- const lines = [];
1675
- const write = opts.out ?? ((s) => process.stdout.write(s));
1676
- const print = (s = "") => {
1677
- lines.push(s);
1678
- write(`${s}
1679
- `);
1680
- };
1681
- const cwd = opts.cwd ?? process.cwd();
1682
- const home = opts.home ?? homedir4();
1683
1673
  const os = opts.os ?? CURRENT_OS;
1684
- const env = opts.env ?? process.env;
1685
- print(`mcph doctor \u2014 ${(/* @__PURE__ */ new Date()).toISOString()}`);
1686
- print(`mcph version: ${VERSION}`);
1687
- print(`platform: ${os}`);
1688
- print("");
1689
- const config = await loadMcphConfig({ cwd, home, env });
1690
- print("CONFIG FILES");
1691
- if (config.loadedFiles.length === 0) {
1692
- print(" (none \u2014 using defaults + env)");
1693
- } else {
1694
- for (const f of config.loadedFiles) {
1695
- print(` ${f.scope.padEnd(7)} ${f.path}${schemaSuffix(f)}`);
1696
- }
1674
+ if (!target.availableOn.includes(os)) {
1675
+ const fix = target.clientId === "claude-desktop" && os === "linux" ? "Anthropic ships Claude Desktop on macOS and Windows only. Install Claude Code or Cursor instead." : "Pick a different client or pass --os to override.";
1676
+ err(`mcph install: ${target.label} is not available on ${os}.
1677
+ ${fix}`);
1678
+ return { written: [], wouldWrite: [], messages, exitCode: 2 };
1697
1679
  }
1698
- print("");
1699
- print("TOKEN");
1700
- print(` value: ${tokenFingerprint(config.token)}`);
1701
- print(` source: ${config.tokenSource}`);
1702
- print("");
1703
- print("API BASE");
1704
- print(` value: ${config.apiBase}`);
1705
- print(` source: ${config.apiBaseSource}`);
1706
- print("");
1707
- renderEnvSection({ env, print });
1708
- await renderStateSection({ home, env, print });
1709
- await renderReliabilitySection({ home, env, print });
1710
- renderBackgroundPostersSection({ print });
1711
- const claudeConfigDir = env.CLAUDE_CONFIG_DIR && env.CLAUDE_CONFIG_DIR.length > 0 ? env.CLAUDE_CONFIG_DIR : void 0;
1712
- const clients = probeClients({ home, os, cwd, claudeConfigDir });
1713
- print("INSTALLED CLIENTS (probed config files)");
1714
- for (const c of clients) {
1715
- const status = c.unavailable ? "unavailable on this OS" : c.malformed ? "exists but JSON is malformed \u2014 fix or rerun `mcph install`" : c.hasMcphEntry ? `OK \u2014 has "${ENTRY_NAME}" entry` : c.exists ? `present, no "${ENTRY_NAME}" entry \u2014 run \`mcph install ${c.clientId}${c.scope === "user" ? "" : ` --scope ${c.scope}`}\`` : `not configured \u2014 run \`mcph install ${c.clientId}${c.scope === "user" ? "" : ` --scope ${c.scope}`}\``;
1716
- const label = INSTALL_TARGETS.find((t) => t.clientId === c.clientId)?.label ?? c.clientId;
1717
- print(` ${label} (${c.scope}): ${status}`);
1718
- print(` ${c.path}`);
1680
+ const scope = opts.scope ?? (target.scopes.find((s) => s.scope === "user") ? "user" : target.scopes[0].scope);
1681
+ const scopeSpec = target.scopes.find((s) => s.scope === scope);
1682
+ if (!scopeSpec) {
1683
+ err(
1684
+ `mcph install: ${target.label} does not support scope "${scope}". Available: ${target.scopes.map((s) => s.scope).join(", ")}`
1685
+ );
1686
+ return { written: [], wouldWrite: [], messages, exitCode: 2 };
1719
1687
  }
1720
- print("");
1721
- if (config.warnings.length > 0) {
1722
- print("WARNINGS");
1723
- for (const w of config.warnings) print(` ! ${w}`);
1724
- print("");
1688
+ const projectDir = scopeSpec.requiresProjectDir ? resolve2(opts.projectDir ?? process.cwd()) : void 0;
1689
+ let resolved;
1690
+ try {
1691
+ resolved = resolveInstallPath({
1692
+ clientId: opts.clientId,
1693
+ scope,
1694
+ os,
1695
+ home: opts.home,
1696
+ projectDir,
1697
+ claudeConfigDir: opts.claudeConfigDir
1698
+ });
1699
+ } catch (e) {
1700
+ err(`mcph install: ${e.message}`);
1701
+ return { written: [], wouldWrite: [], messages, exitCode: 2 };
1725
1702
  }
1726
- const shadowHits = scanShellHistoryForShadows({ home, env });
1727
- if (shadowHits.length > 0) {
1728
- print("SHADOWED CLI USAGE (recent shell history)");
1729
- print(" Commands below have MCP servers that can replace them;");
1730
- print(" activate the server and prefer its tools over the CLI.");
1731
- for (const hit of shadowHits) {
1732
- const pluralHit = hit.count === 1 ? "time" : "times";
1733
- print(` ${hit.cli.padEnd(12)} ${hit.count} ${pluralHit} \u2192 server(s): ${hit.namespaces.join(", ")}`);
1734
- }
1735
- print("");
1703
+ log2(`Target: ${target.label} (${scope})`);
1704
+ log2(`File: ${resolved.absolute}`);
1705
+ let token6 = opts.token ?? null;
1706
+ if (!token6) {
1707
+ const cfg = await loadMcphConfig({ home: opts.home, cwd: process.cwd(), env: {} });
1708
+ token6 = cfg.token;
1736
1709
  }
1737
- const skipCheck = opts.skipRegistryCheck === true || Boolean(process.env.VITEST);
1738
- const latest = skipCheck ? null : await fetchLatestVersion(opts.registryFetch);
1739
- const staleHint = latest && VERSION !== "dev" && compareSemver(VERSION, latest) < 0 ? latest : null;
1740
- if (staleHint) {
1741
- print("UPGRADE AVAILABLE");
1742
- print(` Running ${VERSION}; npm latest is ${staleHint}.`);
1743
- print(" Run `mcph upgrade` to see the exact command for your install, or");
1744
- print(" `mcph upgrade --run` to execute it (global-npm installs only).");
1745
- print("");
1710
+ if (!token6) {
1711
+ err(
1712
+ "\nmcph install: no token available.\n Pass one with --token mcp_pat_\u2026, or run `mcph install` with --token once to seed ~/.mcph/config.json,\n or create the token at https://mcp.hosting \u2192 Settings \u2192 API Tokens."
1713
+ );
1714
+ return { written: [], wouldWrite: [], messages, exitCode: 1 };
1746
1715
  }
1747
- let exitCode = 0;
1748
- if (config.token === null) {
1749
- exitCode = 1;
1750
- print("DIAGNOSIS");
1751
- print(" No token resolved \u2014 mcph cannot start.");
1752
- print(" Run `mcph install <client> --token mcp_pat_\u2026` to seed ~/.mcph/config.json.");
1753
- } else if (config.warnings.length > 0) {
1754
- exitCode = 2;
1755
- print("DIAGNOSIS");
1756
- print(" Token present, but warnings above need attention.");
1757
- } else {
1758
- print("DIAGNOSIS");
1759
- print(staleHint ? " Healthy, but an upgrade is available (see above)." : " All good. mcph should start cleanly.");
1716
+ const newEntry = buildLaunchEntry({ os });
1717
+ const containerPath = resolved.containerPath;
1718
+ let existing = {};
1719
+ let existingHasEntry = false;
1720
+ if (existsSync(resolved.absolute)) {
1721
+ let raw;
1722
+ try {
1723
+ raw = await readFile3(resolved.absolute, "utf8");
1724
+ } catch (e) {
1725
+ err(`mcph install: cannot read ${resolved.absolute}: ${e.message}`);
1726
+ return { written: [], wouldWrite: [], messages, exitCode: 1 };
1727
+ }
1728
+ if (raw.trim().length > 0) {
1729
+ try {
1730
+ const parsed = parseJsonc(raw);
1731
+ if (typeof parsed !== "object" || parsed === null || Array.isArray(parsed)) {
1732
+ err(
1733
+ `mcph install: ${resolved.absolute} is not a JSON object \u2014 refusing to overwrite. Edit by hand or rename the file and re-run.`
1734
+ );
1735
+ return { written: [], wouldWrite: [], messages, exitCode: 1 };
1736
+ }
1737
+ existing = parsed;
1738
+ } catch (e) {
1739
+ err(
1740
+ `mcph install: ${resolved.absolute} is not valid JSON (${e.message}). Refusing to overwrite. Fix the file or rename it and re-run.`
1741
+ );
1742
+ return { written: [], wouldWrite: [], messages, exitCode: 1 };
1743
+ }
1744
+ }
1745
+ const container = readNested(existing, containerPath);
1746
+ if (typeof container === "object" && container !== null && !Array.isArray(container)) {
1747
+ existingHasEntry = ENTRY_NAME in container;
1748
+ }
1760
1749
  }
1761
- return { exitCode, lines, snapshot: { version: VERSION, config, clients } };
1762
- }
1763
- async function runDoctorJson(opts) {
1764
- const lines = [];
1765
- const write = opts.out ?? ((s) => process.stdout.write(s));
1766
- const cwd = opts.cwd ?? process.cwd();
1767
- const home = opts.home ?? homedir4();
1768
- const os = opts.os ?? CURRENT_OS;
1769
- const env = opts.env ?? process.env;
1770
- const timestamp = (/* @__PURE__ */ new Date()).toISOString();
1771
- const config = await loadMcphConfig({ cwd, home, env });
1772
- const claudeConfigDir = env.CLAUDE_CONFIG_DIR && env.CLAUDE_CONFIG_DIR.length > 0 ? env.CLAUDE_CONFIG_DIR : void 0;
1773
- const clients = probeClients({ home, os, cwd, claudeConfigDir });
1774
- const envVarNames = [
1775
- "MCPH_POLL_INTERVAL",
1776
- "MCPH_SERVER_CAP",
1777
- "MCPH_MIN_COMPLIANCE",
1778
- "MCPH_AUTO_LOAD",
1779
- "MCPH_AUTO_ACTIVATE",
1780
- "MCPH_PRUNE_RESPONSES"
1781
- ];
1782
- const envOverrides = {};
1783
- for (const name of envVarNames) {
1784
- const raw = env[name];
1785
- envOverrides[name] = raw === void 0 || raw === "" ? null : raw;
1786
- }
1787
- const persistRaw = env.MCPH_DISABLE_PERSISTENCE;
1788
- const persistDisabled = persistRaw !== void 0 && persistRaw !== "" && (persistRaw === "1" || persistRaw.toLowerCase() === "true");
1789
- const state = persistDisabled ? { disabled: true, path: null, savedAt: null, learningEntries: null, packHistoryEntries: null } : await (async () => {
1790
- const filePath = join4(userConfigDir(home), STATE_FILENAME);
1791
- const persisted = await loadState(filePath);
1792
- const fresh = persisted.savedAt === 0;
1793
- return {
1794
- disabled: false,
1795
- path: filePath,
1796
- savedAt: fresh ? null : new Date(persisted.savedAt).toISOString(),
1797
- learningEntries: fresh ? 0 : Object.keys(persisted.learning).length,
1798
- packHistoryEntries: fresh ? 0 : persisted.packHistory.length
1799
- };
1800
- })();
1801
- const reliability = [];
1802
- if (!persistDisabled) {
1803
- const filePath = join4(userConfigDir(home), STATE_FILENAME);
1804
- const persisted = await loadState(filePath);
1805
- if (persisted.savedAt !== 0) {
1806
- const entries = Object.entries(persisted.learning).map(([namespace, usage]) => ({ namespace, usage }));
1807
- for (const { namespace, usage } of selectFlakyNamespaces(entries, 5)) {
1808
- reliability.push({
1809
- namespace,
1810
- dispatched: usage.dispatched,
1811
- succeeded: usage.succeeded,
1812
- successRate: usage.succeeded / usage.dispatched,
1813
- lastUsedAt: new Date(usage.lastUsedAt).toISOString()
1814
- });
1815
- }
1750
+ if (existingHasEntry) {
1751
+ let decision;
1752
+ if (opts.force) decision = "overwrite";
1753
+ else if (opts.skip) decision = "skip";
1754
+ else if (opts.promptAnswer) decision = opts.promptAnswer;
1755
+ else if (opts.io?.isTTY ?? process.stdout.isTTY) {
1756
+ decision = await promptCollision(resolved.absolute, opts.io);
1757
+ } else {
1758
+ err(
1759
+ `mcph install: ${resolved.absolute} already has a "${ENTRY_NAME}" entry and stdin is not a TTY.
1760
+ Re-run with --force to overwrite, --skip to leave it, or --dry-run to preview.`
1761
+ );
1762
+ return { written: [], wouldWrite: [], messages, exitCode: 1 };
1816
1763
  }
1764
+ if (decision === "skip") {
1765
+ log2(`Existing "${ENTRY_NAME}" entry left untouched. Nothing to do.`);
1766
+ return { written: [], wouldWrite: [], messages, exitCode: 0 };
1767
+ }
1768
+ if (decision === "abort") {
1769
+ err("Aborted.");
1770
+ return { written: [], wouldWrite: [], messages, exitCode: 1 };
1771
+ }
1772
+ log2(`Overwriting existing "${ENTRY_NAME}" entry.`);
1817
1773
  }
1818
- const shellShadows = scanShellHistoryForShadows({ home, env });
1819
- const skipCheck = opts.skipRegistryCheck === true || Boolean(process.env.VITEST);
1820
- const latest = skipCheck ? null : await fetchLatestVersion(opts.registryFetch);
1821
- const stale = latest !== null && VERSION !== "dev" && compareSemver(VERSION, latest) < 0;
1822
- let exitCode = 0;
1823
- let summary;
1824
- if (config.token === null) {
1825
- exitCode = 1;
1826
- summary = "No token resolved \u2014 mcph cannot start.";
1827
- } else if (config.warnings.length > 0) {
1828
- exitCode = 2;
1829
- summary = "Token present, but warnings need attention.";
1830
- } else {
1831
- summary = stale ? "Healthy, but an upgrade is available." : "All good. mcph should start cleanly.";
1832
- }
1833
- const snapshotJson = {
1834
- timestamp,
1835
- version: VERSION,
1836
- platform: os,
1837
- token: { fingerprint: tokenFingerprint(config.token), source: config.tokenSource },
1838
- apiBase: { value: config.apiBase, source: config.apiBaseSource },
1839
- loadedFiles: config.loadedFiles.map((f) => ({
1840
- scope: f.scope,
1841
- path: f.path,
1842
- ...f.version !== void 0 ? { schemaVersion: f.version } : {},
1843
- schemaAhead: f.version !== void 0 && f.version > CURRENT_SCHEMA_VERSION
1844
- })),
1845
- warnings: config.warnings,
1846
- env: envOverrides,
1847
- state,
1848
- reliability,
1849
- clients,
1850
- shellShadows,
1851
- upgrade: { current: VERSION, latest, stale },
1852
- diagnosis: { exitCode, summary }
1853
- };
1854
- const blob = JSON.stringify(snapshotJson, null, 2);
1855
- lines.push(blob);
1856
- write(`${blob}
1857
- `);
1858
- return { exitCode, lines, snapshot: { version: VERSION, config, clients } };
1859
- }
1860
- function renderEnvSection(opts) {
1861
- const { env, print } = opts;
1862
- const vars = [
1863
- { name: "MCPH_POLL_INTERVAL", defaultHint: "default 60s" },
1864
- { name: "MCPH_SERVER_CAP", defaultHint: "default 6" },
1865
- { name: "MCPH_MIN_COMPLIANCE", defaultHint: "filter inactive" },
1866
- { name: "MCPH_AUTO_LOAD", defaultHint: "auto-load inactive" },
1867
- { name: "MCPH_AUTO_ACTIVATE", defaultHint: "default on" },
1868
- { name: "MCPH_PRUNE_RESPONSES", defaultHint: "pruning active" }
1869
- ];
1870
- const widest = vars.reduce((m, v) => Math.max(m, v.name.length), 0);
1871
- print("ENVIRONMENT (behavior overrides)");
1872
- for (const v of vars) {
1873
- const raw = env[v.name];
1874
- const value = raw === void 0 || raw === "" ? `(not set \u2014 ${v.defaultHint})` : raw;
1875
- print(` ${v.name.padEnd(widest)} ${value}`);
1876
- }
1877
- print("");
1878
- }
1879
- async function renderStateSection(opts) {
1880
- const { home, env, print } = opts;
1881
- const raw = env.MCPH_DISABLE_PERSISTENCE;
1882
- const disabled = raw !== void 0 && raw !== "" && (raw === "1" || raw.toLowerCase() === "true");
1883
- print("STATE");
1884
- if (disabled) {
1885
- print(" status: disabled via MCPH_DISABLE_PERSISTENCE");
1886
- print("");
1887
- return;
1888
- }
1889
- const filePath = join4(userConfigDir(home), STATE_FILENAME);
1890
- print(` path: ${filePath}`);
1891
- const peek = await peekStateFile(filePath);
1892
- if (peek.kind === "malformed") {
1893
- print(" status: corrupt -- file exists but JSON is unparseable");
1894
- print(` fix: \`mcph reset-learning\` to clear, or open ${filePath} and fix by hand`);
1895
- print(` detail: ${peek.message}`);
1896
- print("");
1897
- return;
1898
- }
1899
- if (peek.kind === "stale-version") {
1900
- print(` status: schema mismatch (file is v${peek.version ?? "?"}, this mcph reads v${peek.expected})`);
1901
- print(" fix: `mcph reset-learning` to drop the old file -- learning will rebuild on use");
1902
- print("");
1903
- return;
1904
- }
1905
- if (peek.kind === "unreadable") {
1906
- print(` status: unreadable (${peek.message})`);
1907
- print("");
1908
- return;
1774
+ const merged = mergeClientConfig(existing, containerPath, newEntry);
1775
+ const clientJson = `${JSON.stringify(merged, null, 2)}
1776
+ `;
1777
+ const writeMcphConfig = !opts.skipMcphConfig;
1778
+ const home = opts.home ?? homedir4();
1779
+ const mcphConfigPath = join4(home, CONFIG_DIRNAME, CONFIG_FILENAME);
1780
+ const mcphConfigComposed = await composeMcphConfig(mcphConfigPath, token6);
1781
+ if (mcphConfigComposed.backupPath) {
1782
+ log2(
1783
+ `mcph install: existing ${mcphConfigPath} was malformed; original bytes backed up to ${mcphConfigComposed.backupPath} before overwriting.`
1784
+ );
1909
1785
  }
1910
- const persisted = await loadState(filePath);
1911
- if (persisted.savedAt === 0) {
1912
- print(" (no persisted state yet \u2014 will be created on the first tool call)");
1913
- } else {
1914
- print(` last saved: ${formatRelativeAge(Date.now() - persisted.savedAt)} ago`);
1915
- print(` learning entries: ${Object.keys(persisted.learning).length}`);
1916
- print(` pack history entries: ${persisted.packHistory.length}`);
1786
+ const mcphConfigJson = mcphConfigComposed.json;
1787
+ const settingsPatch = opts.clientId === "claude-code" ? await prepareClaudeCodeSettingsPatch({
1788
+ scope,
1789
+ home,
1790
+ projectDir,
1791
+ os,
1792
+ claudeConfigDir: opts.claudeConfigDir
1793
+ }) : null;
1794
+ if (opts.dryRun) {
1795
+ log2("\n--- dry run: would write the following ---");
1796
+ if (writeMcphConfig) log2(`# ${mcphConfigPath}
1797
+ ${mcphConfigJson}`);
1798
+ log2(`
1799
+ # ${resolved.absolute}
1800
+ ${clientJson}`);
1801
+ if (settingsPatch?.changed) log2(`# ${settingsPatch.path}
1802
+ ${settingsPatch.nextJson}`);
1803
+ const wouldWrite = [];
1804
+ if (writeMcphConfig) wouldWrite.push(mcphConfigPath);
1805
+ wouldWrite.push(resolved.absolute);
1806
+ if (settingsPatch?.changed) wouldWrite.push(settingsPatch.path);
1807
+ return { written: [], wouldWrite, messages, exitCode: 0 };
1917
1808
  }
1918
- print("");
1919
- }
1920
- async function peekStateFile(filePath) {
1921
- let raw;
1922
- try {
1923
- raw = await readFile3(filePath, "utf8");
1924
- } catch (err) {
1925
- if (err && typeof err === "object" && "code" in err && err.code === "ENOENT") {
1926
- return { kind: "missing" };
1809
+ const written = [];
1810
+ if (writeMcphConfig) {
1811
+ try {
1812
+ await atomicWriteFile(mcphConfigPath, mcphConfigJson);
1813
+ if (process.platform !== "win32") {
1814
+ try {
1815
+ await chmod(mcphConfigPath, 384);
1816
+ } catch {
1817
+ }
1818
+ }
1819
+ } catch (e) {
1820
+ err(`mcph install: failed to write ${mcphConfigPath}: ${e.message}`);
1821
+ return { written: [], wouldWrite: [], messages, exitCode: 1 };
1927
1822
  }
1928
- return { kind: "unreadable", message: err instanceof Error ? err.message : String(err) };
1823
+ log2(`Wrote ${mcphConfigPath}`);
1824
+ written.push(mcphConfigPath);
1929
1825
  }
1930
- let parsed;
1931
1826
  try {
1932
- parsed = JSON.parse(raw);
1933
- } catch (err) {
1934
- return { kind: "malformed", message: err instanceof Error ? err.message : String(err) };
1827
+ await atomicWriteFile(resolved.absolute, clientJson);
1828
+ } catch (e) {
1829
+ err(`mcph install: failed to write ${resolved.absolute}: ${e.message}`);
1830
+ return { written, wouldWrite: [], messages, exitCode: 1 };
1935
1831
  }
1936
- if (!parsed || typeof parsed !== "object") return { kind: "malformed", message: "top-level value is not an object" };
1937
- const version = parsed.version;
1938
- if (version !== STATE_SCHEMA_VERSION) {
1939
- return { kind: "stale-version", version, expected: STATE_SCHEMA_VERSION };
1832
+ log2(`Wrote ${resolved.absolute}`);
1833
+ written.push(resolved.absolute);
1834
+ if (settingsPatch?.changed) {
1835
+ try {
1836
+ await atomicWriteFile(settingsPatch.path, settingsPatch.nextJson);
1837
+ log2(`Wrote ${settingsPatch.path} (added ${CLAUDE_CODE_ALLOW_PATTERN} to permissions.allow)`);
1838
+ written.push(settingsPatch.path);
1839
+ } catch (e) {
1840
+ err(
1841
+ `mcph install: warning \u2014 failed to patch ${settingsPatch.path}: ${e.message}. You may be re-prompted for each mcph tool call; add "${CLAUDE_CODE_ALLOW_PATTERN}" to permissions.allow to silence.`
1842
+ );
1843
+ }
1940
1844
  }
1941
- return { kind: "ok" };
1845
+ if (target.notes) log2(`Note: ${target.notes}`);
1846
+ log2(`
1847
+ \u2713 ${target.label} is configured. Restart it to pick up the new MCP server.`);
1848
+ return { written, wouldWrite: [], messages, exitCode: 0 };
1942
1849
  }
1943
- async function renderReliabilitySection(opts) {
1944
- const { home, env, print } = opts;
1945
- const raw = env.MCPH_DISABLE_PERSISTENCE;
1946
- const disabled = raw !== void 0 && raw !== "" && (raw === "1" || raw.toLowerCase() === "true");
1947
- if (disabled) return;
1948
- const filePath = join4(userConfigDir(home), STATE_FILENAME);
1949
- const persisted = await loadState(filePath);
1950
- if (persisted.savedAt === 0) return;
1951
- const entries = Object.entries(persisted.learning).map(([namespace, usage]) => ({ namespace, usage }));
1952
- const flaky = selectFlakyNamespaces(entries, 5);
1953
- if (flaky.length === 0) return;
1954
- print("RELIABILITY (dormant, <80% success)");
1955
- const now = Date.now();
1956
- for (const { namespace, usage } of flaky) {
1957
- const rate = Math.round(usage.succeeded / usage.dispatched * 100);
1958
- const age = formatRelativeAge(now - usage.lastUsedAt);
1959
- print(` ${namespace} \u2014 ${usage.dispatched} calls, ${rate}% success, last used ${age} ago`);
1960
- }
1961
- print("");
1962
- }
1963
- function renderBackgroundPostersSection(opts) {
1964
- const { print } = opts;
1965
- const analyticsFailure = getLastAnalyticsFailure();
1966
- const reportFailure = getLastReportFailure();
1967
- if (!analyticsFailure && !reportFailure) return;
1968
- const now = Date.now();
1969
- const fmt = (f) => `HTTP ${f.statusCode} from ${f.url}, ${formatRelativeAge(now - f.at)} ago`;
1970
- print("BACKGROUND POSTERS (recent failures)");
1971
- print(` analytics: ${analyticsFailure ? fmt(analyticsFailure) : "(no recent failure)"}`);
1972
- print(` tool-report: ${reportFailure ? fmt(reportFailure) : "(no recent failure)"}`);
1973
- print("");
1974
- }
1975
- function formatRelativeAge(ms) {
1976
- const clamped = Math.max(0, ms);
1977
- const s = Math.floor(clamped / 1e3);
1978
- if (s < 60) return `${s}s`;
1979
- const m = Math.floor(s / 60);
1980
- if (m < 60) return `${m}m`;
1981
- const h = Math.floor(m / 60);
1982
- if (h < 24) return `${h}h`;
1983
- const d = Math.floor(h / 24);
1984
- return `${d}d`;
1985
- }
1986
- function schemaSuffix(f) {
1987
- if (f.version === void 0) return "";
1988
- if (f.version > CURRENT_SCHEMA_VERSION)
1989
- return ` (schema v${f.version}, this mcph supports v${CURRENT_SCHEMA_VERSION})`;
1990
- return ` (schema v${f.version})`;
1991
- }
1992
- function probeClients(opts) {
1993
- const out = [];
1994
- for (const target of INSTALL_TARGETS) {
1995
- const unavailable = !target.availableOn.includes(opts.os);
1996
- if (unavailable) {
1997
- out.push({
1998
- clientId: target.clientId,
1999
- scope: target.scopes[0].scope,
2000
- path: "(n/a)",
2001
- exists: false,
2002
- hasMcphEntry: false,
2003
- malformed: false,
2004
- unavailable: true
2005
- });
2006
- continue;
2007
- }
2008
- for (const scope of target.scopes) {
2009
- let resolved;
2010
- try {
2011
- resolved = resolveInstallPath({
2012
- clientId: target.clientId,
2013
- scope: scope.scope,
2014
- os: opts.os,
2015
- home: opts.home,
2016
- projectDir: scope.requiresProjectDir ? opts.cwd : void 0,
2017
- claudeConfigDir: opts.claudeConfigDir
2018
- });
2019
- } catch {
2020
- continue;
2021
- }
2022
- const exists3 = existsSync(resolved.absolute);
2023
- let hasMcphEntry = false;
2024
- let malformed = false;
2025
- if (exists3) {
2026
- try {
2027
- statSync(resolved.absolute);
2028
- const raw = readFileSync(resolved.absolute, "utf8");
2029
- if (raw.trim().length > 0) {
2030
- const parsed = parseJsonc(raw);
2031
- if (typeof parsed === "object" && parsed !== null && !Array.isArray(parsed)) {
2032
- const container = walkContainer(parsed, resolved.containerPath);
2033
- if (container) hasMcphEntry = ENTRY_NAME in container;
2034
- } else {
2035
- malformed = true;
2036
- }
2037
- }
2038
- } catch {
2039
- malformed = true;
1850
+ async function prepareClaudeCodeSettingsPatch(opts) {
1851
+ const path5 = resolveClaudeCodeSettingsPath(opts.scope, {
1852
+ home: opts.home,
1853
+ projectDir: opts.projectDir,
1854
+ os: opts.os,
1855
+ claudeConfigDir: opts.claudeConfigDir
1856
+ });
1857
+ if (!path5) return null;
1858
+ let existing = {};
1859
+ if (existsSync(path5)) {
1860
+ try {
1861
+ const raw = await readFile3(path5, "utf8");
1862
+ if (raw.trim().length > 0) {
1863
+ const parsed = parseJsonc(raw);
1864
+ if (typeof parsed === "object" && parsed !== null && !Array.isArray(parsed)) {
1865
+ existing = parsed;
1866
+ } else {
1867
+ return { path: path5, nextJson: "", changed: false };
2040
1868
  }
2041
1869
  }
2042
- out.push({
2043
- clientId: target.clientId,
2044
- scope: scope.scope,
2045
- path: resolved.absolute,
2046
- exists: exists3,
2047
- hasMcphEntry,
2048
- malformed,
2049
- unavailable: false
2050
- });
1870
+ } catch {
1871
+ return { path: path5, nextJson: "", changed: false };
2051
1872
  }
2052
1873
  }
1874
+ const merged = mergePermissionsAllow(existing, [CLAUDE_CODE_ALLOW_PATTERN]);
1875
+ const before = JSON.stringify(existing);
1876
+ const after = JSON.stringify(merged);
1877
+ if (before === after) return { path: path5, nextJson: "", changed: false };
1878
+ return { path: path5, nextJson: `${JSON.stringify(merged, null, 2)}
1879
+ `, changed: true };
1880
+ }
1881
+ function mergePermissionsAllow(existing, patterns) {
1882
+ const out = { ...existing };
1883
+ const prev = out.permissions;
1884
+ const perms = typeof prev === "object" && prev !== null && !Array.isArray(prev) ? { ...prev } : {};
1885
+ const prevAllow = perms.allow;
1886
+ const allow = Array.isArray(prevAllow) ? prevAllow.filter((x) => typeof x === "string") : [];
1887
+ for (const p of patterns) {
1888
+ if (!allow.includes(p)) allow.push(p);
1889
+ }
1890
+ perms.allow = allow;
1891
+ out.permissions = perms;
2053
1892
  return out;
2054
1893
  }
2055
- function walkContainer(root, path5) {
1894
+ async function promptCollision(path5, io) {
1895
+ const stdin = io?.stdin ?? process.stdin;
1896
+ const stdout = io?.stdout ?? process.stdout;
1897
+ const rl = createInterface({ input: stdin, output: stdout });
1898
+ try {
1899
+ const answer = (await rl.question(
1900
+ `${path5} already has an "${ENTRY_NAME}" entry.
1901
+ [o]verwrite, [s]kip, or [a]bort? (default: skip) `
1902
+ )).trim().toLowerCase();
1903
+ if (answer.startsWith("o")) return "overwrite";
1904
+ if (answer.startsWith("a")) return "abort";
1905
+ return "skip";
1906
+ } finally {
1907
+ rl.close();
1908
+ }
1909
+ }
1910
+ function readNested(root, containerPath) {
2056
1911
  let cur = root;
2057
- for (const key of path5) {
2058
- if (typeof cur !== "object" || cur === null || Array.isArray(cur)) return null;
1912
+ for (const key of containerPath) {
1913
+ if (typeof cur !== "object" || cur === null || Array.isArray(cur)) return void 0;
2059
1914
  cur = cur[key];
2060
1915
  }
2061
- if (typeof cur !== "object" || cur === null || Array.isArray(cur)) return null;
2062
1916
  return cur;
2063
1917
  }
2064
- async function probeClientsAsync(opts) {
2065
- const result = [];
2066
- for (const target of INSTALL_TARGETS) {
2067
- const unavailable = !target.availableOn.includes(opts.os);
2068
- if (unavailable) {
2069
- result.push({
2070
- clientId: target.clientId,
2071
- scope: target.scopes[0].scope,
2072
- path: "(n/a)",
2073
- exists: false,
2074
- hasMcphEntry: false,
2075
- malformed: false,
2076
- unavailable: true
2077
- });
2078
- continue;
1918
+ function mergeClientConfig(existing, containerPath, entry, entryName = ENTRY_NAME) {
1919
+ if (containerPath.length === 0) throw new Error("mergeClientConfig: containerPath cannot be empty");
1920
+ const out = { ...existing };
1921
+ let parent = out;
1922
+ for (let i = 0; i < containerPath.length - 1; i++) {
1923
+ const key = containerPath[i];
1924
+ const child = parent[key];
1925
+ const cloned = typeof child === "object" && child !== null && !Array.isArray(child) ? { ...child } : {};
1926
+ parent[key] = cloned;
1927
+ parent = cloned;
1928
+ }
1929
+ const leafKey = containerPath[containerPath.length - 1];
1930
+ const prev = parent[leafKey];
1931
+ const container = typeof prev === "object" && prev !== null && !Array.isArray(prev) ? { ...prev } : {};
1932
+ container[entryName] = entry;
1933
+ parent[leafKey] = container;
1934
+ return out;
1935
+ }
1936
+ function removeFromClientConfig(existing, containerPath, entryName) {
1937
+ if (containerPath.length === 0) throw new Error("removeFromClientConfig: containerPath cannot be empty");
1938
+ let probe2 = existing;
1939
+ for (const key of containerPath) {
1940
+ if (typeof probe2 !== "object" || probe2 === null || Array.isArray(probe2)) return existing;
1941
+ probe2 = probe2[key];
1942
+ }
1943
+ if (typeof probe2 !== "object" || probe2 === null || Array.isArray(probe2)) return existing;
1944
+ if (!(entryName in probe2)) return existing;
1945
+ const out = { ...existing };
1946
+ let parent = out;
1947
+ for (let i = 0; i < containerPath.length - 1; i++) {
1948
+ const key = containerPath[i];
1949
+ const child = parent[key];
1950
+ const cloned = { ...child };
1951
+ parent[key] = cloned;
1952
+ parent = cloned;
1953
+ }
1954
+ const leafKey = containerPath[containerPath.length - 1];
1955
+ const container = { ...parent[leafKey] };
1956
+ delete container[entryName];
1957
+ parent[leafKey] = container;
1958
+ return out;
1959
+ }
1960
+ async function composeMcphConfig(path5, token6) {
1961
+ let existing = {};
1962
+ let backupPath;
1963
+ if (existsSync(path5)) {
1964
+ let raw = "";
1965
+ try {
1966
+ raw = await readFile3(path5, "utf8");
1967
+ } catch {
1968
+ raw = "";
2079
1969
  }
2080
- for (const scope of target.scopes) {
2081
- const resolved = resolveInstallPath({
2082
- clientId: target.clientId,
2083
- scope: scope.scope,
2084
- os: opts.os,
2085
- home: opts.home,
2086
- projectDir: scope.requiresProjectDir ? opts.cwd : void 0,
2087
- claudeConfigDir: opts.claudeConfigDir
2088
- });
2089
- const exists3 = existsSync(resolved.absolute);
2090
- let hasMcphEntry = false;
2091
- let malformed = false;
2092
- if (exists3) {
1970
+ if (raw) {
1971
+ try {
1972
+ const parsed = parseJsonc(raw);
1973
+ if (typeof parsed === "object" && parsed !== null && !Array.isArray(parsed)) {
1974
+ existing = parsed;
1975
+ }
1976
+ } catch {
1977
+ const candidate = `${path5}.bak-${Date.now()}`;
2093
1978
  try {
2094
- const raw = await readFile3(resolved.absolute, "utf8");
2095
- if (raw.trim().length > 0) {
2096
- const parsed = parseJsonc(raw);
2097
- if (typeof parsed === "object" && parsed !== null && !Array.isArray(parsed)) {
2098
- const container = walkContainer(parsed, resolved.containerPath);
2099
- if (container) hasMcphEntry = ENTRY_NAME in container;
2100
- } else {
2101
- malformed = true;
2102
- }
2103
- }
1979
+ await atomicWriteFile(candidate, raw);
1980
+ backupPath = candidate;
2104
1981
  } catch {
2105
- malformed = true;
2106
1982
  }
2107
1983
  }
2108
- result.push({
2109
- clientId: target.clientId,
2110
- scope: scope.scope,
2111
- path: resolved.absolute,
2112
- exists: exists3,
2113
- hasMcphEntry,
2114
- malformed,
2115
- unavailable: false
2116
- });
2117
1984
  }
2118
1985
  }
2119
- return result;
2120
- }
2121
- async function fetchLatestVersion(override) {
2122
- if (override) {
2123
- try {
2124
- return await override();
2125
- } catch {
2126
- return null;
1986
+ const next = { version: CURRENT_SCHEMA_VERSION, ...existing };
1987
+ next.token = token6;
1988
+ if (typeof next.version !== "number") next.version = CURRENT_SCHEMA_VERSION;
1989
+ return { json: `${JSON.stringify(next, null, 2)}
1990
+ `, backupPath };
1991
+ }
1992
+ function parseInstallArgs(argv) {
1993
+ if (argv.length === 0) return { ok: false, error: USAGE };
1994
+ const positional = [];
1995
+ const opts = {};
1996
+ for (let i = 0; i < argv.length; i++) {
1997
+ const a = argv[i];
1998
+ const next = () => argv[++i];
1999
+ switch (a) {
2000
+ case "--scope": {
2001
+ const v = next();
2002
+ if (!v || !["user", "project", "local"].includes(v))
2003
+ return { ok: false, error: "--scope requires user|project|local" };
2004
+ opts.scope = v;
2005
+ break;
2006
+ }
2007
+ case "--os": {
2008
+ const v = next();
2009
+ if (!v || !["macos", "linux", "windows"].includes(v))
2010
+ return { ok: false, error: "--os requires macos|linux|windows" };
2011
+ opts.os = v;
2012
+ break;
2013
+ }
2014
+ case "--token": {
2015
+ const v = next();
2016
+ if (!v) return { ok: false, error: "--token requires a value" };
2017
+ opts.token = v;
2018
+ break;
2019
+ }
2020
+ case "--project-dir": {
2021
+ const v = next();
2022
+ if (!v) return { ok: false, error: "--project-dir requires a value" };
2023
+ opts.projectDir = v;
2024
+ break;
2025
+ }
2026
+ case "--force":
2027
+ opts.force = true;
2028
+ break;
2029
+ case "--skip":
2030
+ opts.skip = true;
2031
+ break;
2032
+ case "--dry-run":
2033
+ opts.dryRun = true;
2034
+ break;
2035
+ case "--no-mcph-config":
2036
+ opts.skipMcphConfig = true;
2037
+ break;
2038
+ case "--list":
2039
+ opts.listOnly = true;
2040
+ break;
2041
+ case "--all":
2042
+ opts.all = true;
2043
+ break;
2044
+ case "-h":
2045
+ case "--help":
2046
+ return { ok: false, error: USAGE };
2047
+ default:
2048
+ if (a.startsWith("--")) return { ok: false, error: `Unknown flag: ${a}
2049
+ ${USAGE}` };
2050
+ positional.push(a);
2127
2051
  }
2128
2052
  }
2129
- const ac = new AbortController();
2130
- const timer = setTimeout(() => ac.abort(), 2e3);
2131
- try {
2132
- const res = await fetch("https://registry.npmjs.org/@yawlabs/mcph/latest", {
2133
- signal: ac.signal,
2134
- headers: { accept: "application/json" }
2135
- });
2136
- if (!res.ok) return null;
2137
- const body = await res.json();
2138
- return typeof body.version === "string" ? body.version : null;
2139
- } catch {
2140
- return null;
2141
- } finally {
2142
- clearTimeout(timer);
2143
- }
2144
- }
2145
- var SHELL_HISTORY_TAIL_LINES = 500;
2146
- function scanShellHistoryForShadows(opts) {
2147
- const shadowMap = cliToNamespaces();
2148
- const counts = /* @__PURE__ */ new Map();
2149
- for (const source of shellHistorySources(opts)) {
2150
- const lines = readTailLines(source.path, SHELL_HISTORY_TAIL_LINES);
2151
- for (const raw of lines) {
2152
- const cmd = source.extractCommand(raw);
2153
- if (!cmd) continue;
2154
- const binary = extractLeadingBinary(cmd);
2155
- if (!binary) continue;
2156
- if (!shadowMap.has(binary)) continue;
2157
- counts.set(binary, (counts.get(binary) ?? 0) + 1);
2053
+ if (opts.listOnly || opts.all) {
2054
+ if (positional.length > 0) {
2055
+ return {
2056
+ ok: false,
2057
+ error: `mcph install: ${opts.listOnly ? "--list" : "--all"} does not take a client argument.
2058
+ ${USAGE}`
2059
+ };
2158
2060
  }
2061
+ return { ok: true, options: opts };
2159
2062
  }
2160
- const hits = [];
2161
- for (const [cli, count] of counts) {
2162
- const namespaces = shadowMap.get(cli) ?? [];
2163
- hits.push({ cli, count, namespaces });
2063
+ if (positional.length !== 1)
2064
+ return { ok: false, error: `Expected exactly one client argument, got ${positional.length}.
2065
+ ${USAGE}` };
2066
+ const clientId = positional[0];
2067
+ if (!INSTALL_TARGETS.some((t) => t.clientId === clientId)) {
2068
+ return {
2069
+ ok: false,
2070
+ error: `Unknown client: ${clientId}. Choose: ${INSTALL_TARGETS.map((t) => t.clientId).join(", ")}`
2071
+ };
2164
2072
  }
2165
- hits.sort((a, b) => b.count - a.count);
2166
- return hits;
2073
+ opts.clientId = clientId;
2074
+ return { ok: true, options: opts };
2167
2075
  }
2168
- function shellHistorySources(opts) {
2169
- const sources = [];
2170
- sources.push({ path: join4(opts.home, ".bash_history"), extractCommand: (l) => l.trim() || null });
2171
- sources.push({
2172
- path: join4(opts.home, ".zsh_history"),
2173
- // Zsh extended-history lines look like `: 1700000000:0;npm audit`.
2174
- // Strip the metadata prefix so we get just the command.
2175
- extractCommand: (l) => {
2176
- const trimmed = l.trim();
2177
- if (!trimmed) return null;
2178
- if (trimmed.startsWith(":")) {
2179
- const semi = trimmed.indexOf(";");
2180
- return semi === -1 ? null : trimmed.slice(semi + 1);
2181
- }
2182
- return trimmed;
2183
- }
2184
- });
2185
- const appData = opts.env.APPDATA;
2186
- if (appData) {
2187
- sources.push({
2188
- path: join4(appData, "Microsoft", "Windows", "PowerShell", "PSReadLine", "ConsoleHost_history.txt"),
2189
- extractCommand: (l) => l.trim() || null
2190
- });
2076
+ async function runInstallList(opts, log2) {
2077
+ const home = opts.home ?? homedir4();
2078
+ const cwd = opts.cwd ?? process.cwd();
2079
+ const os = opts.os ?? CURRENT_OS;
2080
+ const probes = await probeClientsAsync({ home, os, cwd, claudeConfigDir: opts.claudeConfigDir });
2081
+ const rows = probes.map((p) => ({
2082
+ client: INSTALL_TARGETS.find((t) => t.clientId === p.clientId)?.label ?? p.clientId,
2083
+ scope: p.scope,
2084
+ path: displayPath(p.path, home),
2085
+ status: statusFor(p)
2086
+ }));
2087
+ const installed = probes.filter((p) => p.hasMcphEntry).length;
2088
+ const available = probes.filter((p) => !p.unavailable).length;
2089
+ log2(`${installed}/${available} client scopes have mcp.hosting configured on ${os}.`);
2090
+ log2("");
2091
+ const widths = {
2092
+ client: Math.max("CLIENT".length, ...rows.map((r) => r.client.length)),
2093
+ scope: Math.max("SCOPE".length, ...rows.map((r) => r.scope.length)),
2094
+ path: Math.max("PATH".length, ...rows.map((r) => r.path.length)),
2095
+ status: Math.max("STATUS".length, ...rows.map((r) => r.status.length))
2096
+ };
2097
+ const header = ` ${"CLIENT".padEnd(widths.client)} ${"SCOPE".padEnd(widths.scope)} ${"PATH".padEnd(widths.path)} ${"STATUS".padEnd(widths.status)}`;
2098
+ log2(header);
2099
+ for (const r of rows) {
2100
+ log2(
2101
+ ` ${r.client.padEnd(widths.client)} ${r.scope.padEnd(widths.scope)} ${r.path.padEnd(widths.path)} ${r.status.padEnd(widths.status)}`
2102
+ );
2191
2103
  }
2192
- return sources;
2104
+ log2("");
2105
+ log2("Install into a specific client: `mcph install <client> [--scope user|project|local]`");
2106
+ log2("Install into every available user-scope client: `mcph install --all`");
2107
+ return { written: [], wouldWrite: [], messages: [], exitCode: 0 };
2193
2108
  }
2194
- function readTailLines(path5, n) {
2195
- try {
2196
- const raw = readFileSync(path5, "utf8");
2197
- const all = raw.split(/\r?\n/);
2198
- return all.length <= n ? all : all.slice(all.length - n);
2199
- } catch {
2200
- return [];
2109
+ function statusFor(p) {
2110
+ if (p.unavailable) return "unavailable";
2111
+ if (p.malformed) return "malformed";
2112
+ if (p.hasMcphEntry) return "installed";
2113
+ if (p.exists) return "other-entries";
2114
+ return "not installed";
2115
+ }
2116
+ function displayPath(abs, home) {
2117
+ if (abs === "(n/a)") return abs;
2118
+ if (home && abs.startsWith(home)) {
2119
+ const tail = abs.slice(home.length).replace(/^[\\/]/, "");
2120
+ return `~${process.platform === "win32" ? "\\" : "/"}${tail}`;
2201
2121
  }
2122
+ return abs;
2202
2123
  }
2203
- function extractLeadingBinary(command) {
2204
- let rest = command.trimStart();
2205
- if (!rest) return null;
2206
- if (rest.startsWith("!")) return null;
2207
- while (/^[A-Z_][A-Z0-9_]*=/i.test(rest)) {
2208
- const space = rest.indexOf(" ");
2209
- if (space === -1) return null;
2210
- rest = rest.slice(space + 1).trimStart();
2124
+ async function runInstallAll(opts, log2, err) {
2125
+ const os = opts.os ?? CURRENT_OS;
2126
+ const targets = INSTALL_TARGETS.filter((t) => t.availableOn.includes(os));
2127
+ if (targets.length === 0) {
2128
+ err(`mcph install --all: no installable clients on ${os}.`);
2129
+ return { written: [], wouldWrite: [], messages: [], exitCode: 1 };
2211
2130
  }
2212
- const prefixes = ["sudo", "time", "command", "exec"];
2213
- const firstWord = rest.split(/\s+/)[0];
2214
- if (prefixes.includes(firstWord)) {
2215
- const space = rest.indexOf(" ");
2216
- if (space === -1) return null;
2217
- rest = rest.slice(space + 1).trimStart();
2131
+ const plans = [];
2132
+ const skipped = [];
2133
+ for (const t of targets) {
2134
+ const userScope = t.scopes.find((s) => s.scope === "user");
2135
+ if (userScope) {
2136
+ plans.push({ clientId: t.clientId, scope: "user" });
2137
+ continue;
2138
+ }
2139
+ const firstNoProj = t.scopes.find((s) => !s.requiresProjectDir);
2140
+ if (firstNoProj) {
2141
+ plans.push({ clientId: t.clientId, scope: firstNoProj.scope });
2142
+ continue;
2143
+ }
2144
+ if (opts.projectDir) {
2145
+ plans.push({ clientId: t.clientId, scope: t.scopes[0].scope });
2146
+ continue;
2147
+ }
2148
+ skipped.push({
2149
+ clientId: t.clientId,
2150
+ reason: `requires --project-dir (scopes: ${t.scopes.map((s) => s.scope).join(", ")})`
2151
+ });
2218
2152
  }
2219
- const first = rest.split(/\s+/)[0];
2220
- if (!first) return null;
2221
- if (/[|&;<>()`$]/.test(first)) return null;
2222
- const slash = Math.max(first.lastIndexOf("/"), first.lastIndexOf("\\"));
2223
- return slash === -1 ? first : first.slice(slash + 1);
2224
- }
2225
- function compareSemver(a, b) {
2226
- const parse = (s) => {
2227
- const m = /^v?(\d+)\.(\d+)\.(\d+)/.exec(s);
2228
- if (!m) return null;
2229
- return [Number(m[1]), Number(m[2]), Number(m[3])];
2230
- };
2231
- const pa = parse(a);
2232
- const pb = parse(b);
2233
- if (!pa || !pb) return 0;
2234
- for (let i = 0; i < 3; i++) {
2235
- if (pa[i] < pb[i]) return -1;
2236
- if (pa[i] > pb[i]) return 1;
2153
+ log2(`Installing into ${plans.length} client${plans.length === 1 ? "" : "s"}\u2026`);
2154
+ if (skipped.length > 0) {
2155
+ for (const s of skipped) log2(` skip ${s.clientId}: ${s.reason}`);
2237
2156
  }
2238
- return 0;
2157
+ log2("");
2158
+ const aggregateWritten = [];
2159
+ const aggregateWouldWrite = [];
2160
+ const aggregateMessages = [];
2161
+ let failed = 0;
2162
+ let succeeded = 0;
2163
+ for (const plan of plans) {
2164
+ log2(`\u2500\u2500 ${plan.clientId} (${plan.scope}) \u2500\u2500`);
2165
+ const result = await runInstall({
2166
+ ...opts,
2167
+ listOnly: false,
2168
+ all: false,
2169
+ clientId: plan.clientId,
2170
+ scope: plan.scope
2171
+ });
2172
+ aggregateWritten.push(...result.written);
2173
+ aggregateWouldWrite.push(...result.wouldWrite);
2174
+ aggregateMessages.push(...result.messages);
2175
+ if (result.exitCode === 0) succeeded += 1;
2176
+ else failed += 1;
2177
+ log2("");
2178
+ }
2179
+ const totalPlanned = plans.length;
2180
+ if (failed === 0) {
2181
+ log2(`\u2713 ${succeeded}/${totalPlanned} clients installed successfully.`);
2182
+ return {
2183
+ written: aggregateWritten,
2184
+ wouldWrite: aggregateWouldWrite,
2185
+ messages: aggregateMessages,
2186
+ exitCode: 0
2187
+ };
2188
+ }
2189
+ err(`${failed}/${totalPlanned} client install${failed === 1 ? "" : "s"} failed. ${succeeded} succeeded.`);
2190
+ return {
2191
+ written: aggregateWritten,
2192
+ wouldWrite: aggregateWouldWrite,
2193
+ messages: aggregateMessages,
2194
+ exitCode: 1
2195
+ };
2239
2196
  }
2240
2197
 
2241
- // src/fuzzy.ts
2242
- function levenshtein(a, b) {
2243
- if (a === b) return 0;
2244
- const aLen = a.length;
2245
- const bLen = b.length;
2246
- if (aLen === 0) return bLen;
2247
- if (bLen === 0) return aLen;
2248
- let prev = new Array(bLen + 1);
2249
- let curr = new Array(bLen + 1);
2250
- for (let j = 0; j <= bLen; j++) prev[j] = j;
2251
- for (let i = 1; i <= aLen; i++) {
2252
- curr[0] = i;
2253
- for (let j = 1; j <= bLen; j++) {
2254
- const cost = a.charCodeAt(i - 1) === b.charCodeAt(j - 1) ? 0 : 1;
2255
- curr[j] = Math.min(
2256
- curr[j - 1] + 1,
2257
- // insertion
2258
- prev[j] + 1,
2259
- // deletion
2260
- prev[j - 1] + cost
2261
- // substitution
2262
- );
2198
+ // src/try-cmd.ts
2199
+ var TRY_USAGE = `Usage: mcph try <slug> [flags]
2200
+
2201
+ Wire a one-off trial of an MCP server into your AI client. No account
2202
+ needed; the trial points directly at the upstream server and expires
2203
+ after --ttl. Run \`mcph try-cleanup <slug>\` to remove it sooner.
2204
+
2205
+ --client <name> claude-code | claude-desktop | cursor | vscode
2206
+ (default: auto-detect, prefers the first installed
2207
+ client in the order probed by \`mcph install --list\`)
2208
+ --ttl <duration> How long the trial lives before doctor GCs it
2209
+ (default: 1h; accepts e.g. 30m, 2h, 7d)
2210
+ --env KEY=value Set an env var on the trial entry. Repeatable.
2211
+ Required env vars not supplied here AND not in your
2212
+ shell's env block the trial with an explainer.
2213
+ --dry-run Print what would happen without writing anything.
2214
+ --base <url> Override the explore endpoint base (default:
2215
+ $MCPH_BASE_URL or https://mcp.hosting).`;
2216
+ var TRY_CLEANUP_USAGE = `Usage: mcph try-cleanup <slug>
2217
+
2218
+ Remove a previously-wired trial: peels the mcph-try-<slug> entry out of
2219
+ the AI client config and deletes the marker under ~/.mcph/trials/. Safe
2220
+ to run after the trial expires (no-op if nothing is wired).`;
2221
+ var TRIAL_SCHEMA_VERSION = 1;
2222
+ var TRIALS_DIRNAME = "trials";
2223
+ var ANON_FILENAME = ".anon";
2224
+ var DEFAULT_BASE_URL = "https://mcp.hosting";
2225
+ var DEFAULT_TTL_MS = 60 * 60 * 1e3;
2226
+ function parseTryArgs(argv) {
2227
+ if (argv.length === 0) return { ok: false, error: TRY_USAGE };
2228
+ const positional = [];
2229
+ const opts = {};
2230
+ const env = {};
2231
+ for (let i = 0; i < argv.length; i++) {
2232
+ const a = argv[i];
2233
+ const next = () => argv[++i];
2234
+ switch (a) {
2235
+ case "--client": {
2236
+ const v = next();
2237
+ if (!v || !["claude-code", "claude-desktop", "cursor", "vscode"].includes(v)) {
2238
+ return { ok: false, error: "--client requires claude-code|claude-desktop|cursor|vscode" };
2239
+ }
2240
+ opts.clientId = v;
2241
+ break;
2242
+ }
2243
+ case "--ttl": {
2244
+ const v = next();
2245
+ if (!v) return { ok: false, error: "--ttl requires a value (e.g. 1h, 30m, 7d)" };
2246
+ if (parseDurationMs(v) === null) {
2247
+ return { ok: false, error: `--ttl: cannot parse "${v}" (try 30m, 1h, 2d)` };
2248
+ }
2249
+ opts.ttl = v;
2250
+ break;
2251
+ }
2252
+ case "--env": {
2253
+ const v = next();
2254
+ if (!v || !v.includes("=")) return { ok: false, error: "--env requires KEY=value" };
2255
+ const eq = v.indexOf("=");
2256
+ const key = v.slice(0, eq);
2257
+ const val = v.slice(eq + 1);
2258
+ if (!/^[A-Za-z_][A-Za-z0-9_]*$/.test(key)) {
2259
+ return { ok: false, error: `--env: invalid KEY "${key}"` };
2260
+ }
2261
+ env[key] = val;
2262
+ break;
2263
+ }
2264
+ case "--dry-run":
2265
+ opts.dryRun = true;
2266
+ break;
2267
+ case "--base": {
2268
+ const v = next();
2269
+ if (!v) return { ok: false, error: "--base requires a URL" };
2270
+ opts.baseUrl = v;
2271
+ break;
2272
+ }
2273
+ case "-h":
2274
+ case "--help":
2275
+ return { ok: false, error: TRY_USAGE };
2276
+ default:
2277
+ if (a.startsWith("--")) return { ok: false, error: `Unknown flag: ${a}
2278
+ ${TRY_USAGE}` };
2279
+ positional.push(a);
2263
2280
  }
2264
- [prev, curr] = [curr, prev];
2265
2281
  }
2266
- return prev[bLen];
2282
+ if (positional.length !== 1) {
2283
+ return { ok: false, error: `Expected exactly one server slug, got ${positional.length}.
2284
+ ${TRY_USAGE}` };
2285
+ }
2286
+ opts.slug = positional[0];
2287
+ if (Object.keys(env).length > 0) opts.envOverrides = env;
2288
+ return { ok: true, options: opts };
2267
2289
  }
2268
- function closestNames(query, candidates, limit) {
2269
- if (limit <= 0) return [];
2270
- const q = query.toLowerCase();
2271
- const scored = [];
2272
- for (const c of candidates) {
2273
- if (c === query) continue;
2274
- const lc = c.toLowerCase();
2275
- let score = null;
2276
- if (lc === q) {
2277
- score = 0;
2278
- } else if (lc.startsWith(q) || q.startsWith(lc)) {
2279
- score = 1;
2280
- } else if (lc.includes(q) || q.includes(lc)) {
2281
- score = 2;
2282
- } else {
2283
- const d = levenshtein(q, lc);
2284
- if (d <= 2) score = 2 + d;
2290
+ function parseTryCleanupArgs(argv) {
2291
+ if (argv.length === 0) return { ok: false, error: TRY_CLEANUP_USAGE };
2292
+ const opts = {};
2293
+ const positional = [];
2294
+ for (let i = 0; i < argv.length; i++) {
2295
+ const a = argv[i];
2296
+ if (a === "-h" || a === "--help") return { ok: false, error: TRY_CLEANUP_USAGE };
2297
+ if (a === "--base") {
2298
+ const v = argv[++i];
2299
+ if (!v) return { ok: false, error: "--base requires a URL" };
2300
+ opts.baseUrl = v;
2301
+ continue;
2285
2302
  }
2286
- if (score !== null) scored.push({ name: c, score });
2303
+ if (a.startsWith("--")) return { ok: false, error: `Unknown flag: ${a}
2304
+ ${TRY_CLEANUP_USAGE}` };
2305
+ positional.push(a);
2287
2306
  }
2288
- scored.sort((a, b) => {
2289
- if (a.score !== b.score) return a.score - b.score;
2290
- return a.name.localeCompare(b.name);
2291
- });
2292
- return scored.slice(0, limit).map((s) => s.name);
2307
+ if (positional.length !== 1) {
2308
+ return { ok: false, error: `Expected exactly one slug.
2309
+ ${TRY_CLEANUP_USAGE}` };
2310
+ }
2311
+ opts.slug = positional[0];
2312
+ return { ok: true, options: opts };
2293
2313
  }
2294
-
2295
- // src/install-cmd.ts
2296
- import { existsSync as existsSync2 } from "fs";
2297
- import { chmod, readFile as readFile4 } from "fs/promises";
2298
- import { homedir as homedir5 } from "os";
2299
- import { join as join5, resolve as resolve2 } from "path";
2300
- import { createInterface } from "readline/promises";
2301
- var USAGE = "Usage: mcph install <claude-code|claude-desktop|cursor|vscode> [--scope user|project|local]\n [--token <mcp_pat_\u2026>] [--project-dir <path>] [--os macos|linux|windows]\n [--force | --skip] [--dry-run] [--no-mcph-config]\n mcph install --list (detect clients; no writes)\n mcph install --all [--token <mcp_pat_\u2026>] (install into every detected client)";
2302
- async function runInstall(opts) {
2303
- const stdout = opts.io?.stdout ?? process.stdout;
2304
- const stderr = opts.io?.stderr ?? process.stderr;
2305
- const messages = [];
2306
- const log2 = (s) => {
2307
- messages.push(s);
2308
- stdout.write(`${s}
2309
- `);
2310
- };
2311
- const err = (s) => {
2312
- messages.push(s);
2313
- stderr.write(`${s}
2314
- `);
2315
- };
2316
- if (opts.listOnly && opts.all) {
2317
- err("mcph install: --list and --all are mutually exclusive");
2318
- return { written: [], wouldWrite: [], messages, exitCode: 2 };
2314
+ function parseDurationMs(s) {
2315
+ const m = /^(\d+)\s*([smhd])$/i.exec(s.trim());
2316
+ if (!m) return null;
2317
+ const n = Number(m[1]);
2318
+ if (!Number.isFinite(n) || n <= 0) return null;
2319
+ const unit = m[2].toLowerCase();
2320
+ const factor = unit === "s" ? 1e3 : unit === "m" ? 6e4 : unit === "h" ? 36e5 : 864e5;
2321
+ return n * factor;
2322
+ }
2323
+ function trialsDir(home = homedir5()) {
2324
+ return join5(home, CONFIG_DIRNAME, TRIALS_DIRNAME);
2325
+ }
2326
+ function trialMarkerPath(slug, home = homedir5()) {
2327
+ return join5(trialsDir(home), `${slug}.json`);
2328
+ }
2329
+ function anonIdPath(home = homedir5()) {
2330
+ return join5(trialsDir(home), ANON_FILENAME);
2331
+ }
2332
+ function computeAnonId() {
2333
+ const h = createHash("sha256");
2334
+ h.update(hostname());
2335
+ try {
2336
+ h.update(userInfo().username);
2337
+ } catch {
2319
2338
  }
2320
- if (opts.listOnly) return runInstallList(opts, log2);
2321
- if (opts.all) return runInstallAll(opts, log2, err);
2322
- if (opts.force && opts.skip) {
2323
- err("mcph install: --force and --skip are mutually exclusive");
2324
- return { written: [], wouldWrite: [], messages, exitCode: 2 };
2339
+ return h.digest("hex").slice(0, 16);
2340
+ }
2341
+ async function loadOrCreateAnonId(home = homedir5()) {
2342
+ const path5 = anonIdPath(home);
2343
+ if (existsSync2(path5)) {
2344
+ try {
2345
+ const raw = (await readFile4(path5, "utf8")).trim();
2346
+ if (/^[0-9a-f]{16}$/.test(raw)) return raw;
2347
+ } catch {
2348
+ }
2325
2349
  }
2326
- if (!opts.clientId) {
2327
- err(`mcph install: client argument required
2328
- ${USAGE}`);
2329
- return { written: [], wouldWrite: [], messages, exitCode: 2 };
2350
+ const id = computeAnonId();
2351
+ try {
2352
+ await mkdir3(trialsDir(home), { recursive: true });
2353
+ await atomicWriteFile(path5, `${id}
2354
+ `);
2355
+ if (process.platform !== "win32") {
2356
+ try {
2357
+ await chmod2(path5, 384);
2358
+ } catch {
2359
+ }
2360
+ }
2361
+ } catch {
2330
2362
  }
2331
- const target = INSTALL_TARGETS.find((t) => t.clientId === opts.clientId);
2332
- if (!target) {
2333
- err(`mcph install: unknown client ${opts.clientId}
2334
- ${USAGE}`);
2335
- return { written: [], wouldWrite: [], messages, exitCode: 2 };
2363
+ return id;
2364
+ }
2365
+ async function defaultFetchExplore(baseUrl, slug) {
2366
+ const url = `${baseUrl.replace(/\/$/, "")}/api/explore/${encodeURIComponent(slug)}`;
2367
+ const ac = new AbortController();
2368
+ const timer = setTimeout(() => ac.abort(), 1e4);
2369
+ try {
2370
+ const res = await fetch(url, { signal: ac.signal, headers: { accept: "application/json" } });
2371
+ if (res.status === 404) {
2372
+ throw new Error(`mcph try: no server with slug "${slug}" \u2014 check ${baseUrl}/explore for the catalog.`);
2373
+ }
2374
+ if (!res.ok) {
2375
+ throw new Error(`mcph try: ${url} returned HTTP ${res.status}`);
2376
+ }
2377
+ const body = await res.json();
2378
+ return validateExploreResponse(body, slug);
2379
+ } finally {
2380
+ clearTimeout(timer);
2336
2381
  }
2337
- const os = opts.os ?? CURRENT_OS;
2338
- if (!target.availableOn.includes(os)) {
2339
- const fix = target.clientId === "claude-desktop" && os === "linux" ? "Anthropic ships Claude Desktop on macOS and Windows only. Install Claude Code or Cursor instead." : "Pick a different client or pass --os to override.";
2340
- err(`mcph install: ${target.label} is not available on ${os}.
2341
- ${fix}`);
2342
- return { written: [], wouldWrite: [], messages, exitCode: 2 };
2382
+ }
2383
+ function validateExploreResponse(body, slug) {
2384
+ if (!body || typeof body !== "object") {
2385
+ throw new Error(`mcph try: /api/explore/${slug} returned a non-object response.`);
2343
2386
  }
2344
- const scope = opts.scope ?? (target.scopes.find((s) => s.scope === "user") ? "user" : target.scopes[0].scope);
2345
- const scopeSpec = target.scopes.find((s) => s.scope === scope);
2346
- if (!scopeSpec) {
2347
- err(
2348
- `mcph install: ${target.label} does not support scope "${scope}". Available: ${target.scopes.map((s) => s.scope).join(", ")}`
2349
- );
2350
- return { written: [], wouldWrite: [], messages, exitCode: 2 };
2387
+ const b = body;
2388
+ if (typeof b.slug !== "string" || typeof b.name !== "string" || typeof b.command !== "string") {
2389
+ throw new Error(`mcph try: /api/explore/${slug} missing required string fields (slug/name/command).`);
2351
2390
  }
2352
- const projectDir = scopeSpec.requiresProjectDir ? resolve2(opts.projectDir ?? process.cwd()) : void 0;
2353
- let resolved;
2391
+ if (!Array.isArray(b.args) || !b.args.every((x) => typeof x === "string")) {
2392
+ throw new Error(`mcph try: /api/explore/${slug} has invalid args (expected string[]).`);
2393
+ }
2394
+ const req = Array.isArray(b.requiredEnvVars) ? b.requiredEnvVars.filter((x) => typeof x === "string") : [];
2395
+ const out = {
2396
+ slug: b.slug,
2397
+ name: b.name,
2398
+ command: b.command,
2399
+ args: b.args,
2400
+ requiredEnvVars: req
2401
+ };
2402
+ if (typeof b.docUrl === "string") out.docUrl = b.docUrl;
2403
+ return out;
2404
+ }
2405
+ async function defaultPostEvent(baseUrl, body) {
2406
+ const url = `${baseUrl.replace(/\/$/, "")}/api/try/event`;
2354
2407
  try {
2355
- resolved = resolveInstallPath({
2356
- clientId: opts.clientId,
2357
- scope,
2358
- os,
2359
- home: opts.home,
2360
- projectDir,
2361
- claudeConfigDir: opts.claudeConfigDir
2408
+ const res = await request5(url, {
2409
+ method: "POST",
2410
+ headers: { "content-type": "application/json" },
2411
+ body: JSON.stringify(body),
2412
+ headersTimeout: 5e3,
2413
+ bodyTimeout: 5e3
2362
2414
  });
2363
- } catch (e) {
2364
- err(`mcph install: ${e.message}`);
2365
- return { written: [], wouldWrite: [], messages, exitCode: 2 };
2415
+ await res.body.text().catch(() => {
2416
+ });
2417
+ } catch (err) {
2418
+ log("debug", "try-event post failed", { error: err.message });
2366
2419
  }
2367
- log2(`Target: ${target.label} (${scope})`);
2368
- log2(`File: ${resolved.absolute}`);
2369
- let token6 = opts.token ?? null;
2370
- if (!token6) {
2371
- const cfg = await loadMcphConfig({ home: opts.home, cwd: process.cwd(), env: {} });
2372
- token6 = cfg.token;
2420
+ }
2421
+ async function autoDetectClient(opts) {
2422
+ const probes = await probeClientsAsync({
2423
+ home: opts.home,
2424
+ os: opts.os,
2425
+ cwd: opts.cwd,
2426
+ claudeConfigDir: opts.claudeConfigDir
2427
+ });
2428
+ for (const p of probes) {
2429
+ if (!p.unavailable && p.exists && !p.malformed) return p.clientId;
2373
2430
  }
2374
- if (!token6) {
2375
- err(
2376
- "\nmcph install: no token available.\n Pass one with --token mcp_pat_\u2026, or run `mcph install` with --token once to seed ~/.mcph/config.json,\n or create the token at https://mcp.hosting \u2192 Settings \u2192 API Tokens."
2377
- );
2378
- return { written: [], wouldWrite: [], messages, exitCode: 1 };
2431
+ for (const p of probes) {
2432
+ if (!p.unavailable) return p.clientId;
2379
2433
  }
2380
- const newEntry = buildLaunchEntry({ os });
2381
- const containerPath = resolved.containerPath;
2382
- let existing = {};
2383
- let existingHasEntry = false;
2384
- if (existsSync2(resolved.absolute)) {
2385
- let raw;
2386
- try {
2387
- raw = await readFile4(resolved.absolute, "utf8");
2388
- } catch (e) {
2389
- err(`mcph install: cannot read ${resolved.absolute}: ${e.message}`);
2390
- return { written: [], wouldWrite: [], messages, exitCode: 1 };
2391
- }
2392
- if (raw.trim().length > 0) {
2393
- try {
2394
- const parsed = parseJsonc(raw);
2395
- if (typeof parsed !== "object" || parsed === null || Array.isArray(parsed)) {
2396
- err(
2397
- `mcph install: ${resolved.absolute} is not a JSON object \u2014 refusing to overwrite. Edit by hand or rename the file and re-run.`
2398
- );
2399
- return { written: [], wouldWrite: [], messages, exitCode: 1 };
2434
+ return "claude-code";
2435
+ }
2436
+ async function runTry(opts) {
2437
+ const out = opts.out ?? ((s) => process.stdout.write(s));
2438
+ const err = opts.err ?? ((s) => process.stderr.write(s));
2439
+ const print = (s = "") => out(`${s}
2440
+ `);
2441
+ const printErr = (s) => err(`${s}
2442
+ `);
2443
+ if (!opts.slug) {
2444
+ printErr(TRY_USAGE);
2445
+ return { exitCode: 2, written: [] };
2446
+ }
2447
+ const slug = opts.slug;
2448
+ if (!/^[a-z0-9][a-z0-9-]{0,63}$/.test(slug)) {
2449
+ printErr(`mcph try: invalid slug "${slug}" (lowercase letters, digits, and dashes only).`);
2450
+ return { exitCode: 2, written: [] };
2451
+ }
2452
+ const env = opts.env ?? process.env;
2453
+ const home = opts.home ?? homedir5();
2454
+ const cwd = opts.cwd ?? process.cwd();
2455
+ const os = opts.os ?? CURRENT_OS;
2456
+ const now = opts.now ? opts.now() : Date.now();
2457
+ const baseUrl = opts.baseUrl ?? env.MCPH_BASE_URL ?? DEFAULT_BASE_URL;
2458
+ const ttlMs = opts.ttl ? parseDurationMs(opts.ttl) ?? DEFAULT_TTL_MS : DEFAULT_TTL_MS;
2459
+ const claudeConfigDir = env.CLAUDE_CONFIG_DIR && env.CLAUDE_CONFIG_DIR.length > 0 ? env.CLAUDE_CONFIG_DIR : void 0;
2460
+ const fetchExplore = opts.fetchExplore ?? defaultFetchExplore;
2461
+ let server;
2462
+ try {
2463
+ server = await fetchExplore(baseUrl, slug);
2464
+ } catch (e) {
2465
+ printErr(e.message);
2466
+ return { exitCode: 1, written: [] };
2467
+ }
2468
+ const clientId = opts.clientId ?? await autoDetectClient({ home, os, cwd, claudeConfigDir });
2469
+ const scope = clientId === "vscode" ? "project" : "user";
2470
+ const projectDir = scope === "project" ? resolve3(cwd) : void 0;
2471
+ let resolved;
2472
+ try {
2473
+ resolved = resolveInstallPath({ clientId, scope, os, home, projectDir, claudeConfigDir });
2474
+ } catch (e) {
2475
+ printErr(`mcph try: ${e.message}`);
2476
+ return { exitCode: 1, written: [] };
2477
+ }
2478
+ const supplied = { ...env, ...opts.envOverrides ?? {} };
2479
+ const missing = (server.requiredEnvVars ?? []).filter((k) => !supplied[k] || supplied[k] === "");
2480
+ if (missing.length > 0) {
2481
+ printErr(`mcph try: ${server.name} needs the following env var(s) before it can run:`);
2482
+ for (const k of missing) printErr(` - ${k}`);
2483
+ printErr("");
2484
+ printErr("Set them via --env KEY=value (repeatable) or your shell, then re-run:");
2485
+ const example = missing.map((k) => `--env ${k}=...`).join(" ");
2486
+ printErr(` mcph try ${slug} ${example}`);
2487
+ if (server.docUrl) printErr(`Docs: ${server.docUrl}`);
2488
+ return { exitCode: 1, written: [] };
2489
+ }
2490
+ const trialEnv = {};
2491
+ for (const k of server.requiredEnvVars ?? []) {
2492
+ const v = supplied[k];
2493
+ if (v) trialEnv[k] = v;
2494
+ }
2495
+ for (const [k, v] of Object.entries(opts.envOverrides ?? {})) {
2496
+ if (!(k in trialEnv)) trialEnv[k] = v;
2497
+ }
2498
+ const entry = buildLaunchEntry({
2499
+ os,
2500
+ upstream: {
2501
+ command: server.command,
2502
+ args: server.args,
2503
+ env: Object.keys(trialEnv).length > 0 ? trialEnv : void 0
2504
+ }
2505
+ });
2506
+ const entryName = `mcph-try-${slug}`;
2507
+ const expiresAt = now + ttlMs;
2508
+ const marker = {
2509
+ schemaVersion: TRIAL_SCHEMA_VERSION,
2510
+ slug,
2511
+ name: server.name,
2512
+ expiresAt,
2513
+ clientPath: resolved.absolute,
2514
+ clientName: clientId,
2515
+ containerPath: resolved.containerPath,
2516
+ entryName,
2517
+ createdAt: now
2518
+ };
2519
+ let existing = {};
2520
+ if (existsSync2(resolved.absolute)) {
2521
+ try {
2522
+ const raw = await readFile4(resolved.absolute, "utf8");
2523
+ if (raw.trim().length > 0) {
2524
+ const parsed = parseJsonc(raw);
2525
+ if (typeof parsed === "object" && parsed !== null && !Array.isArray(parsed)) {
2526
+ existing = parsed;
2527
+ } else {
2528
+ printErr(`mcph try: ${resolved.absolute} is not a JSON object \u2014 refusing to overwrite.`);
2529
+ return { exitCode: 1, written: [] };
2400
2530
  }
2401
- existing = parsed;
2402
- } catch (e) {
2403
- err(
2404
- `mcph install: ${resolved.absolute} is not valid JSON (${e.message}). Refusing to overwrite. Fix the file or rename it and re-run.`
2405
- );
2406
- return { written: [], wouldWrite: [], messages, exitCode: 1 };
2407
2531
  }
2532
+ } catch (e) {
2533
+ printErr(`mcph try: ${resolved.absolute} is not valid JSON (${e.message}). Refusing to overwrite.`);
2534
+ return { exitCode: 1, written: [] };
2408
2535
  }
2409
- const container = readNested(existing, containerPath);
2410
- if (typeof container === "object" && container !== null && !Array.isArray(container)) {
2411
- existingHasEntry = ENTRY_NAME in container;
2536
+ }
2537
+ const merged = mergeClientConfig(existing, resolved.containerPath, entry, entryName);
2538
+ const clientJson = `${JSON.stringify(merged, null, 2)}
2539
+ `;
2540
+ const markerJson = `${JSON.stringify(marker, null, 2)}
2541
+ `;
2542
+ if (opts.dryRun) {
2543
+ print(`mcph try (dry-run): would write ${resolved.absolute}`);
2544
+ print(` entry name: ${entryName}`);
2545
+ print(` command: ${entry.command} ${entry.args.join(" ")}`);
2546
+ if (entry.env) print(` env keys: ${Object.keys(entry.env).join(", ")}`);
2547
+ print(` expires: ${new Date(expiresAt).toISOString()}`);
2548
+ print(` marker: ${trialMarkerPath(slug, home)}`);
2549
+ return { exitCode: 0, written: [], marker };
2550
+ }
2551
+ const written = [];
2552
+ try {
2553
+ await mkdir3(trialsDir(home), { recursive: true });
2554
+ await atomicWriteFile(trialMarkerPath(slug, home), markerJson);
2555
+ written.push(trialMarkerPath(slug, home));
2556
+ } catch (e) {
2557
+ printErr(`mcph try: failed to write trial marker: ${e.message}`);
2558
+ return { exitCode: 1, written: [] };
2559
+ }
2560
+ try {
2561
+ await atomicWriteFile(resolved.absolute, clientJson);
2562
+ written.push(resolved.absolute);
2563
+ } catch (e) {
2564
+ printErr(`mcph try: failed to write ${resolved.absolute}: ${e.message}`);
2565
+ await unlink2(trialMarkerPath(slug, home)).catch(() => void 0);
2566
+ return { exitCode: 1, written: [] };
2567
+ }
2568
+ const anonId = await loadOrCreateAnonId(home);
2569
+ const postEvent = opts.postEvent ?? defaultPostEvent;
2570
+ postEvent(baseUrl, { slug, action: "try", anonId }).catch(() => void 0);
2571
+ const ttlPretty = formatTtl(ttlMs);
2572
+ print(`Trial wired: ${server.name} via mcph-try-${slug} -> ${resolved.absolute}`);
2573
+ print(`Expires in ${ttlPretty}; remove sooner with: mcph try-cleanup ${slug}`);
2574
+ print(`Liking it? Sign up at ${baseUrl}/signup to keep ${server.name} on every machine.`);
2575
+ return { exitCode: 0, written, marker };
2576
+ }
2577
+ async function runTryCleanup(opts) {
2578
+ const out = opts.out ?? ((s) => process.stdout.write(s));
2579
+ const err = opts.err ?? ((s) => process.stderr.write(s));
2580
+ const print = (s = "") => out(`${s}
2581
+ `);
2582
+ const printErr = (s) => err(`${s}
2583
+ `);
2584
+ if (!opts.slug) {
2585
+ printErr(TRY_CLEANUP_USAGE);
2586
+ return { exitCode: 2, written: [] };
2587
+ }
2588
+ const slug = opts.slug;
2589
+ if (!/^[a-z0-9][a-z0-9-]{0,63}$/.test(slug)) {
2590
+ printErr(`mcph try-cleanup: invalid slug "${slug}".`);
2591
+ return { exitCode: 2, written: [] };
2592
+ }
2593
+ const env = opts.env ?? process.env;
2594
+ const home = opts.home ?? homedir5();
2595
+ const baseUrl = opts.baseUrl ?? env.MCPH_BASE_URL ?? DEFAULT_BASE_URL;
2596
+ const markerPath = trialMarkerPath(slug, home);
2597
+ if (!existsSync2(markerPath)) {
2598
+ print(`mcph try-cleanup: no trial marker for "${slug}" (nothing to do).`);
2599
+ return { exitCode: 0, written: [] };
2600
+ }
2601
+ let marker;
2602
+ try {
2603
+ const raw = await readFile4(markerPath, "utf8");
2604
+ const parsed = JSON.parse(raw);
2605
+ if (!parsed || typeof parsed !== "object" || typeof parsed.entryName !== "string") {
2606
+ throw new Error("marker is missing required fields");
2412
2607
  }
2608
+ marker = parsed;
2609
+ } catch (e) {
2610
+ printErr(`mcph try-cleanup: marker at ${markerPath} is unreadable (${e.message}).`);
2611
+ return { exitCode: 1, written: [] };
2413
2612
  }
2414
- if (existingHasEntry) {
2415
- let decision;
2416
- if (opts.force) decision = "overwrite";
2417
- else if (opts.skip) decision = "skip";
2418
- else if (opts.promptAnswer) decision = opts.promptAnswer;
2419
- else if (opts.io?.isTTY ?? process.stdout.isTTY) {
2420
- decision = await promptCollision(resolved.absolute, opts.io);
2421
- } else {
2422
- err(
2423
- `mcph install: ${resolved.absolute} already has a "${ENTRY_NAME}" entry and stdin is not a TTY.
2424
- Re-run with --force to overwrite, --skip to leave it, or --dry-run to preview.`
2613
+ if (existsSync2(marker.clientPath)) {
2614
+ try {
2615
+ const raw = await readFile4(marker.clientPath, "utf8");
2616
+ if (raw.trim().length > 0) {
2617
+ const parsed = parseJsonc(raw);
2618
+ if (typeof parsed === "object" && parsed !== null && !Array.isArray(parsed)) {
2619
+ const stripped = removeFromClientConfig(
2620
+ parsed,
2621
+ marker.containerPath,
2622
+ marker.entryName
2623
+ );
2624
+ if (stripped !== parsed) {
2625
+ await atomicWriteFile(marker.clientPath, `${JSON.stringify(stripped, null, 2)}
2626
+ `);
2627
+ print(`Removed ${marker.entryName} from ${marker.clientPath}`);
2628
+ }
2629
+ }
2630
+ }
2631
+ } catch (e) {
2632
+ printErr(
2633
+ `mcph try-cleanup: warning \u2014 couldn't strip ${marker.entryName} from ${marker.clientPath} (${e.message}).`
2425
2634
  );
2426
- return { written: [], wouldWrite: [], messages, exitCode: 1 };
2427
2635
  }
2428
- if (decision === "skip") {
2429
- log2(`Existing "${ENTRY_NAME}" entry left untouched. Nothing to do.`);
2430
- return { written: [], wouldWrite: [], messages, exitCode: 0 };
2636
+ }
2637
+ try {
2638
+ await unlink2(markerPath);
2639
+ } catch (e) {
2640
+ printErr(`mcph try-cleanup: couldn't delete marker ${markerPath} (${e.message}).`);
2641
+ return { exitCode: 1, written: [] };
2642
+ }
2643
+ const anonId = await loadOrCreateAnonId(home);
2644
+ const postEvent = opts.postEvent ?? defaultPostEvent;
2645
+ postEvent(baseUrl, { slug, action: "cleanup", anonId }).catch(() => void 0);
2646
+ print(`Trial for "${slug}" cleaned up.`);
2647
+ return { exitCode: 0, written: [marker.clientPath] };
2648
+ }
2649
+ function formatTtl(ms) {
2650
+ const clamped = Math.max(0, ms);
2651
+ if (clamped < 6e4) return `${Math.round(clamped / 1e3)}s`;
2652
+ if (clamped < 36e5) return `${Math.round(clamped / 6e4)}m`;
2653
+ if (clamped < 864e5) return `${Math.round(clamped / 36e5)}h`;
2654
+ return `${Math.round(clamped / 864e5)}d`;
2655
+ }
2656
+ async function scanTrials(opts = {}) {
2657
+ const home = opts.home ?? homedir5();
2658
+ const now = opts.now ? opts.now() : Date.now();
2659
+ const dir = trialsDir(home);
2660
+ const result = { live: [], expired: [], malformed: [] };
2661
+ if (!existsSync2(dir)) return result;
2662
+ let entries;
2663
+ try {
2664
+ entries = await readdir(dir);
2665
+ } catch {
2666
+ return result;
2667
+ }
2668
+ for (const filename of entries) {
2669
+ if (!filename.endsWith(".json")) continue;
2670
+ const path5 = join5(dir, filename);
2671
+ try {
2672
+ const raw = await readFile4(path5, "utf8");
2673
+ const parsed = JSON.parse(raw);
2674
+ if (!parsed || typeof parsed !== "object" || typeof parsed.slug !== "string" || typeof parsed.expiresAt !== "number" || typeof parsed.clientPath !== "string" || !Array.isArray(parsed.containerPath) || typeof parsed.entryName !== "string") {
2675
+ result.malformed.push(path5);
2676
+ continue;
2677
+ }
2678
+ const msUntilExpiry = parsed.expiresAt - now;
2679
+ const expired = msUntilExpiry <= 0;
2680
+ const entry = { marker: parsed, msUntilExpiry, expired };
2681
+ if (expired) result.expired.push(entry);
2682
+ else result.live.push(entry);
2683
+ } catch {
2684
+ result.malformed.push(path5);
2431
2685
  }
2432
- if (decision === "abort") {
2433
- err("Aborted.");
2434
- return { written: [], wouldWrite: [], messages, exitCode: 1 };
2686
+ }
2687
+ return result;
2688
+ }
2689
+ async function gcExpiredTrials(opts) {
2690
+ const home = opts.home ?? homedir5();
2691
+ const env = opts.env ?? process.env;
2692
+ const baseUrl = opts.baseUrl ?? env.MCPH_BASE_URL ?? DEFAULT_BASE_URL;
2693
+ const postEvent = opts.postEvent ?? defaultPostEvent;
2694
+ const scan = await scanTrials({ home, now: opts.now });
2695
+ if (scan.expired.length === 0) return { cleared: 0, failed: 0 };
2696
+ const anonId = await loadOrCreateAnonId(home);
2697
+ let cleared = 0;
2698
+ let failed = 0;
2699
+ for (const { marker } of scan.expired) {
2700
+ try {
2701
+ if (existsSync2(marker.clientPath)) {
2702
+ const raw = await readFile4(marker.clientPath, "utf8");
2703
+ if (raw.trim().length > 0) {
2704
+ const parsed = parseJsonc(raw);
2705
+ if (typeof parsed === "object" && parsed !== null && !Array.isArray(parsed)) {
2706
+ const stripped = removeFromClientConfig(
2707
+ parsed,
2708
+ marker.containerPath,
2709
+ marker.entryName
2710
+ );
2711
+ if (stripped !== parsed) {
2712
+ await atomicWriteFile(marker.clientPath, `${JSON.stringify(stripped, null, 2)}
2713
+ `);
2714
+ }
2715
+ }
2716
+ }
2717
+ }
2718
+ await unlink2(trialMarkerPath(marker.slug, home));
2719
+ postEvent(baseUrl, { slug: marker.slug, action: "expiry-gc", anonId }).catch(() => void 0);
2720
+ cleared++;
2721
+ } catch (e) {
2722
+ log("debug", "trial gc failed", { slug: marker.slug, error: e.message });
2723
+ failed++;
2435
2724
  }
2436
- log2(`Overwriting existing "${ENTRY_NAME}" entry.`);
2437
2725
  }
2438
- const merged = mergeClientConfig(existing, containerPath, newEntry);
2439
- const clientJson = `${JSON.stringify(merged, null, 2)}
2440
- `;
2441
- const writeMcphConfig = !opts.skipMcphConfig;
2442
- const home = opts.home ?? homedir5();
2443
- const mcphConfigPath = join5(home, CONFIG_DIRNAME, CONFIG_FILENAME);
2444
- const mcphConfigComposed = await composeMcphConfig(mcphConfigPath, token6);
2445
- if (mcphConfigComposed.backupPath) {
2446
- log2(
2447
- `mcph install: existing ${mcphConfigPath} was malformed; original bytes backed up to ${mcphConfigComposed.backupPath} before overwriting.`
2448
- );
2726
+ return { cleared, failed };
2727
+ }
2728
+
2729
+ // src/usage-hints.ts
2730
+ var MAX_PEERS = 3;
2731
+ var MIN_SUCCESS_TO_SHOW = 1;
2732
+ var RELIABILITY_MIN_OBSERVATIONS = 3;
2733
+ var RELIABILITY_THRESHOLD = 0.8;
2734
+ function buildCoUsageMap(packs) {
2735
+ const result = /* @__PURE__ */ new Map();
2736
+ for (const pack of packs) {
2737
+ for (const ns of pack.namespaces) {
2738
+ const bucket = result.get(ns) ?? /* @__PURE__ */ new Set();
2739
+ for (const peer of pack.namespaces) {
2740
+ if (peer !== ns) bucket.add(peer);
2741
+ }
2742
+ result.set(ns, bucket);
2743
+ }
2744
+ }
2745
+ const sorted = /* @__PURE__ */ new Map();
2746
+ for (const [ns, peers] of result) {
2747
+ sorted.set(ns, Array.from(peers).sort());
2748
+ }
2749
+ return sorted;
2750
+ }
2751
+ function formatUsageHint(usage, coUsedWith) {
2752
+ const parts = [];
2753
+ if (usage && usage.succeeded >= MIN_SUCCESS_TO_SHOW) {
2754
+ parts.push(`used ${usage.succeeded}x`);
2755
+ }
2756
+ if (coUsedWith.length > 0) {
2757
+ const shown = coUsedWith.slice(0, MAX_PEERS);
2758
+ const more = coUsedWith.length - shown.length;
2759
+ const names = shown.map((n) => `"${n}"`).join(", ");
2760
+ const tail = more > 0 ? ` +${more} more` : "";
2761
+ parts.push(`often loaded with ${names}${tail}`);
2762
+ }
2763
+ if (parts.length === 0) return null;
2764
+ return `usage: ${parts.join("; ")}`;
2765
+ }
2766
+ function formatReliabilityWarning(usage) {
2767
+ if (!usage || usage.dispatched < RELIABILITY_MIN_OBSERVATIONS) return null;
2768
+ const rate = usage.succeeded / usage.dispatched;
2769
+ if (rate >= RELIABILITY_THRESHOLD) return null;
2770
+ const pct = Math.round(rate * 100);
2771
+ return `reliability: ${pct}% success across ${usage.dispatched} past calls`;
2772
+ }
2773
+ function selectFlakyNamespaces(entries, limit) {
2774
+ if (limit <= 0) return [];
2775
+ return Array.from(entries).filter(({ usage }) => {
2776
+ if (usage.dispatched < RELIABILITY_MIN_OBSERVATIONS) return false;
2777
+ return usage.succeeded / usage.dispatched < RELIABILITY_THRESHOLD;
2778
+ }).sort((a, b) => {
2779
+ const aRate = a.usage.succeeded / a.usage.dispatched;
2780
+ const bRate = b.usage.succeeded / b.usage.dispatched;
2781
+ if (aRate !== bRate) return aRate - bRate;
2782
+ if (a.usage.dispatched !== b.usage.dispatched) return b.usage.dispatched - a.usage.dispatched;
2783
+ return a.namespace.localeCompare(b.namespace);
2784
+ }).slice(0, limit);
2785
+ }
2786
+
2787
+ // src/doctor-cmd.ts
2788
+ var VERSION = true ? "0.55.0" : "dev";
2789
+ async function runDoctor(opts = {}) {
2790
+ if (opts.json) return runDoctorJson(opts);
2791
+ const lines = [];
2792
+ const write = opts.out ?? ((s) => process.stdout.write(s));
2793
+ const print = (s = "") => {
2794
+ lines.push(s);
2795
+ write(`${s}
2796
+ `);
2797
+ };
2798
+ const cwd = opts.cwd ?? process.cwd();
2799
+ const home = opts.home ?? homedir6();
2800
+ const os = opts.os ?? CURRENT_OS;
2801
+ const env = opts.env ?? process.env;
2802
+ print(`mcph doctor \u2014 ${(/* @__PURE__ */ new Date()).toISOString()}`);
2803
+ print(`mcph version: ${VERSION}`);
2804
+ print(`platform: ${os}`);
2805
+ print("");
2806
+ const config = await loadMcphConfig({ cwd, home, env });
2807
+ print("CONFIG FILES");
2808
+ if (config.loadedFiles.length === 0) {
2809
+ print(" (none \u2014 using defaults + env)");
2810
+ } else {
2811
+ for (const f of config.loadedFiles) {
2812
+ print(` ${f.scope.padEnd(7)} ${f.path}${schemaSuffix(f)}`);
2813
+ }
2814
+ }
2815
+ print("");
2816
+ print("TOKEN");
2817
+ print(` value: ${tokenFingerprint(config.token)}`);
2818
+ print(` source: ${config.tokenSource}`);
2819
+ print("");
2820
+ print("API BASE");
2821
+ print(` value: ${config.apiBase}`);
2822
+ print(` source: ${config.apiBaseSource}`);
2823
+ print("");
2824
+ renderEnvSection({ env, print });
2825
+ await renderStateSection({ home, env, print });
2826
+ await renderReliabilitySection({ home, env, print });
2827
+ await renderTrialsSection({ home, env, print, postEvent: opts.postTryEvent, now: opts.now });
2828
+ renderBackgroundPostersSection({ print });
2829
+ const claudeConfigDir = env.CLAUDE_CONFIG_DIR && env.CLAUDE_CONFIG_DIR.length > 0 ? env.CLAUDE_CONFIG_DIR : void 0;
2830
+ const clients = probeClients({ home, os, cwd, claudeConfigDir });
2831
+ print("INSTALLED CLIENTS (probed config files)");
2832
+ for (const c of clients) {
2833
+ const status = c.unavailable ? "unavailable on this OS" : c.malformed ? "exists but JSON is malformed \u2014 fix or rerun `mcph install`" : c.hasMcphEntry ? `OK \u2014 has "${ENTRY_NAME}" entry` : c.exists ? `present, no "${ENTRY_NAME}" entry \u2014 run \`mcph install ${c.clientId}${c.scope === "user" ? "" : ` --scope ${c.scope}`}\`` : `not configured \u2014 run \`mcph install ${c.clientId}${c.scope === "user" ? "" : ` --scope ${c.scope}`}\``;
2834
+ const label = INSTALL_TARGETS.find((t) => t.clientId === c.clientId)?.label ?? c.clientId;
2835
+ print(` ${label} (${c.scope}): ${status}`);
2836
+ print(` ${c.path}`);
2837
+ }
2838
+ print("");
2839
+ if (config.warnings.length > 0) {
2840
+ print("WARNINGS");
2841
+ for (const w of config.warnings) print(` ! ${w}`);
2842
+ print("");
2843
+ }
2844
+ const shadowHits = scanShellHistoryForShadows({ home, env });
2845
+ if (shadowHits.length > 0) {
2846
+ print("SHADOWED CLI USAGE (recent shell history)");
2847
+ print(" Commands below have MCP servers that can replace them;");
2848
+ print(" activate the server and prefer its tools over the CLI.");
2849
+ for (const hit of shadowHits) {
2850
+ const pluralHit = hit.count === 1 ? "time" : "times";
2851
+ print(` ${hit.cli.padEnd(12)} ${hit.count} ${pluralHit} \u2192 server(s): ${hit.namespaces.join(", ")}`);
2852
+ }
2853
+ print("");
2854
+ }
2855
+ const skipCheck = opts.skipRegistryCheck === true || Boolean(process.env.VITEST);
2856
+ const latest = skipCheck ? null : await fetchLatestVersion(opts.registryFetch);
2857
+ const staleHint = latest && VERSION !== "dev" && compareSemver(VERSION, latest) < 0 ? latest : null;
2858
+ if (staleHint) {
2859
+ print("UPGRADE AVAILABLE");
2860
+ print(` Running ${VERSION}; npm latest is ${staleHint}.`);
2861
+ print(" Run `mcph upgrade` to see the exact command for your install, or");
2862
+ print(" `mcph upgrade --run` to execute it (global-npm installs only).");
2863
+ print("");
2864
+ }
2865
+ let exitCode = 0;
2866
+ if (config.token === null) {
2867
+ exitCode = 1;
2868
+ print("DIAGNOSIS");
2869
+ print(" No token resolved \u2014 mcph cannot start.");
2870
+ print(" Run `mcph install <client> --token mcp_pat_\u2026` to seed ~/.mcph/config.json.");
2871
+ } else if (config.warnings.length > 0) {
2872
+ exitCode = 2;
2873
+ print("DIAGNOSIS");
2874
+ print(" Token present, but warnings above need attention.");
2875
+ } else {
2876
+ print("DIAGNOSIS");
2877
+ print(staleHint ? " Healthy, but an upgrade is available (see above)." : " All good. mcph should start cleanly.");
2878
+ }
2879
+ return { exitCode, lines, snapshot: { version: VERSION, config, clients } };
2880
+ }
2881
+ async function runDoctorJson(opts) {
2882
+ const lines = [];
2883
+ const write = opts.out ?? ((s) => process.stdout.write(s));
2884
+ const cwd = opts.cwd ?? process.cwd();
2885
+ const home = opts.home ?? homedir6();
2886
+ const os = opts.os ?? CURRENT_OS;
2887
+ const env = opts.env ?? process.env;
2888
+ const timestamp = (/* @__PURE__ */ new Date()).toISOString();
2889
+ const config = await loadMcphConfig({ cwd, home, env });
2890
+ const claudeConfigDir = env.CLAUDE_CONFIG_DIR && env.CLAUDE_CONFIG_DIR.length > 0 ? env.CLAUDE_CONFIG_DIR : void 0;
2891
+ const clients = probeClients({ home, os, cwd, claudeConfigDir });
2892
+ const envVarNames = [
2893
+ "MCPH_POLL_INTERVAL",
2894
+ "MCPH_SERVER_CAP",
2895
+ "MCPH_MIN_COMPLIANCE",
2896
+ "MCPH_AUTO_LOAD",
2897
+ "MCPH_AUTO_ACTIVATE",
2898
+ "MCPH_PRUNE_RESPONSES"
2899
+ ];
2900
+ const envOverrides = {};
2901
+ for (const name of envVarNames) {
2902
+ const raw = env[name];
2903
+ envOverrides[name] = raw === void 0 || raw === "" ? null : raw;
2904
+ }
2905
+ const persistRaw = env.MCPH_DISABLE_PERSISTENCE;
2906
+ const persistDisabled = persistRaw !== void 0 && persistRaw !== "" && (persistRaw === "1" || persistRaw.toLowerCase() === "true");
2907
+ const state = persistDisabled ? { disabled: true, path: null, savedAt: null, learningEntries: null, packHistoryEntries: null } : await (async () => {
2908
+ const filePath = join6(userConfigDir(home), STATE_FILENAME);
2909
+ const persisted = await loadState(filePath);
2910
+ const fresh = persisted.savedAt === 0;
2911
+ return {
2912
+ disabled: false,
2913
+ path: filePath,
2914
+ savedAt: fresh ? null : new Date(persisted.savedAt).toISOString(),
2915
+ learningEntries: fresh ? 0 : Object.keys(persisted.learning).length,
2916
+ packHistoryEntries: fresh ? 0 : persisted.packHistory.length
2917
+ };
2918
+ })();
2919
+ const reliability = [];
2920
+ if (!persistDisabled) {
2921
+ const filePath = join6(userConfigDir(home), STATE_FILENAME);
2922
+ const persisted = await loadState(filePath);
2923
+ if (persisted.savedAt !== 0) {
2924
+ const entries = Object.entries(persisted.learning).map(([namespace, usage]) => ({ namespace, usage }));
2925
+ for (const { namespace, usage } of selectFlakyNamespaces(entries, 5)) {
2926
+ reliability.push({
2927
+ namespace,
2928
+ dispatched: usage.dispatched,
2929
+ succeeded: usage.succeeded,
2930
+ successRate: usage.succeeded / usage.dispatched,
2931
+ lastUsedAt: new Date(usage.lastUsedAt).toISOString()
2932
+ });
2933
+ }
2934
+ }
2935
+ }
2936
+ const shellShadows = scanShellHistoryForShadows({ home, env });
2937
+ const skipCheck = opts.skipRegistryCheck === true || Boolean(process.env.VITEST);
2938
+ const latest = skipCheck ? null : await fetchLatestVersion(opts.registryFetch);
2939
+ const stale = latest !== null && VERSION !== "dev" && compareSemver(VERSION, latest) < 0;
2940
+ let exitCode = 0;
2941
+ let summary;
2942
+ if (config.token === null) {
2943
+ exitCode = 1;
2944
+ summary = "No token resolved \u2014 mcph cannot start.";
2945
+ } else if (config.warnings.length > 0) {
2946
+ exitCode = 2;
2947
+ summary = "Token present, but warnings need attention.";
2948
+ } else {
2949
+ summary = stale ? "Healthy, but an upgrade is available." : "All good. mcph should start cleanly.";
2950
+ }
2951
+ const snapshotJson = {
2952
+ timestamp,
2953
+ version: VERSION,
2954
+ platform: os,
2955
+ token: { fingerprint: tokenFingerprint(config.token), source: config.tokenSource },
2956
+ apiBase: { value: config.apiBase, source: config.apiBaseSource },
2957
+ loadedFiles: config.loadedFiles.map((f) => ({
2958
+ scope: f.scope,
2959
+ path: f.path,
2960
+ ...f.version !== void 0 ? { schemaVersion: f.version } : {},
2961
+ schemaAhead: f.version !== void 0 && f.version > CURRENT_SCHEMA_VERSION
2962
+ })),
2963
+ warnings: config.warnings,
2964
+ env: envOverrides,
2965
+ state,
2966
+ reliability,
2967
+ clients,
2968
+ shellShadows,
2969
+ upgrade: { current: VERSION, latest, stale },
2970
+ diagnosis: { exitCode, summary }
2971
+ };
2972
+ const blob = JSON.stringify(snapshotJson, null, 2);
2973
+ lines.push(blob);
2974
+ write(`${blob}
2975
+ `);
2976
+ return { exitCode, lines, snapshot: { version: VERSION, config, clients } };
2977
+ }
2978
+ function renderEnvSection(opts) {
2979
+ const { env, print } = opts;
2980
+ const vars = [
2981
+ { name: "MCPH_POLL_INTERVAL", defaultHint: "default 60s" },
2982
+ { name: "MCPH_SERVER_CAP", defaultHint: "default 6" },
2983
+ { name: "MCPH_MIN_COMPLIANCE", defaultHint: "filter inactive" },
2984
+ { name: "MCPH_AUTO_LOAD", defaultHint: "auto-load inactive" },
2985
+ { name: "MCPH_AUTO_ACTIVATE", defaultHint: "default on" },
2986
+ { name: "MCPH_PRUNE_RESPONSES", defaultHint: "pruning active" }
2987
+ ];
2988
+ const widest = vars.reduce((m, v) => Math.max(m, v.name.length), 0);
2989
+ print("ENVIRONMENT (behavior overrides)");
2990
+ for (const v of vars) {
2991
+ const raw = env[v.name];
2992
+ const value = raw === void 0 || raw === "" ? `(not set \u2014 ${v.defaultHint})` : raw;
2993
+ print(` ${v.name.padEnd(widest)} ${value}`);
2994
+ }
2995
+ print("");
2996
+ }
2997
+ async function renderStateSection(opts) {
2998
+ const { home, env, print } = opts;
2999
+ const raw = env.MCPH_DISABLE_PERSISTENCE;
3000
+ const disabled = raw !== void 0 && raw !== "" && (raw === "1" || raw.toLowerCase() === "true");
3001
+ print("STATE");
3002
+ if (disabled) {
3003
+ print(" status: disabled via MCPH_DISABLE_PERSISTENCE");
3004
+ print("");
3005
+ return;
3006
+ }
3007
+ const filePath = join6(userConfigDir(home), STATE_FILENAME);
3008
+ print(` path: ${filePath}`);
3009
+ const peek = await peekStateFile(filePath);
3010
+ if (peek.kind === "malformed") {
3011
+ print(" status: corrupt -- file exists but JSON is unparseable");
3012
+ print(` fix: \`mcph reset-learning\` to clear, or open ${filePath} and fix by hand`);
3013
+ print(` detail: ${peek.message}`);
3014
+ print("");
3015
+ return;
2449
3016
  }
2450
- const mcphConfigJson = mcphConfigComposed.json;
2451
- const settingsPatch = opts.clientId === "claude-code" ? await prepareClaudeCodeSettingsPatch({
2452
- scope,
2453
- home,
2454
- projectDir,
2455
- os,
2456
- claudeConfigDir: opts.claudeConfigDir
2457
- }) : null;
2458
- if (opts.dryRun) {
2459
- log2("\n--- dry run: would write the following ---");
2460
- if (writeMcphConfig) log2(`# ${mcphConfigPath}
2461
- ${mcphConfigJson}`);
2462
- log2(`
2463
- # ${resolved.absolute}
2464
- ${clientJson}`);
2465
- if (settingsPatch?.changed) log2(`# ${settingsPatch.path}
2466
- ${settingsPatch.nextJson}`);
2467
- const wouldWrite = [];
2468
- if (writeMcphConfig) wouldWrite.push(mcphConfigPath);
2469
- wouldWrite.push(resolved.absolute);
2470
- if (settingsPatch?.changed) wouldWrite.push(settingsPatch.path);
2471
- return { written: [], wouldWrite, messages, exitCode: 0 };
3017
+ if (peek.kind === "stale-version") {
3018
+ print(` status: schema mismatch (file is v${peek.version ?? "?"}, this mcph reads v${peek.expected})`);
3019
+ print(" fix: `mcph reset-learning` to drop the old file -- learning will rebuild on use");
3020
+ print("");
3021
+ return;
2472
3022
  }
2473
- const written = [];
2474
- if (writeMcphConfig) {
2475
- try {
2476
- await atomicWriteFile(mcphConfigPath, mcphConfigJson);
2477
- if (process.platform !== "win32") {
2478
- try {
2479
- await chmod(mcphConfigPath, 384);
2480
- } catch {
2481
- }
2482
- }
2483
- } catch (e) {
2484
- err(`mcph install: failed to write ${mcphConfigPath}: ${e.message}`);
2485
- return { written: [], wouldWrite: [], messages, exitCode: 1 };
3023
+ if (peek.kind === "unreadable") {
3024
+ print(` status: unreadable (${peek.message})`);
3025
+ print("");
3026
+ return;
3027
+ }
3028
+ const persisted = await loadState(filePath);
3029
+ if (persisted.savedAt === 0) {
3030
+ print(" (no persisted state yet \u2014 will be created on the first tool call)");
3031
+ } else {
3032
+ print(` last saved: ${formatRelativeAge(Date.now() - persisted.savedAt)} ago`);
3033
+ print(` learning entries: ${Object.keys(persisted.learning).length}`);
3034
+ print(` pack history entries: ${persisted.packHistory.length}`);
3035
+ }
3036
+ print("");
3037
+ }
3038
+ async function peekStateFile(filePath) {
3039
+ let raw;
3040
+ try {
3041
+ raw = await readFile5(filePath, "utf8");
3042
+ } catch (err) {
3043
+ if (err && typeof err === "object" && "code" in err && err.code === "ENOENT") {
3044
+ return { kind: "missing" };
2486
3045
  }
2487
- log2(`Wrote ${mcphConfigPath}`);
2488
- written.push(mcphConfigPath);
3046
+ return { kind: "unreadable", message: err instanceof Error ? err.message : String(err) };
2489
3047
  }
3048
+ let parsed;
2490
3049
  try {
2491
- await atomicWriteFile(resolved.absolute, clientJson);
2492
- } catch (e) {
2493
- err(`mcph install: failed to write ${resolved.absolute}: ${e.message}`);
2494
- return { written, wouldWrite: [], messages, exitCode: 1 };
3050
+ parsed = JSON.parse(raw);
3051
+ } catch (err) {
3052
+ return { kind: "malformed", message: err instanceof Error ? err.message : String(err) };
2495
3053
  }
2496
- log2(`Wrote ${resolved.absolute}`);
2497
- written.push(resolved.absolute);
2498
- if (settingsPatch?.changed) {
2499
- try {
2500
- await atomicWriteFile(settingsPatch.path, settingsPatch.nextJson);
2501
- log2(`Wrote ${settingsPatch.path} (added ${CLAUDE_CODE_ALLOW_PATTERN} to permissions.allow)`);
2502
- written.push(settingsPatch.path);
2503
- } catch (e) {
2504
- err(
2505
- `mcph install: warning \u2014 failed to patch ${settingsPatch.path}: ${e.message}. You may be re-prompted for each mcph tool call; add "${CLAUDE_CODE_ALLOW_PATTERN}" to permissions.allow to silence.`
2506
- );
2507
- }
3054
+ if (!parsed || typeof parsed !== "object") return { kind: "malformed", message: "top-level value is not an object" };
3055
+ const version = parsed.version;
3056
+ if (version !== STATE_SCHEMA_VERSION) {
3057
+ return { kind: "stale-version", version, expected: STATE_SCHEMA_VERSION };
2508
3058
  }
2509
- if (target.notes) log2(`Note: ${target.notes}`);
2510
- log2(`
2511
- \u2713 ${target.label} is configured. Restart it to pick up the new MCP server.`);
2512
- return { written, wouldWrite: [], messages, exitCode: 0 };
3059
+ return { kind: "ok" };
2513
3060
  }
2514
- async function prepareClaudeCodeSettingsPatch(opts) {
2515
- const path5 = resolveClaudeCodeSettingsPath(opts.scope, {
2516
- home: opts.home,
2517
- projectDir: opts.projectDir,
2518
- os: opts.os,
2519
- claudeConfigDir: opts.claudeConfigDir
2520
- });
2521
- if (!path5) return null;
2522
- let existing = {};
2523
- if (existsSync2(path5)) {
2524
- try {
2525
- const raw = await readFile4(path5, "utf8");
2526
- if (raw.trim().length > 0) {
2527
- const parsed = parseJsonc(raw);
2528
- if (typeof parsed === "object" && parsed !== null && !Array.isArray(parsed)) {
2529
- existing = parsed;
2530
- } else {
2531
- return { path: path5, nextJson: "", changed: false };
2532
- }
2533
- }
2534
- } catch {
2535
- return { path: path5, nextJson: "", changed: false };
2536
- }
3061
+ async function renderReliabilitySection(opts) {
3062
+ const { home, env, print } = opts;
3063
+ const raw = env.MCPH_DISABLE_PERSISTENCE;
3064
+ const disabled = raw !== void 0 && raw !== "" && (raw === "1" || raw.toLowerCase() === "true");
3065
+ if (disabled) return;
3066
+ const filePath = join6(userConfigDir(home), STATE_FILENAME);
3067
+ const persisted = await loadState(filePath);
3068
+ if (persisted.savedAt === 0) return;
3069
+ const entries = Object.entries(persisted.learning).map(([namespace, usage]) => ({ namespace, usage }));
3070
+ const flaky = selectFlakyNamespaces(entries, 5);
3071
+ if (flaky.length === 0) return;
3072
+ print("RELIABILITY (dormant, <80% success)");
3073
+ const now = Date.now();
3074
+ for (const { namespace, usage } of flaky) {
3075
+ const rate = Math.round(usage.succeeded / usage.dispatched * 100);
3076
+ const age = formatRelativeAge(now - usage.lastUsedAt);
3077
+ print(` ${namespace} \u2014 ${usage.dispatched} calls, ${rate}% success, last used ${age} ago`);
2537
3078
  }
2538
- const merged = mergePermissionsAllow(existing, [CLAUDE_CODE_ALLOW_PATTERN]);
2539
- const before = JSON.stringify(existing);
2540
- const after = JSON.stringify(merged);
2541
- if (before === after) return { path: path5, nextJson: "", changed: false };
2542
- return { path: path5, nextJson: `${JSON.stringify(merged, null, 2)}
2543
- `, changed: true };
3079
+ print("");
2544
3080
  }
2545
- function mergePermissionsAllow(existing, patterns) {
2546
- const out = { ...existing };
2547
- const prev = out.permissions;
2548
- const perms = typeof prev === "object" && prev !== null && !Array.isArray(prev) ? { ...prev } : {};
2549
- const prevAllow = perms.allow;
2550
- const allow = Array.isArray(prevAllow) ? prevAllow.filter((x) => typeof x === "string") : [];
2551
- for (const p of patterns) {
2552
- if (!allow.includes(p)) allow.push(p);
3081
+ async function renderTrialsSection(opts) {
3082
+ const { home, env, print, postEvent, now } = opts;
3083
+ const gc = await gcExpiredTrials({ home, env, postEvent, now }).catch(() => ({ cleared: 0, failed: 0 }));
3084
+ const scan = await scanTrials({ home, now });
3085
+ if (scan.live.length === 0 && gc.cleared === 0 && scan.malformed.length === 0) return;
3086
+ print("TRIALS (mcph try)");
3087
+ if (gc.cleared > 0) {
3088
+ print(` swept ${gc.cleared} expired trial${gc.cleared === 1 ? "" : "s"} this run`);
3089
+ }
3090
+ for (const { marker, msUntilExpiry } of scan.live) {
3091
+ print(` ${marker.slug} -> ${marker.clientName} (${marker.clientPath}) \u2014 expires in ${formatTtl(msUntilExpiry)}`);
3092
+ }
3093
+ for (const path5 of scan.malformed) {
3094
+ print(` ! malformed marker at ${path5} (delete by hand)`);
3095
+ }
3096
+ print("");
3097
+ }
3098
+ function renderBackgroundPostersSection(opts) {
3099
+ const { print } = opts;
3100
+ const analyticsFailure = getLastAnalyticsFailure();
3101
+ const reportFailure = getLastReportFailure();
3102
+ if (!analyticsFailure && !reportFailure) return;
3103
+ const now = Date.now();
3104
+ const fmt = (f) => `HTTP ${f.statusCode} from ${f.url}, ${formatRelativeAge(now - f.at)} ago`;
3105
+ print("BACKGROUND POSTERS (recent failures)");
3106
+ print(` analytics: ${analyticsFailure ? fmt(analyticsFailure) : "(no recent failure)"}`);
3107
+ print(` tool-report: ${reportFailure ? fmt(reportFailure) : "(no recent failure)"}`);
3108
+ print("");
3109
+ }
3110
+ function formatRelativeAge(ms) {
3111
+ const clamped = Math.max(0, ms);
3112
+ const s = Math.floor(clamped / 1e3);
3113
+ if (s < 60) return `${s}s`;
3114
+ const m = Math.floor(s / 60);
3115
+ if (m < 60) return `${m}m`;
3116
+ const h = Math.floor(m / 60);
3117
+ if (h < 24) return `${h}h`;
3118
+ const d = Math.floor(h / 24);
3119
+ return `${d}d`;
3120
+ }
3121
+ function schemaSuffix(f) {
3122
+ if (f.version === void 0) return "";
3123
+ if (f.version > CURRENT_SCHEMA_VERSION)
3124
+ return ` (schema v${f.version}, this mcph supports v${CURRENT_SCHEMA_VERSION})`;
3125
+ return ` (schema v${f.version})`;
3126
+ }
3127
+ function probeClients(opts) {
3128
+ const out = [];
3129
+ for (const target of INSTALL_TARGETS) {
3130
+ const unavailable = !target.availableOn.includes(opts.os);
3131
+ if (unavailable) {
3132
+ out.push({
3133
+ clientId: target.clientId,
3134
+ scope: target.scopes[0].scope,
3135
+ path: "(n/a)",
3136
+ exists: false,
3137
+ hasMcphEntry: false,
3138
+ malformed: false,
3139
+ unavailable: true
3140
+ });
3141
+ continue;
3142
+ }
3143
+ for (const scope of target.scopes) {
3144
+ let resolved;
3145
+ try {
3146
+ resolved = resolveInstallPath({
3147
+ clientId: target.clientId,
3148
+ scope: scope.scope,
3149
+ os: opts.os,
3150
+ home: opts.home,
3151
+ projectDir: scope.requiresProjectDir ? opts.cwd : void 0,
3152
+ claudeConfigDir: opts.claudeConfigDir
3153
+ });
3154
+ } catch {
3155
+ continue;
3156
+ }
3157
+ const exists3 = existsSync3(resolved.absolute);
3158
+ let hasMcphEntry = false;
3159
+ let malformed = false;
3160
+ if (exists3) {
3161
+ try {
3162
+ statSync(resolved.absolute);
3163
+ const raw = readFileSync(resolved.absolute, "utf8");
3164
+ if (raw.trim().length > 0) {
3165
+ const parsed = parseJsonc(raw);
3166
+ if (typeof parsed === "object" && parsed !== null && !Array.isArray(parsed)) {
3167
+ const container = walkContainer(parsed, resolved.containerPath);
3168
+ if (container) hasMcphEntry = ENTRY_NAME in container;
3169
+ } else {
3170
+ malformed = true;
3171
+ }
3172
+ }
3173
+ } catch {
3174
+ malformed = true;
3175
+ }
3176
+ }
3177
+ out.push({
3178
+ clientId: target.clientId,
3179
+ scope: scope.scope,
3180
+ path: resolved.absolute,
3181
+ exists: exists3,
3182
+ hasMcphEntry,
3183
+ malformed,
3184
+ unavailable: false
3185
+ });
3186
+ }
2553
3187
  }
2554
- perms.allow = allow;
2555
- out.permissions = perms;
2556
3188
  return out;
2557
3189
  }
2558
- async function promptCollision(path5, io) {
2559
- const stdin = io?.stdin ?? process.stdin;
2560
- const stdout = io?.stdout ?? process.stdout;
2561
- const rl = createInterface({ input: stdin, output: stdout });
2562
- try {
2563
- const answer = (await rl.question(
2564
- `${path5} already has an "${ENTRY_NAME}" entry.
2565
- [o]verwrite, [s]kip, or [a]bort? (default: skip) `
2566
- )).trim().toLowerCase();
2567
- if (answer.startsWith("o")) return "overwrite";
2568
- if (answer.startsWith("a")) return "abort";
2569
- return "skip";
2570
- } finally {
2571
- rl.close();
2572
- }
2573
- }
2574
- function readNested(root, containerPath) {
3190
+ function walkContainer(root, path5) {
2575
3191
  let cur = root;
2576
- for (const key of containerPath) {
2577
- if (typeof cur !== "object" || cur === null || Array.isArray(cur)) return void 0;
3192
+ for (const key of path5) {
3193
+ if (typeof cur !== "object" || cur === null || Array.isArray(cur)) return null;
2578
3194
  cur = cur[key];
2579
3195
  }
3196
+ if (typeof cur !== "object" || cur === null || Array.isArray(cur)) return null;
2580
3197
  return cur;
2581
3198
  }
2582
- function mergeClientConfig(existing, containerPath, entry) {
2583
- if (containerPath.length === 0) throw new Error("mergeClientConfig: containerPath cannot be empty");
2584
- const out = { ...existing };
2585
- let parent = out;
2586
- for (let i = 0; i < containerPath.length - 1; i++) {
2587
- const key = containerPath[i];
2588
- const child = parent[key];
2589
- const cloned = typeof child === "object" && child !== null && !Array.isArray(child) ? { ...child } : {};
2590
- parent[key] = cloned;
2591
- parent = cloned;
2592
- }
2593
- const leafKey = containerPath[containerPath.length - 1];
2594
- const prev = parent[leafKey];
2595
- const container = typeof prev === "object" && prev !== null && !Array.isArray(prev) ? { ...prev } : {};
2596
- container[ENTRY_NAME] = entry;
2597
- parent[leafKey] = container;
2598
- return out;
2599
- }
2600
- async function composeMcphConfig(path5, token6) {
2601
- let existing = {};
2602
- let backupPath;
2603
- if (existsSync2(path5)) {
2604
- let raw = "";
2605
- try {
2606
- raw = await readFile4(path5, "utf8");
2607
- } catch {
2608
- raw = "";
3199
+ async function probeClientsAsync(opts) {
3200
+ const result = [];
3201
+ for (const target of INSTALL_TARGETS) {
3202
+ const unavailable = !target.availableOn.includes(opts.os);
3203
+ if (unavailable) {
3204
+ result.push({
3205
+ clientId: target.clientId,
3206
+ scope: target.scopes[0].scope,
3207
+ path: "(n/a)",
3208
+ exists: false,
3209
+ hasMcphEntry: false,
3210
+ malformed: false,
3211
+ unavailable: true
3212
+ });
3213
+ continue;
2609
3214
  }
2610
- if (raw) {
2611
- try {
2612
- const parsed = parseJsonc(raw);
2613
- if (typeof parsed === "object" && parsed !== null && !Array.isArray(parsed)) {
2614
- existing = parsed;
2615
- }
2616
- } catch {
2617
- const candidate = `${path5}.bak-${Date.now()}`;
3215
+ for (const scope of target.scopes) {
3216
+ const resolved = resolveInstallPath({
3217
+ clientId: target.clientId,
3218
+ scope: scope.scope,
3219
+ os: opts.os,
3220
+ home: opts.home,
3221
+ projectDir: scope.requiresProjectDir ? opts.cwd : void 0,
3222
+ claudeConfigDir: opts.claudeConfigDir
3223
+ });
3224
+ const exists3 = existsSync3(resolved.absolute);
3225
+ let hasMcphEntry = false;
3226
+ let malformed = false;
3227
+ if (exists3) {
2618
3228
  try {
2619
- await atomicWriteFile(candidate, raw);
2620
- backupPath = candidate;
3229
+ const raw = await readFile5(resolved.absolute, "utf8");
3230
+ if (raw.trim().length > 0) {
3231
+ const parsed = parseJsonc(raw);
3232
+ if (typeof parsed === "object" && parsed !== null && !Array.isArray(parsed)) {
3233
+ const container = walkContainer(parsed, resolved.containerPath);
3234
+ if (container) hasMcphEntry = ENTRY_NAME in container;
3235
+ } else {
3236
+ malformed = true;
3237
+ }
3238
+ }
2621
3239
  } catch {
3240
+ malformed = true;
2622
3241
  }
2623
3242
  }
2624
- }
2625
- }
2626
- const next = { version: CURRENT_SCHEMA_VERSION, ...existing };
2627
- next.token = token6;
2628
- if (typeof next.version !== "number") next.version = CURRENT_SCHEMA_VERSION;
2629
- return { json: `${JSON.stringify(next, null, 2)}
2630
- `, backupPath };
2631
- }
2632
- function parseInstallArgs(argv) {
2633
- if (argv.length === 0) return { ok: false, error: USAGE };
2634
- const positional = [];
2635
- const opts = {};
2636
- for (let i = 0; i < argv.length; i++) {
2637
- const a = argv[i];
2638
- const next = () => argv[++i];
2639
- switch (a) {
2640
- case "--scope": {
2641
- const v = next();
2642
- if (!v || !["user", "project", "local"].includes(v))
2643
- return { ok: false, error: "--scope requires user|project|local" };
2644
- opts.scope = v;
2645
- break;
2646
- }
2647
- case "--os": {
2648
- const v = next();
2649
- if (!v || !["macos", "linux", "windows"].includes(v))
2650
- return { ok: false, error: "--os requires macos|linux|windows" };
2651
- opts.os = v;
2652
- break;
2653
- }
2654
- case "--token": {
2655
- const v = next();
2656
- if (!v) return { ok: false, error: "--token requires a value" };
2657
- opts.token = v;
2658
- break;
2659
- }
2660
- case "--project-dir": {
2661
- const v = next();
2662
- if (!v) return { ok: false, error: "--project-dir requires a value" };
2663
- opts.projectDir = v;
2664
- break;
2665
- }
2666
- case "--force":
2667
- opts.force = true;
2668
- break;
2669
- case "--skip":
2670
- opts.skip = true;
2671
- break;
2672
- case "--dry-run":
2673
- opts.dryRun = true;
2674
- break;
2675
- case "--no-mcph-config":
2676
- opts.skipMcphConfig = true;
2677
- break;
2678
- case "--list":
2679
- opts.listOnly = true;
2680
- break;
2681
- case "--all":
2682
- opts.all = true;
2683
- break;
2684
- case "-h":
2685
- case "--help":
2686
- return { ok: false, error: USAGE };
2687
- default:
2688
- if (a.startsWith("--")) return { ok: false, error: `Unknown flag: ${a}
2689
- ${USAGE}` };
2690
- positional.push(a);
2691
- }
2692
- }
2693
- if (opts.listOnly || opts.all) {
2694
- if (positional.length > 0) {
2695
- return {
2696
- ok: false,
2697
- error: `mcph install: ${opts.listOnly ? "--list" : "--all"} does not take a client argument.
2698
- ${USAGE}`
2699
- };
2700
- }
2701
- return { ok: true, options: opts };
2702
- }
2703
- if (positional.length !== 1)
2704
- return { ok: false, error: `Expected exactly one client argument, got ${positional.length}.
2705
- ${USAGE}` };
2706
- const clientId = positional[0];
2707
- if (!INSTALL_TARGETS.some((t) => t.clientId === clientId)) {
2708
- return {
2709
- ok: false,
2710
- error: `Unknown client: ${clientId}. Choose: ${INSTALL_TARGETS.map((t) => t.clientId).join(", ")}`
2711
- };
2712
- }
2713
- opts.clientId = clientId;
2714
- return { ok: true, options: opts };
2715
- }
2716
- async function runInstallList(opts, log2) {
2717
- const home = opts.home ?? homedir5();
2718
- const cwd = opts.cwd ?? process.cwd();
2719
- const os = opts.os ?? CURRENT_OS;
2720
- const probes = await probeClientsAsync({ home, os, cwd, claudeConfigDir: opts.claudeConfigDir });
2721
- const rows = probes.map((p) => ({
2722
- client: INSTALL_TARGETS.find((t) => t.clientId === p.clientId)?.label ?? p.clientId,
2723
- scope: p.scope,
2724
- path: displayPath(p.path, home),
2725
- status: statusFor(p)
2726
- }));
2727
- const installed = probes.filter((p) => p.hasMcphEntry).length;
2728
- const available = probes.filter((p) => !p.unavailable).length;
2729
- log2(`${installed}/${available} client scopes have mcp.hosting configured on ${os}.`);
2730
- log2("");
2731
- const widths = {
2732
- client: Math.max("CLIENT".length, ...rows.map((r) => r.client.length)),
2733
- scope: Math.max("SCOPE".length, ...rows.map((r) => r.scope.length)),
2734
- path: Math.max("PATH".length, ...rows.map((r) => r.path.length)),
2735
- status: Math.max("STATUS".length, ...rows.map((r) => r.status.length))
2736
- };
2737
- const header = ` ${"CLIENT".padEnd(widths.client)} ${"SCOPE".padEnd(widths.scope)} ${"PATH".padEnd(widths.path)} ${"STATUS".padEnd(widths.status)}`;
2738
- log2(header);
2739
- for (const r of rows) {
2740
- log2(
2741
- ` ${r.client.padEnd(widths.client)} ${r.scope.padEnd(widths.scope)} ${r.path.padEnd(widths.path)} ${r.status.padEnd(widths.status)}`
2742
- );
2743
- }
2744
- log2("");
2745
- log2("Install into a specific client: `mcph install <client> [--scope user|project|local]`");
2746
- log2("Install into every available user-scope client: `mcph install --all`");
2747
- return { written: [], wouldWrite: [], messages: [], exitCode: 0 };
2748
- }
2749
- function statusFor(p) {
2750
- if (p.unavailable) return "unavailable";
2751
- if (p.malformed) return "malformed";
2752
- if (p.hasMcphEntry) return "installed";
2753
- if (p.exists) return "other-entries";
2754
- return "not installed";
2755
- }
2756
- function displayPath(abs, home) {
2757
- if (abs === "(n/a)") return abs;
2758
- if (home && abs.startsWith(home)) {
2759
- const tail = abs.slice(home.length).replace(/^[\\/]/, "");
2760
- return `~${process.platform === "win32" ? "\\" : "/"}${tail}`;
3243
+ result.push({
3244
+ clientId: target.clientId,
3245
+ scope: scope.scope,
3246
+ path: resolved.absolute,
3247
+ exists: exists3,
3248
+ hasMcphEntry,
3249
+ malformed,
3250
+ unavailable: false
3251
+ });
3252
+ }
2761
3253
  }
2762
- return abs;
3254
+ return result;
2763
3255
  }
2764
- async function runInstallAll(opts, log2, err) {
2765
- const os = opts.os ?? CURRENT_OS;
2766
- const targets = INSTALL_TARGETS.filter((t) => t.availableOn.includes(os));
2767
- if (targets.length === 0) {
2768
- err(`mcph install --all: no installable clients on ${os}.`);
2769
- return { written: [], wouldWrite: [], messages: [], exitCode: 1 };
2770
- }
2771
- const plans = [];
2772
- const skipped = [];
2773
- for (const t of targets) {
2774
- const userScope = t.scopes.find((s) => s.scope === "user");
2775
- if (userScope) {
2776
- plans.push({ clientId: t.clientId, scope: "user" });
2777
- continue;
3256
+ async function fetchLatestVersion(override) {
3257
+ if (override) {
3258
+ try {
3259
+ return await override();
3260
+ } catch {
3261
+ return null;
2778
3262
  }
2779
- const firstNoProj = t.scopes.find((s) => !s.requiresProjectDir);
2780
- if (firstNoProj) {
2781
- plans.push({ clientId: t.clientId, scope: firstNoProj.scope });
2782
- continue;
3263
+ }
3264
+ const ac = new AbortController();
3265
+ const timer = setTimeout(() => ac.abort(), 2e3);
3266
+ try {
3267
+ const res = await fetch("https://registry.npmjs.org/@yawlabs/mcph/latest", {
3268
+ signal: ac.signal,
3269
+ headers: { accept: "application/json" }
3270
+ });
3271
+ if (!res.ok) return null;
3272
+ const body = await res.json();
3273
+ return typeof body.version === "string" ? body.version : null;
3274
+ } catch {
3275
+ return null;
3276
+ } finally {
3277
+ clearTimeout(timer);
3278
+ }
3279
+ }
3280
+ var SHELL_HISTORY_TAIL_LINES = 500;
3281
+ function scanShellHistoryForShadows(opts) {
3282
+ const shadowMap = cliToNamespaces();
3283
+ const counts = /* @__PURE__ */ new Map();
3284
+ for (const source of shellHistorySources(opts)) {
3285
+ const lines = readTailLines(source.path, SHELL_HISTORY_TAIL_LINES);
3286
+ for (const raw of lines) {
3287
+ const cmd = source.extractCommand(raw);
3288
+ if (!cmd) continue;
3289
+ const binary = extractLeadingBinary(cmd);
3290
+ if (!binary) continue;
3291
+ if (!shadowMap.has(binary)) continue;
3292
+ counts.set(binary, (counts.get(binary) ?? 0) + 1);
2783
3293
  }
2784
- if (opts.projectDir) {
2785
- plans.push({ clientId: t.clientId, scope: t.scopes[0].scope });
2786
- continue;
3294
+ }
3295
+ const hits = [];
3296
+ for (const [cli, count] of counts) {
3297
+ const namespaces = shadowMap.get(cli) ?? [];
3298
+ hits.push({ cli, count, namespaces });
3299
+ }
3300
+ hits.sort((a, b) => b.count - a.count);
3301
+ return hits;
3302
+ }
3303
+ function shellHistorySources(opts) {
3304
+ const sources = [];
3305
+ sources.push({ path: join6(opts.home, ".bash_history"), extractCommand: (l) => l.trim() || null });
3306
+ sources.push({
3307
+ path: join6(opts.home, ".zsh_history"),
3308
+ // Zsh extended-history lines look like `: 1700000000:0;npm audit`.
3309
+ // Strip the metadata prefix so we get just the command.
3310
+ extractCommand: (l) => {
3311
+ const trimmed = l.trim();
3312
+ if (!trimmed) return null;
3313
+ if (trimmed.startsWith(":")) {
3314
+ const semi = trimmed.indexOf(";");
3315
+ return semi === -1 ? null : trimmed.slice(semi + 1);
3316
+ }
3317
+ return trimmed;
2787
3318
  }
2788
- skipped.push({
2789
- clientId: t.clientId,
2790
- reason: `requires --project-dir (scopes: ${t.scopes.map((s) => s.scope).join(", ")})`
3319
+ });
3320
+ const appData = opts.env.APPDATA;
3321
+ if (appData) {
3322
+ sources.push({
3323
+ path: join6(appData, "Microsoft", "Windows", "PowerShell", "PSReadLine", "ConsoleHost_history.txt"),
3324
+ extractCommand: (l) => l.trim() || null
2791
3325
  });
2792
3326
  }
2793
- log2(`Installing into ${plans.length} client${plans.length === 1 ? "" : "s"}\u2026`);
2794
- if (skipped.length > 0) {
2795
- for (const s of skipped) log2(` skip ${s.clientId}: ${s.reason}`);
3327
+ return sources;
3328
+ }
3329
+ function readTailLines(path5, n) {
3330
+ try {
3331
+ const raw = readFileSync(path5, "utf8");
3332
+ const all = raw.split(/\r?\n/);
3333
+ return all.length <= n ? all : all.slice(all.length - n);
3334
+ } catch {
3335
+ return [];
2796
3336
  }
2797
- log2("");
2798
- const aggregateWritten = [];
2799
- const aggregateWouldWrite = [];
2800
- const aggregateMessages = [];
2801
- let failed = 0;
2802
- let succeeded = 0;
2803
- for (const plan of plans) {
2804
- log2(`\u2500\u2500 ${plan.clientId} (${plan.scope}) \u2500\u2500`);
2805
- const result = await runInstall({
2806
- ...opts,
2807
- listOnly: false,
2808
- all: false,
2809
- clientId: plan.clientId,
2810
- scope: plan.scope
2811
- });
2812
- aggregateWritten.push(...result.written);
2813
- aggregateWouldWrite.push(...result.wouldWrite);
2814
- aggregateMessages.push(...result.messages);
2815
- if (result.exitCode === 0) succeeded += 1;
2816
- else failed += 1;
2817
- log2("");
3337
+ }
3338
+ function extractLeadingBinary(command) {
3339
+ let rest = command.trimStart();
3340
+ if (!rest) return null;
3341
+ if (rest.startsWith("!")) return null;
3342
+ while (/^[A-Z_][A-Z0-9_]*=/i.test(rest)) {
3343
+ const space = rest.indexOf(" ");
3344
+ if (space === -1) return null;
3345
+ rest = rest.slice(space + 1).trimStart();
2818
3346
  }
2819
- const totalPlanned = plans.length;
2820
- if (failed === 0) {
2821
- log2(`\u2713 ${succeeded}/${totalPlanned} clients installed successfully.`);
2822
- return {
2823
- written: aggregateWritten,
2824
- wouldWrite: aggregateWouldWrite,
2825
- messages: aggregateMessages,
2826
- exitCode: 0
2827
- };
3347
+ const prefixes = ["sudo", "time", "command", "exec"];
3348
+ const firstWord = rest.split(/\s+/)[0];
3349
+ if (prefixes.includes(firstWord)) {
3350
+ const space = rest.indexOf(" ");
3351
+ if (space === -1) return null;
3352
+ rest = rest.slice(space + 1).trimStart();
2828
3353
  }
2829
- err(`${failed}/${totalPlanned} client install${failed === 1 ? "" : "s"} failed. ${succeeded} succeeded.`);
2830
- return {
2831
- written: aggregateWritten,
2832
- wouldWrite: aggregateWouldWrite,
2833
- messages: aggregateMessages,
2834
- exitCode: 1
3354
+ const first = rest.split(/\s+/)[0];
3355
+ if (!first) return null;
3356
+ if (/[|&;<>()`$]/.test(first)) return null;
3357
+ const slash = Math.max(first.lastIndexOf("/"), first.lastIndexOf("\\"));
3358
+ return slash === -1 ? first : first.slice(slash + 1);
3359
+ }
3360
+ function compareSemver(a, b) {
3361
+ const parse = (s) => {
3362
+ const m = /^v?(\d+)\.(\d+)\.(\d+)/.exec(s);
3363
+ if (!m) return null;
3364
+ return [Number(m[1]), Number(m[2]), Number(m[3])];
2835
3365
  };
3366
+ const pa = parse(a);
3367
+ const pb = parse(b);
3368
+ if (!pa || !pb) return 0;
3369
+ for (let i = 0; i < 3; i++) {
3370
+ if (pa[i] < pb[i]) return -1;
3371
+ if (pa[i] > pb[i]) return 1;
3372
+ }
3373
+ return 0;
3374
+ }
3375
+
3376
+ // src/fuzzy.ts
3377
+ function levenshtein(a, b) {
3378
+ if (a === b) return 0;
3379
+ const aLen = a.length;
3380
+ const bLen = b.length;
3381
+ if (aLen === 0) return bLen;
3382
+ if (bLen === 0) return aLen;
3383
+ let prev = new Array(bLen + 1);
3384
+ let curr = new Array(bLen + 1);
3385
+ for (let j = 0; j <= bLen; j++) prev[j] = j;
3386
+ for (let i = 1; i <= aLen; i++) {
3387
+ curr[0] = i;
3388
+ for (let j = 1; j <= bLen; j++) {
3389
+ const cost = a.charCodeAt(i - 1) === b.charCodeAt(j - 1) ? 0 : 1;
3390
+ curr[j] = Math.min(
3391
+ curr[j - 1] + 1,
3392
+ // insertion
3393
+ prev[j] + 1,
3394
+ // deletion
3395
+ prev[j - 1] + cost
3396
+ // substitution
3397
+ );
3398
+ }
3399
+ [prev, curr] = [curr, prev];
3400
+ }
3401
+ return prev[bLen];
3402
+ }
3403
+ function closestNames(query, candidates, limit) {
3404
+ if (limit <= 0) return [];
3405
+ const q = query.toLowerCase();
3406
+ const scored = [];
3407
+ for (const c of candidates) {
3408
+ if (c === query) continue;
3409
+ const lc = c.toLowerCase();
3410
+ let score = null;
3411
+ if (lc === q) {
3412
+ score = 0;
3413
+ } else if (lc.startsWith(q) || q.startsWith(lc)) {
3414
+ score = 1;
3415
+ } else if (lc.includes(q) || q.includes(lc)) {
3416
+ score = 2;
3417
+ } else {
3418
+ const d = levenshtein(q, lc);
3419
+ if (d <= 2) score = 2 + d;
3420
+ }
3421
+ if (score !== null) scored.push({ name: c, score });
3422
+ }
3423
+ scored.sort((a, b) => {
3424
+ if (a.score !== b.score) return a.score - b.score;
3425
+ return a.name.localeCompare(b.name);
3426
+ });
3427
+ return scored.slice(0, limit).map((s) => s.name);
2836
3428
  }
2837
3429
 
2838
3430
  // src/reset-learning-cmd.ts
2839
- import { unlink as unlink2 } from "fs/promises";
2840
- import { homedir as homedir6 } from "os";
2841
- import { join as join6 } from "path";
3431
+ import { unlink as unlink3 } from "fs/promises";
3432
+ import { homedir as homedir7 } from "os";
3433
+ import { join as join7 } from "path";
2842
3434
  var RESET_LEARNING_USAGE = `Usage: mcph reset-learning
2843
3435
 
2844
3436
  Delete ~/.mcph/state.json so cross-session learning starts fresh.
@@ -2860,7 +3452,7 @@ ${RESET_LEARNING_USAGE}`
2860
3452
  return { kind: "ok", options: {} };
2861
3453
  }
2862
3454
  async function runResetLearning(opts = {}) {
2863
- const home = opts.home ?? homedir6();
3455
+ const home = opts.home ?? homedir7();
2864
3456
  const env = opts.env ?? process.env;
2865
3457
  const write = opts.out ?? ((s) => process.stdout.write(s));
2866
3458
  const writeErr = opts.err ?? ((s) => process.stderr.write(s));
@@ -2875,7 +3467,7 @@ async function runResetLearning(opts = {}) {
2875
3467
  writeErr(`${s}
2876
3468
  `);
2877
3469
  };
2878
- const filePath = join6(userConfigDir(home), STATE_FILENAME);
3470
+ const filePath = join7(userConfigDir(home), STATE_FILENAME);
2879
3471
  const raw = env.MCPH_DISABLE_PERSISTENCE;
2880
3472
  const disabled = raw !== void 0 && raw !== "" && (raw === "1" || raw.toLowerCase() === "true");
2881
3473
  if (disabled) {
@@ -2886,7 +3478,7 @@ async function runResetLearning(opts = {}) {
2886
3478
  const learningCount = Object.keys(persisted.learning).length;
2887
3479
  const packCount = persisted.packHistory.length;
2888
3480
  try {
2889
- await unlink2(filePath);
3481
+ await unlink3(filePath);
2890
3482
  } catch (err) {
2891
3483
  if (isFileNotFound2(err)) {
2892
3484
  print("mcph reset-learning: no persisted state to reset.");
@@ -2908,9 +3500,9 @@ function isFileNotFound2(err) {
2908
3500
  }
2909
3501
 
2910
3502
  // src/server.ts
2911
- import { readFile as readFile6 } from "fs/promises";
2912
- import { homedir as homedir7 } from "os";
2913
- import { isAbsolute, relative, resolve as resolve3 } from "path";
3503
+ import { readFile as readFile7 } from "fs/promises";
3504
+ import { homedir as homedir8 } from "os";
3505
+ import { isAbsolute, relative, resolve as resolve4 } from "path";
2914
3506
  import { Server } from "@modelcontextprotocol/sdk/server/index.js";
2915
3507
  import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
2916
3508
  import {
@@ -2921,7 +3513,7 @@ import {
2921
3513
  ListToolsRequestSchema,
2922
3514
  ReadResourceRequestSchema
2923
3515
  } from "@modelcontextprotocol/sdk/types.js";
2924
- import { request as request9 } from "undici";
3516
+ import { request as request10 } from "undici";
2925
3517
 
2926
3518
  // src/auto-upgrade.ts
2927
3519
  import { spawn as spawn3 } from "child_process";
@@ -3015,10 +3607,10 @@ async function defaultFetchLatest() {
3015
3607
  }
3016
3608
  }
3017
3609
  async function defaultSpawn(cmd, args) {
3018
- return new Promise((resolve4) => {
3610
+ return new Promise((resolve5) => {
3019
3611
  const child = spawn2(cmd, args, { stdio: "inherit", shell: process.platform === "win32" });
3020
- child.on("close", (code) => resolve4(typeof code === "number" ? code : 1));
3021
- child.on("error", () => resolve4(1));
3612
+ child.on("close", (code) => resolve5(typeof code === "number" ? code : 1));
3613
+ child.on("error", () => resolve5(1));
3022
3614
  });
3023
3615
  }
3024
3616
  async function runUpgrade(opts = {}) {
@@ -3109,7 +3701,7 @@ Or re-run with --run to upgrade in place.`);
3109
3701
  return { exitCode: 3, lines };
3110
3702
  }
3111
3703
  function readCurrentVersion() {
3112
- return true ? "0.53.0" : "dev";
3704
+ return true ? "0.55.0" : "dev";
3113
3705
  }
3114
3706
 
3115
3707
  // src/auto-upgrade.ts
@@ -3157,7 +3749,7 @@ function defaultSpawn2(cmd, args) {
3157
3749
  async function maybeAutoUpgrade(deps = {}) {
3158
3750
  const optOut = process.env.MCPH_AUTO_UPGRADE;
3159
3751
  if (optOut === "0" || optOut?.toLowerCase() === "false") return;
3160
- const current = deps.currentVersion ?? (true ? "0.53.0" : "dev");
3752
+ const current = deps.currentVersion ?? (true ? "0.55.0" : "dev");
3161
3753
  if (current === "dev") return;
3162
3754
  const method = detectInstallMethod(deps.argvPath ?? process.argv[1]);
3163
3755
  const latest = await (deps.fetchLatestImpl ?? fetchLatestVersion2)();
@@ -3528,13 +4120,13 @@ function stepBindingKey(step, index) {
3528
4120
  }
3529
4121
 
3530
4122
  // src/guide.ts
3531
- import { readFile as readFile5 } from "fs/promises";
4123
+ import { readFile as readFile6 } from "fs/promises";
3532
4124
  var GUIDE_READ_TIMEOUT_MS = 1e3;
3533
4125
  async function readGuide(path5, scope) {
3534
4126
  let raw;
3535
4127
  try {
3536
4128
  raw = await Promise.race([
3537
- readFile5(path5, "utf8"),
4129
+ readFile6(path5, "utf8"),
3538
4130
  new Promise(
3539
4131
  (_, reject) => setTimeout(() => reject(new Error("guide read timeout")), GUIDE_READ_TIMEOUT_MS)
3540
4132
  )
@@ -3641,7 +4233,7 @@ function truncateForWarning(msg) {
3641
4233
  }
3642
4234
 
3643
4235
  // src/heartbeat.ts
3644
- import { request as request5 } from "undici";
4236
+ import { request as request6 } from "undici";
3645
4237
  var HEARTBEAT_PATH = "/api/connect/heartbeat";
3646
4238
  var apiUrl3 = "";
3647
4239
  var token3 = "";
@@ -3656,7 +4248,7 @@ function initHeartbeat(url, tok) {
3656
4248
  async function reportHeartbeat(clientName, clientVersion, isRefresh = false) {
3657
4249
  if (!apiUrl3 || !token3) return;
3658
4250
  try {
3659
- const res = await request5(`${apiUrl3.replace(/\/$/, "")}${HEARTBEAT_PATH}`, {
4251
+ const res = await request6(`${apiUrl3.replace(/\/$/, "")}${HEARTBEAT_PATH}`, {
3660
4252
  method: "POST",
3661
4253
  headers: {
3662
4254
  Authorization: `Bearer ${token3}`,
@@ -4771,7 +5363,7 @@ function rankServers(context, servers) {
4771
5363
  }
4772
5364
 
4773
5365
  // src/rerank.ts
4774
- import { request as request6 } from "undici";
5366
+ import { request as request7 } from "undici";
4775
5367
  var apiUrl4 = "";
4776
5368
  var token4 = "";
4777
5369
  var RERANK_TIMEOUT_MS = 2e3;
@@ -4787,7 +5379,7 @@ async function rerank(intent, candidateIds, limit) {
4787
5379
  if (candidateIds && candidateIds.length > 0) payload.candidateIds = candidateIds;
4788
5380
  if (typeof limit === "number" && limit > 0) payload.limit = limit;
4789
5381
  try {
4790
- const res = await request6(`${apiUrl4.replace(/\/$/, "")}/api/connect/rerank`, {
5382
+ const res = await request7(`${apiUrl4.replace(/\/$/, "")}/api/connect/rerank`, {
4791
5383
  method: "POST",
4792
5384
  headers: {
4793
5385
  Authorization: `Bearer ${token4}`,
@@ -4820,7 +5412,7 @@ async function rerank(intent, candidateIds, limit) {
4820
5412
 
4821
5413
  // src/runtime-detect.ts
4822
5414
  import { spawn as spawn4 } from "child_process";
4823
- import { request as request7 } from "undici";
5415
+ import { request as request8 } from "undici";
4824
5416
  var PROBE_TIMEOUT_MS = 3e3;
4825
5417
  var RUNTIME_REPORT_PATH = "/api/connect/runtimes";
4826
5418
  var apiUrl5 = "";
@@ -4866,12 +5458,12 @@ var PROBES = {
4866
5458
  }
4867
5459
  };
4868
5460
  async function probe(name, p) {
4869
- return new Promise((resolve4) => {
5461
+ return new Promise((resolve5) => {
4870
5462
  let settled = false;
4871
5463
  const settle = (v) => {
4872
5464
  if (settled) return;
4873
5465
  settled = true;
4874
- resolve4(v);
5466
+ resolve5(v);
4875
5467
  };
4876
5468
  let stdout = "";
4877
5469
  let stderr = "";
@@ -4948,7 +5540,7 @@ async function reportRuntimes() {
4948
5540
  return;
4949
5541
  }
4950
5542
  try {
4951
- const res = await request7(`${apiUrl5.replace(/\/$/, "")}${RUNTIME_REPORT_PATH}`, {
5543
+ const res = await request8(`${apiUrl5.replace(/\/$/, "")}${RUNTIME_REPORT_PATH}`, {
4952
5544
  method: "POST",
4953
5545
  headers: {
4954
5546
  Authorization: `Bearer ${token5}`,
@@ -5103,12 +5695,12 @@ import {
5103
5695
 
5104
5696
  // src/uv-bootstrap.ts
5105
5697
  import { spawn as spawn5 } from "child_process";
5106
- import { createHash } from "crypto";
5698
+ import { createHash as createHash2 } from "crypto";
5107
5699
  import { createWriteStream } from "fs";
5108
5700
  import fs from "fs/promises";
5109
5701
  import path4 from "path";
5110
5702
  import { pipeline } from "stream/promises";
5111
- import { request as request8 } from "undici";
5703
+ import { request as request9 } from "undici";
5112
5704
  var UV_VERSION = "0.11.7";
5113
5705
  var RELEASE_BASE = `https://github.com/astral-sh/uv/releases/download/${UV_VERSION}`;
5114
5706
  function uvTarget() {
@@ -5146,12 +5738,12 @@ async function exists2(p) {
5146
5738
  }
5147
5739
  }
5148
5740
  async function onPath(cmd) {
5149
- return new Promise((resolve4) => {
5741
+ return new Promise((resolve5) => {
5150
5742
  let settled = false;
5151
5743
  const settle = (v) => {
5152
5744
  if (settled) return;
5153
5745
  settled = true;
5154
- resolve4(v);
5746
+ resolve5(v);
5155
5747
  };
5156
5748
  let child;
5157
5749
  try {
@@ -5185,7 +5777,7 @@ async function onPath(cmd) {
5185
5777
  async function fetchWithRedirects(url, maxHops = 5) {
5186
5778
  let current = url;
5187
5779
  for (let i = 0; i < maxHops; i++) {
5188
- const res = await request8(current, { method: "GET" });
5780
+ const res = await request9(current, { method: "GET" });
5189
5781
  if (res.statusCode >= 300 && res.statusCode < 400) {
5190
5782
  const loc = res.headers.location;
5191
5783
  if (!loc) throw new Error(`Redirect without Location header from ${current}`);
@@ -5215,7 +5807,7 @@ async function extractArchive(archivePath, destDir) {
5215
5807
  }
5216
5808
  }
5217
5809
  function runCommand(cmd, args) {
5218
- return new Promise((resolve4, reject) => {
5810
+ return new Promise((resolve5, reject) => {
5219
5811
  const child = spawn5(cmd, args, {
5220
5812
  stdio: ["ignore", "pipe", "pipe"],
5221
5813
  shell: false,
@@ -5227,7 +5819,7 @@ function runCommand(cmd, args) {
5227
5819
  });
5228
5820
  child.on("error", reject);
5229
5821
  child.on("close", (code) => {
5230
- if (code === 0) resolve4();
5822
+ if (code === 0) resolve5();
5231
5823
  else reject(new Error(`${cmd} exited ${code}: ${stderr.trim()}`));
5232
5824
  });
5233
5825
  });
@@ -5267,7 +5859,7 @@ async function resolveUv() {
5267
5859
  const shaUrl = `${archiveUrl}.sha256`;
5268
5860
  const [archiveBuf, shaBuf] = await Promise.all([fetchWithRedirects(archiveUrl), fetchWithRedirects(shaUrl)]);
5269
5861
  const expected = shaBuf.toString("utf8").trim().split(/\s+/)[0];
5270
- const actual = createHash("sha256").update(archiveBuf).digest("hex");
5862
+ const actual = createHash2("sha256").update(archiveBuf).digest("hex");
5271
5863
  if (!expected || expected.toLowerCase() !== actual.toLowerCase()) {
5272
5864
  throw new Error(`uv archive checksum mismatch (expected ${expected}, got ${actual})`);
5273
5865
  }
@@ -5334,7 +5926,7 @@ function categorizeSpawnError(err) {
5334
5926
  }
5335
5927
  async function connectToUpstream(config, onDisconnect, onListChanged) {
5336
5928
  const client = new Client(
5337
- { name: "mcph", version: true ? "0.53.0" : "dev" },
5929
+ { name: "mcph", version: true ? "0.55.0" : "dev" },
5338
5930
  { capabilities: {} }
5339
5931
  );
5340
5932
  let transport;
@@ -5642,7 +6234,7 @@ var ConnectServer = class _ConnectServer {
5642
6234
  this.apiUrl = apiUrl6;
5643
6235
  this.token = token6;
5644
6236
  this.server = new Server(
5645
- { name: "mcph", version: true ? "0.53.0" : "dev" },
6237
+ { name: "mcph", version: true ? "0.55.0" : "dev" },
5646
6238
  {
5647
6239
  capabilities: {
5648
6240
  tools: { listChanged: true },
@@ -5791,23 +6383,23 @@ var ConnectServer = class _ConnectServer {
5791
6383
  this.server.setRequestHandler(ListToolsRequestSchema, async () => ({
5792
6384
  tools: buildToolList(this.connections, this.getDeferredServers(), this.toolFilters)
5793
6385
  }));
5794
- this.server.setRequestHandler(CallToolRequestSchema, async (request10, extra) => {
5795
- const { name, arguments: args } = request10.params;
6386
+ this.server.setRequestHandler(CallToolRequestSchema, async (request11, extra) => {
6387
+ const { name, arguments: args } = request11.params;
5796
6388
  return this.handleToolCall(name, args ?? {}, extra);
5797
6389
  });
5798
6390
  this.server.setRequestHandler(ListResourcesRequestSchema, async () => ({
5799
6391
  resources: buildResourceList(this.connections, this.getBuiltinResources())
5800
6392
  }));
5801
- this.server.setRequestHandler(ReadResourceRequestSchema, async (request10) => {
5802
- return routeResourceRead(request10.params.uri, this.resourceRoutes, this.connections, this.getBuiltinResourceMap());
6393
+ this.server.setRequestHandler(ReadResourceRequestSchema, async (request11) => {
6394
+ return routeResourceRead(request11.params.uri, this.resourceRoutes, this.connections, this.getBuiltinResourceMap());
5803
6395
  });
5804
6396
  this.server.setRequestHandler(ListPromptsRequestSchema, async () => ({
5805
6397
  prompts: buildPromptList(this.connections)
5806
6398
  }));
5807
- this.server.setRequestHandler(GetPromptRequestSchema, async (request10) => {
6399
+ this.server.setRequestHandler(GetPromptRequestSchema, async (request11) => {
5808
6400
  return routePromptGet(
5809
- request10.params.name,
5810
- request10.params.arguments,
6401
+ request11.params.name,
6402
+ request11.params.arguments,
5811
6403
  this.promptRoutes,
5812
6404
  this.connections
5813
6405
  );
@@ -7132,7 +7724,7 @@ ${activeCount} loaded in this session, ${totalTools} tools in context${tokenSumm
7132
7724
  }
7133
7725
  const ALLOWED_FILENAMES = ["claude_desktop_config.json", "mcp.json", "settings.json", "mcp_config.json"];
7134
7726
  try {
7135
- const resolved = filepath.startsWith("~/") || filepath.startsWith("~\\") ? resolve3(homedir7(), filepath.slice(2)) : resolve3(filepath);
7727
+ const resolved = filepath.startsWith("~/") || filepath.startsWith("~\\") ? resolve4(homedir8(), filepath.slice(2)) : resolve4(filepath);
7136
7728
  const resolvedBasename = resolved.split(/[/\\]/).pop() || "";
7137
7729
  if (!ALLOWED_FILENAMES.includes(resolvedBasename)) {
7138
7730
  return {
@@ -7149,7 +7741,7 @@ ${activeCount} loaded in this session, ${totalTools} tools in context${tokenSumm
7149
7741
  const rel = relative(base, p);
7150
7742
  return rel === "" || !rel.startsWith("..") && !isAbsolute(rel);
7151
7743
  };
7152
- if (!isUnder(homedir7(), resolved) && !isUnder(process.cwd(), resolved)) {
7744
+ if (!isUnder(homedir8(), resolved) && !isUnder(process.cwd(), resolved)) {
7153
7745
  return {
7154
7746
  content: [
7155
7747
  { type: "text", text: "Import path must be under your home directory or the current working directory." }
@@ -7157,7 +7749,7 @@ ${activeCount} loaded in this session, ${totalTools} tools in context${tokenSumm
7157
7749
  isError: true
7158
7750
  };
7159
7751
  }
7160
- const raw = await readFile6(resolved, "utf-8");
7752
+ const raw = await readFile7(resolved, "utf-8");
7161
7753
  const parsed = JSON.parse(raw);
7162
7754
  if (!parsed.mcpServers || typeof parsed.mcpServers !== "object" || Array.isArray(parsed.mcpServers)) {
7163
7755
  return {
@@ -7193,7 +7785,7 @@ ${activeCount} loaded in this session, ${totalTools} tools in context${tokenSumm
7193
7785
  nsToKeys.set(s.namespace, existing);
7194
7786
  }
7195
7787
  const collisions = [...nsToKeys.entries()].filter(([, keys]) => keys.length > 1);
7196
- const res = await request9(`${this.apiUrl.replace(/\/$/, "")}/api/connect/import`, {
7788
+ const res = await request10(`${this.apiUrl.replace(/\/$/, "")}/api/connect/import`, {
7197
7789
  method: "POST",
7198
7790
  headers: {
7199
7791
  Authorization: `Bearer ${this.token}`,
@@ -7254,7 +7846,7 @@ Use mcp_connect_discover to see imported servers.`
7254
7846
  }
7255
7847
  const payload = built.payload;
7256
7848
  try {
7257
- const res = await request9(`${this.apiUrl.replace(/\/$/, "")}/api/connect/servers`, {
7849
+ const res = await request10(`${this.apiUrl.replace(/\/$/, "")}/api/connect/servers`, {
7258
7850
  method: "POST",
7259
7851
  headers: {
7260
7852
  Authorization: `Bearer ${this.token}`,
@@ -7879,6 +8471,8 @@ var KNOWN_SUBCOMMANDS = [
7879
8471
  "bundles",
7880
8472
  "completion",
7881
8473
  "upgrade",
8474
+ "try",
8475
+ "try-cleanup",
7882
8476
  "help",
7883
8477
  "--help",
7884
8478
  "-h",
@@ -7958,6 +8552,22 @@ if (subcommand === "compliance") {
7958
8552
  process.exit(2);
7959
8553
  }
7960
8554
  runUpgrade(parsed.options).then((r) => process.exit(r.exitCode));
8555
+ } else if (subcommand === "try") {
8556
+ const parsed = parseTryArgs(process.argv.slice(3));
8557
+ if (!parsed.ok) {
8558
+ process.stderr.write(`${parsed.error}
8559
+ `);
8560
+ process.exit(2);
8561
+ }
8562
+ runTry(parsed.options).then((r) => process.exit(r.exitCode));
8563
+ } else if (subcommand === "try-cleanup") {
8564
+ const parsed = parseTryCleanupArgs(process.argv.slice(3));
8565
+ if (!parsed.ok) {
8566
+ process.stderr.write(`${parsed.error}
8567
+ `);
8568
+ process.exit(2);
8569
+ }
8570
+ runTryCleanup(parsed.options).then((r) => process.exit(r.exitCode));
7961
8571
  } else if (subcommand === "--help" || subcommand === "-h" || subcommand === "help") {
7962
8572
  process.stdout.write(`
7963
8573
  mcph \u2014 one install, every MCP server, managed from the cloud.
@@ -7974,6 +8584,10 @@ if (subcommand === "compliance") {
7974
8584
  install --list List which MCP clients are installed on this
7975
8585
  machine (read-only; no writes).
7976
8586
  install --all Configure every installed MCP client in one go.
8587
+ try <slug> Wire a one-off trial of an upstream MCP server
8588
+ into your AI client. No account needed; expires
8589
+ after --ttl (default 1h). Doctor GCs it after.
8590
+ try-cleanup <slug> Remove a wired trial early.
7977
8591
 
7978
8592
  Inspection:
7979
8593
  doctor Diagnose setup: config, token, clients, learning,
@@ -8023,6 +8637,8 @@ if (subcommand === "compliance") {
8023
8637
  upgraded in the background).
8024
8638
  MCPH_PRUNE_RESPONSES Set to \`0\` to disable response pruning.
8025
8639
  MCPH_DISABLE_PERSISTENCE Disable cross-session learning state.
8640
+ MCPH_BASE_URL Override the host \`mcph try\` queries for
8641
+ /api/explore/:slug (default https://mcp.hosting).
8026
8642
 
8027
8643
  Config resolution (highest precedence first):
8028
8644
  1. MCPH_TOKEN / MCPH_URL env vars
@@ -8040,7 +8656,7 @@ if (subcommand === "compliance") {
8040
8656
  `);
8041
8657
  process.exit(0);
8042
8658
  } else if (subcommand === "--version" || subcommand === "-V") {
8043
- process.stdout.write(`mcph ${true ? "0.53.0" : "dev"}
8659
+ process.stdout.write(`mcph ${true ? "0.55.0" : "dev"}
8044
8660
  `);
8045
8661
  process.exit(0);
8046
8662
  } else if (subcommand && !subcommand.startsWith("-")) {