mcpman 0.1.1 → 0.3.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.cjs CHANGED
@@ -37,6 +37,90 @@ var init_cjs_shims = __esm({
37
37
  }
38
38
  });
39
39
 
40
+ // src/core/trust-scorer.ts
41
+ var trust_scorer_exports = {};
42
+ __export(trust_scorer_exports, {
43
+ computeTrustScore: () => computeTrustScore
44
+ });
45
+ function vulnScore(vulns) {
46
+ let score = 100;
47
+ for (const v of vulns) {
48
+ if (v.severity === "critical") score -= 25;
49
+ else if (v.severity === "high") score -= 15;
50
+ else if (v.severity === "moderate") score -= 10;
51
+ else score -= 5;
52
+ }
53
+ return Math.max(0, score);
54
+ }
55
+ function downloadScore(weeklyDownloads) {
56
+ if (weeklyDownloads <= 0) return 0;
57
+ if (weeklyDownloads >= 1e6) return 100;
58
+ if (weeklyDownloads >= 1e5) return 80;
59
+ if (weeklyDownloads >= 1e4) return 60;
60
+ if (weeklyDownloads >= 1e3) return 40;
61
+ if (weeklyDownloads >= 100) return 20;
62
+ return 10;
63
+ }
64
+ function ageScore(packageAge) {
65
+ if (packageAge <= 0) return 0;
66
+ if (packageAge >= 730) return 100;
67
+ if (packageAge >= 365) return 80;
68
+ if (packageAge >= 180) return 60;
69
+ if (packageAge >= 30) return 30;
70
+ return 10;
71
+ }
72
+ function publishScore(lastPublish) {
73
+ const daysSince = Math.floor((Date.now() - new Date(lastPublish).getTime()) / 864e5);
74
+ if (daysSince <= 30) return 100;
75
+ if (daysSince <= 90) return 80;
76
+ if (daysSince <= 180) return 60;
77
+ if (daysSince <= 365) return 40;
78
+ return 20;
79
+ }
80
+ function maintainerScore(count, deprecated) {
81
+ let score = 0;
82
+ if (count >= 3) score = 90;
83
+ else if (count === 2) score = 70;
84
+ else score = 50;
85
+ if (!deprecated) score += 10;
86
+ return Math.min(100, score);
87
+ }
88
+ function toRiskLevel(score) {
89
+ if (score >= 80) return "LOW";
90
+ if (score >= 50) return "MEDIUM";
91
+ if (score >= 20) return "HIGH";
92
+ return "CRITICAL";
93
+ }
94
+ function computeTrustScore(metadata, vulns) {
95
+ if (!metadata) {
96
+ const vScore = vulnScore(vulns);
97
+ const score2 = Math.round(vScore * WEIGHT_VULNS * 100) / 100;
98
+ return { score: Math.round(score2), riskLevel: toRiskLevel(score2) };
99
+ }
100
+ const scores = {
101
+ vulns: vulnScore(vulns),
102
+ downloads: downloadScore(metadata.weeklyDownloads),
103
+ age: ageScore(metadata.packageAge),
104
+ publish: publishScore(metadata.lastPublish),
105
+ maintainers: maintainerScore(metadata.maintainerCount, metadata.deprecated)
106
+ };
107
+ const weighted = scores.vulns * WEIGHT_VULNS + scores.downloads * WEIGHT_DOWNLOADS + scores.age * WEIGHT_AGE + scores.publish * WEIGHT_PUBLISH_FREQ + scores.maintainers * WEIGHT_MAINTAINERS;
108
+ const score = Math.min(100, Math.max(0, Math.round(weighted)));
109
+ return { score, riskLevel: toRiskLevel(score) };
110
+ }
111
+ var WEIGHT_VULNS, WEIGHT_DOWNLOADS, WEIGHT_AGE, WEIGHT_PUBLISH_FREQ, WEIGHT_MAINTAINERS;
112
+ var init_trust_scorer = __esm({
113
+ "src/core/trust-scorer.ts"() {
114
+ "use strict";
115
+ init_cjs_shims();
116
+ WEIGHT_VULNS = 0.3;
117
+ WEIGHT_DOWNLOADS = 0.2;
118
+ WEIGHT_AGE = 0.15;
119
+ WEIGHT_PUBLISH_FREQ = 0.15;
120
+ WEIGHT_MAINTAINERS = 0.2;
121
+ }
122
+ });
123
+
40
124
  // src/clients/types.ts
41
125
  var ConfigParseError, ConfigWriteError;
42
126
  var init_types = __esm({
@@ -64,43 +148,43 @@ var init_types = __esm({
64
148
  async function atomicWrite(filePath, content) {
65
149
  const tmpPath = `${filePath}.tmp`;
66
150
  try {
67
- await import_node_fs.default.promises.mkdir(import_node_path.default.dirname(filePath), { recursive: true });
68
- await import_node_fs.default.promises.writeFile(tmpPath, content, { encoding: "utf-8", mode: 384 });
69
- await import_node_fs.default.promises.rename(tmpPath, filePath);
151
+ await import_node_fs3.default.promises.mkdir(import_node_path3.default.dirname(filePath), { recursive: true });
152
+ await import_node_fs3.default.promises.writeFile(tmpPath, content, { encoding: "utf-8", mode: 384 });
153
+ await import_node_fs3.default.promises.rename(tmpPath, filePath);
70
154
  } catch (err) {
71
155
  try {
72
- await import_node_fs.default.promises.unlink(tmpPath);
156
+ await import_node_fs3.default.promises.unlink(tmpPath);
73
157
  } catch {
74
158
  }
75
159
  throw err;
76
160
  }
77
161
  }
78
- async function pathExists(p5) {
162
+ async function pathExists(p11) {
79
163
  try {
80
- await import_node_fs.default.promises.access(p5);
164
+ await import_node_fs3.default.promises.access(p11);
81
165
  return true;
82
166
  } catch {
83
167
  return false;
84
168
  }
85
169
  }
86
- var import_node_fs, import_node_path, BaseClientHandler;
170
+ var import_node_fs3, import_node_path3, BaseClientHandler;
87
171
  var init_base_client_handler = __esm({
88
172
  "src/clients/base-client-handler.ts"() {
89
173
  "use strict";
90
174
  init_cjs_shims();
91
- import_node_fs = __toESM(require("fs"), 1);
92
- import_node_path = __toESM(require("path"), 1);
175
+ import_node_fs3 = __toESM(require("fs"), 1);
176
+ import_node_path3 = __toESM(require("path"), 1);
93
177
  init_types();
94
178
  BaseClientHandler = class {
95
179
  async isInstalled() {
96
- const dir = import_node_path.default.dirname(this.getConfigPath());
180
+ const dir = import_node_path3.default.dirname(this.getConfigPath());
97
181
  return pathExists(dir);
98
182
  }
99
183
  /** Read raw JSON from disk, return empty object if file missing */
100
184
  async readRaw() {
101
185
  const configPath = this.getConfigPath();
102
186
  try {
103
- const raw = await import_node_fs.default.promises.readFile(configPath, "utf-8");
187
+ const raw = await import_node_fs3.default.promises.readFile(configPath, "utf-8");
104
188
  return JSON.parse(raw);
105
189
  } catch (err) {
106
190
  if (err.code === "ENOENT") {
@@ -151,26 +235,26 @@ var init_base_client_handler = __esm({
151
235
 
152
236
  // src/utils/paths.ts
153
237
  function getHomedir() {
154
- return import_node_os.default.homedir();
238
+ return import_node_os3.default.homedir();
155
239
  }
156
240
  function getAppDataDir() {
157
241
  const home = getHomedir();
158
242
  if (process.platform === "darwin") {
159
- return import_node_path2.default.join(home, "Library", "Application Support");
243
+ return import_node_path4.default.join(home, "Library", "Application Support");
160
244
  }
161
245
  if (process.platform === "win32") {
162
- return process.env.APPDATA ?? import_node_path2.default.join(home, "AppData", "Roaming");
246
+ return process.env.APPDATA ?? import_node_path4.default.join(home, "AppData", "Roaming");
163
247
  }
164
- return process.env.XDG_CONFIG_HOME ?? import_node_path2.default.join(home, ".config");
248
+ return process.env.XDG_CONFIG_HOME ?? import_node_path4.default.join(home, ".config");
165
249
  }
166
250
  function resolveConfigPath(client) {
167
251
  const appData = getAppDataDir();
168
252
  const home = getHomedir();
169
253
  switch (client) {
170
254
  case "claude-desktop":
171
- return import_node_path2.default.join(appData, "Claude", "claude_desktop_config.json");
255
+ return import_node_path4.default.join(appData, "Claude", "claude_desktop_config.json");
172
256
  case "cursor":
173
- return import_node_path2.default.join(
257
+ return import_node_path4.default.join(
174
258
  appData,
175
259
  "Cursor",
176
260
  "User",
@@ -179,7 +263,7 @@ function resolveConfigPath(client) {
179
263
  "mcp.json"
180
264
  );
181
265
  case "windsurf":
182
- return import_node_path2.default.join(
266
+ return import_node_path4.default.join(
183
267
  appData,
184
268
  "Windsurf",
185
269
  "User",
@@ -189,21 +273,21 @@ function resolveConfigPath(client) {
189
273
  );
190
274
  case "vscode":
191
275
  if (process.platform === "darwin") {
192
- return import_node_path2.default.join(appData, "Code", "User", "settings.json");
276
+ return import_node_path4.default.join(appData, "Code", "User", "settings.json");
193
277
  }
194
278
  if (process.platform === "win32") {
195
- return import_node_path2.default.join(appData, "Code", "User", "settings.json");
279
+ return import_node_path4.default.join(appData, "Code", "User", "settings.json");
196
280
  }
197
- return import_node_path2.default.join(home, ".config", "Code", "User", "settings.json");
281
+ return import_node_path4.default.join(home, ".config", "Code", "User", "settings.json");
198
282
  }
199
283
  }
200
- var import_node_os, import_node_path2;
284
+ var import_node_os3, import_node_path4;
201
285
  var init_paths = __esm({
202
286
  "src/utils/paths.ts"() {
203
287
  "use strict";
204
288
  init_cjs_shims();
205
- import_node_os = __toESM(require("os"), 1);
206
- import_node_path2 = __toESM(require("path"), 1);
289
+ import_node_os3 = __toESM(require("os"), 1);
290
+ import_node_path4 = __toESM(require("path"), 1);
207
291
  }
208
292
  });
209
293
 
@@ -313,32 +397,947 @@ function getClient(type) {
313
397
  return new WindsurfHandler();
314
398
  }
315
399
  }
316
- async function getInstalledClients() {
317
- const all = getAllClientTypes().map(getClient);
318
- const results = await Promise.all(
319
- all.map(async (handler) => ({ handler, installed: await handler.isInstalled() }))
400
+ async function getInstalledClients() {
401
+ const all = getAllClientTypes().map(getClient);
402
+ const results = await Promise.all(
403
+ all.map(async (handler) => ({ handler, installed: await handler.isInstalled() }))
404
+ );
405
+ return results.filter((r) => r.installed).map((r) => r.handler);
406
+ }
407
+ var init_client_detector = __esm({
408
+ "src/clients/client-detector.ts"() {
409
+ "use strict";
410
+ init_cjs_shims();
411
+ init_claude_desktop();
412
+ init_cursor();
413
+ init_vscode();
414
+ init_windsurf();
415
+ }
416
+ });
417
+
418
+ // src/core/vault-service.ts
419
+ var vault_service_exports = {};
420
+ __export(vault_service_exports, {
421
+ clearPasswordCache: () => clearPasswordCache,
422
+ decrypt: () => decrypt,
423
+ encrypt: () => encrypt,
424
+ getMasterPassword: () => getMasterPassword,
425
+ getSecret: () => getSecret,
426
+ getSecretsForServer: () => getSecretsForServer,
427
+ getVaultPath: () => getVaultPath,
428
+ listSecrets: () => listSecrets,
429
+ readVault: () => readVault,
430
+ removeSecret: () => removeSecret,
431
+ setSecret: () => setSecret,
432
+ writeVault: () => writeVault
433
+ });
434
+ function getVaultPath() {
435
+ return import_node_path6.default.join(import_node_os4.default.homedir(), ".mcpman", "vault.enc");
436
+ }
437
+ function readVault(vaultPath = getVaultPath()) {
438
+ const empty = { version: 1, servers: {} };
439
+ try {
440
+ const raw = import_node_fs4.default.readFileSync(vaultPath, "utf-8");
441
+ const parsed = JSON.parse(raw);
442
+ if (parsed.version !== 1 || typeof parsed.servers !== "object") return empty;
443
+ return parsed;
444
+ } catch {
445
+ return empty;
446
+ }
447
+ }
448
+ function writeVault(data, vaultPath = getVaultPath()) {
449
+ const dir = import_node_path6.default.dirname(vaultPath);
450
+ import_node_fs4.default.mkdirSync(dir, { recursive: true });
451
+ const tmp = `${vaultPath}.tmp`;
452
+ import_node_fs4.default.writeFileSync(tmp, JSON.stringify(data, null, 2), { mode: 384 });
453
+ if (process.platform !== "win32") {
454
+ import_node_fs4.default.chmodSync(tmp, 384);
455
+ }
456
+ import_node_fs4.default.renameSync(tmp, vaultPath);
457
+ if (process.platform !== "win32") {
458
+ import_node_fs4.default.chmodSync(vaultPath, 384);
459
+ }
460
+ }
461
+ function encrypt(value, password2) {
462
+ const salt = import_node_crypto2.default.randomBytes(16);
463
+ const key = import_node_crypto2.default.pbkdf2Sync(password2, salt, 1e5, 32, "sha256");
464
+ const iv = import_node_crypto2.default.randomBytes(16);
465
+ const cipher = import_node_crypto2.default.createCipheriv("aes-256-cbc", key, iv);
466
+ const encrypted = Buffer.concat([cipher.update(value, "utf-8"), cipher.final()]);
467
+ return {
468
+ salt: salt.toString("hex"),
469
+ iv: iv.toString("hex"),
470
+ data: encrypted.toString("hex")
471
+ };
472
+ }
473
+ function decrypt(entry, password2) {
474
+ const salt = Buffer.from(entry.salt, "hex");
475
+ const key = import_node_crypto2.default.pbkdf2Sync(password2, salt, 1e5, 32, "sha256");
476
+ const iv = Buffer.from(entry.iv, "hex");
477
+ const decipher = import_node_crypto2.default.createDecipheriv("aes-256-cbc", key, iv);
478
+ const decrypted = Buffer.concat([
479
+ decipher.update(Buffer.from(entry.data, "hex")),
480
+ decipher.final()
481
+ // throws ERR_OSSL_BAD_DECRYPT on wrong password
482
+ ]);
483
+ return decrypted.toString("utf-8");
484
+ }
485
+ async function getMasterPassword(confirm8 = false) {
486
+ if (_cachedPassword) return _cachedPassword;
487
+ const password2 = await p3.password({
488
+ message: "Enter vault master password:",
489
+ validate: (v) => v.length < 8 ? "Password must be at least 8 characters" : void 0
490
+ });
491
+ if (p3.isCancel(password2)) {
492
+ p3.cancel("Vault access cancelled.");
493
+ process.exit(0);
494
+ }
495
+ if (confirm8) {
496
+ const confirm22 = await p3.password({ message: "Confirm master password:" });
497
+ if (p3.isCancel(confirm22) || confirm22 !== password2) {
498
+ p3.cancel("Passwords do not match.");
499
+ process.exit(1);
500
+ }
501
+ }
502
+ _cachedPassword = password2;
503
+ return _cachedPassword;
504
+ }
505
+ function clearPasswordCache() {
506
+ _cachedPassword = null;
507
+ }
508
+ function setSecret(server, key, value, password2, vaultPath = getVaultPath()) {
509
+ const vault = readVault(vaultPath);
510
+ if (!vault.servers[server]) vault.servers[server] = {};
511
+ vault.servers[server][key] = encrypt(value, password2);
512
+ writeVault(vault, vaultPath);
513
+ }
514
+ function getSecret(server, key, password2, vaultPath = getVaultPath()) {
515
+ const vault = readVault(vaultPath);
516
+ const entry = vault.servers[server]?.[key];
517
+ if (!entry) return null;
518
+ return decrypt(entry, password2);
519
+ }
520
+ function getSecretsForServer(server, password2, vaultPath = getVaultPath()) {
521
+ const vault = readVault(vaultPath);
522
+ const entries = vault.servers[server];
523
+ if (!entries) return {};
524
+ const result = {};
525
+ for (const [k, entry] of Object.entries(entries)) {
526
+ result[k] = decrypt(entry, password2);
527
+ }
528
+ return result;
529
+ }
530
+ function removeSecret(server, key, vaultPath = getVaultPath()) {
531
+ const vault = readVault(vaultPath);
532
+ if (vault.servers[server]) {
533
+ delete vault.servers[server][key];
534
+ if (Object.keys(vault.servers[server]).length === 0) {
535
+ delete vault.servers[server];
536
+ }
537
+ writeVault(vault, vaultPath);
538
+ }
539
+ }
540
+ function listSecrets(server, vaultPath = getVaultPath()) {
541
+ const vault = readVault(vaultPath);
542
+ const entries = server ? vault.servers[server] ? { [server]: vault.servers[server] } : {} : vault.servers;
543
+ return Object.entries(entries).map(([srv, keys]) => ({
544
+ server: srv,
545
+ keys: Object.keys(keys)
546
+ }));
547
+ }
548
+ var import_node_crypto2, import_node_fs4, import_node_os4, import_node_path6, p3, _cachedPassword;
549
+ var init_vault_service = __esm({
550
+ "src/core/vault-service.ts"() {
551
+ "use strict";
552
+ init_cjs_shims();
553
+ import_node_crypto2 = __toESM(require("crypto"), 1);
554
+ import_node_fs4 = __toESM(require("fs"), 1);
555
+ import_node_os4 = __toESM(require("os"), 1);
556
+ import_node_path6 = __toESM(require("path"), 1);
557
+ p3 = __toESM(require("@clack/prompts"), 1);
558
+ _cachedPassword = null;
559
+ process.on("exit", () => {
560
+ _cachedPassword = null;
561
+ });
562
+ }
563
+ });
564
+
565
+ // src/index.ts
566
+ init_cjs_shims();
567
+ var import_citty10 = require("citty");
568
+
569
+ // src/commands/audit.ts
570
+ init_cjs_shims();
571
+ var import_citty = require("citty");
572
+ var p = __toESM(require("@clack/prompts"), 1);
573
+ var import_picocolors = __toESM(require("picocolors"), 1);
574
+ var import_nanospinner = require("nanospinner");
575
+
576
+ // src/core/lockfile.ts
577
+ init_cjs_shims();
578
+ var import_node_fs = __toESM(require("fs"), 1);
579
+ var import_node_path = __toESM(require("path"), 1);
580
+ var import_node_os = __toESM(require("os"), 1);
581
+ var LOCKFILE_NAME = "mcpman.lock";
582
+ function findLockfile() {
583
+ let dir = process.cwd();
584
+ while (true) {
585
+ const candidate = import_node_path.default.join(dir, LOCKFILE_NAME);
586
+ if (import_node_fs.default.existsSync(candidate)) return candidate;
587
+ const parent = import_node_path.default.dirname(dir);
588
+ if (parent === dir) break;
589
+ dir = parent;
590
+ }
591
+ return null;
592
+ }
593
+ function getGlobalLockfilePath() {
594
+ return import_node_path.default.join(import_node_os.default.homedir(), ".mcpman", LOCKFILE_NAME);
595
+ }
596
+ function resolveLockfilePath() {
597
+ return findLockfile() ?? getGlobalLockfilePath();
598
+ }
599
+ function readLockfile(filePath) {
600
+ const target = filePath ?? resolveLockfilePath();
601
+ if (!import_node_fs.default.existsSync(target)) {
602
+ return { lockfileVersion: 1, servers: {} };
603
+ }
604
+ try {
605
+ const raw = import_node_fs.default.readFileSync(target, "utf-8");
606
+ return JSON.parse(raw);
607
+ } catch {
608
+ return { lockfileVersion: 1, servers: {} };
609
+ }
610
+ }
611
+ function serialize(data) {
612
+ const sorted = {
613
+ lockfileVersion: data.lockfileVersion,
614
+ servers: Object.fromEntries(
615
+ Object.entries(data.servers).sort(([a], [b]) => a.localeCompare(b))
616
+ )
617
+ };
618
+ return JSON.stringify(sorted, null, 2) + "\n";
619
+ }
620
+ function writeLockfile(data, filePath) {
621
+ const target = filePath ?? resolveLockfilePath();
622
+ const dir = import_node_path.default.dirname(target);
623
+ if (!import_node_fs.default.existsSync(dir)) {
624
+ import_node_fs.default.mkdirSync(dir, { recursive: true });
625
+ }
626
+ const tmp = `${target}.tmp`;
627
+ import_node_fs.default.writeFileSync(tmp, serialize(data), "utf-8");
628
+ import_node_fs.default.renameSync(tmp, target);
629
+ }
630
+ function addEntry(name, entry, filePath) {
631
+ const data = readLockfile(filePath);
632
+ data.servers[name] = entry;
633
+ writeLockfile(data, filePath);
634
+ }
635
+ function createEmptyLockfile(filePath) {
636
+ writeLockfile({ lockfileVersion: 1, servers: {} }, filePath);
637
+ }
638
+
639
+ // src/core/security-scanner.ts
640
+ init_cjs_shims();
641
+ var import_node_fs2 = __toESM(require("fs"), 1);
642
+ var import_node_os2 = __toESM(require("os"), 1);
643
+ var import_node_path2 = __toESM(require("path"), 1);
644
+ var CACHE_PATH = import_node_path2.default.join(import_node_os2.default.homedir(), ".mcpman", ".audit-cache.json");
645
+ var CACHE_TTL_MS = 24 * 60 * 60 * 1e3;
646
+ function readCache() {
647
+ try {
648
+ if (!import_node_fs2.default.existsSync(CACHE_PATH)) return {};
649
+ return JSON.parse(import_node_fs2.default.readFileSync(CACHE_PATH, "utf-8"));
650
+ } catch {
651
+ return {};
652
+ }
653
+ }
654
+ function writeCache(cache) {
655
+ const dir = import_node_path2.default.dirname(CACHE_PATH);
656
+ if (!import_node_fs2.default.existsSync(dir)) import_node_fs2.default.mkdirSync(dir, { recursive: true });
657
+ const tmp = `${CACHE_PATH}.tmp`;
658
+ import_node_fs2.default.writeFileSync(tmp, JSON.stringify(cache, null, 2), "utf-8");
659
+ import_node_fs2.default.renameSync(tmp, CACHE_PATH);
660
+ }
661
+ function getCachedReport(name, version) {
662
+ const cache = readCache();
663
+ const key = `${name}@${version}`;
664
+ const entry = cache[key];
665
+ if (!entry) return null;
666
+ if (Date.now() - entry.timestamp > CACHE_TTL_MS) return null;
667
+ return entry.report;
668
+ }
669
+ function cacheReport(name, version, report) {
670
+ const cache = readCache();
671
+ cache[`${name}@${version}`] = { report, timestamp: Date.now() };
672
+ writeCache(cache);
673
+ }
674
+ async function fetchNpmMetadata(packageName) {
675
+ const timeout = 1e4;
676
+ const signal = AbortSignal.timeout(timeout);
677
+ try {
678
+ const [regRes, dlRes] = await Promise.all([
679
+ fetch(`https://registry.npmjs.org/${encodeURIComponent(packageName)}`, { signal }),
680
+ fetch(`https://api.npmjs.org/downloads/point/last-week/${encodeURIComponent(packageName)}`, {
681
+ signal: AbortSignal.timeout(timeout)
682
+ })
683
+ ]);
684
+ if (!regRes.ok) return null;
685
+ const reg = await regRes.json();
686
+ const time = reg["time"] ?? {};
687
+ const created = time["created"] ? new Date(time["created"]) : null;
688
+ const modified = time["modified"] ? new Date(time["modified"]) : null;
689
+ const packageAge = created ? Math.floor((Date.now() - created.getTime()) / 864e5) : 0;
690
+ const maintainers = Array.isArray(reg["maintainers"]) ? reg["maintainers"] : [];
691
+ const latestVersion = reg["dist-tags"]?.["latest"] ?? "";
692
+ const versionData = reg["versions"]?.[latestVersion];
693
+ const deprecated = typeof versionData?.["deprecated"] === "string";
694
+ let weeklyDownloads = 0;
695
+ if (dlRes.ok) {
696
+ const dl = await dlRes.json();
697
+ weeklyDownloads = typeof dl["downloads"] === "number" ? dl["downloads"] : 0;
698
+ }
699
+ return {
700
+ weeklyDownloads,
701
+ lastPublish: modified?.toISOString() ?? (/* @__PURE__ */ new Date()).toISOString(),
702
+ packageAge,
703
+ maintainerCount: maintainers.length,
704
+ deprecated
705
+ };
706
+ } catch {
707
+ return null;
708
+ }
709
+ }
710
+ async function fetchVulnerabilities(packageName, version) {
711
+ try {
712
+ const res = await fetch("https://api.osv.dev/v1/query", {
713
+ method: "POST",
714
+ headers: { "Content-Type": "application/json" },
715
+ body: JSON.stringify({ package: { name: packageName, ecosystem: "npm" }, version }),
716
+ signal: AbortSignal.timeout(1e4)
717
+ });
718
+ if (!res.ok) return [];
719
+ const data = await res.json();
720
+ const vulns = Array.isArray(data["vulns"]) ? data["vulns"] : [];
721
+ return vulns.map((v) => {
722
+ const severity = v["database_specific"]?.["severity"];
723
+ const sev = typeof severity === "string" ? severity.toLowerCase() : "moderate";
724
+ const refs = Array.isArray(v["references"]) ? v["references"] : [];
725
+ return {
726
+ severity: ["low", "moderate", "high", "critical"].includes(sev) ? sev : "moderate",
727
+ title: typeof v["summary"] === "string" ? v["summary"] : typeof v["id"] === "string" ? v["id"] : "Unknown vulnerability",
728
+ url: typeof refs[0]?.["url"] === "string" ? refs[0]["url"] : void 0
729
+ };
730
+ });
731
+ } catch {
732
+ return [];
733
+ }
734
+ }
735
+ async function scanServer(name, entry) {
736
+ if (entry.source !== "npm") {
737
+ return {
738
+ server: name,
739
+ source: entry.source,
740
+ score: null,
741
+ riskLevel: "UNKNOWN",
742
+ vulnerabilities: [],
743
+ metadata: null
744
+ };
745
+ }
746
+ const cached = getCachedReport(name, entry.version);
747
+ if (cached) return cached;
748
+ const [metadata, vulnerabilities] = await Promise.all([
749
+ fetchNpmMetadata(name),
750
+ fetchVulnerabilities(name, entry.version)
751
+ ]);
752
+ const { computeTrustScore: computeTrustScore2 } = await Promise.resolve().then(() => (init_trust_scorer(), trust_scorer_exports));
753
+ const { score, riskLevel } = computeTrustScore2(metadata, vulnerabilities);
754
+ const report = {
755
+ server: name,
756
+ source: "npm",
757
+ score,
758
+ riskLevel,
759
+ vulnerabilities,
760
+ metadata
761
+ };
762
+ cacheReport(name, entry.version, report);
763
+ return report;
764
+ }
765
+ async function scanAllServers(servers, concurrency = 3) {
766
+ const entries = Object.entries(servers);
767
+ const results = [];
768
+ const executing = /* @__PURE__ */ new Set();
769
+ for (const [name, entry] of entries) {
770
+ const p11 = scanServer(name, entry).then((r) => {
771
+ results.push(r);
772
+ executing.delete(p11);
773
+ });
774
+ executing.add(p11);
775
+ if (executing.size >= concurrency) await Promise.race(executing);
776
+ }
777
+ await Promise.all(executing);
778
+ return results;
779
+ }
780
+
781
+ // src/core/version-checker.ts
782
+ init_cjs_shims();
783
+ function compareVersions(a, b) {
784
+ const aParts = a.replace(/^v/, "").split(".").map(Number);
785
+ const bParts = b.replace(/^v/, "").split(".").map(Number);
786
+ const len = Math.max(aParts.length, bParts.length);
787
+ for (let i = 0; i < len; i++) {
788
+ const aN = aParts[i] ?? 0;
789
+ const bN = bParts[i] ?? 0;
790
+ if (Number.isNaN(aN) || Number.isNaN(bN)) return 0;
791
+ if (aN < bN) return -1;
792
+ if (aN > bN) return 1;
793
+ }
794
+ return 0;
795
+ }
796
+ function detectUpdateType(current, latest) {
797
+ const cParts = current.replace(/^v/, "").split(".").map(Number);
798
+ const lParts = latest.replace(/^v/, "").split(".").map(Number);
799
+ if ((lParts[0] ?? 0) > (cParts[0] ?? 0)) return "major";
800
+ if ((lParts[1] ?? 0) > (cParts[1] ?? 0)) return "minor";
801
+ return "patch";
802
+ }
803
+ async function fetchNpmLatest(packageName) {
804
+ try {
805
+ const res = await fetch(
806
+ `https://registry.npmjs.org/${encodeURIComponent(packageName)}/latest`,
807
+ {
808
+ headers: { Accept: "application/json" },
809
+ signal: AbortSignal.timeout(8e3)
810
+ }
811
+ );
812
+ if (!res.ok) return null;
813
+ const data = await res.json();
814
+ return typeof data.version === "string" ? data.version : null;
815
+ } catch {
816
+ return null;
817
+ }
818
+ }
819
+ async function fetchSmitheryLatest(name) {
820
+ try {
821
+ const res = await fetch(
822
+ `https://registry.smithery.ai/servers/${encodeURIComponent(name)}`,
823
+ {
824
+ headers: { Accept: "application/json" },
825
+ signal: AbortSignal.timeout(8e3)
826
+ }
827
+ );
828
+ if (!res.ok) return null;
829
+ const data = await res.json();
830
+ return typeof data.version === "string" ? data.version : null;
831
+ } catch {
832
+ return null;
833
+ }
834
+ }
835
+ async function fetchGithubLatest(resolved) {
836
+ const match = resolved.match(/github\.com\/([^/]+)\/([^/]+)/);
837
+ if (!match) return null;
838
+ const [, owner, repo] = match;
839
+ try {
840
+ const res = await fetch(
841
+ `https://api.github.com/repos/${owner}/${repo}/releases/latest`,
842
+ {
843
+ headers: { Accept: "application/json" },
844
+ signal: AbortSignal.timeout(8e3)
845
+ }
846
+ );
847
+ if (!res.ok) return null;
848
+ const data = await res.json();
849
+ return typeof data.tag_name === "string" ? data.tag_name.replace(/^v/, "") : null;
850
+ } catch {
851
+ return null;
852
+ }
853
+ }
854
+ async function checkVersion(name, lockEntry) {
855
+ const current = lockEntry.version;
856
+ let latest = null;
857
+ if (lockEntry.source === "npm") {
858
+ latest = await fetchNpmLatest(name);
859
+ } else if (lockEntry.source === "smithery") {
860
+ latest = await fetchSmitheryLatest(name);
861
+ } else if (lockEntry.source === "github") {
862
+ latest = await fetchGithubLatest(lockEntry.resolved);
863
+ }
864
+ if (!latest || latest === current) {
865
+ return {
866
+ server: name,
867
+ source: lockEntry.source,
868
+ currentVersion: current,
869
+ latestVersion: latest ?? current,
870
+ hasUpdate: false
871
+ };
872
+ }
873
+ const hasUpdate = compareVersions(current, latest) === -1;
874
+ return {
875
+ server: name,
876
+ source: lockEntry.source,
877
+ currentVersion: current,
878
+ latestVersion: latest,
879
+ hasUpdate,
880
+ updateType: hasUpdate ? detectUpdateType(current, latest) : void 0
881
+ };
882
+ }
883
+ async function checkAllVersions(lockfile) {
884
+ const entries = Object.entries(lockfile.servers);
885
+ if (entries.length === 0) return [];
886
+ const results = [];
887
+ const executing = /* @__PURE__ */ new Set();
888
+ for (const [name, entry] of entries) {
889
+ const p11 = checkVersion(name, entry).then((r) => {
890
+ results.push(r);
891
+ executing.delete(p11);
892
+ });
893
+ executing.add(p11);
894
+ if (executing.size >= 5) {
895
+ await Promise.race(executing);
896
+ }
897
+ }
898
+ await Promise.all(executing);
899
+ return results;
900
+ }
901
+
902
+ // src/core/server-updater.ts
903
+ init_cjs_shims();
904
+
905
+ // src/core/server-resolver.ts
906
+ init_cjs_shims();
907
+
908
+ // src/core/registry.ts
909
+ init_cjs_shims();
910
+ var import_node_crypto = require("crypto");
911
+ function computeIntegrity(resolvedUrl) {
912
+ const hash = (0, import_node_crypto.createHash)("sha512").update(resolvedUrl).digest("base64");
913
+ return `sha512-${hash}`;
914
+ }
915
+ async function resolveFromSmithery(name) {
916
+ const url = `https://registry.smithery.ai/servers/${encodeURIComponent(name)}`;
917
+ let data;
918
+ try {
919
+ const res = await fetch(url, {
920
+ headers: { Accept: "application/json" },
921
+ signal: AbortSignal.timeout(8e3)
922
+ });
923
+ if (res.status === 404) {
924
+ throw new Error(`Server '${name}' not found on Smithery registry`);
925
+ }
926
+ if (!res.ok) {
927
+ throw new Error(`Smithery API error: ${res.status}`);
928
+ }
929
+ data = await res.json();
930
+ } catch (err) {
931
+ if (err instanceof Error && err.message.includes("not found")) throw err;
932
+ throw new Error(
933
+ `Cannot reach Smithery registry: ${err instanceof Error ? err.message : String(err)}`
934
+ );
935
+ }
936
+ const version = typeof data.version === "string" ? data.version : "latest";
937
+ const command = typeof data.command === "string" ? data.command : "npx";
938
+ const args = Array.isArray(data.args) ? data.args : ["-y", `${name}@${version}`];
939
+ const envVars = Array.isArray(data.envVars) ? data.envVars : [];
940
+ const resolved = typeof data.resolved === "string" ? data.resolved : `smithery:${name}@${version}`;
941
+ return {
942
+ name,
943
+ version,
944
+ description: typeof data.description === "string" ? data.description : "",
945
+ runtime: "node",
946
+ command,
947
+ args,
948
+ envVars,
949
+ resolved
950
+ };
951
+ }
952
+ async function resolveFromNpm(packageName) {
953
+ const url = `https://registry.npmjs.org/${encodeURIComponent(packageName)}/latest`;
954
+ let data;
955
+ try {
956
+ const res = await fetch(url, {
957
+ headers: { Accept: "application/json" },
958
+ signal: AbortSignal.timeout(8e3)
959
+ });
960
+ if (res.status === 404) {
961
+ throw new Error(`Package '${packageName}' not found on npm`);
962
+ }
963
+ if (!res.ok) {
964
+ throw new Error(`npm registry error: ${res.status}`);
965
+ }
966
+ data = await res.json();
967
+ } catch (err) {
968
+ if (err instanceof Error && err.message.includes("not found")) throw err;
969
+ throw new Error(
970
+ `Cannot reach npm registry: ${err instanceof Error ? err.message : String(err)}`
971
+ );
972
+ }
973
+ const version = typeof data.version === "string" ? data.version : "latest";
974
+ const resolved = `https://registry.npmjs.org/${packageName}/-/${packageName.replace(/^@[^/]+\//, "")}-${version}.tgz`;
975
+ const mcpField = data.mcp && typeof data.mcp === "object" ? data.mcp : null;
976
+ const envVars = mcpField?.envVars ? mcpField.envVars : [];
977
+ return {
978
+ name: packageName,
979
+ version,
980
+ description: typeof data.description === "string" ? data.description : "",
981
+ runtime: "node",
982
+ command: "npx",
983
+ args: ["-y", `${packageName}@${version}`],
984
+ envVars,
985
+ resolved
986
+ };
987
+ }
988
+ async function resolveFromGitHub(githubUrl) {
989
+ const match = githubUrl.match(/github\.com\/([^/]+)\/([^/]+)/);
990
+ if (!match) {
991
+ throw new Error(`Invalid GitHub URL: ${githubUrl}`);
992
+ }
993
+ const [, owner, repo] = match;
994
+ const rawUrl = `https://raw.githubusercontent.com/${owner}/${repo}/main/package.json`;
995
+ let pkgData = {};
996
+ try {
997
+ const res = await fetch(rawUrl, { signal: AbortSignal.timeout(8e3) });
998
+ if (res.ok) {
999
+ pkgData = await res.json();
1000
+ }
1001
+ } catch {
1002
+ }
1003
+ const version = typeof pkgData.version === "string" ? pkgData.version : "main";
1004
+ const name = typeof pkgData.name === "string" ? pkgData.name : `${owner}/${repo}`;
1005
+ return {
1006
+ name,
1007
+ version,
1008
+ description: typeof pkgData.description === "string" ? pkgData.description : "",
1009
+ runtime: "node",
1010
+ command: "npx",
1011
+ args: ["-y", githubUrl],
1012
+ envVars: [],
1013
+ resolved: githubUrl
1014
+ };
1015
+ }
1016
+
1017
+ // src/core/server-resolver.ts
1018
+ function detectSource(input) {
1019
+ if (input.startsWith("smithery:")) {
1020
+ return { type: "smithery", input: input.slice(9) };
1021
+ }
1022
+ if (input.startsWith("https://github.com/") || input.startsWith("github.com/")) {
1023
+ return { type: "github", input };
1024
+ }
1025
+ return { type: "npm", input };
1026
+ }
1027
+ function parseEnvFlags(envFlags) {
1028
+ if (!envFlags) return {};
1029
+ const flags = Array.isArray(envFlags) ? envFlags : [envFlags];
1030
+ const result = {};
1031
+ for (const flag of flags) {
1032
+ const idx = flag.indexOf("=");
1033
+ if (idx > 0) {
1034
+ result[flag.slice(0, idx)] = flag.slice(idx + 1);
1035
+ }
1036
+ }
1037
+ return result;
1038
+ }
1039
+ async function resolveServer(input) {
1040
+ const source = detectSource(input);
1041
+ switch (source.type) {
1042
+ case "smithery":
1043
+ return resolveFromSmithery(source.input);
1044
+ case "github":
1045
+ return resolveFromGitHub(source.input);
1046
+ case "npm":
1047
+ return resolveFromNpm(source.input);
1048
+ }
1049
+ }
1050
+
1051
+ // src/core/server-updater.ts
1052
+ async function applyServerUpdate(serverName, lockEntry, clients) {
1053
+ const fromVersion = lockEntry.version;
1054
+ const input = lockEntry.source === "smithery" ? `smithery:${serverName}` : lockEntry.source === "github" ? lockEntry.resolved : serverName;
1055
+ try {
1056
+ const metadata = await resolveServer(input);
1057
+ const integrity = computeIntegrity(metadata.resolved);
1058
+ addEntry(serverName, {
1059
+ ...lockEntry,
1060
+ version: metadata.version,
1061
+ resolved: metadata.resolved,
1062
+ integrity,
1063
+ command: metadata.command,
1064
+ args: metadata.args,
1065
+ installedAt: (/* @__PURE__ */ new Date()).toISOString()
1066
+ });
1067
+ const targetClients = clients.filter(
1068
+ (c) => lockEntry.clients.includes(c.type)
1069
+ );
1070
+ for (const client of targetClients) {
1071
+ try {
1072
+ await client.addServer(serverName, {
1073
+ command: metadata.command,
1074
+ args: metadata.args
1075
+ });
1076
+ } catch {
1077
+ }
1078
+ }
1079
+ return {
1080
+ server: serverName,
1081
+ success: true,
1082
+ fromVersion,
1083
+ toVersion: metadata.version
1084
+ };
1085
+ } catch (err) {
1086
+ return {
1087
+ server: serverName,
1088
+ success: false,
1089
+ fromVersion,
1090
+ toVersion: fromVersion,
1091
+ error: err instanceof Error ? err.message : String(err)
1092
+ };
1093
+ }
1094
+ }
1095
+
1096
+ // src/commands/audit.ts
1097
+ function colorRisk(level, score) {
1098
+ const label = score !== null ? `${score}/100 (${level})` : level;
1099
+ if (level === "LOW") return import_picocolors.default.green(label);
1100
+ if (level === "MEDIUM") return import_picocolors.default.yellow(label);
1101
+ if (level === "HIGH") return import_picocolors.default.red(label);
1102
+ if (level === "CRITICAL") return import_picocolors.default.bold(import_picocolors.default.red(label));
1103
+ return import_picocolors.default.dim(label);
1104
+ }
1105
+ function daysAgo(isoDate) {
1106
+ const days = Math.floor((Date.now() - new Date(isoDate).getTime()) / 864e5);
1107
+ if (days === 0) return "today";
1108
+ if (days === 1) return "1 day ago";
1109
+ return `${days} days ago`;
1110
+ }
1111
+ function countVulns(vulns) {
1112
+ const c = { critical: 0, high: 0, moderate: 0, low: 0 };
1113
+ for (const v of vulns) c[v.severity]++;
1114
+ if (vulns.length === 0) return import_picocolors.default.green("none");
1115
+ const parts = [];
1116
+ if (c.critical) parts.push(import_picocolors.default.bold(import_picocolors.default.red(`${c.critical} critical`)));
1117
+ if (c.high) parts.push(import_picocolors.default.red(`${c.high} high`));
1118
+ if (c.moderate) parts.push(import_picocolors.default.yellow(`${c.moderate} moderate`));
1119
+ if (c.low) parts.push(import_picocolors.default.dim(`${c.low} low`));
1120
+ return parts.join(", ");
1121
+ }
1122
+ function printReport(report) {
1123
+ const riskColored = colorRisk(report.riskLevel, report.score);
1124
+ const icon = report.riskLevel === "LOW" ? import_picocolors.default.green("\u25CF") : report.riskLevel === "MEDIUM" ? import_picocolors.default.yellow("\u25CF") : report.riskLevel === "UNKNOWN" ? import_picocolors.default.dim("\u25CB") : import_picocolors.default.red("\u25CF");
1125
+ console.log(` ${icon} ${import_picocolors.default.bold(report.server)} Score: ${riskColored}`);
1126
+ if (report.source !== "npm") {
1127
+ console.log(` ${import_picocolors.default.dim("Non-npm source \u2014 security data unavailable")}`);
1128
+ console.log();
1129
+ return;
1130
+ }
1131
+ if (report.metadata) {
1132
+ const { weeklyDownloads, packageAge, lastPublish, maintainerCount, deprecated } = report.metadata;
1133
+ const dlStr = weeklyDownloads.toLocaleString();
1134
+ console.log(
1135
+ ` ${import_picocolors.default.dim("Downloads:")} ${dlStr}/week ${import_picocolors.default.dim("|")} ${import_picocolors.default.dim("Age:")} ${packageAge}d ${import_picocolors.default.dim("|")} ${import_picocolors.default.dim("Last publish:")} ${daysAgo(lastPublish)} ${import_picocolors.default.dim("|")} ${import_picocolors.default.dim("Maintainers:")} ${maintainerCount}` + (deprecated ? import_picocolors.default.red(" [DEPRECATED]") : "")
1136
+ );
1137
+ }
1138
+ console.log(` ${import_picocolors.default.dim("Vulnerabilities:")} ${countVulns(report.vulnerabilities)}`);
1139
+ if (report.vulnerabilities.length > 0) {
1140
+ for (const v of report.vulnerabilities) {
1141
+ const sevColor = v.severity === "critical" || v.severity === "high" ? import_picocolors.default.red : import_picocolors.default.yellow;
1142
+ const url = v.url ? import_picocolors.default.dim(` ${v.url}`) : "";
1143
+ console.log(` ${sevColor("\u25B8")} [${v.severity}] ${v.title}${url}`);
1144
+ }
1145
+ }
1146
+ console.log();
1147
+ }
1148
+ var audit_default = (0, import_citty.defineCommand)({
1149
+ meta: {
1150
+ name: "audit",
1151
+ description: "Scan installed MCP servers for security vulnerabilities and trust scores"
1152
+ },
1153
+ args: {
1154
+ server: {
1155
+ type: "positional",
1156
+ description: "Specific server to audit (omit to audit all)",
1157
+ required: false
1158
+ },
1159
+ json: {
1160
+ type: "boolean",
1161
+ description: "Output results as JSON",
1162
+ default: false
1163
+ },
1164
+ fix: {
1165
+ type: "boolean",
1166
+ description: "Apply updates to fix vulnerable packages",
1167
+ default: false
1168
+ },
1169
+ yes: {
1170
+ type: "boolean",
1171
+ description: "Skip confirmation prompt (use with --fix)",
1172
+ default: false
1173
+ }
1174
+ },
1175
+ async run({ args }) {
1176
+ const lockfile = readLockfile();
1177
+ const { servers } = lockfile;
1178
+ if (Object.keys(servers).length === 0) {
1179
+ console.log(import_picocolors.default.dim("\n No MCP servers installed. Run mcpman install <server> to get started.\n"));
1180
+ return;
1181
+ }
1182
+ const targets = {};
1183
+ if (args.server) {
1184
+ if (!servers[args.server]) {
1185
+ console.error(import_picocolors.default.red(`
1186
+ Server "${args.server}" not found in lockfile.
1187
+ `));
1188
+ process.exit(1);
1189
+ }
1190
+ targets[args.server] = servers[args.server];
1191
+ } else {
1192
+ Object.assign(targets, servers);
1193
+ }
1194
+ const spinner5 = (0, import_nanospinner.createSpinner)(`Scanning ${Object.keys(targets).length} server(s)...`).start();
1195
+ let reports;
1196
+ try {
1197
+ reports = args.server ? [await scanServer(args.server, targets[args.server])] : await scanAllServers(targets);
1198
+ } catch (err) {
1199
+ spinner5.error({ text: "Scan failed" });
1200
+ console.error(import_picocolors.default.red(String(err)));
1201
+ process.exit(1);
1202
+ }
1203
+ spinner5.success({ text: `Scanned ${reports.length} server(s)` });
1204
+ if (args.json) {
1205
+ console.log(JSON.stringify(reports, null, 2));
1206
+ return;
1207
+ }
1208
+ console.log(import_picocolors.default.bold("\n mcpman audit\n"));
1209
+ console.log(import_picocolors.default.dim(" " + "\u2500".repeat(60)));
1210
+ for (const report of reports) {
1211
+ printReport(report);
1212
+ }
1213
+ console.log(import_picocolors.default.dim(" " + "\u2500".repeat(60)));
1214
+ const withIssues = reports.filter(
1215
+ (r) => r.riskLevel !== "LOW" && r.riskLevel !== "UNKNOWN"
1216
+ );
1217
+ const npmReports = reports.filter((r) => r.source === "npm");
1218
+ const parts = [];
1219
+ parts.push(`${reports.length} server(s) scanned`);
1220
+ if (npmReports.length < reports.length) {
1221
+ parts.push(import_picocolors.default.dim(`${reports.length - npmReports.length} non-npm (unverified)`));
1222
+ }
1223
+ if (withIssues.length > 0) {
1224
+ parts.push(import_picocolors.default.yellow(`${withIssues.length} with issues`));
1225
+ } else {
1226
+ parts.push(import_picocolors.default.green("all clear"));
1227
+ }
1228
+ console.log(`
1229
+ Summary: ${parts.join(" | ")}
1230
+ `);
1231
+ if (args.fix) {
1232
+ await runAuditFix(reports, lockfile.servers, args.yes);
1233
+ }
1234
+ }
1235
+ });
1236
+ async function loadClients() {
1237
+ try {
1238
+ const mod = await Promise.resolve().then(() => (init_client_detector(), client_detector_exports));
1239
+ return mod.getInstalledClients();
1240
+ } catch {
1241
+ return [];
1242
+ }
1243
+ }
1244
+ async function runAuditFix(reports, servers, skipConfirm) {
1245
+ const npmWithVulns = reports.filter(
1246
+ (r) => r.vulnerabilities.length > 0 && r.source === "npm"
320
1247
  );
321
- return results.filter((r) => r.installed).map((r) => r.handler);
322
- }
323
- var init_client_detector = __esm({
324
- "src/clients/client-detector.ts"() {
325
- "use strict";
326
- init_cjs_shims();
327
- init_claude_desktop();
328
- init_cursor();
329
- init_vscode();
330
- init_windsurf();
1248
+ const nonNpmWithVulns = reports.filter(
1249
+ (r) => r.vulnerabilities.length > 0 && r.source !== "npm"
1250
+ );
1251
+ if (nonNpmWithVulns.length > 0) {
1252
+ console.log(import_picocolors.default.yellow(" Non-npm servers require manual update:"));
1253
+ for (const r of nonNpmWithVulns) {
1254
+ console.log(` ${import_picocolors.default.dim("\u2192")} ${r.server} (${r.source})`);
1255
+ }
1256
+ console.log();
331
1257
  }
332
- });
333
-
334
- // src/index.ts
335
- init_cjs_shims();
336
- var import_citty6 = require("citty");
1258
+ if (npmWithVulns.length === 0) {
1259
+ console.log(import_picocolors.default.green(" No fixable vulnerabilities found.\n"));
1260
+ return;
1261
+ }
1262
+ const versionSpinner = (0, import_nanospinner.createSpinner)("Checking for available updates...").start();
1263
+ const versionChecks = await Promise.all(
1264
+ npmWithVulns.map((r) => checkVersion(r.server, servers[r.server]))
1265
+ );
1266
+ versionSpinner.success({ text: "Version check complete" });
1267
+ const updatable = versionChecks.filter((u) => u.hasUpdate);
1268
+ if (updatable.length === 0) {
1269
+ console.log(import_picocolors.default.yellow(
1270
+ " Vulnerable servers have no newer versions available yet.\n Allow time for registry to publish fixes.\n"
1271
+ ));
1272
+ return;
1273
+ }
1274
+ console.log(import_picocolors.default.bold(`
1275
+ ${updatable.length} server(s) can be updated to fix vulnerabilities:
1276
+ `));
1277
+ for (const u of updatable) {
1278
+ console.log(` ${import_picocolors.default.cyan("\u2192")} ${u.server} ${import_picocolors.default.dim(u.currentVersion)} \u2192 ${import_picocolors.default.green(u.latestVersion)}`);
1279
+ }
1280
+ console.log();
1281
+ if (!skipConfirm) {
1282
+ const confirmed = await p.confirm({
1283
+ message: `Update ${updatable.length} vulnerable server(s)?`,
1284
+ initialValue: true
1285
+ });
1286
+ if (p.isCancel(confirmed) || !confirmed) {
1287
+ p.outro("Cancelled.");
1288
+ return;
1289
+ }
1290
+ }
1291
+ const clients = await loadClients();
1292
+ let successCount = 0;
1293
+ const results = [];
1294
+ for (const u of updatable) {
1295
+ const s = (0, import_nanospinner.createSpinner)(`Updating ${u.server}...`).start();
1296
+ const result = await applyServerUpdate(u.server, servers[u.server], clients);
1297
+ if (result.success) {
1298
+ s.success({ text: `${import_picocolors.default.green("\u2713")} ${u.server}: ${result.fromVersion} \u2192 ${result.toVersion}` });
1299
+ successCount++;
1300
+ } else {
1301
+ s.error({ text: `${import_picocolors.default.red("\u2717")} ${u.server}: ${result.error}` });
1302
+ }
1303
+ results.push({
1304
+ server: u.server,
1305
+ from: result.fromVersion,
1306
+ to: result.toVersion,
1307
+ ok: result.success,
1308
+ error: result.error
1309
+ });
1310
+ }
1311
+ console.log();
1312
+ if (successCount > 0) {
1313
+ const updatedNames = results.filter((r) => r.ok).map((r) => r.server);
1314
+ const freshLockfile = readLockfile();
1315
+ const rescanSpinner = (0, import_nanospinner.createSpinner)("Re-scanning updated servers...").start();
1316
+ const afterReports = await Promise.all(
1317
+ updatedNames.map((name) => scanServer(name, freshLockfile.servers[name]))
1318
+ );
1319
+ rescanSpinner.success({ text: "Re-scan complete" });
1320
+ console.log(import_picocolors.default.bold("\n Before / After:\n"));
1321
+ for (const after of afterReports) {
1322
+ const before = reports.find((r) => r.server === after.server);
1323
+ const beforeVulns = before?.vulnerabilities.length ?? 0;
1324
+ const afterVulns = after.vulnerabilities.length;
1325
+ const improved = afterVulns < beforeVulns ? import_picocolors.default.green("improved") : import_picocolors.default.yellow("unchanged");
1326
+ console.log(
1327
+ ` ${import_picocolors.default.bold(after.server)} vulns: ${beforeVulns} \u2192 ${afterVulns} [${improved}]`
1328
+ );
1329
+ }
1330
+ console.log();
1331
+ }
1332
+ console.log(`
1333
+ ${successCount} of ${updatable.length} server(s) updated.
1334
+ `);
1335
+ }
337
1336
 
338
1337
  // src/commands/doctor.ts
339
1338
  init_cjs_shims();
340
- var import_citty = require("citty");
341
- var import_picocolors = __toESM(require("picocolors"), 1);
1339
+ var import_citty2 = require("citty");
1340
+ var import_picocolors2 = __toESM(require("picocolors"), 1);
342
1341
 
343
1342
  // src/core/server-inventory.ts
344
1343
  init_cjs_shims();
@@ -609,12 +1608,12 @@ async function quickHealthProbe(config, timeoutMs = 3e3) {
609
1608
 
610
1609
  // src/commands/doctor.ts
611
1610
  var CHECK_ICON = {
612
- pass: import_picocolors.default.green("\u2713"),
613
- fail: import_picocolors.default.red("\u2717"),
614
- skip: import_picocolors.default.dim("-"),
615
- warn: import_picocolors.default.yellow("\u26A0")
1611
+ pass: import_picocolors2.default.green("\u2713"),
1612
+ fail: import_picocolors2.default.red("\u2717"),
1613
+ skip: import_picocolors2.default.dim("-"),
1614
+ warn: import_picocolors2.default.yellow("\u26A0")
616
1615
  };
617
- var doctor_default = (0, import_citty.defineCommand)({
1616
+ var doctor_default = (0, import_citty2.defineCommand)({
618
1617
  meta: {
619
1618
  name: "doctor",
620
1619
  description: "Check MCP server health and configuration"
@@ -627,10 +1626,10 @@ var doctor_default = (0, import_citty.defineCommand)({
627
1626
  }
628
1627
  },
629
1628
  async run({ args }) {
630
- console.log(import_picocolors.default.bold("\n mcpman doctor\n"));
1629
+ console.log(import_picocolors2.default.bold("\n mcpman doctor\n"));
631
1630
  const servers = await getInstalledServers();
632
1631
  if (servers.length === 0) {
633
- console.log(import_picocolors.default.dim(" No MCP servers installed. Run mcpman install <server> to get started."));
1632
+ console.log(import_picocolors2.default.dim(" No MCP servers installed. Run mcpman install <server> to get started."));
634
1633
  return;
635
1634
  }
636
1635
  const tasks = servers.map((s) => () => checkServerHealth(s.name, s.config));
@@ -642,14 +1641,14 @@ var doctor_default = (0, import_citty.defineCommand)({
642
1641
  if (result.status === "healthy") passed++;
643
1642
  else failed++;
644
1643
  }
645
- console.log(import_picocolors.default.dim(" " + "\u2500".repeat(50)));
1644
+ console.log(import_picocolors2.default.dim(" " + "\u2500".repeat(50)));
646
1645
  const parts = [];
647
- if (passed > 0) parts.push(import_picocolors.default.green(`${passed} healthy`));
648
- if (failed > 0) parts.push(import_picocolors.default.red(`${failed} unhealthy`));
1646
+ if (passed > 0) parts.push(import_picocolors2.default.green(`${passed} healthy`));
1647
+ if (failed > 0) parts.push(import_picocolors2.default.red(`${failed} unhealthy`));
649
1648
  console.log(` Summary: ${parts.join(", ")}`);
650
1649
  if (failed > 0) {
651
1650
  if (!args.fix) {
652
- console.log(import_picocolors.default.dim(` Run ${import_picocolors.default.cyan("mcpman doctor --fix")} for fix suggestions.
1651
+ console.log(import_picocolors2.default.dim(` Run ${import_picocolors2.default.cyan("mcpman doctor --fix")} for fix suggestions.
653
1652
  `));
654
1653
  }
655
1654
  process.exit(1);
@@ -658,13 +1657,13 @@ var doctor_default = (0, import_citty.defineCommand)({
658
1657
  }
659
1658
  });
660
1659
  function printServerResult(result, showFix) {
661
- const icon = result.status === "healthy" ? import_picocolors.default.green("\u25CF") : import_picocolors.default.red("\u25CF");
662
- console.log(` ${icon} ${import_picocolors.default.bold(result.serverName)}`);
1660
+ const icon = result.status === "healthy" ? import_picocolors2.default.green("\u25CF") : import_picocolors2.default.red("\u25CF");
1661
+ console.log(` ${icon} ${import_picocolors2.default.bold(result.serverName)}`);
663
1662
  for (const check of result.checks) {
664
1663
  const checkIcon = check.skipped ? CHECK_ICON.skip : check.passed ? CHECK_ICON.pass : CHECK_ICON.fail;
665
1664
  console.log(` ${checkIcon} ${check.name}: ${check.message}`);
666
1665
  if (showFix && !check.passed && !check.skipped && check.fix) {
667
- console.log(` ${import_picocolors.default.yellow("\u2192")} Fix: ${import_picocolors.default.cyan(check.fix)}`);
1666
+ console.log(` ${import_picocolors2.default.yellow("\u2192")} Fix: ${import_picocolors2.default.cyan(check.fix)}`);
668
1667
  }
669
1668
  }
670
1669
  console.log();
@@ -673,11 +1672,11 @@ async function runParallel(tasks, concurrency) {
673
1672
  const results = [];
674
1673
  const executing = /* @__PURE__ */ new Set();
675
1674
  for (const task of tasks) {
676
- const p5 = task().then((r) => {
1675
+ const p11 = task().then((r) => {
677
1676
  results.push(r);
678
- executing.delete(p5);
1677
+ executing.delete(p11);
679
1678
  });
680
- executing.add(p5);
1679
+ executing.add(p11);
681
1680
  if (executing.size >= concurrency) {
682
1681
  await Promise.race(executing);
683
1682
  }
@@ -688,184 +1687,10 @@ async function runParallel(tasks, concurrency) {
688
1687
 
689
1688
  // src/commands/init.ts
690
1689
  init_cjs_shims();
691
- var import_citty2 = require("citty");
692
- var p = __toESM(require("@clack/prompts"), 1);
693
- var import_node_path4 = __toESM(require("path"), 1);
694
-
695
- // src/core/lockfile.ts
696
- init_cjs_shims();
697
- var import_node_fs2 = __toESM(require("fs"), 1);
698
- var import_node_path3 = __toESM(require("path"), 1);
699
- var import_node_os2 = __toESM(require("os"), 1);
700
- var LOCKFILE_NAME = "mcpman.lock";
701
- function findLockfile() {
702
- let dir = process.cwd();
703
- while (true) {
704
- const candidate = import_node_path3.default.join(dir, LOCKFILE_NAME);
705
- if (import_node_fs2.default.existsSync(candidate)) return candidate;
706
- const parent = import_node_path3.default.dirname(dir);
707
- if (parent === dir) break;
708
- dir = parent;
709
- }
710
- return null;
711
- }
712
- function getGlobalLockfilePath() {
713
- return import_node_path3.default.join(import_node_os2.default.homedir(), ".mcpman", LOCKFILE_NAME);
714
- }
715
- function resolveLockfilePath() {
716
- return findLockfile() ?? getGlobalLockfilePath();
717
- }
718
- function readLockfile(filePath) {
719
- const target = filePath ?? resolveLockfilePath();
720
- if (!import_node_fs2.default.existsSync(target)) {
721
- return { lockfileVersion: 1, servers: {} };
722
- }
723
- try {
724
- const raw = import_node_fs2.default.readFileSync(target, "utf-8");
725
- return JSON.parse(raw);
726
- } catch {
727
- return { lockfileVersion: 1, servers: {} };
728
- }
729
- }
730
- function serialize(data) {
731
- const sorted = {
732
- lockfileVersion: data.lockfileVersion,
733
- servers: Object.fromEntries(
734
- Object.entries(data.servers).sort(([a], [b]) => a.localeCompare(b))
735
- )
736
- };
737
- return JSON.stringify(sorted, null, 2) + "\n";
738
- }
739
- function writeLockfile(data, filePath) {
740
- const target = filePath ?? resolveLockfilePath();
741
- const dir = import_node_path3.default.dirname(target);
742
- if (!import_node_fs2.default.existsSync(dir)) {
743
- import_node_fs2.default.mkdirSync(dir, { recursive: true });
744
- }
745
- const tmp = `${target}.tmp`;
746
- import_node_fs2.default.writeFileSync(tmp, serialize(data), "utf-8");
747
- import_node_fs2.default.renameSync(tmp, target);
748
- }
749
- function addEntry(name, entry, filePath) {
750
- const data = readLockfile(filePath);
751
- data.servers[name] = entry;
752
- writeLockfile(data, filePath);
753
- }
754
- function createEmptyLockfile(filePath) {
755
- writeLockfile({ lockfileVersion: 1, servers: {} }, filePath);
756
- }
757
-
758
- // src/core/registry.ts
759
- init_cjs_shims();
760
- var import_node_crypto = require("crypto");
761
- function computeIntegrity(resolvedUrl) {
762
- const hash = (0, import_node_crypto.createHash)("sha512").update(resolvedUrl).digest("base64");
763
- return `sha512-${hash}`;
764
- }
765
- async function resolveFromSmithery(name) {
766
- const url = `https://registry.smithery.ai/servers/${encodeURIComponent(name)}`;
767
- let data;
768
- try {
769
- const res = await fetch(url, {
770
- headers: { Accept: "application/json" },
771
- signal: AbortSignal.timeout(8e3)
772
- });
773
- if (res.status === 404) {
774
- throw new Error(`Server '${name}' not found on Smithery registry`);
775
- }
776
- if (!res.ok) {
777
- throw new Error(`Smithery API error: ${res.status}`);
778
- }
779
- data = await res.json();
780
- } catch (err) {
781
- if (err instanceof Error && err.message.includes("not found")) throw err;
782
- throw new Error(
783
- `Cannot reach Smithery registry: ${err instanceof Error ? err.message : String(err)}`
784
- );
785
- }
786
- const version = typeof data.version === "string" ? data.version : "latest";
787
- const command = typeof data.command === "string" ? data.command : "npx";
788
- const args = Array.isArray(data.args) ? data.args : ["-y", `${name}@${version}`];
789
- const envVars = Array.isArray(data.envVars) ? data.envVars : [];
790
- const resolved = typeof data.resolved === "string" ? data.resolved : `smithery:${name}@${version}`;
791
- return {
792
- name,
793
- version,
794
- description: typeof data.description === "string" ? data.description : "",
795
- runtime: "node",
796
- command,
797
- args,
798
- envVars,
799
- resolved
800
- };
801
- }
802
- async function resolveFromNpm(packageName) {
803
- const url = `https://registry.npmjs.org/${encodeURIComponent(packageName)}/latest`;
804
- let data;
805
- try {
806
- const res = await fetch(url, {
807
- headers: { Accept: "application/json" },
808
- signal: AbortSignal.timeout(8e3)
809
- });
810
- if (res.status === 404) {
811
- throw new Error(`Package '${packageName}' not found on npm`);
812
- }
813
- if (!res.ok) {
814
- throw new Error(`npm registry error: ${res.status}`);
815
- }
816
- data = await res.json();
817
- } catch (err) {
818
- if (err instanceof Error && err.message.includes("not found")) throw err;
819
- throw new Error(
820
- `Cannot reach npm registry: ${err instanceof Error ? err.message : String(err)}`
821
- );
822
- }
823
- const version = typeof data.version === "string" ? data.version : "latest";
824
- const resolved = `https://registry.npmjs.org/${packageName}/-/${packageName.replace(/^@[^/]+\//, "")}-${version}.tgz`;
825
- const mcpField = data.mcp && typeof data.mcp === "object" ? data.mcp : null;
826
- const envVars = mcpField?.envVars ? mcpField.envVars : [];
827
- return {
828
- name: packageName,
829
- version,
830
- description: typeof data.description === "string" ? data.description : "",
831
- runtime: "node",
832
- command: "npx",
833
- args: ["-y", `${packageName}@${version}`],
834
- envVars,
835
- resolved
836
- };
837
- }
838
- async function resolveFromGitHub(githubUrl) {
839
- const match = githubUrl.match(/github\.com\/([^/]+)\/([^/]+)/);
840
- if (!match) {
841
- throw new Error(`Invalid GitHub URL: ${githubUrl}`);
842
- }
843
- const [, owner, repo] = match;
844
- const rawUrl = `https://raw.githubusercontent.com/${owner}/${repo}/main/package.json`;
845
- let pkgData = {};
846
- try {
847
- const res = await fetch(rawUrl, { signal: AbortSignal.timeout(8e3) });
848
- if (res.ok) {
849
- pkgData = await res.json();
850
- }
851
- } catch {
852
- }
853
- const version = typeof pkgData.version === "string" ? pkgData.version : "main";
854
- const name = typeof pkgData.name === "string" ? pkgData.name : `${owner}/${repo}`;
855
- return {
856
- name,
857
- version,
858
- description: typeof pkgData.description === "string" ? pkgData.description : "",
859
- runtime: "node",
860
- command: "npx",
861
- args: ["-y", githubUrl],
862
- envVars: [],
863
- resolved: githubUrl
864
- };
865
- }
866
-
867
- // src/commands/init.ts
868
- var init_default = (0, import_citty2.defineCommand)({
1690
+ var import_citty3 = require("citty");
1691
+ var p2 = __toESM(require("@clack/prompts"), 1);
1692
+ var import_node_path5 = __toESM(require("path"), 1);
1693
+ var init_default = (0, import_citty3.defineCommand)({
869
1694
  meta: {
870
1695
  name: "init",
871
1696
  description: "Initialize mcpman.lock in the current project"
@@ -880,17 +1705,17 @@ var init_default = (0, import_citty2.defineCommand)({
880
1705
  },
881
1706
  async run({ args }) {
882
1707
  const nonInteractive = args.yes || !process.stdout.isTTY;
883
- p.intro("mcpman init");
884
- const targetPath = import_node_path4.default.join(process.cwd(), LOCKFILE_NAME);
1708
+ p2.intro("mcpman init");
1709
+ const targetPath = import_node_path5.default.join(process.cwd(), LOCKFILE_NAME);
885
1710
  const existing = findLockfile();
886
1711
  if (existing) {
887
1712
  if (nonInteractive) {
888
- p.log.warn(`Lockfile already exists: ${existing} \u2014 overwriting (non-interactive).`);
1713
+ p2.log.warn(`Lockfile already exists: ${existing} \u2014 overwriting (non-interactive).`);
889
1714
  } else {
890
- p.log.warn(`Lockfile already exists: ${existing}`);
891
- const overwrite = await p.confirm({ message: "Overwrite?" });
892
- if (p.isCancel(overwrite) || !overwrite) {
893
- p.outro("Cancelled.");
1715
+ p2.log.warn(`Lockfile already exists: ${existing}`);
1716
+ const overwrite = await p2.confirm({ message: "Overwrite?" });
1717
+ if (p2.isCancel(overwrite) || !overwrite) {
1718
+ p2.outro("Cancelled.");
894
1719
  return;
895
1720
  }
896
1721
  }
@@ -900,7 +1725,7 @@ var init_default = (0, import_citty2.defineCommand)({
900
1725
  const mod = await Promise.resolve().then(() => (init_client_detector(), client_detector_exports));
901
1726
  clients = await mod.getInstalledClients();
902
1727
  } catch {
903
- p.log.warn("Could not detect AI clients \u2014 creating empty lockfile.");
1728
+ p2.log.warn("Could not detect AI clients \u2014 creating empty lockfile.");
904
1729
  }
905
1730
  const clientServers = [];
906
1731
  for (const client of clients) {
@@ -914,26 +1739,26 @@ var init_default = (0, import_citty2.defineCommand)({
914
1739
  }
915
1740
  createEmptyLockfile(targetPath);
916
1741
  if (clientServers.length === 0) {
917
- p.log.info("No existing servers found in any client config.");
918
- p.outro(`Created ${LOCKFILE_NAME} \u2014 add it to version control!`);
1742
+ p2.log.info("No existing servers found in any client config.");
1743
+ p2.outro(`Created ${LOCKFILE_NAME} \u2014 add it to version control!`);
919
1744
  return;
920
1745
  }
921
1746
  let selected;
922
1747
  if (nonInteractive) {
923
1748
  selected = clientServers.map((cs) => cs.client.type);
924
- p.log.info(`Non-interactive mode: importing all ${clientServers.length} client(s).`);
1749
+ p2.log.info(`Non-interactive mode: importing all ${clientServers.length} client(s).`);
925
1750
  } else {
926
1751
  const options = clientServers.map((cs) => ({
927
1752
  value: cs.client.type,
928
1753
  label: `${cs.client.displayName} (${Object.keys(cs.servers).length} servers)`
929
1754
  }));
930
- const toImport = await p.multiselect({
1755
+ const toImport = await p2.multiselect({
931
1756
  message: "Import existing servers into lockfile?",
932
1757
  options,
933
1758
  required: false
934
1759
  });
935
- if (p.isCancel(toImport)) {
936
- p.outro(`Created empty ${LOCKFILE_NAME}`);
1760
+ if (p2.isCancel(toImport)) {
1761
+ p2.outro(`Created empty ${LOCKFILE_NAME}`);
937
1762
  return;
938
1763
  }
939
1764
  selected = toImport;
@@ -959,7 +1784,7 @@ var init_default = (0, import_citty2.defineCommand)({
959
1784
  importCount++;
960
1785
  }
961
1786
  }
962
- p.outro(
1787
+ p2.outro(
963
1788
  `Created ${LOCKFILE_NAME} with ${importCount} server(s) \u2014 commit to version control!`
964
1789
  );
965
1790
  }
@@ -967,49 +1792,48 @@ var init_default = (0, import_citty2.defineCommand)({
967
1792
 
968
1793
  // src/commands/install.ts
969
1794
  init_cjs_shims();
970
- var import_citty3 = require("citty");
1795
+ var import_citty4 = require("citty");
971
1796
 
972
1797
  // src/core/installer.ts
973
1798
  init_cjs_shims();
974
- var p2 = __toESM(require("@clack/prompts"), 1);
1799
+ var p5 = __toESM(require("@clack/prompts"), 1);
975
1800
 
976
- // src/core/server-resolver.ts
1801
+ // src/core/installer-vault-helpers.ts
977
1802
  init_cjs_shims();
978
- function detectSource(input) {
979
- if (input.startsWith("smithery:")) {
980
- return { type: "smithery", input: input.slice(9) };
981
- }
982
- if (input.startsWith("https://github.com/") || input.startsWith("github.com/")) {
983
- return { type: "github", input };
984
- }
985
- return { type: "npm", input };
986
- }
987
- function parseEnvFlags(envFlags) {
988
- if (!envFlags) return {};
989
- const flags = Array.isArray(envFlags) ? envFlags : [envFlags];
990
- const result = {};
991
- for (const flag of flags) {
992
- const idx = flag.indexOf("=");
993
- if (idx > 0) {
994
- result[flag.slice(0, idx)] = flag.slice(idx + 1);
995
- }
996
- }
997
- return result;
998
- }
999
- async function resolveServer(input) {
1000
- const source = detectSource(input);
1001
- switch (source.type) {
1002
- case "smithery":
1003
- return resolveFromSmithery(source.input);
1004
- case "github":
1005
- return resolveFromGitHub(source.input);
1006
- case "npm":
1007
- return resolveFromNpm(source.input);
1803
+ var p4 = __toESM(require("@clack/prompts"), 1);
1804
+ init_vault_service();
1805
+ async function tryLoadVaultSecrets(serverName) {
1806
+ try {
1807
+ const entries = listSecrets(serverName);
1808
+ if (entries.length === 0 || entries[0].keys.length === 0) {
1809
+ return {};
1810
+ }
1811
+ const password2 = await getMasterPassword();
1812
+ return getSecretsForServer(serverName, password2);
1813
+ } catch {
1814
+ return {};
1815
+ }
1816
+ }
1817
+ async function offerVaultSave(serverName, newVars, yes) {
1818
+ if (Object.keys(newVars).length === 0) return;
1819
+ if (yes) return;
1820
+ try {
1821
+ const save = await p4.confirm({
1822
+ message: `Save ${Object.keys(newVars).length} env var(s) to encrypted vault for future installs?`
1823
+ });
1824
+ if (p4.isCancel(save) || !save) return;
1825
+ const password2 = await getMasterPassword();
1826
+ for (const [key, value] of Object.entries(newVars)) {
1827
+ setSecret(serverName, key, value, password2);
1828
+ }
1829
+ p4.log.success(`Credentials saved to vault for '${serverName}'`);
1830
+ } catch (err) {
1831
+ p4.log.warn(`Could not save to vault: ${err instanceof Error ? err.message : String(err)}`);
1008
1832
  }
1009
1833
  }
1010
1834
 
1011
1835
  // src/core/installer.ts
1012
- async function loadClients() {
1836
+ async function loadClients2() {
1013
1837
  try {
1014
1838
  const mod = await Promise.resolve().then(() => (init_client_detector(), client_detector_exports));
1015
1839
  return mod.getInstalledClients();
@@ -1018,83 +1842,86 @@ async function loadClients() {
1018
1842
  }
1019
1843
  }
1020
1844
  async function installServer(input, options = {}) {
1021
- p2.intro("mcpman install");
1022
- const spinner2 = p2.spinner();
1023
- spinner2.start("Resolving server...");
1845
+ p5.intro("mcpman install");
1846
+ const spinner5 = p5.spinner();
1847
+ spinner5.start("Resolving server...");
1024
1848
  let metadata;
1025
1849
  try {
1026
1850
  metadata = await resolveServer(input);
1027
1851
  } catch (err) {
1028
- spinner2.stop("Resolution failed");
1029
- p2.log.error(err instanceof Error ? err.message : String(err));
1852
+ spinner5.stop("Resolution failed");
1853
+ p5.log.error(err instanceof Error ? err.message : String(err));
1030
1854
  process.exit(1);
1031
1855
  }
1032
- spinner2.stop(`Found: ${metadata.name}@${metadata.version}`);
1033
- const clients = await loadClients();
1856
+ spinner5.stop(`Found: ${metadata.name}@${metadata.version}`);
1857
+ const clients = await loadClients2();
1034
1858
  if (clients.length === 0) {
1035
- p2.log.warn("No supported AI clients detected on this machine.");
1036
- p2.log.info("Supported: Claude Desktop, Cursor, VS Code, Windsurf");
1859
+ p5.log.warn("No supported AI clients detected on this machine.");
1860
+ p5.log.info("Supported: Claude Desktop, Cursor, VS Code, Windsurf");
1037
1861
  process.exit(1);
1038
1862
  }
1039
1863
  let selectedClients;
1040
1864
  if (options.client) {
1041
1865
  const found = clients.find((c) => c.type === options.client || c.displayName.toLowerCase() === options.client?.toLowerCase());
1042
1866
  if (!found) {
1043
- p2.log.error(`Client '${options.client}' not found or not installed.`);
1044
- p2.log.info(`Available: ${clients.map((c) => c.type).join(", ")}`);
1867
+ p5.log.error(`Client '${options.client}' not found or not installed.`);
1868
+ p5.log.info(`Available: ${clients.map((c) => c.type).join(", ")}`);
1045
1869
  process.exit(1);
1046
1870
  }
1047
1871
  selectedClients = [found];
1048
1872
  } else if (options.yes || clients.length === 1) {
1049
1873
  selectedClients = clients;
1050
1874
  } else {
1051
- const chosen = await p2.multiselect({
1875
+ const chosen = await p5.multiselect({
1052
1876
  message: "Install to which client(s)?",
1053
1877
  options: clients.map((c) => ({ value: c.type, label: c.displayName })),
1054
1878
  required: true
1055
1879
  });
1056
- if (p2.isCancel(chosen)) {
1057
- p2.outro("Cancelled.");
1880
+ if (p5.isCancel(chosen)) {
1881
+ p5.outro("Cancelled.");
1058
1882
  process.exit(0);
1059
1883
  }
1060
1884
  selectedClients = clients.filter((c) => chosen.includes(c.type));
1061
1885
  }
1062
1886
  const providedEnv = parseEnvFlags(options.env);
1063
- const collectedEnv = { ...providedEnv };
1887
+ const vaultEnv = await tryLoadVaultSecrets(metadata.name);
1888
+ const collectedEnv = { ...vaultEnv, ...providedEnv };
1889
+ const newlyEnteredVars = {};
1064
1890
  const requiredVars = metadata.envVars.filter((e) => e.required && !(e.name in collectedEnv));
1065
1891
  for (const envVar of requiredVars) {
1066
1892
  if (options.yes && envVar.default) {
1067
1893
  collectedEnv[envVar.name] = envVar.default;
1068
1894
  continue;
1069
1895
  }
1070
- const val = await p2.text({
1896
+ const val = await p5.text({
1071
1897
  message: `${envVar.name}${envVar.description ? ` \u2014 ${envVar.description}` : ""}`,
1072
1898
  placeholder: envVar.default ?? "",
1073
1899
  validate: (v) => envVar.required && !v ? "Required" : void 0
1074
1900
  });
1075
- if (p2.isCancel(val)) {
1076
- p2.outro("Cancelled.");
1901
+ if (p5.isCancel(val)) {
1902
+ p5.outro("Cancelled.");
1077
1903
  process.exit(0);
1078
1904
  }
1079
1905
  collectedEnv[envVar.name] = val;
1906
+ newlyEnteredVars[envVar.name] = val;
1080
1907
  }
1081
1908
  const entry = {
1082
1909
  command: metadata.command,
1083
1910
  args: metadata.args,
1084
1911
  ...Object.keys(collectedEnv).length > 0 ? { env: collectedEnv } : {}
1085
1912
  };
1086
- spinner2.start("Writing config...");
1913
+ spinner5.start("Writing config...");
1087
1914
  const clientTypes = [];
1088
1915
  for (const client of selectedClients) {
1089
1916
  try {
1090
1917
  await client.addServer(metadata.name, entry);
1091
1918
  clientTypes.push(client.type);
1092
1919
  } catch (err) {
1093
- spinner2.stop("Partial failure");
1094
- p2.log.warn(`Failed to write to ${client.displayName}: ${err instanceof Error ? err.message : String(err)}`);
1920
+ spinner5.stop("Partial failure");
1921
+ p5.log.warn(`Failed to write to ${client.displayName}: ${err instanceof Error ? err.message : String(err)}`);
1095
1922
  }
1096
1923
  }
1097
- spinner2.stop("Config written");
1924
+ spinner5.stop("Config written");
1098
1925
  const source = detectSource(input);
1099
1926
  const integrity = computeIntegrity(metadata.resolved);
1100
1927
  addEntry(metadata.name, {
@@ -1110,13 +1937,14 @@ async function installServer(input, options = {}) {
1110
1937
  clients: clientTypes
1111
1938
  });
1112
1939
  const lockPath = findLockfile() ?? "mcpman.lock (global)";
1113
- p2.log.success(`Lockfile updated: ${lockPath}`);
1114
- p2.outro(`${metadata.name}@${metadata.version} installed to ${clientTypes.join(", ")}`);
1940
+ p5.log.success(`Lockfile updated: ${lockPath}`);
1941
+ await offerVaultSave(metadata.name, newlyEnteredVars, options.yes ?? false);
1942
+ p5.outro(`${metadata.name}@${metadata.version} installed to ${clientTypes.join(", ")}`);
1115
1943
  }
1116
1944
 
1117
1945
  // src/utils/logger.ts
1118
1946
  init_cjs_shims();
1119
- var import_picocolors2 = __toESM(require("picocolors"), 1);
1947
+ var import_picocolors3 = __toESM(require("picocolors"), 1);
1120
1948
  var noColor = process.env.NO_COLOR !== void 0 || process.argv.includes("--no-color");
1121
1949
  var isVerbose = process.argv.includes("--verbose");
1122
1950
  var isJson = process.argv.includes("--json");
@@ -1125,19 +1953,19 @@ function colorize(fn, text2) {
1125
1953
  }
1126
1954
  function info(message) {
1127
1955
  if (isJson) return;
1128
- console.log(`${colorize(import_picocolors2.default.cyan, "i")} ${message}`);
1956
+ console.log(`${colorize(import_picocolors3.default.cyan, "i")} ${message}`);
1129
1957
  }
1130
1958
  function error(message) {
1131
1959
  if (isJson) return;
1132
- console.error(`${colorize(import_picocolors2.default.red, "\u2717")} ${message}`);
1960
+ console.error(`${colorize(import_picocolors3.default.red, "\u2717")} ${message}`);
1133
1961
  }
1134
1962
  function json(data) {
1135
1963
  console.log(JSON.stringify(data, null, 2));
1136
1964
  }
1137
1965
 
1138
1966
  // src/commands/install.ts
1139
- var p3 = __toESM(require("@clack/prompts"), 1);
1140
- var install_default = (0, import_citty3.defineCommand)({
1967
+ var p6 = __toESM(require("@clack/prompts"), 1);
1968
+ var install_default = (0, import_citty4.defineCommand)({
1141
1969
  meta: {
1142
1970
  name: "install",
1143
1971
  description: "Install an MCP server into one or more AI clients"
@@ -1186,8 +2014,8 @@ async function restoreFromLockfile() {
1186
2014
  info("Lockfile is empty \u2014 nothing to restore.");
1187
2015
  return;
1188
2016
  }
1189
- p3.intro(`mcpman install (restore from ${lockPath})`);
1190
- p3.log.info(`Restoring ${entries.length} server(s)...`);
2017
+ p6.intro(`mcpman install (restore from ${lockPath})`);
2018
+ p6.log.info(`Restoring ${entries.length} server(s)...`);
1191
2019
  for (const [name, entry] of entries) {
1192
2020
  const input = entry.source === "smithery" ? `smithery:${name}` : entry.source === "github" ? entry.resolved : name;
1193
2021
  await installServer(input, {
@@ -1195,19 +2023,19 @@ async function restoreFromLockfile() {
1195
2023
  yes: true
1196
2024
  });
1197
2025
  }
1198
- p3.outro("Restore complete.");
2026
+ p6.outro("Restore complete.");
1199
2027
  }
1200
2028
 
1201
2029
  // src/commands/list.ts
1202
2030
  init_cjs_shims();
1203
- var import_citty4 = require("citty");
1204
- var import_picocolors3 = __toESM(require("picocolors"), 1);
2031
+ var import_citty5 = require("citty");
2032
+ var import_picocolors4 = __toESM(require("picocolors"), 1);
1205
2033
  var STATUS_ICON = {
1206
- healthy: import_picocolors3.default.green("\u25CF"),
1207
- unhealthy: import_picocolors3.default.red("\u25CF"),
1208
- unknown: import_picocolors3.default.dim("\u25CB")
2034
+ healthy: import_picocolors4.default.green("\u25CF"),
2035
+ unhealthy: import_picocolors4.default.red("\u25CF"),
2036
+ unknown: import_picocolors4.default.dim("\u25CB")
1209
2037
  };
1210
- var list_default = (0, import_citty4.defineCommand)({
2038
+ var list_default = (0, import_citty5.defineCommand)({
1211
2039
  meta: {
1212
2040
  name: "list",
1213
2041
  description: "List installed MCP servers"
@@ -1227,7 +2055,7 @@ var list_default = (0, import_citty4.defineCommand)({
1227
2055
  const servers = await getInstalledServers(args.client);
1228
2056
  if (servers.length === 0) {
1229
2057
  const filter = args.client ? ` for client "${args.client}"` : "";
1230
- console.log(import_picocolors3.default.dim(`No MCP servers installed${filter}. Run ${import_picocolors3.default.cyan("mcpman install <server>")} to get started.`));
2058
+ console.log(import_picocolors4.default.dim(`No MCP servers installed${filter}. Run ${import_picocolors4.default.cyan("mcpman install <server>")} to get started.`));
1231
2059
  return;
1232
2060
  }
1233
2061
  const withStatus = await Promise.all(
@@ -1251,8 +2079,8 @@ var list_default = (0, import_citty4.defineCommand)({
1251
2079
  const nameWidth = Math.max(4, ...withStatus.map((s) => s.name.length));
1252
2080
  const clientsWidth = Math.max(7, ...withStatus.map((s) => formatClients(s.clients).length));
1253
2081
  const header = ` ${pad("NAME", nameWidth)} ${pad("CLIENT(S)", clientsWidth)} ${pad("COMMAND", 20)} STATUS`;
1254
- console.log(import_picocolors3.default.dim(header));
1255
- console.log(import_picocolors3.default.dim(` ${"-".repeat(nameWidth)} ${"-".repeat(clientsWidth)} ${"-".repeat(20)} ------`));
2082
+ console.log(import_picocolors4.default.dim(header));
2083
+ console.log(import_picocolors4.default.dim(` ${"-".repeat(nameWidth)} ${"-".repeat(clientsWidth)} ${"-".repeat(20)} ------`));
1256
2084
  for (const s of withStatus) {
1257
2085
  const icon = STATUS_ICON[s.status];
1258
2086
  const clientsStr = formatClients(s.clients);
@@ -1260,7 +2088,7 @@ var list_default = (0, import_citty4.defineCommand)({
1260
2088
  console.log(` ${pad(s.name, nameWidth)} ${pad(clientsStr, clientsWidth)} ${pad(cmdStr, 20)} ${icon} ${s.status}`);
1261
2089
  }
1262
2090
  const clientSet = new Set(withStatus.flatMap((s) => s.clients));
1263
- console.log(import_picocolors3.default.dim(`
2091
+ console.log(import_picocolors4.default.dim(`
1264
2092
  ${withStatus.length} server${withStatus.length !== 1 ? "s" : ""} \xB7 ${clientSet.size} client${clientSet.size !== 1 ? "s" : ""}`));
1265
2093
  }
1266
2094
  });
@@ -1282,9 +2110,9 @@ function formatClients(clients) {
1282
2110
 
1283
2111
  // src/commands/remove.ts
1284
2112
  init_cjs_shims();
1285
- var import_citty5 = require("citty");
1286
- var p4 = __toESM(require("@clack/prompts"), 1);
1287
- var import_picocolors4 = __toESM(require("picocolors"), 1);
2113
+ var import_citty6 = require("citty");
2114
+ var p7 = __toESM(require("@clack/prompts"), 1);
2115
+ var import_picocolors5 = __toESM(require("picocolors"), 1);
1288
2116
  init_client_detector();
1289
2117
  var CLIENT_DISPLAY2 = {
1290
2118
  "claude-desktop": "Claude",
@@ -1295,7 +2123,7 @@ var CLIENT_DISPLAY2 = {
1295
2123
  function clientDisplayName(type) {
1296
2124
  return CLIENT_DISPLAY2[type] ?? type;
1297
2125
  }
1298
- var remove_default = (0, import_citty5.defineCommand)({
2126
+ var remove_default = (0, import_citty6.defineCommand)({
1299
2127
  meta: {
1300
2128
  name: "remove",
1301
2129
  description: "Remove an MCP server from one or more AI clients"
@@ -1322,17 +2150,17 @@ var remove_default = (0, import_citty5.defineCommand)({
1322
2150
  }
1323
2151
  },
1324
2152
  async run({ args }) {
1325
- p4.intro(import_picocolors4.default.bold("mcpman remove"));
2153
+ p7.intro(import_picocolors5.default.bold("mcpman remove"));
1326
2154
  const serverName = args.server;
1327
2155
  const servers = await getInstalledServers();
1328
2156
  const match = servers.find((s) => s.name === serverName);
1329
2157
  if (!match) {
1330
- p4.log.warn(`Server "${serverName}" is not installed.`);
2158
+ p7.log.warn(`Server "${serverName}" is not installed.`);
1331
2159
  const similar = servers.filter((s) => s.name.includes(serverName) || serverName.includes(s.name));
1332
2160
  if (similar.length > 0) {
1333
- p4.log.info(`Did you mean: ${similar.map((s) => import_picocolors4.default.cyan(s.name)).join(", ")}?`);
2161
+ p7.log.info(`Did you mean: ${similar.map((s) => import_picocolors5.default.cyan(s.name)).join(", ")}?`);
1334
2162
  }
1335
- p4.outro("Nothing to remove.");
2163
+ p7.outro("Nothing to remove.");
1336
2164
  return;
1337
2165
  }
1338
2166
  let targetClients;
@@ -1340,15 +2168,15 @@ var remove_default = (0, import_citty5.defineCommand)({
1340
2168
  targetClients = match.clients;
1341
2169
  } else if (args.client) {
1342
2170
  if (!match.clients.includes(args.client)) {
1343
- p4.log.warn(`Server "${serverName}" is not installed in client "${args.client}".`);
1344
- p4.outro("Nothing to remove.");
2171
+ p7.log.warn(`Server "${serverName}" is not installed in client "${args.client}".`);
2172
+ p7.outro("Nothing to remove.");
1345
2173
  return;
1346
2174
  }
1347
2175
  targetClients = [args.client];
1348
2176
  } else if (match.clients.length === 1) {
1349
2177
  targetClients = match.clients;
1350
2178
  } else {
1351
- const selected = await p4.multiselect({
2179
+ const selected = await p7.multiselect({
1352
2180
  message: `Remove "${serverName}" from which clients?`,
1353
2181
  options: match.clients.map((c) => ({
1354
2182
  value: c,
@@ -1356,19 +2184,19 @@ var remove_default = (0, import_citty5.defineCommand)({
1356
2184
  })),
1357
2185
  required: true
1358
2186
  });
1359
- if (p4.isCancel(selected)) {
1360
- p4.outro("Cancelled.");
2187
+ if (p7.isCancel(selected)) {
2188
+ p7.outro("Cancelled.");
1361
2189
  process.exit(0);
1362
2190
  }
1363
2191
  targetClients = selected;
1364
2192
  }
1365
2193
  if (!args.yes) {
1366
2194
  const clientNames = targetClients.map(clientDisplayName).join(", ");
1367
- const confirmed = await p4.confirm({
1368
- message: `Remove ${import_picocolors4.default.cyan(serverName)} from ${import_picocolors4.default.yellow(clientNames)}?`
2195
+ const confirmed = await p7.confirm({
2196
+ message: `Remove ${import_picocolors5.default.cyan(serverName)} from ${import_picocolors5.default.yellow(clientNames)}?`
1369
2197
  });
1370
- if (p4.isCancel(confirmed) || !confirmed) {
1371
- p4.outro("Cancelled.");
2198
+ if (p7.isCancel(confirmed) || !confirmed) {
2199
+ p7.outro("Cancelled.");
1372
2200
  return;
1373
2201
  }
1374
2202
  }
@@ -1382,25 +2210,606 @@ var remove_default = (0, import_citty5.defineCommand)({
1382
2210
  }
1383
2211
  try {
1384
2212
  await handler.removeServer(serverName);
1385
- p4.log.success(`Removed from ${clientDisplayName(clientType)}`);
2213
+ p7.log.success(`Removed from ${clientDisplayName(clientType)}`);
1386
2214
  } catch (err) {
1387
2215
  const msg = err instanceof Error ? err.message : String(err);
1388
2216
  errors.push(`${clientDisplayName(clientType)}: ${msg}`);
1389
2217
  }
1390
2218
  }
1391
2219
  if (errors.length > 0) {
1392
- for (const e of errors) p4.log.error(e);
1393
- p4.outro(import_picocolors4.default.red("Completed with errors."));
2220
+ for (const e of errors) p7.log.error(e);
2221
+ p7.outro(import_picocolors5.default.red("Completed with errors."));
2222
+ process.exit(1);
2223
+ }
2224
+ p7.outro(import_picocolors5.default.green(`Removed "${serverName}" successfully.`));
2225
+ }
2226
+ });
2227
+
2228
+ // src/commands/secrets.ts
2229
+ init_cjs_shims();
2230
+ var import_citty7 = require("citty");
2231
+ var import_picocolors6 = __toESM(require("picocolors"), 1);
2232
+ var p8 = __toESM(require("@clack/prompts"), 1);
2233
+ init_vault_service();
2234
+ function maskValue(value) {
2235
+ if (value.length <= 8) return "***";
2236
+ return `${value.slice(0, 4)}***${value.slice(-3)}`;
2237
+ }
2238
+ function parseKeyValue(input) {
2239
+ const idx = input.indexOf("=");
2240
+ if (idx <= 0) return null;
2241
+ return { key: input.slice(0, idx), value: input.slice(idx + 1) };
2242
+ }
2243
+ var setCommand = (0, import_citty7.defineCommand)({
2244
+ meta: { name: "set", description: "Store an encrypted secret for a server" },
2245
+ args: {
2246
+ server: {
2247
+ type: "positional",
2248
+ description: "Server name (e.g. @modelcontextprotocol/server-github)",
2249
+ required: true
2250
+ },
2251
+ keyvalue: {
2252
+ type: "positional",
2253
+ description: "KEY=VALUE pair to store",
2254
+ required: true
2255
+ }
2256
+ },
2257
+ async run({ args }) {
2258
+ const parsed = parseKeyValue(args.keyvalue);
2259
+ if (!parsed) {
2260
+ console.error(import_picocolors6.default.red("\u2717") + " Invalid format. Expected KEY=VALUE");
2261
+ process.exit(1);
2262
+ }
2263
+ p8.intro(import_picocolors6.default.cyan("mcpman secrets set"));
2264
+ const isNew = listSecrets(args.server).length === 0 || !listSecrets(args.server)[0]?.keys.includes(parsed.key);
2265
+ const vaultPath = (await Promise.resolve().then(() => (init_vault_service(), vault_service_exports))).getVaultPath();
2266
+ const vaultExists = (await import("fs")).existsSync(vaultPath);
2267
+ const password2 = await getMasterPassword(!vaultExists && isNew);
2268
+ const spin = p8.spinner();
2269
+ spin.start("Encrypting secret...");
2270
+ try {
2271
+ setSecret(args.server, parsed.key, parsed.value, password2);
2272
+ spin.stop(
2273
+ `${import_picocolors6.default.green("\u2713")} Stored ${import_picocolors6.default.bold(parsed.key)} for ${import_picocolors6.default.cyan(args.server)}`
2274
+ );
2275
+ } catch (err) {
2276
+ spin.stop(import_picocolors6.default.red("\u2717") + " Failed to store secret");
2277
+ console.error(import_picocolors6.default.dim(String(err)));
2278
+ process.exit(1);
2279
+ }
2280
+ p8.outro(import_picocolors6.default.dim("Secret encrypted and saved to vault."));
2281
+ }
2282
+ });
2283
+ var listCommand = (0, import_citty7.defineCommand)({
2284
+ meta: { name: "list", description: "List secret keys stored in the vault" },
2285
+ args: {
2286
+ server: {
2287
+ type: "positional",
2288
+ description: "Filter by server name (optional)",
2289
+ required: false
2290
+ }
2291
+ },
2292
+ async run({ args }) {
2293
+ const results = listSecrets(args.server || void 0);
2294
+ if (results.length === 0) {
2295
+ const filter = args.server ? ` for ${import_picocolors6.default.cyan(args.server)}` : "";
2296
+ console.log(import_picocolors6.default.dim(`No secrets stored${filter}.`));
2297
+ return;
2298
+ }
2299
+ console.log("");
2300
+ for (const { server, keys } of results) {
2301
+ console.log(import_picocolors6.default.bold(import_picocolors6.default.cyan(server)));
2302
+ for (const key of keys) {
2303
+ console.log(` ${import_picocolors6.default.green("\u25CF")} ${import_picocolors6.default.bold(key)} ${import_picocolors6.default.dim(maskValue("\u2022\u2022\u2022\u2022\u2022\u2022\u2022\u2022\u2022\u2022\u2022\u2022"))}`);
2304
+ }
2305
+ console.log("");
2306
+ }
2307
+ const total = results.reduce((n, r) => n + r.keys.length, 0);
2308
+ console.log(import_picocolors6.default.dim(` ${total} secret${total !== 1 ? "s" : ""} in ${results.length} server${results.length !== 1 ? "s" : ""}`));
2309
+ }
2310
+ });
2311
+ var removeCommand = (0, import_citty7.defineCommand)({
2312
+ meta: { name: "remove", description: "Delete a secret from the vault" },
2313
+ args: {
2314
+ server: {
2315
+ type: "positional",
2316
+ description: "Server name",
2317
+ required: true
2318
+ },
2319
+ key: {
2320
+ type: "positional",
2321
+ description: "Secret key to remove",
2322
+ required: true
2323
+ }
2324
+ },
2325
+ async run({ args }) {
2326
+ const confirmed = await p8.confirm({
2327
+ message: `Remove ${import_picocolors6.default.bold(args.key)} from ${import_picocolors6.default.cyan(args.server)}?`,
2328
+ initialValue: false
2329
+ });
2330
+ if (p8.isCancel(confirmed) || !confirmed) {
2331
+ p8.cancel("Cancelled.");
2332
+ return;
2333
+ }
2334
+ try {
2335
+ removeSecret(args.server, args.key);
2336
+ console.log(`${import_picocolors6.default.green("\u2713")} Removed ${import_picocolors6.default.bold(args.key)} from ${import_picocolors6.default.cyan(args.server)}`);
2337
+ } catch (err) {
2338
+ console.error(import_picocolors6.default.red("\u2717") + " Failed to remove secret");
2339
+ console.error(import_picocolors6.default.dim(String(err)));
2340
+ process.exit(1);
2341
+ }
2342
+ }
2343
+ });
2344
+ var secrets_default = (0, import_citty7.defineCommand)({
2345
+ meta: {
2346
+ name: "secrets",
2347
+ description: "Manage encrypted secrets for MCP servers"
2348
+ },
2349
+ subCommands: {
2350
+ set: setCommand,
2351
+ list: listCommand,
2352
+ remove: removeCommand
2353
+ }
2354
+ });
2355
+
2356
+ // src/commands/sync.ts
2357
+ init_cjs_shims();
2358
+ var import_citty8 = require("citty");
2359
+ var p9 = __toESM(require("@clack/prompts"), 1);
2360
+ var import_picocolors7 = __toESM(require("picocolors"), 1);
2361
+
2362
+ // src/core/config-diff.ts
2363
+ init_cjs_shims();
2364
+ function reconstructServerEntry(lockEntry) {
2365
+ const entry = {
2366
+ command: lockEntry.command
2367
+ };
2368
+ if (lockEntry.args && lockEntry.args.length > 0) {
2369
+ entry.args = lockEntry.args;
2370
+ }
2371
+ if (lockEntry.envVars && lockEntry.envVars.length > 0) {
2372
+ entry.env = Object.fromEntries(lockEntry.envVars.map((k) => [k, ""]));
2373
+ }
2374
+ return entry;
2375
+ }
2376
+ function computeDiff(lockfile, clientConfigs, options = {}) {
2377
+ const actions = [];
2378
+ for (const [server, lockEntry] of Object.entries(lockfile.servers)) {
2379
+ for (const client of lockEntry.clients) {
2380
+ const config = clientConfigs.get(client);
2381
+ if (!config) continue;
2382
+ if (server in config.servers) {
2383
+ actions.push({ server, client, action: "ok" });
2384
+ } else {
2385
+ actions.push({
2386
+ server,
2387
+ client,
2388
+ action: "add",
2389
+ entry: reconstructServerEntry(lockEntry)
2390
+ });
2391
+ }
2392
+ }
2393
+ }
2394
+ const extraAction = options.remove ? "remove" : "extra";
2395
+ for (const [client, config] of clientConfigs) {
2396
+ for (const server of Object.keys(config.servers)) {
2397
+ if (!(server in lockfile.servers)) {
2398
+ actions.push({ server, client, action: extraAction });
2399
+ }
2400
+ }
2401
+ }
2402
+ return actions;
2403
+ }
2404
+ function computeDiffFromClient(sourceClient, clientConfigs, options = {}) {
2405
+ const actions = [];
2406
+ const sourceConfig = clientConfigs.get(sourceClient);
2407
+ if (!sourceConfig) return [];
2408
+ const extraAction = options.remove ? "remove" : "extra";
2409
+ for (const [client, config] of clientConfigs) {
2410
+ if (client === sourceClient) continue;
2411
+ for (const [server, entry] of Object.entries(sourceConfig.servers)) {
2412
+ if (server in config.servers) {
2413
+ actions.push({ server, client, action: "ok" });
2414
+ } else {
2415
+ actions.push({ server, client, action: "add", entry });
2416
+ }
2417
+ }
2418
+ for (const server of Object.keys(config.servers)) {
2419
+ if (!(server in sourceConfig.servers)) {
2420
+ actions.push({ server, client, action: extraAction });
2421
+ }
2422
+ }
2423
+ }
2424
+ return actions;
2425
+ }
2426
+
2427
+ // src/core/sync-engine.ts
2428
+ init_cjs_shims();
2429
+ init_client_detector();
2430
+ async function applySyncActions(actions, clients) {
2431
+ const result = { applied: 0, removed: 0, failed: 0, errors: [] };
2432
+ const addActions = actions.filter((a) => a.action === "add" && a.entry);
2433
+ for (const action of addActions) {
2434
+ const handler = clients.get(action.client);
2435
+ if (!handler || !action.entry) {
2436
+ result.failed++;
2437
+ result.errors.push({
2438
+ server: action.server,
2439
+ client: action.client,
2440
+ error: "No handler available for client"
2441
+ });
2442
+ continue;
2443
+ }
2444
+ try {
2445
+ await handler.addServer(action.server, action.entry);
2446
+ result.applied++;
2447
+ } catch (err) {
2448
+ result.failed++;
2449
+ result.errors.push({
2450
+ server: action.server,
2451
+ client: action.client,
2452
+ error: String(err)
2453
+ });
2454
+ }
2455
+ }
2456
+ const removeActions = actions.filter((a) => a.action === "remove");
2457
+ for (const action of removeActions) {
2458
+ const handler = clients.get(action.client);
2459
+ if (!handler) {
2460
+ result.failed++;
2461
+ result.errors.push({
2462
+ server: action.server,
2463
+ client: action.client,
2464
+ error: "No handler available for client"
2465
+ });
2466
+ continue;
2467
+ }
2468
+ try {
2469
+ await handler.removeServer(action.server);
2470
+ result.removed++;
2471
+ } catch (err) {
2472
+ result.failed++;
2473
+ result.errors.push({
2474
+ server: action.server,
2475
+ client: action.client,
2476
+ error: String(err)
2477
+ });
2478
+ }
2479
+ }
2480
+ return result;
2481
+ }
2482
+ async function getClientConfigs() {
2483
+ const configs = /* @__PURE__ */ new Map();
2484
+ const handlers = /* @__PURE__ */ new Map();
2485
+ let installedClients;
2486
+ try {
2487
+ installedClients = await getInstalledClients();
2488
+ } catch {
2489
+ return { configs, handlers };
2490
+ }
2491
+ await Promise.all(
2492
+ installedClients.map(async (handler) => {
2493
+ try {
2494
+ const config = await handler.readConfig();
2495
+ configs.set(handler.type, config);
2496
+ handlers.set(handler.type, handler);
2497
+ } catch (err) {
2498
+ console.warn(`[mcpman] Warning: could not read config for ${handler.displayName}: ${String(err)}`);
2499
+ }
2500
+ })
2501
+ );
2502
+ return { configs, handlers };
2503
+ }
2504
+
2505
+ // src/commands/sync.ts
2506
+ var VALID_CLIENTS = ["claude-desktop", "cursor", "vscode", "windsurf"];
2507
+ var CLIENT_DISPLAY3 = {
2508
+ "claude-desktop": "Claude Desktop",
2509
+ cursor: "Cursor",
2510
+ vscode: "VS Code",
2511
+ windsurf: "Windsurf"
2512
+ };
2513
+ var sync_default = (0, import_citty8.defineCommand)({
2514
+ meta: {
2515
+ name: "sync",
2516
+ description: "Sync MCP server configs across all detected AI clients"
2517
+ },
2518
+ args: {
2519
+ "dry-run": {
2520
+ type: "boolean",
2521
+ description: "Preview changes without applying them",
2522
+ default: false
2523
+ },
2524
+ remove: {
2525
+ type: "boolean",
2526
+ description: "Remove extra servers not in lockfile",
2527
+ default: false
2528
+ },
2529
+ source: {
2530
+ type: "string",
2531
+ description: "Use a specific client as source of truth (claude-desktop, cursor, vscode, windsurf)"
2532
+ },
2533
+ yes: {
2534
+ type: "boolean",
2535
+ description: "Skip confirmation prompt",
2536
+ default: false
2537
+ }
2538
+ },
2539
+ async run({ args }) {
2540
+ p9.intro(`${import_picocolors7.default.cyan("mcpman sync")}`);
2541
+ const sourceClient = args.source;
2542
+ if (sourceClient && !VALID_CLIENTS.includes(sourceClient)) {
2543
+ p9.log.error(`Invalid --source "${sourceClient}". Must be one of: ${VALID_CLIENTS.join(", ")}`);
2544
+ process.exit(1);
2545
+ }
2546
+ const spinner5 = p9.spinner();
2547
+ spinner5.start("Detecting clients and reading configs...");
2548
+ const { configs, handlers } = await getClientConfigs();
2549
+ spinner5.stop(`Found ${configs.size} client(s)`);
2550
+ if (configs.size === 0) {
2551
+ p9.log.warn("No AI clients detected. Install Claude Desktop, Cursor, VS Code, or Windsurf first.");
2552
+ process.exit(0);
2553
+ }
2554
+ const diffOptions = { remove: args.remove };
2555
+ let actions;
2556
+ if (sourceClient) {
2557
+ if (!configs.has(sourceClient)) {
2558
+ p9.log.error(`Source client "${sourceClient}" is not detected or its config is unreadable.`);
2559
+ process.exit(1);
2560
+ }
2561
+ p9.log.info(`Using ${CLIENT_DISPLAY3[sourceClient]} as source of truth`);
2562
+ actions = computeDiffFromClient(sourceClient, configs, diffOptions);
2563
+ } else {
2564
+ const lockfile = readLockfile();
2565
+ actions = computeDiff(lockfile, configs, diffOptions);
2566
+ }
2567
+ printDiffTable(actions);
2568
+ const addCount = actions.filter((a) => a.action === "add").length;
2569
+ const extraCount = actions.filter((a) => a.action === "extra").length;
2570
+ const removeCount = actions.filter((a) => a.action === "remove").length;
2571
+ if (addCount === 0 && removeCount === 0 && extraCount === 0) {
2572
+ p9.outro(import_picocolors7.default.green("All clients are in sync."));
2573
+ process.exit(0);
2574
+ }
2575
+ const parts = [];
2576
+ if (addCount > 0) parts.push(import_picocolors7.default.green(`${addCount} to add`));
2577
+ if (removeCount > 0) parts.push(import_picocolors7.default.red(`${removeCount} to remove`));
2578
+ if (extraCount > 0) parts.push(import_picocolors7.default.yellow(`${extraCount} extra (informational)`));
2579
+ p9.log.info(parts.join(" \xB7 "));
2580
+ if (args["dry-run"]) {
2581
+ p9.outro(import_picocolors7.default.dim("Dry run \u2014 no changes applied."));
2582
+ process.exit(1);
2583
+ }
2584
+ if (addCount === 0 && removeCount === 0) {
2585
+ p9.outro(import_picocolors7.default.dim("No additions needed. Extra servers left untouched."));
2586
+ process.exit(1);
2587
+ }
2588
+ if (!args.yes) {
2589
+ const actionParts = [];
2590
+ if (addCount > 0) actionParts.push(`${addCount} addition(s)`);
2591
+ if (removeCount > 0) actionParts.push(`${removeCount} removal(s)`);
2592
+ const confirmed = await p9.confirm({
2593
+ message: `Apply ${actionParts.join(" and ")} to client configs?`,
2594
+ initialValue: true
2595
+ });
2596
+ if (p9.isCancel(confirmed) || !confirmed) {
2597
+ p9.outro(import_picocolors7.default.dim("Cancelled \u2014 no changes applied."));
2598
+ process.exit(0);
2599
+ }
2600
+ }
2601
+ spinner5.start("Applying sync changes...");
2602
+ const result = await applySyncActions(actions, handlers);
2603
+ spinner5.stop("Done");
2604
+ if (result.applied > 0) {
2605
+ p9.log.success(`Added ${result.applied} server(s) to client configs.`);
2606
+ }
2607
+ if (result.removed > 0) {
2608
+ p9.log.success(`Removed ${result.removed} server(s) from client configs.`);
2609
+ }
2610
+ if (result.failed > 0) {
2611
+ for (const e of result.errors) {
2612
+ p9.log.error(`Failed to sync "${e.server}" on ${e.client}: ${e.error}`);
2613
+ }
2614
+ }
2615
+ p9.outro(result.failed === 0 ? import_picocolors7.default.green("Sync complete.") : import_picocolors7.default.yellow("Sync complete with errors."));
2616
+ process.exit(result.failed > 0 ? 1 : 0);
2617
+ }
2618
+ });
2619
+ function printDiffTable(actions) {
2620
+ if (actions.length === 0) {
2621
+ p9.log.info("No actions to display.");
2622
+ return;
2623
+ }
2624
+ const nameWidth = Math.max(6, ...actions.map((a) => a.server.length));
2625
+ const clientWidth = Math.max(6, ...actions.map((a) => CLIENT_DISPLAY3[a.client]?.length ?? a.client.length));
2626
+ const header = ` ${pad2("SERVER", nameWidth)} ${pad2("CLIENT", clientWidth)} STATUS`;
2627
+ console.log(import_picocolors7.default.dim(header));
2628
+ console.log(import_picocolors7.default.dim(` ${"-".repeat(nameWidth)} ${"-".repeat(clientWidth)} ------`));
2629
+ for (const action of actions) {
2630
+ const clientDisplay = CLIENT_DISPLAY3[action.client] ?? action.client;
2631
+ const [icon, statusText] = formatAction(action.action);
2632
+ console.log(` ${pad2(action.server, nameWidth)} ${pad2(clientDisplay, clientWidth)} ${icon} ${statusText}`);
2633
+ }
2634
+ console.log("");
2635
+ }
2636
+ function formatAction(action) {
2637
+ switch (action) {
2638
+ case "add":
2639
+ return [import_picocolors7.default.green("+"), import_picocolors7.default.green("missing \u2014 will add")];
2640
+ case "extra":
2641
+ return [import_picocolors7.default.yellow("?"), import_picocolors7.default.yellow("extra (not in lockfile)")];
2642
+ case "remove":
2643
+ return [import_picocolors7.default.red("\u2013"), import_picocolors7.default.red("extra \u2014 will remove")];
2644
+ case "ok":
2645
+ return [import_picocolors7.default.dim("\xB7"), import_picocolors7.default.dim("in sync")];
2646
+ }
2647
+ }
2648
+ function pad2(s, width) {
2649
+ return s.length >= width ? s : s + " ".repeat(width - s.length);
2650
+ }
2651
+
2652
+ // src/commands/update.ts
2653
+ init_cjs_shims();
2654
+ var import_citty9 = require("citty");
2655
+ var p10 = __toESM(require("@clack/prompts"), 1);
2656
+ var import_picocolors9 = __toESM(require("picocolors"), 1);
2657
+
2658
+ // src/core/update-notifier.ts
2659
+ init_cjs_shims();
2660
+ var import_node_fs5 = __toESM(require("fs"), 1);
2661
+ var import_node_path7 = __toESM(require("path"), 1);
2662
+ var import_node_os5 = __toESM(require("os"), 1);
2663
+ var import_picocolors8 = __toESM(require("picocolors"), 1);
2664
+ var CACHE_FILE = import_node_path7.default.join(import_node_os5.default.homedir(), ".mcpman", ".update-check");
2665
+ var TTL_MS = 24 * 60 * 60 * 1e3;
2666
+ function writeUpdateCache(data) {
2667
+ try {
2668
+ const dir = import_node_path7.default.dirname(CACHE_FILE);
2669
+ if (!import_node_fs5.default.existsSync(dir)) import_node_fs5.default.mkdirSync(dir, { recursive: true });
2670
+ const tmp = `${CACHE_FILE}.tmp`;
2671
+ import_node_fs5.default.writeFileSync(tmp, JSON.stringify(data, null, 2), "utf-8");
2672
+ import_node_fs5.default.renameSync(tmp, CACHE_FILE);
2673
+ } catch {
2674
+ }
2675
+ }
2676
+
2677
+ // src/commands/update.ts
2678
+ async function loadClients3() {
2679
+ try {
2680
+ const mod = await Promise.resolve().then(() => (init_client_detector(), client_detector_exports));
2681
+ return mod.getInstalledClients();
2682
+ } catch {
2683
+ return [];
2684
+ }
2685
+ }
2686
+ function printTable(updates) {
2687
+ const NAME_W = 28;
2688
+ const VER_W = 10;
2689
+ const header = [
2690
+ "NAME".padEnd(NAME_W),
2691
+ "CURRENT".padEnd(VER_W),
2692
+ "LATEST".padEnd(VER_W),
2693
+ "STATUS"
2694
+ ].join(" ");
2695
+ console.log(import_picocolors9.default.bold(`
2696
+ ${header}`));
2697
+ console.log(import_picocolors9.default.dim(` ${"\u2500".repeat(NAME_W + VER_W * 2 + 20)}`));
2698
+ for (const u of updates) {
2699
+ const nameCol = u.server.slice(0, NAME_W).padEnd(NAME_W);
2700
+ const curCol = u.currentVersion.padEnd(VER_W);
2701
+ const latCol = u.latestVersion.padEnd(VER_W);
2702
+ const statusCol = u.hasUpdate ? import_picocolors9.default.yellow(`Update available${u.updateType ? ` [${u.updateType}]` : ""}`) : import_picocolors9.default.green("Up to date");
2703
+ console.log(` ${nameCol} ${curCol} ${latCol} ${statusCol}`);
2704
+ }
2705
+ console.log();
2706
+ }
2707
+ var update_default = (0, import_citty9.defineCommand)({
2708
+ meta: {
2709
+ name: "update",
2710
+ description: "Check for and apply updates to installed MCP servers"
2711
+ },
2712
+ args: {
2713
+ server: {
2714
+ type: "positional",
2715
+ description: "Server name to update (omit to update all)",
2716
+ required: false
2717
+ },
2718
+ check: {
2719
+ type: "boolean",
2720
+ description: "Check only \u2014 do not apply updates",
2721
+ default: false
2722
+ },
2723
+ yes: {
2724
+ type: "boolean",
2725
+ description: "Skip confirmation prompt",
2726
+ default: false
2727
+ },
2728
+ json: {
2729
+ type: "boolean",
2730
+ description: "Output results as JSON",
2731
+ default: false
2732
+ }
2733
+ },
2734
+ async run({ args }) {
2735
+ const lockfile = readLockfile();
2736
+ const servers = lockfile.servers;
2737
+ const targetEntries = args.server ? Object.entries(servers).filter(([name]) => name === args.server) : Object.entries(servers);
2738
+ if (targetEntries.length === 0) {
2739
+ if (args.server) {
2740
+ console.error(`Server '${args.server}' not found in lockfile.`);
2741
+ } else {
2742
+ console.log("No servers installed. Run mcpman install <server> first.");
2743
+ }
2744
+ process.exit(1);
2745
+ }
2746
+ const spinner5 = p10.spinner();
2747
+ spinner5.start("Checking versions...");
2748
+ let updates;
2749
+ try {
2750
+ const partialLock = {
2751
+ lockfileVersion: 1,
2752
+ servers: Object.fromEntries(targetEntries)
2753
+ };
2754
+ updates = await checkAllVersions(partialLock);
2755
+ } catch (err) {
2756
+ spinner5.stop("Version check failed");
2757
+ console.error(err instanceof Error ? err.message : String(err));
1394
2758
  process.exit(1);
1395
2759
  }
1396
- p4.outro(import_picocolors4.default.green(`Removed "${serverName}" successfully.`));
2760
+ spinner5.stop(`Checked ${updates.length} server(s)`);
2761
+ if (args.json) {
2762
+ console.log(JSON.stringify(updates, null, 2));
2763
+ return;
2764
+ }
2765
+ printTable(updates);
2766
+ const outdated = updates.filter((u) => u.hasUpdate);
2767
+ if (outdated.length === 0) {
2768
+ console.log(import_picocolors9.default.green(" All servers are up to date."));
2769
+ return;
2770
+ }
2771
+ if (args.check) {
2772
+ console.log(import_picocolors9.default.yellow(` ${outdated.length} update(s) available. Run mcpman update to apply.`));
2773
+ return;
2774
+ }
2775
+ if (!args.yes) {
2776
+ const confirmed = await p10.confirm({
2777
+ message: `Apply ${outdated.length} update(s)?`,
2778
+ initialValue: true
2779
+ });
2780
+ if (p10.isCancel(confirmed) || !confirmed) {
2781
+ p10.outro("Cancelled.");
2782
+ return;
2783
+ }
2784
+ }
2785
+ const clients = await loadClients3();
2786
+ let successCount = 0;
2787
+ for (const update of outdated) {
2788
+ const s = p10.spinner();
2789
+ s.start(`Updating ${update.server}...`);
2790
+ const result = await applyServerUpdate(
2791
+ update.server,
2792
+ servers[update.server],
2793
+ clients
2794
+ );
2795
+ if (result.success) {
2796
+ s.stop(`${import_picocolors9.default.green("\u2713")} ${update.server}: ${result.fromVersion} \u2192 ${result.toVersion}`);
2797
+ successCount++;
2798
+ } else {
2799
+ s.stop(`${import_picocolors9.default.red("\u2717")} ${update.server}: ${result.error}`);
2800
+ }
2801
+ }
2802
+ const freshLockfile = readLockfile(resolveLockfilePath());
2803
+ const freshUpdates = await checkAllVersions(freshLockfile);
2804
+ writeUpdateCache({ lastCheck: (/* @__PURE__ */ new Date()).toISOString(), updates: freshUpdates });
2805
+ p10.outro(`${successCount} of ${outdated.length} server(s) updated.`);
1397
2806
  }
1398
2807
  });
1399
2808
 
1400
2809
  // src/utils/constants.ts
1401
2810
  init_cjs_shims();
1402
2811
  var APP_NAME = "mcpman";
1403
- var APP_VERSION = "0.1.1";
2812
+ var APP_VERSION = "0.3.0";
1404
2813
  var APP_DESCRIPTION = "The package manager for MCP servers";
1405
2814
 
1406
2815
  // src/index.ts
@@ -1408,7 +2817,7 @@ process.on("SIGINT", () => {
1408
2817
  console.log("\nAborted.");
1409
2818
  process.exit(130);
1410
2819
  });
1411
- var main = (0, import_citty6.defineCommand)({
2820
+ var main = (0, import_citty10.defineCommand)({
1412
2821
  meta: {
1413
2822
  name: APP_NAME,
1414
2823
  version: APP_VERSION,
@@ -1419,7 +2828,11 @@ var main = (0, import_citty6.defineCommand)({
1419
2828
  list: list_default,
1420
2829
  remove: remove_default,
1421
2830
  doctor: doctor_default,
1422
- init: init_default
2831
+ init: init_default,
2832
+ secrets: secrets_default,
2833
+ sync: sync_default,
2834
+ audit: audit_default,
2835
+ update: update_default
1423
2836
  }
1424
2837
  });
1425
- (0, import_citty6.runMain)(main);
2838
+ (0, import_citty10.runMain)(main);