@yawlabs/mcph 0.52.0 → 0.54.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.
- package/dist/index.js +1785 -1169
- 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:
|
|
249
|
-
const homeResolved =
|
|
250
|
-
let dir =
|
|
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((
|
|
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
|
-
|
|
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
|
-
|
|
946
|
+
resolve5(null);
|
|
947
947
|
return;
|
|
948
948
|
}
|
|
949
|
-
|
|
949
|
+
resolve5(parsed);
|
|
950
950
|
} catch {
|
|
951
951
|
process.stderr.write(`
|
|
952
952
|
mcp-compliance exited ${code} without valid JSON output.
|
|
953
953
|
`);
|
|
954
|
-
|
|
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
|
|
995
|
-
import { homedir as
|
|
996
|
-
import { join as
|
|
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/
|
|
1613
|
-
|
|
1614
|
-
|
|
1615
|
-
|
|
1616
|
-
|
|
1617
|
-
|
|
1618
|
-
|
|
1619
|
-
|
|
1620
|
-
|
|
1621
|
-
|
|
1622
|
-
|
|
1623
|
-
|
|
1624
|
-
|
|
1625
|
-
|
|
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
|
-
|
|
1629
|
-
|
|
1630
|
-
|
|
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
|
-
|
|
1633
|
-
|
|
1634
|
-
|
|
1635
|
-
|
|
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
|
-
|
|
1640
|
-
|
|
1641
|
-
|
|
1642
|
-
|
|
1643
|
-
|
|
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.52.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
|
-
|
|
1685
|
-
|
|
1686
|
-
|
|
1687
|
-
|
|
1688
|
-
|
|
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
|
-
|
|
1699
|
-
|
|
1700
|
-
|
|
1701
|
-
|
|
1702
|
-
|
|
1703
|
-
|
|
1704
|
-
|
|
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
|
-
|
|
1721
|
-
|
|
1722
|
-
|
|
1723
|
-
|
|
1724
|
-
|
|
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
|
-
|
|
1727
|
-
|
|
1728
|
-
|
|
1729
|
-
|
|
1730
|
-
|
|
1731
|
-
|
|
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
|
-
|
|
1738
|
-
|
|
1739
|
-
|
|
1740
|
-
|
|
1741
|
-
|
|
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
|
-
|
|
1748
|
-
|
|
1749
|
-
|
|
1750
|
-
|
|
1751
|
-
|
|
1752
|
-
|
|
1753
|
-
|
|
1754
|
-
|
|
1755
|
-
|
|
1756
|
-
|
|
1757
|
-
|
|
1758
|
-
|
|
1759
|
-
|
|
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
|
-
|
|
1762
|
-
|
|
1763
|
-
|
|
1764
|
-
|
|
1765
|
-
|
|
1766
|
-
|
|
1767
|
-
|
|
1768
|
-
|
|
1769
|
-
|
|
1770
|
-
|
|
1771
|
-
|
|
1772
|
-
|
|
1773
|
-
|
|
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
|
|
1819
|
-
const
|
|
1820
|
-
|
|
1821
|
-
const
|
|
1822
|
-
|
|
1823
|
-
|
|
1824
|
-
|
|
1825
|
-
|
|
1826
|
-
|
|
1827
|
-
|
|
1828
|
-
|
|
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
|
|
1911
|
-
|
|
1912
|
-
|
|
1913
|
-
|
|
1914
|
-
|
|
1915
|
-
|
|
1916
|
-
|
|
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
|
-
|
|
1919
|
-
|
|
1920
|
-
|
|
1921
|
-
|
|
1922
|
-
|
|
1923
|
-
|
|
1924
|
-
|
|
1925
|
-
|
|
1926
|
-
|
|
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
|
-
|
|
1823
|
+
log2(`Wrote ${mcphConfigPath}`);
|
|
1824
|
+
written.push(mcphConfigPath);
|
|
1929
1825
|
}
|
|
1930
|
-
let parsed;
|
|
1931
1826
|
try {
|
|
1932
|
-
|
|
1933
|
-
} catch (
|
|
1934
|
-
|
|
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
|
-
|
|
1937
|
-
|
|
1938
|
-
if (
|
|
1939
|
-
|
|
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
|
-
|
|
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
|
|
1944
|
-
const
|
|
1945
|
-
|
|
1946
|
-
|
|
1947
|
-
|
|
1948
|
-
|
|
1949
|
-
|
|
1950
|
-
if (
|
|
1951
|
-
|
|
1952
|
-
|
|
1953
|
-
|
|
1954
|
-
|
|
1955
|
-
|
|
1956
|
-
|
|
1957
|
-
|
|
1958
|
-
|
|
1959
|
-
|
|
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
|
-
|
|
2043
|
-
|
|
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
|
|
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
|
|
2058
|
-
if (typeof cur !== "object" || cur === null || Array.isArray(cur)) return
|
|
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
|
-
|
|
2065
|
-
|
|
2066
|
-
|
|
2067
|
-
|
|
2068
|
-
|
|
2069
|
-
|
|
2070
|
-
|
|
2071
|
-
|
|
2072
|
-
|
|
2073
|
-
|
|
2074
|
-
|
|
2075
|
-
|
|
2076
|
-
|
|
2077
|
-
|
|
2078
|
-
|
|
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
|
-
|
|
2081
|
-
|
|
2082
|
-
|
|
2083
|
-
|
|
2084
|
-
|
|
2085
|
-
|
|
2086
|
-
|
|
2087
|
-
|
|
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
|
-
|
|
2095
|
-
|
|
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
|
-
|
|
2120
|
-
|
|
2121
|
-
|
|
2122
|
-
|
|
2123
|
-
|
|
2124
|
-
|
|
2125
|
-
|
|
2126
|
-
|
|
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
|
-
|
|
2130
|
-
|
|
2131
|
-
|
|
2132
|
-
|
|
2133
|
-
|
|
2134
|
-
|
|
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
|
-
|
|
2161
|
-
|
|
2162
|
-
|
|
2163
|
-
|
|
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
|
-
|
|
2166
|
-
return
|
|
2073
|
+
opts.clientId = clientId;
|
|
2074
|
+
return { ok: true, options: opts };
|
|
2167
2075
|
}
|
|
2168
|
-
function
|
|
2169
|
-
const
|
|
2170
|
-
|
|
2171
|
-
|
|
2172
|
-
|
|
2173
|
-
|
|
2174
|
-
|
|
2175
|
-
|
|
2176
|
-
|
|
2177
|
-
|
|
2178
|
-
|
|
2179
|
-
|
|
2180
|
-
|
|
2181
|
-
|
|
2182
|
-
|
|
2183
|
-
|
|
2184
|
-
|
|
2185
|
-
|
|
2186
|
-
|
|
2187
|
-
|
|
2188
|
-
|
|
2189
|
-
|
|
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
|
-
|
|
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
|
|
2195
|
-
|
|
2196
|
-
|
|
2197
|
-
|
|
2198
|
-
|
|
2199
|
-
|
|
2200
|
-
|
|
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
|
|
2204
|
-
|
|
2205
|
-
|
|
2206
|
-
if (
|
|
2207
|
-
|
|
2208
|
-
|
|
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
|
|
2213
|
-
const
|
|
2214
|
-
|
|
2215
|
-
const
|
|
2216
|
-
if (
|
|
2217
|
-
|
|
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
|
-
|
|
2220
|
-
if (
|
|
2221
|
-
|
|
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
|
-
|
|
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/
|
|
2242
|
-
|
|
2243
|
-
|
|
2244
|
-
|
|
2245
|
-
|
|
2246
|
-
|
|
2247
|
-
|
|
2248
|
-
|
|
2249
|
-
|
|
2250
|
-
|
|
2251
|
-
|
|
2252
|
-
|
|
2253
|
-
|
|
2254
|
-
|
|
2255
|
-
|
|
2256
|
-
|
|
2257
|
-
|
|
2258
|
-
|
|
2259
|
-
|
|
2260
|
-
|
|
2261
|
-
|
|
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
|
-
|
|
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
|
|
2269
|
-
if (
|
|
2270
|
-
const
|
|
2271
|
-
const
|
|
2272
|
-
for (
|
|
2273
|
-
|
|
2274
|
-
|
|
2275
|
-
|
|
2276
|
-
|
|
2277
|
-
|
|
2278
|
-
|
|
2279
|
-
|
|
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 (
|
|
2303
|
+
if (a.startsWith("--")) return { ok: false, error: `Unknown flag: ${a}
|
|
2304
|
+
${TRY_CLEANUP_USAGE}` };
|
|
2305
|
+
positional.push(a);
|
|
2287
2306
|
}
|
|
2288
|
-
|
|
2289
|
-
|
|
2290
|
-
|
|
2291
|
-
}
|
|
2292
|
-
|
|
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
|
-
|
|
2296
|
-
|
|
2297
|
-
|
|
2298
|
-
|
|
2299
|
-
|
|
2300
|
-
|
|
2301
|
-
|
|
2302
|
-
|
|
2303
|
-
|
|
2304
|
-
|
|
2305
|
-
|
|
2306
|
-
|
|
2307
|
-
|
|
2308
|
-
|
|
2309
|
-
|
|
2310
|
-
|
|
2311
|
-
|
|
2312
|
-
|
|
2313
|
-
|
|
2314
|
-
|
|
2315
|
-
|
|
2316
|
-
|
|
2317
|
-
|
|
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
|
-
|
|
2321
|
-
|
|
2322
|
-
|
|
2323
|
-
|
|
2324
|
-
|
|
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
|
-
|
|
2327
|
-
|
|
2328
|
-
|
|
2329
|
-
|
|
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
|
-
|
|
2332
|
-
|
|
2333
|
-
|
|
2334
|
-
|
|
2335
|
-
|
|
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
|
-
|
|
2338
|
-
|
|
2339
|
-
|
|
2340
|
-
|
|
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
|
|
2345
|
-
|
|
2346
|
-
|
|
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
|
-
|
|
2353
|
-
|
|
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
|
-
|
|
2356
|
-
|
|
2357
|
-
|
|
2358
|
-
|
|
2359
|
-
|
|
2360
|
-
|
|
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
|
-
|
|
2364
|
-
|
|
2365
|
-
|
|
2415
|
+
await res.body.text().catch(() => {
|
|
2416
|
+
});
|
|
2417
|
+
} catch (err) {
|
|
2418
|
+
log("debug", "try-event post failed", { error: err.message });
|
|
2366
2419
|
}
|
|
2367
|
-
|
|
2368
|
-
|
|
2369
|
-
|
|
2370
|
-
|
|
2371
|
-
|
|
2372
|
-
|
|
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
|
-
|
|
2375
|
-
|
|
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
|
-
|
|
2381
|
-
|
|
2382
|
-
|
|
2383
|
-
|
|
2384
|
-
|
|
2385
|
-
|
|
2386
|
-
|
|
2387
|
-
|
|
2388
|
-
|
|
2389
|
-
|
|
2390
|
-
|
|
2391
|
-
}
|
|
2392
|
-
|
|
2393
|
-
|
|
2394
|
-
|
|
2395
|
-
|
|
2396
|
-
|
|
2397
|
-
|
|
2398
|
-
|
|
2399
|
-
|
|
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
|
-
|
|
2410
|
-
|
|
2411
|
-
|
|
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 (
|
|
2415
|
-
|
|
2416
|
-
|
|
2417
|
-
|
|
2418
|
-
|
|
2419
|
-
|
|
2420
|
-
|
|
2421
|
-
|
|
2422
|
-
|
|
2423
|
-
|
|
2424
|
-
|
|
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
|
-
|
|
2429
|
-
|
|
2430
|
-
|
|
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
|
-
|
|
2433
|
-
|
|
2434
|
-
|
|
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
|
-
|
|
2439
|
-
|
|
2440
|
-
|
|
2441
|
-
|
|
2442
|
-
|
|
2443
|
-
|
|
2444
|
-
|
|
2445
|
-
|
|
2446
|
-
|
|
2447
|
-
|
|
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.54.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
|
-
|
|
2451
|
-
|
|
2452
|
-
|
|
2453
|
-
|
|
2454
|
-
|
|
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
|
-
|
|
2474
|
-
|
|
2475
|
-
|
|
2476
|
-
|
|
2477
|
-
|
|
2478
|
-
|
|
2479
|
-
|
|
2480
|
-
|
|
2481
|
-
|
|
2482
|
-
|
|
2483
|
-
|
|
2484
|
-
|
|
2485
|
-
|
|
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
|
-
|
|
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
|
-
|
|
2492
|
-
} catch (
|
|
2493
|
-
|
|
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
|
-
|
|
2497
|
-
|
|
2498
|
-
if (
|
|
2499
|
-
|
|
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
|
-
|
|
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
|
|
2515
|
-
const
|
|
2516
|
-
|
|
2517
|
-
|
|
2518
|
-
|
|
2519
|
-
|
|
2520
|
-
|
|
2521
|
-
if (
|
|
2522
|
-
|
|
2523
|
-
|
|
2524
|
-
|
|
2525
|
-
|
|
2526
|
-
|
|
2527
|
-
|
|
2528
|
-
|
|
2529
|
-
|
|
2530
|
-
|
|
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
|
-
|
|
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
|
|
2546
|
-
const
|
|
2547
|
-
const
|
|
2548
|
-
const
|
|
2549
|
-
|
|
2550
|
-
|
|
2551
|
-
|
|
2552
|
-
|
|
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
|
-
|
|
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
|
|
2577
|
-
if (typeof cur !== "object" || cur === null || Array.isArray(cur)) return
|
|
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
|
|
2583
|
-
|
|
2584
|
-
const
|
|
2585
|
-
|
|
2586
|
-
|
|
2587
|
-
|
|
2588
|
-
|
|
2589
|
-
|
|
2590
|
-
|
|
2591
|
-
|
|
2592
|
-
|
|
2593
|
-
|
|
2594
|
-
|
|
2595
|
-
|
|
2596
|
-
|
|
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
|
-
|
|
2611
|
-
|
|
2612
|
-
|
|
2613
|
-
|
|
2614
|
-
|
|
2615
|
-
|
|
2616
|
-
|
|
2617
|
-
|
|
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
|
|
2620
|
-
|
|
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
|
-
|
|
2627
|
-
|
|
2628
|
-
|
|
2629
|
-
|
|
2630
|
-
|
|
2631
|
-
|
|
2632
|
-
|
|
2633
|
-
|
|
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
|
|
3254
|
+
return result;
|
|
2763
3255
|
}
|
|
2764
|
-
async function
|
|
2765
|
-
|
|
2766
|
-
|
|
2767
|
-
|
|
2768
|
-
|
|
2769
|
-
|
|
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
|
-
|
|
2780
|
-
|
|
2781
|
-
|
|
2782
|
-
|
|
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
|
-
|
|
2785
|
-
|
|
2786
|
-
|
|
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
|
-
|
|
2789
|
-
|
|
2790
|
-
|
|
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
|
-
|
|
2794
|
-
|
|
2795
|
-
|
|
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
|
-
|
|
2798
|
-
|
|
2799
|
-
|
|
2800
|
-
|
|
2801
|
-
|
|
2802
|
-
|
|
2803
|
-
|
|
2804
|
-
|
|
2805
|
-
|
|
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
|
|
2820
|
-
|
|
2821
|
-
|
|
2822
|
-
|
|
2823
|
-
|
|
2824
|
-
|
|
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
|
-
|
|
2830
|
-
return
|
|
2831
|
-
|
|
2832
|
-
|
|
2833
|
-
|
|
2834
|
-
|
|
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
|
|
2840
|
-
import { homedir as
|
|
2841
|
-
import { join as
|
|
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 ??
|
|
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 =
|
|
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
|
|
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
|
|
2912
|
-
import { homedir as
|
|
2913
|
-
import { isAbsolute, relative, resolve as
|
|
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
|
|
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((
|
|
3610
|
+
return new Promise((resolve5) => {
|
|
3019
3611
|
const child = spawn2(cmd, args, { stdio: "inherit", shell: process.platform === "win32" });
|
|
3020
|
-
child.on("close", (code) =>
|
|
3021
|
-
child.on("error", () =>
|
|
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.
|
|
3704
|
+
return true ? "0.54.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.
|
|
3752
|
+
const current = deps.currentVersion ?? (true ? "0.54.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
|
|
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
|
-
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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((
|
|
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
|
-
|
|
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
|
|
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
|
|
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((
|
|
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
|
-
|
|
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
|
|
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((
|
|
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)
|
|
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 =
|
|
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.
|
|
5929
|
+
{ name: "mcph", version: true ? "0.54.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.
|
|
6237
|
+
{ name: "mcph", version: true ? "0.54.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 (
|
|
5795
|
-
const { name, arguments: args } =
|
|
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 (
|
|
5802
|
-
return routeResourceRead(
|
|
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 (
|
|
6399
|
+
this.server.setRequestHandler(GetPromptRequestSchema, async (request11) => {
|
|
5808
6400
|
return routePromptGet(
|
|
5809
|
-
|
|
5810
|
-
|
|
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("~\\") ?
|
|
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(
|
|
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
|
|
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
|
|
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
|
|
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.
|
|
8659
|
+
process.stdout.write(`mcph ${true ? "0.54.0" : "dev"}
|
|
8044
8660
|
`);
|
|
8045
8661
|
process.exit(0);
|
|
8046
8662
|
} else if (subcommand && !subcommand.startsWith("-")) {
|