@mycelish/cli 0.2.7 → 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.
Files changed (201) hide show
  1. package/dist/commands/disable.test.js +0 -20
  2. package/dist/commands/disable.test.js.map +1 -1
  3. package/dist/commands/doctor.d.ts.map +1 -1
  4. package/dist/commands/doctor.js +65 -0
  5. package/dist/commands/doctor.js.map +1 -1
  6. package/dist/commands/enable.test.js +0 -18
  7. package/dist/commands/enable.test.js.map +1 -1
  8. package/dist/commands/health-checks/runner.d.ts.map +1 -1
  9. package/dist/commands/health-checks/runner.js +23 -1
  10. package/dist/commands/health-checks/runner.js.map +1 -1
  11. package/dist/commands/marketplace.d.ts.map +1 -1
  12. package/dist/commands/marketplace.js +34 -1
  13. package/dist/commands/marketplace.js.map +1 -1
  14. package/dist/commands/remote.d.ts.map +1 -1
  15. package/dist/commands/remote.js +17 -0
  16. package/dist/commands/remote.js.map +1 -1
  17. package/dist/commands/remove.d.ts +16 -7
  18. package/dist/commands/remove.d.ts.map +1 -1
  19. package/dist/commands/remove.js +234 -30
  20. package/dist/commands/remove.js.map +1 -1
  21. package/dist/commands/remove.test.js +18 -0
  22. package/dist/commands/remove.test.js.map +1 -1
  23. package/dist/commands/status.test.js +6 -1
  24. package/dist/commands/status.test.js.map +1 -1
  25. package/dist/commands/sync.d.ts.map +1 -1
  26. package/dist/commands/sync.js +20 -1
  27. package/dist/commands/sync.js.map +1 -1
  28. package/dist/core/auto-adapter.d.ts.map +1 -1
  29. package/dist/core/auto-adapter.js +2 -0
  30. package/dist/core/auto-adapter.js.map +1 -1
  31. package/dist/core/config-merger.d.ts.map +1 -1
  32. package/dist/core/config-merger.js +39 -6
  33. package/dist/core/config-merger.js.map +1 -1
  34. package/dist/core/config-merger.test.js +28 -0
  35. package/dist/core/config-merger.test.js.map +1 -1
  36. package/dist/core/content-hash.d.ts +3 -0
  37. package/dist/core/content-hash.d.ts.map +1 -0
  38. package/dist/core/content-hash.js +11 -0
  39. package/dist/core/content-hash.js.map +1 -0
  40. package/dist/core/env-template.d.ts +8 -0
  41. package/dist/core/env-template.d.ts.map +1 -1
  42. package/dist/core/env-template.js +67 -1
  43. package/dist/core/env-template.js.map +1 -1
  44. package/dist/core/env-template.test.js +100 -1
  45. package/dist/core/env-template.test.js.map +1 -1
  46. package/dist/core/manifest-migrator.d.ts +0 -1
  47. package/dist/core/manifest-migrator.d.ts.map +1 -1
  48. package/dist/core/manifest-migrator.js +1 -1
  49. package/dist/core/manifest-migrator.js.map +1 -1
  50. package/dist/core/manifest-state.d.ts +8 -2
  51. package/dist/core/manifest-state.d.ts.map +1 -1
  52. package/dist/core/manifest-state.js +32 -1
  53. package/dist/core/manifest-state.js.map +1 -1
  54. package/dist/core/manifest-state.test.js +3 -7
  55. package/dist/core/manifest-state.test.js.map +1 -1
  56. package/dist/core/marketplace-cache.d.ts +12 -0
  57. package/dist/core/marketplace-cache.d.ts.map +1 -0
  58. package/dist/core/marketplace-cache.js +127 -0
  59. package/dist/core/marketplace-cache.js.map +1 -0
  60. package/dist/core/marketplace-cache.test.d.ts +2 -0
  61. package/dist/core/marketplace-cache.test.d.ts.map +1 -0
  62. package/dist/core/marketplace-cache.test.js +122 -0
  63. package/dist/core/marketplace-cache.test.js.map +1 -0
  64. package/dist/core/marketplace-constants.d.ts +14 -0
  65. package/dist/core/marketplace-constants.d.ts.map +1 -0
  66. package/dist/core/marketplace-constants.js +14 -0
  67. package/dist/core/marketplace-constants.js.map +1 -0
  68. package/dist/core/marketplace-deduplicator.d.ts +13 -0
  69. package/dist/core/marketplace-deduplicator.d.ts.map +1 -0
  70. package/dist/core/marketplace-deduplicator.js +84 -0
  71. package/dist/core/marketplace-deduplicator.js.map +1 -0
  72. package/dist/core/marketplace-deduplicator.test.d.ts +2 -0
  73. package/dist/core/marketplace-deduplicator.test.d.ts.map +1 -0
  74. package/dist/core/marketplace-deduplicator.test.js +70 -0
  75. package/dist/core/marketplace-deduplicator.test.js.map +1 -0
  76. package/dist/core/marketplace-registry.d.ts.map +1 -1
  77. package/dist/core/marketplace-registry.js +10 -16
  78. package/dist/core/marketplace-registry.js.map +1 -1
  79. package/dist/core/marketplace-registry.test.js +5 -4
  80. package/dist/core/marketplace-registry.test.js.map +1 -1
  81. package/dist/core/marketplace-sources.d.ts +68 -15
  82. package/dist/core/marketplace-sources.d.ts.map +1 -1
  83. package/dist/core/marketplace-sources.js +415 -91
  84. package/dist/core/marketplace-sources.js.map +1 -1
  85. package/dist/core/marketplace-sources.test.d.ts +2 -0
  86. package/dist/core/marketplace-sources.test.d.ts.map +1 -0
  87. package/dist/core/marketplace-sources.test.js +240 -0
  88. package/dist/core/marketplace-sources.test.js.map +1 -0
  89. package/dist/core/marketplace.d.ts +22 -5
  90. package/dist/core/marketplace.d.ts.map +1 -1
  91. package/dist/core/marketplace.js +371 -120
  92. package/dist/core/marketplace.js.map +1 -1
  93. package/dist/core/marketplace.test.js +90 -39
  94. package/dist/core/marketplace.test.js.map +1 -1
  95. package/dist/core/migrator/executor.d.ts.map +1 -1
  96. package/dist/core/migrator/executor.js +11 -2
  97. package/dist/core/migrator/executor.js.map +1 -1
  98. package/dist/core/migrator/manifest.d.ts +10 -1
  99. package/dist/core/migrator/manifest.d.ts.map +1 -1
  100. package/dist/core/migrator/manifest.js +12 -2
  101. package/dist/core/migrator/manifest.js.map +1 -1
  102. package/dist/core/plugin-scanner.test.js +0 -4
  103. package/dist/core/plugin-scanner.test.js.map +1 -1
  104. package/dist/core/plugin-state.d.ts.map +1 -1
  105. package/dist/core/plugin-state.js +50 -18
  106. package/dist/core/plugin-state.js.map +1 -1
  107. package/dist/core/plugin-takeover.d.ts +15 -0
  108. package/dist/core/plugin-takeover.d.ts.map +1 -1
  109. package/dist/core/plugin-takeover.js +163 -3
  110. package/dist/core/plugin-takeover.js.map +1 -1
  111. package/dist/core/presets.test.js +0 -3
  112. package/dist/core/presets.test.js.map +1 -1
  113. package/dist/core/secret-detector.d.ts +20 -0
  114. package/dist/core/secret-detector.d.ts.map +1 -0
  115. package/dist/core/secret-detector.js +83 -0
  116. package/dist/core/secret-detector.js.map +1 -0
  117. package/dist/core/secret-detector.test.d.ts +2 -0
  118. package/dist/core/secret-detector.test.d.ts.map +1 -0
  119. package/dist/core/secret-detector.test.js +91 -0
  120. package/dist/core/secret-detector.test.js.map +1 -0
  121. package/dist/core/security-scanner.d.ts +24 -0
  122. package/dist/core/security-scanner.d.ts.map +1 -0
  123. package/dist/core/security-scanner.js +182 -0
  124. package/dist/core/security-scanner.js.map +1 -0
  125. package/dist/core/security-scanner.test.d.ts +2 -0
  126. package/dist/core/security-scanner.test.d.ts.map +1 -0
  127. package/dist/core/security-scanner.test.js +120 -0
  128. package/dist/core/security-scanner.test.js.map +1 -0
  129. package/dist/core/snapshot.d.ts.map +1 -1
  130. package/dist/core/snapshot.js +0 -6
  131. package/dist/core/snapshot.js.map +1 -1
  132. package/dist/core/snapshot.test.js +0 -4
  133. package/dist/core/snapshot.test.js.map +1 -1
  134. package/dist/core/state-verifier.d.ts.map +1 -1
  135. package/dist/core/state-verifier.js +0 -1
  136. package/dist/core/state-verifier.js.map +1 -1
  137. package/dist/dashboard/assets/_baseUniq-CaVE7eUV.js +1 -0
  138. package/dist/dashboard/assets/arc-CR894Erh.js +1 -0
  139. package/dist/dashboard/assets/architectureDiagram-VXUJARFQ-vuLSFK92.js +36 -0
  140. package/dist/dashboard/assets/blockDiagram-VD42YOAC-CjXwNwFV.js +122 -0
  141. package/dist/dashboard/assets/c4Diagram-YG6GDRKO-BUk0KT3V.js +10 -0
  142. package/dist/dashboard/assets/channel-BzUEK7Iv.js +1 -0
  143. package/dist/dashboard/assets/chunk-4BX2VUAB-BCCD1RD0.js +1 -0
  144. package/dist/dashboard/assets/chunk-55IACEB6-7M5H4j_M.js +1 -0
  145. package/dist/dashboard/assets/chunk-B4BG7PRW-CkuZp9sw.js +165 -0
  146. package/dist/dashboard/assets/chunk-DI55MBZ5-aoWuqaxJ.js +220 -0
  147. package/dist/dashboard/assets/chunk-FMBD7UC4-zGR2QW6c.js +15 -0
  148. package/dist/dashboard/assets/chunk-QN33PNHL-Lj8Zu7hm.js +1 -0
  149. package/dist/dashboard/assets/chunk-QZHKN3VN-JLEIDbAK.js +1 -0
  150. package/dist/dashboard/assets/chunk-TZMSLE5B-BB4GoxaV.js +1 -0
  151. package/dist/dashboard/assets/classDiagram-2ON5EDUG-CMpOlMXG.js +1 -0
  152. package/dist/dashboard/assets/classDiagram-v2-WZHVMYZB-CMpOlMXG.js +1 -0
  153. package/dist/dashboard/assets/clone-CHi7kZ5h.js +1 -0
  154. package/dist/dashboard/assets/cose-bilkent-S5V4N54A-DLEOmN1S.js +1 -0
  155. package/dist/dashboard/assets/cytoscape.esm-5J0xJHOV.js +321 -0
  156. package/dist/dashboard/assets/dagre-6UL2VRFP-BKeNwVe7.js +4 -0
  157. package/dist/dashboard/assets/defaultLocale-DX6XiGOO.js +1 -0
  158. package/dist/dashboard/assets/diagram-PSM6KHXK-BwTCZWHJ.js +24 -0
  159. package/dist/dashboard/assets/diagram-QEK2KX5R-Bg109hRA.js +43 -0
  160. package/dist/dashboard/assets/diagram-S2PKOQOG-CjzZEN97.js +24 -0
  161. package/dist/dashboard/assets/erDiagram-Q2GNP2WA-C_o1kY7B.js +60 -0
  162. package/dist/dashboard/assets/flowDiagram-NV44I4VS-CGcmMJTD.js +162 -0
  163. package/dist/dashboard/assets/ganttDiagram-JELNMOA3-Dh1Atgfm.js +267 -0
  164. package/dist/dashboard/assets/gitGraphDiagram-NY62KEGX-BkHRiPWO.js +65 -0
  165. package/dist/dashboard/assets/graph-gTAnu0gr.js +1 -0
  166. package/dist/dashboard/assets/index-DFP3ko3G.css +1 -0
  167. package/dist/dashboard/assets/index-Dr2pvJml.js +189 -0
  168. package/dist/dashboard/assets/infoDiagram-WHAUD3N6-CdqkT51i.js +2 -0
  169. package/dist/dashboard/assets/init-Gi6I4Gst.js +1 -0
  170. package/dist/dashboard/assets/journeyDiagram-XKPGCS4Q-D6H-T7IF.js +139 -0
  171. package/dist/dashboard/assets/kanban-definition-3W4ZIXB7-CdtlB9dE.js +89 -0
  172. package/dist/dashboard/assets/katex-DhXJpUyf.js +261 -0
  173. package/dist/dashboard/assets/layout-5P3G0oCF.js +1 -0
  174. package/dist/dashboard/assets/linear-BEr0iAwY.js +1 -0
  175. package/dist/dashboard/assets/mermaid.core-DY8Lk9ir.js +250 -0
  176. package/dist/dashboard/assets/min-rFBMICyx.js +1 -0
  177. package/dist/dashboard/assets/mindmap-definition-VGOIOE7T-BBgtd54T.js +68 -0
  178. package/dist/dashboard/assets/ordinal-Cboi1Yqb.js +1 -0
  179. package/dist/dashboard/assets/pieDiagram-ADFJNKIX-fgAzUnCv.js +30 -0
  180. package/dist/dashboard/assets/quadrantDiagram-AYHSOK5B-BtBFRMty.js +7 -0
  181. package/dist/dashboard/assets/requirementDiagram-UZGBJVZJ-Dsb2J8qM.js +64 -0
  182. package/dist/dashboard/assets/sankeyDiagram-TZEHDZUN-DgeXoGZy.js +10 -0
  183. package/dist/dashboard/assets/sequenceDiagram-WL72ISMW-CW8ApGYu.js +145 -0
  184. package/dist/dashboard/assets/stateDiagram-FKZM4ZOC-CFKNRDGr.js +1 -0
  185. package/dist/dashboard/assets/stateDiagram-v2-4FDKWEC3-B3pJgIRu.js +1 -0
  186. package/dist/dashboard/assets/timeline-definition-IT6M3QCI-DeE7K_Tn.js +61 -0
  187. package/dist/dashboard/assets/treemap-KMMF4GRG-DgksShPO.js +128 -0
  188. package/dist/dashboard/assets/xychartDiagram-PRI3JC2R-C8fn_Zm6.js +7 -0
  189. package/dist/dashboard/index.html +2 -2
  190. package/dist/routes/marketplace.d.ts.map +1 -1
  191. package/dist/routes/marketplace.js +179 -1
  192. package/dist/routes/marketplace.js.map +1 -1
  193. package/dist/routes/remove.d.ts.map +1 -1
  194. package/dist/routes/remove.js +4 -3
  195. package/dist/routes/remove.js.map +1 -1
  196. package/dist/routes/state.d.ts.map +1 -1
  197. package/dist/routes/state.js +63 -11
  198. package/dist/routes/state.js.map +1 -1
  199. package/package.json +2 -2
  200. package/dist/dashboard/assets/index-B15EvyT1.css +0 -1
  201. package/dist/dashboard/assets/index-Bt5n5lhF.js +0 -150
@@ -2,51 +2,9 @@ import { MARKETPLACE_SOURCES as MS } from "@mycelish/core";
2
2
  import * as fs from "node:fs/promises";
3
3
  import * as os from "node:os";
4
4
  import * as path from "node:path";
5
- // ============================================================================
6
- // npm download counts
7
- // ============================================================================
8
- export async function fetchNpmDownloads(names) {
9
- const result = {};
10
- if (names.length === 0)
11
- return result;
12
- const fetches = names.slice(0, 20).map(async (name) => {
13
- try {
14
- const res = await fetch(`https://api.npmjs.org/downloads/point/last-week/${encodeURIComponent(name)}`);
15
- if (res.ok) {
16
- const data = (await res.json());
17
- if (data.downloads)
18
- result[name] = data.downloads;
19
- }
20
- }
21
- catch {
22
- // Non-critical
23
- }
24
- });
25
- await Promise.allSettled(fetches);
26
- return result;
27
- }
28
- // ============================================================================
29
- // OpenSkills (npm registry)
30
- // ============================================================================
31
- export async function searchOpenSkills(query) {
32
- const res = await fetch(`https://registry.npmjs.org/-/v1/search?text=openskills+${encodeURIComponent(query)}&size=12`);
33
- if (!res.ok)
34
- throw new Error(`openskills search failed: ${res.statusText}`);
35
- const data = (await res.json());
36
- const names = data.objects.map(o => o.package.name);
37
- const downloads = await fetchNpmDownloads(names);
38
- const entries = data.objects.map((o) => ({
39
- name: o.package.name,
40
- description: o.package.description || "",
41
- author: o.package.author?.name,
42
- version: o.package.version,
43
- latestVersion: o.package.version,
44
- downloads: downloads[o.package.name],
45
- source: MS.OPENSKILLS,
46
- type: "skill",
47
- }));
48
- return { entries, total: entries.length, source: MS.OPENSKILLS };
49
- }
5
+ import { cachedFetch } from "./marketplace-cache.js";
6
+ import { computeContentHash } from "./content-hash.js";
7
+ import { MARKETPLACE_FETCH_LIMIT, TIMEOUT_GITHUB, TIMEOUT_UNGH, TIMEOUT_NPM, TIMEOUT_GLAMA, BATCH_NPM, BATCH_GITHUB, } from "./marketplace-constants.js";
50
8
  // ============================================================================
51
9
  // Claude Plugins (local installed_plugins.json v2)
52
10
  // ============================================================================
@@ -55,29 +13,19 @@ export async function listInstalledPlugins() {
55
13
  const filePath = path.join(os.homedir(), ".claude", "plugins", "installed_plugins.json");
56
14
  const raw = await fs.readFile(filePath, "utf-8");
57
15
  const data = JSON.parse(raw);
58
- if (!Array.isArray(data) && data.version === 2 && data.plugins) {
16
+ if (data.version === 2 && data.plugins) {
59
17
  return parseV2Plugins(data.plugins);
60
18
  }
61
- if (Array.isArray(data)) {
62
- return data.map((p) => ({
63
- name: p.name,
64
- description: p.description || "",
65
- version: p.version,
66
- author: p.author,
67
- installed: true,
68
- source: MS.CLAUDE_PLUGINS,
69
- type: "plugin",
70
- }));
71
- }
72
19
  return [];
73
20
  }
74
21
  catch (e) {
75
22
  if (e && typeof e === "object" && e.code !== "ENOENT") {
76
- console.warn("Failed to read installed plugins:", e);
23
+ // Non-critical warning skip logging to avoid noise
77
24
  }
78
25
  return [];
79
26
  }
80
27
  }
28
+ /** Pure parser — no filtering. Visibility policy is handled by getLivePluginState. */
81
29
  function parseV2Plugins(plugins) {
82
30
  const entries = [];
83
31
  for (const [key, installs] of Object.entries(plugins)) {
@@ -91,27 +39,102 @@ function parseV2Plugins(plugins) {
91
39
  name: pluginName,
92
40
  description: marketplace ? `From ${marketplace}` : "",
93
41
  version: latest.version,
94
- latestVersion: latest.version,
95
42
  installedVersion: latest.version,
96
43
  installed: true,
97
44
  updatedAt: latest.lastUpdated ? new Date(latest.lastUpdated).toISOString().slice(0, 10) : undefined,
98
45
  source: MS.CLAUDE_PLUGINS,
99
46
  type: "plugin",
47
+ category: marketplace,
100
48
  });
101
49
  }
102
50
  return entries;
103
51
  }
104
52
  export async function searchClaudePlugins(query) {
105
53
  const plugins = await listInstalledPlugins();
54
+ await enrichPluginsWithLatestVersions(plugins);
106
55
  const q = query.toLowerCase();
107
56
  const entries = plugins.filter((p) => p.name.toLowerCase().includes(q) || p.description.toLowerCase().includes(q));
108
57
  return { entries, total: entries.length, source: MS.CLAUDE_PLUGINS };
109
58
  }
59
+ /**
60
+ * Enrich installed plugins with latest versions from their marketplace repos.
61
+ * Reads known_marketplaces.json → fetches marketplace.json from GitHub → extracts latest versions.
62
+ */
63
+ export async function enrichPluginsWithLatestVersions(plugins) {
64
+ try {
65
+ const knownPath = path.join(os.homedir(), ".claude", "plugins", "known_marketplaces.json");
66
+ const raw = await fs.readFile(knownPath, "utf-8");
67
+ const known = JSON.parse(raw);
68
+ // Build a map: marketplace name → GitHub owner/repo
69
+ const repoMap = new Map();
70
+ for (const [name, info] of Object.entries(known)) {
71
+ if (info.source?.source === "github" && info.source.repo) {
72
+ const parts = info.source.repo.split("/");
73
+ if (parts.length === 2)
74
+ repoMap.set(name, { owner: parts[0], repo: parts[1] });
75
+ }
76
+ }
77
+ if (repoMap.size === 0)
78
+ return;
79
+ const marketplacePlugins = new Map();
80
+ const fetches = [...repoMap.entries()].map(async ([marketplace, { owner, repo }]) => {
81
+ try {
82
+ const data = await cachedFetch(`plugin-meta-${marketplace}`, async () => {
83
+ const headers = {};
84
+ const ghToken = process.env.GITHUB_TOKEN || process.env.GH_TOKEN;
85
+ if (ghToken)
86
+ headers.Authorization = `Bearer ${ghToken}`;
87
+ const res = await fetch(`https://raw.githubusercontent.com/${owner}/${repo}/main/.claude-plugin/marketplace.json`, { headers, signal: AbortSignal.timeout(TIMEOUT_GITHUB) });
88
+ if (!res.ok)
89
+ return { plugins: [] };
90
+ return (await res.json());
91
+ });
92
+ const meta = new Map();
93
+ for (const p of data.plugins ?? []) {
94
+ if (!p.name)
95
+ continue;
96
+ // Extract the actual plugin repo URL from the source field
97
+ let pluginUrl;
98
+ if (p.source?.url) {
99
+ pluginUrl = p.source.url.replace(/\.git$/, "");
100
+ }
101
+ meta.set(p.name, { version: p.version, url: pluginUrl });
102
+ }
103
+ marketplacePlugins.set(marketplace, meta);
104
+ }
105
+ catch {
106
+ // Non-critical
107
+ }
108
+ });
109
+ await Promise.allSettled(fetches);
110
+ // Enrich entries with latest versions and correct plugin URLs
111
+ for (const plugin of plugins) {
112
+ const mpName = plugin.category;
113
+ const mpMeta = mpName ? marketplacePlugins.get(mpName) : undefined;
114
+ const meta = mpMeta?.get(plugin.name)
115
+ ?? [...marketplacePlugins.values()].find(m => m.has(plugin.name))?.get(plugin.name);
116
+ if (meta?.version)
117
+ plugin.latestVersion = meta.version;
118
+ // Use the actual plugin repo URL, not the marketplace repo
119
+ if (!plugin.url && meta?.url) {
120
+ plugin.url = meta.url;
121
+ }
122
+ // Fallback: marketplace repo if no per-plugin URL
123
+ if (!plugin.url && mpName && repoMap.has(mpName)) {
124
+ const { owner, repo } = repoMap.get(mpName);
125
+ plugin.url = `https://github.com/${owner}/${repo}`;
126
+ }
127
+ }
128
+ }
129
+ catch {
130
+ // known_marketplaces.json missing or unreadable — skip
131
+ }
132
+ }
110
133
  const MCP_REGISTRY_URL = "https://registry.modelcontextprotocol.io";
111
134
  export async function fetchMcpServers(query) {
112
135
  const url = query
113
- ? `${MCP_REGISTRY_URL}/v0.1/servers?q=${encodeURIComponent(query)}&limit=20`
114
- : `${MCP_REGISTRY_URL}/v0.1/servers?limit=20`;
136
+ ? `${MCP_REGISTRY_URL}/v0.1/servers?search=${encodeURIComponent(query)}&limit=${MARKETPLACE_FETCH_LIMIT}`
137
+ : `${MCP_REGISTRY_URL}/v0.1/servers?limit=${MARKETPLACE_FETCH_LIMIT}`;
115
138
  const res = await fetch(url);
116
139
  if (!res.ok)
117
140
  throw new Error(`MCP Registry failed: ${res.statusText}`);
@@ -120,6 +143,8 @@ export async function fetchMcpServers(query) {
120
143
  }
121
144
  export function mcpServerToEntry(s) {
122
145
  const srv = s.server;
146
+ const url = srv.repository?.url || srv.websiteUrl || undefined;
147
+ const npmPkg = srv.packages?.find(p => p.registryType === "npm")?.identifier;
123
148
  return {
124
149
  name: srv.name,
125
150
  description: srv.description || "",
@@ -127,23 +152,69 @@ export function mcpServerToEntry(s) {
127
152
  latestVersion: srv.version,
128
153
  source: MS.MCP_REGISTRY,
129
154
  type: "mcp",
155
+ url,
156
+ npmPackage: npmPkg,
130
157
  };
131
158
  }
132
- export async function searchMcpRegistry(query) {
133
- const servers = await fetchMcpServers(query);
159
+ export async function searchMcpRegistry(query, options) {
160
+ const servers = query
161
+ ? await fetchMcpServers(query) // user search: live
162
+ : await cachedFetch("mcp-registry", () => fetchMcpServers(""), options); // browse: cached
134
163
  const entries = servers.map(mcpServerToEntry);
135
164
  return { entries, total: entries.length, source: MS.MCP_REGISTRY };
136
165
  }
166
+ const GLAMA_API = "https://glama.ai/api/mcp/v1/servers";
167
+ export async function fetchGlamaServers(query) {
168
+ const params = new URLSearchParams({ limit: String(MARKETPLACE_FETCH_LIMIT) });
169
+ if (query)
170
+ params.set("query", query);
171
+ const res = await fetch(`${GLAMA_API}?${params}`, { signal: AbortSignal.timeout(TIMEOUT_GLAMA) });
172
+ if (!res.ok)
173
+ throw new Error(`Glama API failed: ${res.statusText}`);
174
+ const data = (await res.json());
175
+ return data.servers || [];
176
+ }
177
+ export function glamaServerToEntry(s) {
178
+ return {
179
+ name: s.name,
180
+ description: s.description || "",
181
+ source: MS.GLAMA,
182
+ type: "mcp",
183
+ url: s.repository?.url || s.url || undefined,
184
+ };
185
+ }
186
+ export async function searchGlama(query, options) {
187
+ const servers = query
188
+ ? await fetchGlamaServers(query)
189
+ : await cachedFetch("glama", () => fetchGlamaServers(""), options);
190
+ const entries = servers.map(glamaServerToEntry);
191
+ return { entries, total: entries.length, source: MS.GLAMA };
192
+ }
193
+ // ============================================================================
194
+ // GitHub tree (uses cachedFetch for L1+L2 caching)
195
+ // ============================================================================
196
+ async function fetchGitHubTree(owner, repo, options) {
197
+ return cachedFetch(`github-${owner}-${repo}`, async () => {
198
+ const headers = { Accept: "application/vnd.github.v3+json" };
199
+ const ghToken = process.env.GITHUB_TOKEN || process.env.GH_TOKEN;
200
+ if (ghToken)
201
+ headers.Authorization = `Bearer ${ghToken}`;
202
+ const res = await fetch(`https://api.github.com/repos/${owner}/${repo}/git/trees/main?recursive=1`, { headers });
203
+ if (!res.ok) {
204
+ const hint = res.status === 403 ? " (rate limited — try again later or set GITHUB_TOKEN)" : "";
205
+ throw new Error(`GitHub API ${res.status} for ${owner}/${repo}${hint}`);
206
+ }
207
+ const body = (await res.json());
208
+ return body.tree;
209
+ }, options);
210
+ }
137
211
  // ============================================================================
138
212
  // Anthropic Skills (GitHub)
139
213
  // ============================================================================
140
- export async function fetchAnthropicSkillsList() {
141
- const res = await fetch("https://api.github.com/repos/anthropics/skills/git/trees/main?recursive=1", { headers: { Accept: "application/vnd.github.v3+json" } });
142
- if (!res.ok)
143
- return [];
144
- const data = (await res.json());
214
+ export async function fetchAnthropicSkillsList(options) {
215
+ const tree = await fetchGitHubTree("anthropics", "skills", options);
145
216
  const skills = [];
146
- for (const t of data.tree) {
217
+ for (const t of tree) {
147
218
  if (t.type === "blob" && t.path.endsWith("/SKILL.md") && t.path.startsWith("skills/")) {
148
219
  const parts = t.path.split("/");
149
220
  if (parts.length === 3)
@@ -152,8 +223,11 @@ export async function fetchAnthropicSkillsList() {
152
223
  }
153
224
  return skills;
154
225
  }
155
- export async function searchAnthropicSkills(query) {
156
- const allSkills = await fetchAnthropicSkillsList();
226
+ export async function searchAnthropicSkills(query, options) {
227
+ const [allSkills, stars] = await Promise.all([
228
+ fetchAnthropicSkillsList(options),
229
+ fetchGitHubRepoStars("anthropics", "skills", options),
230
+ ]);
157
231
  const q = query.toLowerCase();
158
232
  const filtered = q ? allSkills.filter(s => s.toLowerCase().includes(q)) : allSkills;
159
233
  const entries = filtered.map(name => ({
@@ -162,40 +236,290 @@ export async function searchAnthropicSkills(query) {
162
236
  author: "anthropics",
163
237
  source: MS.ANTHROPIC_SKILLS,
164
238
  type: "skill",
239
+ stars,
240
+ url: `https://github.com/anthropics/skills/tree/main/skills/${name}`,
165
241
  }));
166
242
  return { entries, total: entries.length, source: MS.ANTHROPIC_SKILLS };
167
243
  }
168
- export async function searchClawHub(query) {
169
- const res = await fetch(`https://clawhub.ai/api/v1/search?q=${encodeURIComponent(query)}&limit=12`, { headers: { Accept: "application/json" } });
170
- if (!res.ok)
171
- throw new Error(`ClawHub search failed: ${res.statusText}`);
172
- const data = (await res.json());
173
- const entries = (data.results || []).map((item) => ({
174
- name: item.slug,
175
- description: item.summary || item.displayName || "",
176
- version: item.version,
177
- latestVersion: item.version,
178
- updatedAt: item.updatedAt ? new Date(item.updatedAt).toISOString().slice(0, 10) : undefined,
179
- source: MS.CLAWHUB,
180
- type: "skill",
244
+ /**
245
+ * Parse a github.com URL into owner/repo.
246
+ * Returns null if not a GitHub URL.
247
+ */
248
+ const GITHUB_NAME_RE = /^[a-zA-Z0-9._-]+$/;
249
+ export function parseGitHubUrl(url) {
250
+ const m = url.match(/github\.com\/([^/]+)\/([^/]+)/);
251
+ if (!m)
252
+ return null;
253
+ const owner = m[1];
254
+ const repo = m[2].replace(/\.git$/, "");
255
+ if (!GITHUB_NAME_RE.test(owner) || !GITHUB_NAME_RE.test(repo))
256
+ return null;
257
+ return { owner, repo };
258
+ }
259
+ /**
260
+ * Search a GitHub repo for skills, agents, and commands.
261
+ * Uses the recursive tree endpoint (same pattern as fetchAnthropicSkillsList).
262
+ */
263
+ async function fetchGitHubRepoStars(owner, repo, options) {
264
+ try {
265
+ const data = await cachedFetch(`github-stars-${owner}-${repo}`, async () => {
266
+ // Tier 1: ungh.cc — free, no auth, no rate limit
267
+ try {
268
+ const unghRes = await fetch(`https://ungh.cc/repos/${owner}/${repo}`, { signal: AbortSignal.timeout(TIMEOUT_UNGH) });
269
+ if (unghRes.ok) {
270
+ const json = (await unghRes.json());
271
+ if (json.repo?.stars != null)
272
+ return { stars: json.repo.stars };
273
+ }
274
+ }
275
+ catch {
276
+ // Fall through to GitHub API
277
+ }
278
+ // Tier 2/3: GitHub API (with token if available, unauth otherwise)
279
+ const headers = { Accept: "application/vnd.github.v3+json" };
280
+ const ghToken = process.env.GITHUB_TOKEN || process.env.GH_TOKEN;
281
+ if (ghToken)
282
+ headers.Authorization = `Bearer ${ghToken}`;
283
+ const res = await fetch(`https://api.github.com/repos/${owner}/${repo}`, { headers, signal: AbortSignal.timeout(TIMEOUT_GITHUB) });
284
+ if (!res.ok)
285
+ throw new Error(`GitHub API ${res.status} for ${owner}/${repo}`);
286
+ const json = (await res.json());
287
+ return { stars: json.stargazers_count };
288
+ }, options);
289
+ return data.stars;
290
+ }
291
+ catch {
292
+ return undefined;
293
+ }
294
+ }
295
+ export async function searchGitHubRepo(owner, repo, query, sourceName, options) {
296
+ const [items, stars] = await Promise.all([
297
+ fetchGitHubRepoItems(owner, repo, options),
298
+ fetchGitHubRepoStars(owner, repo, options),
299
+ ]);
300
+ const q = query.toLowerCase();
301
+ const filtered = q
302
+ ? items.filter(i => i.name.toLowerCase().includes(q) || (i.description?.toLowerCase().includes(q) ?? false))
303
+ : items;
304
+ const entries = filtered.map(item => ({
305
+ name: item.name,
306
+ description: item.description || `${item.type} from ${owner}/${repo}`,
307
+ author: owner,
308
+ source: sourceName,
309
+ type: item.type,
310
+ stars,
311
+ url: `https://github.com/${owner}/${repo}/tree/main/${item.path}`,
181
312
  }));
182
- return { entries, total: entries.length, source: MS.CLAWHUB };
313
+ // If the repo has multiple item types, add a plugin entry representing the whole repo
314
+ const types = new Set(items.map(i => i.type));
315
+ if (types.size > 1 && (!q || repo.toLowerCase().includes(q) || owner.toLowerCase().includes(q))) {
316
+ const counts = [...types].map(t => `${items.filter(i => i.type === t).length} ${t}s`).join(", ");
317
+ entries.unshift({
318
+ name: repo,
319
+ description: `Plugin bundle: ${counts}`,
320
+ author: owner,
321
+ source: sourceName,
322
+ type: "plugin",
323
+ stars,
324
+ url: `https://github.com/${owner}/${repo}`,
325
+ });
326
+ }
327
+ return { entries, total: entries.length, source: sourceName };
328
+ }
329
+ /**
330
+ * Fetch all items (skills, agents, commands) from a GitHub repo tree.
331
+ */
332
+ export async function fetchGitHubRepoItems(owner, repo, options) {
333
+ const tree = await fetchGitHubTree(owner, repo, options);
334
+ const items = [];
335
+ const dirMap = {
336
+ skills: "skill",
337
+ agents: "agent",
338
+ commands: "command",
339
+ };
340
+ for (const t of tree) {
341
+ if (t.type !== "blob")
342
+ continue;
343
+ for (const [dir, itemType] of Object.entries(dirMap)) {
344
+ if (t.path.startsWith(`${dir}/`) && t.path.endsWith(".md")) {
345
+ const parts = t.path.split("/");
346
+ // skills/name/SKILL.md (depth 3) or agents/name.md (depth 2)
347
+ let name;
348
+ if (itemType === "skill" && parts.length === 3 && parts[2] === "SKILL.md") {
349
+ name = parts[1];
350
+ }
351
+ else if (itemType !== "skill" && parts.length === 2) {
352
+ name = parts[1].replace(/\.md$/, "");
353
+ }
354
+ if (name) {
355
+ items.push({ name, type: itemType, path: t.path });
356
+ }
357
+ }
358
+ }
359
+ }
360
+ return items;
361
+ }
362
+ /**
363
+ * Install an item from a GitHub repo by downloading its raw content.
364
+ */
365
+ export async function installGitHubRepoItem(owner, repo, entry) {
366
+ const itemType = entry.type;
367
+ let remotePath;
368
+ let localDir;
369
+ let fileName;
370
+ const globalDir = path.join(os.homedir(), ".mycelium", "global");
371
+ if (itemType === "skill") {
372
+ remotePath = `skills/${encodeURIComponent(entry.name)}/SKILL.md`;
373
+ localDir = path.join(globalDir, "skills", entry.name);
374
+ fileName = "SKILL.md";
375
+ }
376
+ else if (itemType === "agent") {
377
+ remotePath = `agents/${encodeURIComponent(entry.name)}.md`;
378
+ localDir = path.join(globalDir, "agents");
379
+ fileName = `${entry.name}.md`;
380
+ }
381
+ else {
382
+ remotePath = `commands/${encodeURIComponent(entry.name)}.md`;
383
+ localDir = path.join(globalDir, "commands");
384
+ fileName = `${entry.name}.md`;
385
+ }
386
+ const rawUrl = `https://raw.githubusercontent.com/${owner}/${repo}/main/${remotePath}`;
387
+ const ghRes = await fetch(rawUrl);
388
+ if (!ghRes.ok)
389
+ throw new Error(`Download failed: ${ghRes.statusText}`);
390
+ const content = await ghRes.text();
391
+ await fs.mkdir(localDir, { recursive: true });
392
+ const filePath = path.join(localDir, fileName);
393
+ await fs.writeFile(filePath, content, "utf-8");
394
+ return { success: true, path: filePath, contentHash: computeContentHash(content) };
183
395
  }
184
396
  // ============================================================================
185
- // SkillsMP (disabled — needs API key)
397
+ // npm Download Enrichment
186
398
  // ============================================================================
187
- export async function searchSkillsmp(_query) {
188
- return { entries: [], total: 0, source: MS.SKILLSMP };
399
+ /**
400
+ * Enrich marketplace entries with npm weekly download counts.
401
+ * Tries the entry name as an npm package name (works for most MCP servers).
402
+ * Uses cachedFetch to avoid redundant API calls.
403
+ */
404
+ export async function enrichWithNpmDownloads(entries) {
405
+ // Only enrich MCP-type entries (not plugins — plugin names don't match npm packages)
406
+ const candidates = entries.filter(e => e.downloads == null && e.type === "mcp");
407
+ if (candidates.length === 0)
408
+ return;
409
+ // Batch: max BATCH_NPM concurrent
410
+ const batch = candidates.slice(0, BATCH_NPM);
411
+ await Promise.allSettled(batch.map(async (entry) => {
412
+ try {
413
+ // Use the npm package name — for scoped names like @modelcontextprotocol/server-*
414
+ // the entry name is usually the npm package name for MCP entries
415
+ const pkgName = entry.name;
416
+ const data = await cachedFetch(`npm-dl-${pkgName}`, async () => {
417
+ // First verify the package exists and is MCP-related (check keywords)
418
+ const metaRes = await fetch(`https://registry.npmjs.org/${encodeURIComponent(pkgName)}`, { signal: AbortSignal.timeout(TIMEOUT_NPM), headers: { Accept: "application/vnd.npm.install-v1+json" } });
419
+ if (!metaRes.ok)
420
+ return { downloads: undefined };
421
+ const meta = (await metaRes.json());
422
+ // Validate it's actually an MCP package (description or keywords mention "mcp")
423
+ const desc = (meta.description || "").toLowerCase();
424
+ const kw = (meta.keywords || []).join(" ").toLowerCase();
425
+ if (!desc.includes("mcp") && !kw.includes("mcp") && !desc.includes("model context protocol")) {
426
+ return { downloads: undefined };
427
+ }
428
+ const dlRes = await fetch(`https://api.npmjs.org/downloads/point/last-week/${encodeURIComponent(pkgName)}`, { signal: AbortSignal.timeout(TIMEOUT_NPM) });
429
+ if (!dlRes.ok)
430
+ return { downloads: undefined };
431
+ const json = (await dlRes.json());
432
+ return { downloads: json.downloads };
433
+ });
434
+ if (data.downloads != null && data.downloads > 0) {
435
+ entry.downloads = data.downloads;
436
+ }
437
+ }
438
+ catch {
439
+ // Non-critical
440
+ }
441
+ }));
442
+ }
443
+ // ============================================================================
444
+ // GitHub Stars Enrichment (for entries with GitHub URLs)
445
+ // ============================================================================
446
+ /**
447
+ * Resolve a GitHub URL from an npm package name via the npm registry.
448
+ * Returns a github.com URL or undefined.
449
+ */
450
+ async function resolveGitHubUrlFromNpm(pkg) {
451
+ try {
452
+ const data = await cachedFetch(`npm-repo-${pkg}`, async () => {
453
+ const res = await fetch(`https://registry.npmjs.org/${encodeURIComponent(pkg)}`, { signal: AbortSignal.timeout(TIMEOUT_NPM) });
454
+ if (!res.ok)
455
+ throw new Error(`npm ${res.status}`);
456
+ const json = (await res.json());
457
+ const repoUrl = typeof json.repository === "string" ? json.repository : json.repository?.url;
458
+ return { repoUrl: repoUrl || null };
459
+ });
460
+ if (data.repoUrl && data.repoUrl.includes("github.com")) {
461
+ // Normalize git+https://github.com/foo/bar.git → https://github.com/foo/bar
462
+ return data.repoUrl.replace(/^git\+/, "").replace(/\.git$/, "");
463
+ }
464
+ return undefined;
465
+ }
466
+ catch {
467
+ return undefined;
468
+ }
469
+ }
470
+ /**
471
+ * Enrich marketplace entries with GitHub star counts.
472
+ * For entries without a GitHub URL but with an npm package, resolves the repo via npm first.
473
+ * Uses ungh.cc → GitHub API (token) → GitHub API (unauth) priority chain.
474
+ */
475
+ export async function enrichWithGitHubStars(entries) {
476
+ // Phase 1: resolve GitHub URLs from npm packages for entries missing a GitHub URL
477
+ const needsNpmResolve = entries.filter(e => e.stars == null && e.npmPackage && (!e.url || !e.url.includes("github.com")));
478
+ if (needsNpmResolve.length > 0) {
479
+ await Promise.allSettled(needsNpmResolve.map(async (entry) => {
480
+ const ghUrl = await resolveGitHubUrlFromNpm(entry.npmPackage);
481
+ if (ghUrl)
482
+ entry.url = ghUrl;
483
+ }));
484
+ }
485
+ // Phase 2: enrich with stars
486
+ const needsStars = entries.filter(e => e.url && e.stars == null && e.url.includes("github.com"));
487
+ if (needsStars.length === 0)
488
+ return;
489
+ // Extract owner/repo from URL, dedupe repos
490
+ const repoMap = new Map();
491
+ for (const entry of needsStars) {
492
+ const parsed = parseGitHubUrl(entry.url);
493
+ if (!parsed)
494
+ continue;
495
+ const key = `${parsed.owner}/${parsed.repo}`;
496
+ if (!repoMap.has(key))
497
+ repoMap.set(key, []);
498
+ repoMap.get(key).push(entry);
499
+ }
500
+ // Fetch stars in parallel (max BATCH_GITHUB repos per batch to be polite)
501
+ const repos = [...repoMap.entries()].slice(0, BATCH_GITHUB);
502
+ await Promise.allSettled(repos.map(async ([repo, repoEntries]) => {
503
+ try {
504
+ const [owner, repoName] = repo.split("/");
505
+ const stars = await fetchGitHubRepoStars(owner, repoName);
506
+ if (stars != null) {
507
+ for (const entry of repoEntries)
508
+ entry.stars = stars;
509
+ }
510
+ }
511
+ catch {
512
+ // Non-critical — skip enrichment for failed repos
513
+ }
514
+ }));
189
515
  }
190
516
  // ============================================================================
191
517
  // Searcher map
192
518
  // ============================================================================
193
519
  export const KNOWN_SEARCHERS = {
194
- [MS.SKILLSMP]: searchSkillsmp,
195
- [MS.OPENSKILLS]: searchOpenSkills,
196
520
  [MS.CLAUDE_PLUGINS]: searchClaudePlugins,
197
521
  [MS.MCP_REGISTRY]: searchMcpRegistry,
522
+ [MS.GLAMA]: searchGlama,
198
523
  [MS.ANTHROPIC_SKILLS]: searchAnthropicSkills,
199
- [MS.CLAWHUB]: searchClawHub,
200
524
  };
201
525
  //# sourceMappingURL=marketplace-sources.js.map