@shnitzel/plugscout 0.3.1

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/LICENSE +21 -0
  2. package/README.md +228 -0
  3. package/assets/cli/logo.txt +24 -0
  4. package/config/item-insights.json +316 -0
  5. package/config/providers.json +46 -0
  6. package/config/ranking-policy.json +14 -0
  7. package/config/recommendation-weights.json +7 -0
  8. package/config/registries.json +1423 -0
  9. package/config/security-policy.json +19 -0
  10. package/config/sources.json +30 -0
  11. package/data/catalog/items.json +182109 -0
  12. package/data/catalog/mcps.json +163843 -0
  13. package/data/catalog/skills.json +4768 -0
  14. package/data/catalog/sync-state.json +62 -0
  15. package/data/curated/mcps.json +78 -0
  16. package/data/curated/skills.json +174 -0
  17. package/data/quarantine/quarantined.json +3 -0
  18. package/data/raw/2024-05-15/mcps.json +20 -0
  19. package/data/raw/2024-05-20/skills.json +20 -0
  20. package/data/raw/2024-06-05/mcps.json +20 -0
  21. package/data/raw/2024-06-05/skills.json +29 -0
  22. package/data/security-reports/.gitkeep +0 -0
  23. package/data/security-reports/2026-02-06/report.json +8 -0
  24. package/data/security-reports/2026-02-10/report.json +9 -0
  25. package/data/security-reports/2026-02-11/report.json +9 -0
  26. package/data/security-reports/2026-02-12/report.json +9 -0
  27. package/data/security-reports/2026-02-13/report.json +8 -0
  28. package/data/security-reports/2026-02-14/report.json +8 -0
  29. package/data/security-reports/2026-02-23/report.json +8 -0
  30. package/data/security-reports/2026-02-25/report.json +8 -0
  31. package/data/security-reports/2026-02-26/report.json +8 -0
  32. package/data/security-reports/2026-03-10/report.json +8 -0
  33. package/data/security-reports/audits/.gitkeep +0 -0
  34. package/data/security-reports/audits/2026-02-06T10-17-33-872Z-mcp_remote-browser.json +8 -0
  35. package/data/security-reports/audits/2026-02-06T10-17-33-881Z-mcp_remote-browser.json +8 -0
  36. package/data/security-reports/audits/2026-02-10T20-22-24-474Z-mcp_remote-browser.json +8 -0
  37. package/data/security-reports/audits/2026-02-10T20-22-24-483Z-mcp_remote-browser.json +8 -0
  38. package/data/security-reports/audits/2026-02-10T20-42-12-305Z-mcp_remote-browser.json +8 -0
  39. package/data/security-reports/audits/2026-02-10T20-42-12-319Z-mcp_remote-browser.json +8 -0
  40. package/data/security-reports/audits/2026-02-10T20-43-15-728Z-mcp_remote-browser.json +8 -0
  41. package/data/security-reports/audits/2026-02-10T20-43-15-738Z-mcp_remote-browser.json +8 -0
  42. package/data/security-reports/audits/2026-02-10T21-22-14-047Z-mcp_remote-browser.json +8 -0
  43. package/data/security-reports/audits/2026-02-10T21-22-14-051Z-mcp_remote-browser.json +8 -0
  44. package/data/security-reports/audits/2026-02-10T21-29-59-237Z-mcp_remote-browser.json +8 -0
  45. package/data/security-reports/audits/2026-02-10T21-29-59-243Z-mcp_remote-browser.json +8 -0
  46. package/data/security-reports/audits/2026-02-11T20-21-51-074Z-mcp_remote-browser.json +8 -0
  47. package/data/security-reports/audits/2026-02-11T20-21-51-123Z-mcp_remote-browser.json +8 -0
  48. package/data/security-reports/audits/2026-02-11T20-28-33-021Z-mcp_remote-browser.json +8 -0
  49. package/data/security-reports/audits/2026-02-11T20-28-33-026Z-mcp_remote-browser.json +8 -0
  50. package/data/security-reports/audits/2026-02-11T20-34-43-623Z-mcp_remote-browser.json +8 -0
  51. package/data/security-reports/audits/2026-02-11T20-34-43-625Z-mcp_remote-browser.json +8 -0
  52. package/data/security-reports/audits/2026-02-11T21-06-33-281Z-mcp_remote-browser.json +8 -0
  53. package/data/security-reports/audits/2026-02-11T21-06-33-285Z-mcp_remote-browser.json +8 -0
  54. package/data/security-reports/audits/2026-02-11T21-08-58-836Z-mcp_remote-browser.json +8 -0
  55. package/data/security-reports/audits/2026-02-11T21-08-58-843Z-mcp_remote-browser.json +8 -0
  56. package/data/security-reports/audits/2026-02-12T12-26-07-150Z-mcp_remote-browser.json +8 -0
  57. package/data/security-reports/audits/2026-02-12T12-26-07-159Z-mcp_remote-browser.json +8 -0
  58. package/data/security-reports/audits/2026-02-12T14-37-36-565Z-mcp_remote-browser.json +8 -0
  59. package/data/security-reports/audits/2026-02-12T14-37-36-569Z-mcp_remote-browser.json +8 -0
  60. package/data/security-reports/audits/2026-02-12T14-47-32-103Z-mcp_remote-browser.json +8 -0
  61. package/data/security-reports/audits/2026-02-12T14-47-32-213Z-mcp_remote-browser.json +8 -0
  62. package/data/security-reports/audits/2026-02-12T14-47-47-769Z-mcp_filesystem.json +8 -0
  63. package/data/security-reports/audits/2026-02-12T15-05-49-085Z-mcp_remote-browser.json +8 -0
  64. package/data/security-reports/audits/2026-02-12T15-05-49-087Z-mcp_remote-browser.json +8 -0
  65. package/data/security-reports/audits/2026-02-12T16-37-42-204Z-mcp_remote-browser.json +8 -0
  66. package/data/security-reports/audits/2026-02-12T16-37-42-243Z-mcp_remote-browser.json +8 -0
  67. package/data/security-reports/audits/2026-02-12T16-47-16-589Z-mcp_remote-browser.json +8 -0
  68. package/data/security-reports/audits/2026-02-12T16-47-16-596Z-mcp_remote-browser.json +8 -0
  69. package/data/security-reports/audits/2026-02-12T17-38-24-899Z-mcp_remote-browser.json +8 -0
  70. package/data/security-reports/audits/2026-02-12T17-38-24-905Z-mcp_remote-browser.json +8 -0
  71. package/data/security-reports/audits/2026-02-12T17-56-00-835Z-mcp_remote-browser.json +8 -0
  72. package/data/security-reports/audits/2026-02-12T17-56-00-840Z-mcp_remote-browser.json +8 -0
  73. package/data/security-reports/audits/2026-02-12T18-19-26-005Z-mcp_remote-browser.json +8 -0
  74. package/data/security-reports/audits/2026-02-12T18-19-26-008Z-mcp_remote-browser.json +8 -0
  75. package/data/security-reports/audits/2026-02-12T18-34-38-642Z-mcp_remote-browser.json +8 -0
  76. package/data/security-reports/audits/2026-02-12T18-34-38-645Z-mcp_remote-browser.json +8 -0
  77. package/data/security-reports/audits/2026-02-13T05-44-27-648Z-mcp_remote-browser.json +8 -0
  78. package/data/security-reports/audits/2026-02-13T05-44-27-656Z-mcp_remote-browser.json +8 -0
  79. package/data/security-reports/audits/2026-02-13T05-48-50-827Z-mcp_remote-browser.json +8 -0
  80. package/data/security-reports/audits/2026-02-13T05-48-50-900Z-mcp_remote-browser.json +8 -0
  81. package/data/security-reports/audits/2026-02-13T10-53-33-850Z-mcp_remote-browser.json +8 -0
  82. package/data/security-reports/audits/2026-02-13T10-53-33-853Z-mcp_remote-browser.json +8 -0
  83. package/data/security-reports/audits/2026-02-14T17-51-27-279Z-mcp_remote-browser.json +8 -0
  84. package/data/security-reports/audits/2026-02-14T17-51-27-282Z-mcp_remote-browser.json +8 -0
  85. package/data/security-reports/audits/2026-02-14T19-43-39-991Z-mcp_remote-browser.json +8 -0
  86. package/data/security-reports/audits/2026-02-14T19-43-39-997Z-mcp_remote-browser.json +8 -0
  87. package/data/security-reports/audits/2026-02-23T19-24-43-515Z-mcp_remote-browser.json +8 -0
  88. package/data/security-reports/audits/2026-02-23T19-24-43-518Z-mcp_remote-browser.json +8 -0
  89. package/data/security-reports/audits/2026-02-25T14-45-02-763Z-mcp_remote-browser.json +8 -0
  90. package/data/security-reports/audits/2026-02-25T14-45-02-778Z-mcp_remote-browser.json +8 -0
  91. package/data/security-reports/audits/2026-02-25T14-46-58-957Z-mcp_remote-browser.json +8 -0
  92. package/data/security-reports/audits/2026-02-25T14-46-58-960Z-mcp_remote-browser.json +8 -0
  93. package/data/security-reports/audits/2026-02-25T14-57-37-133Z-mcp_remote-browser.json +8 -0
  94. package/data/security-reports/audits/2026-02-25T14-57-37-139Z-mcp_remote-browser.json +8 -0
  95. package/data/security-reports/audits/2026-02-25T15-03-23-507Z-mcp_remote-browser.json +8 -0
  96. package/data/security-reports/audits/2026-02-25T15-03-23-513Z-mcp_remote-browser.json +8 -0
  97. package/data/security-reports/audits/2026-02-25T15-03-41-157Z-mcp_remote-browser.json +8 -0
  98. package/data/security-reports/audits/2026-02-25T15-03-41-162Z-mcp_remote-browser.json +8 -0
  99. package/data/security-reports/audits/2026-02-25T15-05-18-042Z-mcp_remote-browser.json +8 -0
  100. package/data/security-reports/audits/2026-02-25T15-05-18-048Z-mcp_remote-browser.json +8 -0
  101. package/data/security-reports/audits/2026-02-25T15-39-08-519Z-mcp_remote-browser.json +8 -0
  102. package/data/security-reports/audits/2026-02-25T15-39-08-526Z-mcp_remote-browser.json +8 -0
  103. package/data/security-reports/audits/2026-02-25T18-35-54-463Z-mcp_remote-browser.json +8 -0
  104. package/data/security-reports/audits/2026-02-25T18-35-54-466Z-mcp_remote-browser.json +8 -0
  105. package/data/security-reports/audits/2026-02-26T05-52-21-092Z-mcp_remote-browser.json +8 -0
  106. package/data/security-reports/audits/2026-02-26T05-52-21-093Z-mcp_remote-browser.json +8 -0
  107. package/data/security-reports/audits/2026-02-26T05-52-27-076Z-mcp_remote-browser.json +8 -0
  108. package/data/security-reports/audits/2026-02-26T05-52-27-079Z-mcp_remote-browser.json +8 -0
  109. package/data/security-reports/audits/2026-02-26T05-52-27-084Z-mcp_remote-browser.json +8 -0
  110. package/data/security-reports/audits/2026-02-26T05-52-27-086Z-mcp_remote-browser.json +8 -0
  111. package/data/security-reports/audits/2026-02-26T05-52-37-249Z-mcp_remote-browser.json +8 -0
  112. package/data/security-reports/audits/2026-02-26T05-52-37-258Z-mcp_remote-browser.json +8 -0
  113. package/data/security-reports/audits/2026-02-26T05-52-37-259Z-mcp_remote-browser.json +8 -0
  114. package/data/security-reports/audits/2026-02-26T05-52-37-274Z-mcp_remote-browser.json +8 -0
  115. package/data/security-reports/audits/2026-02-26T05-53-28-389Z-mcp_remote-browser.json +8 -0
  116. package/data/security-reports/audits/2026-02-26T05-53-28-391Z-mcp_remote-browser.json +8 -0
  117. package/data/security-reports/audits/2026-02-26T05-53-33-868Z-mcp_remote-browser.json +8 -0
  118. package/data/security-reports/audits/2026-02-26T05-53-33-880Z-mcp_remote-browser.json +8 -0
  119. package/data/security-reports/audits/2026-02-26T05-53-33-892Z-mcp_remote-browser.json +8 -0
  120. package/data/security-reports/audits/2026-02-26T05-53-33-900Z-mcp_remote-browser.json +8 -0
  121. package/data/security-reports/audits/2026-02-26T05-53-43-064Z-mcp_remote-browser.json +8 -0
  122. package/data/security-reports/audits/2026-02-26T05-53-43-066Z-mcp_remote-browser.json +8 -0
  123. package/data/security-reports/audits/2026-02-26T05-53-43-068Z-mcp_remote-browser.json +8 -0
  124. package/data/security-reports/audits/2026-02-26T14-55-47-466Z-claude-plugin_workspace-ops.json +8 -0
  125. package/data/security-reports/audits/2026-02-26T14-55-47-468Z-copilot-extension_repo-security.json +8 -0
  126. package/data/security-reports/audits/2026-02-26T16-55-59-431Z-mcp_remote-browser.json +8 -0
  127. package/data/security-reports/audits/2026-02-26T16-55-59-432Z-mcp_remote-browser.json +8 -0
  128. package/data/security-reports/audits/2026-02-26T16-55-59-435Z-mcp_remote-browser.json +8 -0
  129. package/data/security-reports/audits/2026-02-26T16-55-59-439Z-mcp_remote-browser.json +8 -0
  130. package/data/security-reports/audits/2026-02-26T16-56-08-566Z-mcp_remote-browser.json +8 -0
  131. package/data/security-reports/audits/2026-02-26T16-56-08-570Z-mcp_remote-browser.json +8 -0
  132. package/data/security-reports/audits/2026-02-26T16-56-08-589Z-mcp_remote-browser.json +8 -0
  133. package/data/security-reports/audits/2026-02-26T16-56-08-591Z-mcp_remote-browser.json +8 -0
  134. package/data/security-reports/audits/2026-02-26T16-56-47-356Z-mcp_remote-browser.json +8 -0
  135. package/data/security-reports/audits/2026-02-26T16-56-47-358Z-mcp_remote-browser.json +8 -0
  136. package/data/security-reports/audits/2026-02-26T16-56-53-607Z-mcp_remote-browser.json +8 -0
  137. package/data/security-reports/audits/2026-02-26T16-56-53-612Z-mcp_remote-browser.json +8 -0
  138. package/data/security-reports/audits/2026-02-26T16-56-53-624Z-mcp_remote-browser.json +8 -0
  139. package/data/security-reports/audits/2026-02-26T16-56-53-628Z-mcp_remote-browser.json +8 -0
  140. package/data/security-reports/audits/2026-02-26T16-57-09-879Z-mcp_remote-browser.json +8 -0
  141. package/data/security-reports/audits/2026-02-26T16-57-09-881Z-mcp_remote-browser.json +8 -0
  142. package/data/security-reports/audits/2026-02-26T16-57-10-846Z-mcp_remote-browser.json +8 -0
  143. package/data/security-reports/audits/2026-02-26T16-57-10-848Z-mcp_remote-browser.json +8 -0
  144. package/data/security-reports/audits/2026-03-10T18-15-05-007Z-claude-plugin_playwright.json +8 -0
  145. package/data/security-reports/audits/2026-03-10T18-36-16-092Z-claude-plugin_playwright.json +8 -0
  146. package/data/whitelist/approved.json +5 -0
  147. package/dist/catalog/adapter.js +39 -0
  148. package/dist/catalog/adapters/claude-code-marketplace-v1.js +260 -0
  149. package/dist/catalog/adapters/claude-connectors-scrape-v1.js +107 -0
  150. package/dist/catalog/adapters/claude-plugins-scrape-v1.js +107 -0
  151. package/dist/catalog/adapters/claude-plugins-v0.1.js +48 -0
  152. package/dist/catalog/adapters/copilot-extensions-v0.1.js +48 -0
  153. package/dist/catalog/adapters/copilot-plugin-marketplace-v1.js +117 -0
  154. package/dist/catalog/adapters/mcp-registry-v0.1.js +211 -0
  155. package/dist/catalog/adapters/openai-skills-github-v1.js +100 -0
  156. package/dist/catalog/adapters/openai-skills-v1.js +48 -0
  157. package/dist/catalog/adapters/shared.js +94 -0
  158. package/dist/catalog/remote-registry.js +196 -0
  159. package/dist/catalog/repository.js +161 -0
  160. package/dist/catalog/sync-state.js +61 -0
  161. package/dist/catalog/sync.js +153 -0
  162. package/dist/cli.js +25 -0
  163. package/dist/commands/ExplainerVideo.js +225 -0
  164. package/dist/commands/ingest.js +11 -0
  165. package/dist/commands/validate-data.js +10 -0
  166. package/dist/config/runtime.js +51 -0
  167. package/dist/config/sources.js +21 -0
  168. package/dist/ingestion/mcps.js +77 -0
  169. package/dist/ingestion/skills.js +76 -0
  170. package/dist/install/dependencies.js +58 -0
  171. package/dist/install/review-state.js +70 -0
  172. package/dist/install/skillsh.js +245 -0
  173. package/dist/interfaces/cli/doctor.js +90 -0
  174. package/dist/interfaces/cli/formatters/colors.js +24 -0
  175. package/dist/interfaces/cli/formatters/csv.js +10 -0
  176. package/dist/interfaces/cli/formatters/json.js +3 -0
  177. package/dist/interfaces/cli/formatters/markdown.js +6 -0
  178. package/dist/interfaces/cli/formatters/table.js +82 -0
  179. package/dist/interfaces/cli/index.js +1277 -0
  180. package/dist/interfaces/cli/options.js +93 -0
  181. package/dist/interfaces/cli/output.js +9 -0
  182. package/dist/interfaces/cli/types.js +1 -0
  183. package/dist/interfaces/cli/ui/home.js +114 -0
  184. package/dist/interfaces/cli/ui/web-report.js +384 -0
  185. package/dist/interfaces/cli/update-check.js +180 -0
  186. package/dist/lib/json.js +11 -0
  187. package/dist/lib/logger.js +13 -0
  188. package/dist/lib/paths.js +18 -0
  189. package/dist/lib/validation/contracts.js +245 -0
  190. package/dist/mcps/normalize.js +38 -0
  191. package/dist/models/records.js +31 -0
  192. package/dist/recommendation/engine.js +135 -0
  193. package/dist/recommendation/project-analysis.js +231 -0
  194. package/dist/recommendation/requirements.js +58 -0
  195. package/dist/security/assessment.js +56 -0
  196. package/dist/security/whitelist.js +70 -0
  197. package/dist/skills/normalize.js +39 -0
  198. package/dist/validation/curated.js +72 -0
  199. package/dist/video/Root.js +6 -0
  200. package/dist/video/index.js +3 -0
  201. package/package.json +102 -0
@@ -0,0 +1,153 @@
1
+ import { loadProviders, loadRegistries } from '../config/runtime.js';
2
+ import { logger } from '../lib/logger.js';
3
+ import { CatalogItemSchema } from '../lib/validation/contracts.js';
4
+ import { adaptRegistryEntries } from './adapter.js';
5
+ import { resolveRegistryEntries } from './remote-registry.js';
6
+ import { getStaleRegistries, getUpdatedSince, loadSyncState, saveSyncState, setSuccessfulSync, setUpdatedSince } from './sync-state.js';
7
+ import { saveCatalogItems, saveLegacyCatalogViews } from './repository.js';
8
+ export async function syncCatalogs(today = new Date().toISOString().slice(0, 10), options = {}) {
9
+ const effectiveToday = process.env.SKILLS_MCPS_SYNC_TODAY || today;
10
+ const [registries, providers] = await Promise.all([loadRegistries(), loadProviders()]);
11
+ let syncState = await loadSyncState();
12
+ const selectedKinds = options.kinds?.length ? new Set(options.kinds) : null;
13
+ const allItems = [];
14
+ for (const registry of registries) {
15
+ if (selectedKinds && !selectedKinds.has(registry.kind)) {
16
+ continue;
17
+ }
18
+ if (registry.remote?.provider) {
19
+ const provider = providers.get(registry.remote.provider);
20
+ if (provider && provider.officialOnly && registry.sourceType === 'community-list' && !registry.officialOnly) {
21
+ logger.warn(`Skipping non-official registry ${registry.id} due to provider policy (${provider.id})`);
22
+ continue;
23
+ }
24
+ }
25
+ const updatedSince = registry.remote?.supportsUpdatedSince ? getUpdatedSince(syncState, registry.id) : undefined;
26
+ const resolved = await resolveRegistryEntries(registry, { updatedSince });
27
+ const adaptedEntries = resolved.source === 'remote' ? adaptRegistryEntries(registry, resolved.entries) : resolved.entries;
28
+ // Always include curated local entries so they persist even when remote succeeds
29
+ const curatedEntries = resolved.source === 'remote' ? registry.entries : [];
30
+ allItems.push(...normalizeItems([...adaptedEntries, ...curatedEntries], registry, effectiveToday));
31
+ const nowStamp = new Date().toISOString();
32
+ syncState = setSuccessfulSync(syncState, registry.id, nowStamp);
33
+ if (registry.remote?.supportsUpdatedSince && resolved.source === 'remote') {
34
+ syncState = setUpdatedSince(syncState, registry.id, nowStamp);
35
+ }
36
+ }
37
+ const mergedItems = mergeItemsById(allItems);
38
+ await Promise.all([saveCatalogItems(mergedItems), saveLegacyCatalogViews(mergedItems)]);
39
+ await saveSyncState(syncState);
40
+ const kindCounts = countByKind(mergedItems);
41
+ logger.info(`Synced ${mergedItems.length} items (${kindCounts.skill} skills, ${kindCounts.mcp} MCPs, ${kindCounts['claude-plugin']} Claude plugins, ${kindCounts['claude-connector']} Claude connectors, ${kindCounts['copilot-extension']} Copilot extensions)`);
42
+ const staleRegistries = getStaleRegistries(syncState);
43
+ if (staleRegistries.length > 0) {
44
+ logger.warn(`Stale registries (>48h without success): ${staleRegistries.join(', ')}`);
45
+ }
46
+ return { items: mergedItems, staleRegistries };
47
+ }
48
+ function normalizeItems(records, registry, today) {
49
+ return records
50
+ .map((entry) => {
51
+ const value = ensureObject(entry);
52
+ const entryMetadata = value.metadata && typeof value.metadata === 'object' && !Array.isArray(value.metadata)
53
+ ? value.metadata
54
+ : {};
55
+ return CatalogItemSchema.parse({
56
+ ...value,
57
+ id: normalizeId(String(value.id ?? ''), registry.kind),
58
+ kind: value.kind ?? registry.kind,
59
+ provider: value.provider ?? registry.remote?.provider ?? inferProviderFromKind(registry.kind),
60
+ source: value.source ?? registry.id,
61
+ lastSeenAt: value.lastSeenAt ?? today,
62
+ metadata: {
63
+ ...entryMetadata,
64
+ sourceRegistryId: registry.id,
65
+ sourceType: entryMetadata.sourceType ?? registry.sourceType,
66
+ sourceConfidence: entryMetadata.sourceConfidence ?? defaultSourceConfidence(registry.sourceType)
67
+ }
68
+ });
69
+ })
70
+ .sort((a, b) => a.id.localeCompare(b.id));
71
+ }
72
+ function ensureObject(value) {
73
+ if (!value || typeof value !== 'object' || Array.isArray(value)) {
74
+ throw new Error('Invalid registry entry: expected object');
75
+ }
76
+ return value;
77
+ }
78
+ function normalizeId(id, kind) {
79
+ if (id.startsWith(`${kind}:`)) {
80
+ return id;
81
+ }
82
+ const prefixMap = {
83
+ skill: 'skill',
84
+ mcp: 'mcp',
85
+ 'claude-plugin': 'claude-plugin',
86
+ 'claude-connector': 'claude-connector',
87
+ 'copilot-extension': 'copilot-extension'
88
+ };
89
+ return `${prefixMap[kind]}:${id.replace(/^([a-z-]+):/, '')}`;
90
+ }
91
+ function inferProviderFromKind(kind) {
92
+ if (kind === 'mcp') {
93
+ return 'mcp';
94
+ }
95
+ if (kind === 'claude-plugin') {
96
+ return 'anthropic';
97
+ }
98
+ if (kind === 'claude-connector') {
99
+ return 'anthropic';
100
+ }
101
+ if (kind === 'copilot-extension') {
102
+ return 'github';
103
+ }
104
+ return 'openai';
105
+ }
106
+ function mergeItemsById(items) {
107
+ const state = new Map();
108
+ for (const item of items) {
109
+ const existing = state.get(item.id);
110
+ if (!existing) {
111
+ state.set(item.id, item);
112
+ continue;
113
+ }
114
+ state.set(item.id, {
115
+ ...existing,
116
+ name: item.name.length > existing.name.length ? item.name : existing.name,
117
+ description: item.description.length > existing.description.length ? item.description : existing.description,
118
+ capabilities: dedupe([...existing.capabilities, ...item.capabilities]),
119
+ compatibility: dedupe([...existing.compatibility, ...item.compatibility]),
120
+ maintenanceSignal: Math.max(existing.maintenanceSignal, item.maintenanceSignal),
121
+ adoptionSignal: Math.max(existing.adoptionSignal, item.adoptionSignal),
122
+ provenanceSignal: Math.max(existing.provenanceSignal, item.provenanceSignal),
123
+ freshnessSignal: Math.max(existing.freshnessSignal, item.freshnessSignal),
124
+ lastSeenAt: item.lastSeenAt >= existing.lastSeenAt ? item.lastSeenAt : existing.lastSeenAt,
125
+ metadata: {
126
+ ...existing.metadata,
127
+ ...item.metadata
128
+ }
129
+ });
130
+ }
131
+ return Array.from(state.values()).sort((a, b) => a.id.localeCompare(b.id));
132
+ }
133
+ function dedupe(values) {
134
+ return Array.from(new Set(values.filter((value) => value.trim().length > 0))).sort((a, b) => a.localeCompare(b));
135
+ }
136
+ function countByKind(items) {
137
+ return items.reduce((acc, item) => {
138
+ acc[item.kind] += 1;
139
+ return acc;
140
+ }, {
141
+ skill: 0,
142
+ mcp: 0,
143
+ 'claude-plugin': 0,
144
+ 'claude-connector': 0,
145
+ 'copilot-extension': 0
146
+ });
147
+ }
148
+ function defaultSourceConfidence(sourceType) {
149
+ if (sourceType === 'vendor-feed' || sourceType === 'public-index') {
150
+ return 'official';
151
+ }
152
+ return 'vetted-curated';
153
+ }
package/dist/cli.js ADDED
@@ -0,0 +1,25 @@
1
+ #!/usr/bin/env node
2
+ import { logger } from './lib/logger.js';
3
+ import { runCli } from './interfaces/cli/index.js';
4
+ const [command, ...rest] = process.argv.slice(2);
5
+ async function run() {
6
+ if (!command) {
7
+ await runCli([]);
8
+ return;
9
+ }
10
+ if (command === 'ingest') {
11
+ logger.warn('Deprecated command "ingest". Use "sync" instead.');
12
+ await runCli(['sync']);
13
+ return;
14
+ }
15
+ if (command === 'validate') {
16
+ logger.warn('Deprecated command "validate". Use "whitelist verify" instead.');
17
+ await runCli(['whitelist', 'verify']);
18
+ return;
19
+ }
20
+ await runCli([command, ...rest]);
21
+ }
22
+ run().catch((error) => {
23
+ logger.error(error instanceof Error ? error.message : String(error));
24
+ process.exitCode = 1;
25
+ });
@@ -0,0 +1,225 @@
1
+ import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
2
+ import { AbsoluteFill, Sequence, interpolate, spring, useCurrentFrame, useVideoConfig } from 'remotion';
3
+ const palette = {
4
+ bg: '#050b1a',
5
+ bgGlow: '#12203a',
6
+ panel: '#0f172a',
7
+ panelBorder: '#2b3a55',
8
+ text: '#e5e7eb',
9
+ muted: '#93a4c0',
10
+ accent: '#22d3ee',
11
+ success: '#22c55e',
12
+ warn: '#f59e0b',
13
+ danger: '#ef4444',
14
+ info: '#60a5fa'
15
+ };
16
+ const sceneDuration = 300;
17
+ const sceneCount = 11;
18
+ const FadeScene = ({ duration, children }) => {
19
+ const local = useCurrentFrame();
20
+ const fade = Math.min(24, Math.floor(duration * 0.1));
21
+ const opacity = interpolate(local, [0, fade, duration], [0, 1, 1], {
22
+ extrapolateLeft: 'clamp',
23
+ extrapolateRight: 'clamp'
24
+ });
25
+ return _jsx(AbsoluteFill, { style: { opacity }, children: children });
26
+ };
27
+ const toneColor = (tone) => {
28
+ if (tone === 'success')
29
+ return palette.success;
30
+ if (tone === 'warn')
31
+ return palette.warn;
32
+ if (tone === 'danger')
33
+ return palette.danger;
34
+ if (tone === 'info')
35
+ return palette.info;
36
+ return palette.muted;
37
+ };
38
+ const Panel = ({ title, tone = 'normal', children }) => (_jsxs("div", { style: {
39
+ backgroundColor: palette.panel,
40
+ border: `2px solid ${palette.panelBorder}`,
41
+ borderRadius: 16,
42
+ padding: 20,
43
+ boxShadow: '0 16px 40px rgba(0,0,0,0.35)',
44
+ height: '100%'
45
+ }, children: [_jsx("div", { style: { fontSize: 18, color: toneColor(tone), fontWeight: 700, marginBottom: 12 }, children: title }), children] }));
46
+ const TerminalBlock = ({ command, lines, tone = 'normal' }) => (_jsx(Panel, { title: `$ ${command}`, tone: tone, children: _jsx("div", { style: { display: 'grid', gap: 8 }, children: lines.map((line, index) => (_jsx("div", { style: {
47
+ fontFamily: 'Menlo, Monaco, Consolas, monospace',
48
+ fontSize: 18,
49
+ whiteSpace: 'pre-wrap',
50
+ color: index === lines.length - 1 ? toneColor(tone) : palette.text
51
+ }, children: line }, `${index}-${line}`))) }) }));
52
+ const BulletBlock = ({ title, items, tone = 'info' }) => (_jsx(Panel, { title: title, tone: tone, children: _jsx("div", { style: { display: 'grid', gap: 10 }, children: items.map((item, index) => (_jsx("div", { style: { fontSize: 20, color: palette.text, lineHeight: 1.35 }, children: `${index + 1}. ${item}` }, `${index}-${item}`))) }) }));
53
+ const SceneShell = ({ title, subtitle, sceneNumber, kpis, left, right, footer }) => {
54
+ const frame = useCurrentFrame();
55
+ const { fps } = useVideoConfig();
56
+ const rise = spring({ frame, fps, config: { damping: 16 } });
57
+ const pulse = interpolate(Math.sin(frame / 18), [-1, 1], [0.35, 0.75]);
58
+ const completion = sceneNumber / sceneCount;
59
+ return (_jsxs(AbsoluteFill, { style: {
60
+ fontFamily: 'ui-sans-serif, -apple-system, Segoe UI, Helvetica, Arial, sans-serif',
61
+ color: palette.text,
62
+ background: `radial-gradient(circle at 20% 20%, ${palette.bgGlow} 0%, ${palette.bg} 58%)`,
63
+ padding: '34px 44px',
64
+ gap: 18
65
+ }, children: [_jsxs("div", { style: {
66
+ border: `1px solid ${palette.panelBorder}`,
67
+ borderRadius: 12,
68
+ padding: '10px 14px',
69
+ backgroundColor: 'rgba(15,23,42,0.75)'
70
+ }, children: [_jsxs("div", { style: { display: 'flex', justifyContent: 'space-between', alignItems: 'center', marginBottom: 8 }, children: [_jsx("div", { style: { fontSize: 16, color: palette.accent, fontWeight: 700 }, children: `Walkthrough ${sceneNumber}/${sceneCount}` }), _jsx("div", { style: { fontSize: 15, color: palette.muted }, children: 'toolkit | skills + mcp + plugin security intelligence' })] }), _jsx("div", { style: { height: 8, borderRadius: 999, backgroundColor: '#1e293b', overflow: 'hidden' }, children: _jsx("div", { style: {
71
+ width: `${completion * 100}%`,
72
+ height: '100%',
73
+ borderRadius: 999,
74
+ background: `linear-gradient(90deg, ${palette.info} 0%, ${palette.accent} 100%)`,
75
+ opacity: pulse
76
+ } }) })] }), _jsxs("div", { style: { transform: `translateY(${(1 - rise) * 18}px)` }, children: [_jsx("h1", { style: { margin: 0, fontSize: 48, lineHeight: 1.08 }, children: title }), _jsx("p", { style: { margin: '10px 0 0 0', fontSize: 24, color: palette.accent }, children: subtitle })] }), _jsx("div", { style: { display: 'flex', gap: 10, flexWrap: 'wrap' }, children: kpis.map((kpi, index) => (_jsx("div", { style: {
77
+ border: `1px solid ${palette.panelBorder}`,
78
+ borderRadius: 999,
79
+ padding: '7px 12px',
80
+ backgroundColor: 'rgba(15,23,42,0.82)',
81
+ color: palette.text,
82
+ fontSize: 15
83
+ }, children: kpi }, `${index}-${kpi}`))) }), _jsxs("div", { style: { flex: 1, minHeight: 0, display: 'grid', gridTemplateColumns: '1.15fr 1fr', gap: 16 }, children: [_jsx("div", { style: { minHeight: 0 }, children: left }), _jsx("div", { style: { minHeight: 0 }, children: right })] }), _jsx("div", { style: {
84
+ border: `1px solid ${palette.panelBorder}`,
85
+ borderRadius: 12,
86
+ backgroundColor: 'rgba(15,23,42,0.82)',
87
+ padding: '10px 14px',
88
+ display: 'grid',
89
+ gridTemplateColumns: 'repeat(3, minmax(0, 1fr))',
90
+ gap: 10
91
+ }, children: footer.map((line, index) => (_jsx("div", { style: { color: palette.muted, fontSize: 15 }, children: line }, `${index}-${line}`))) })] }));
92
+ };
93
+ const SceneIntro = () => (_jsx(SceneShell, { sceneNumber: 1, title: "Live CLI Session: Verified End-to-End", subtitle: "Real commands and outputs captured from this repository runtime", kpis: ['about ✓', 'doctor ✓/fail gate', 'recommend ✓', 'sync dry-run ✓'], left: _jsx(TerminalBlock, { command: "npm run about", lines: [
94
+ 'toolkit v0.3.0',
95
+ 'Toolkit: Skills + MCP + Plugin security intelligence framework',
96
+ 'Scope: skills, MCP servers, Claude plugins, Copilot extensions',
97
+ 'Ranking: trust-first (fit + trust - risk penalties + freshness bonus)',
98
+ 'Sources: official-first provider registries with local fallback'
99
+ ] }), right: _jsx(BulletBlock, { title: "Session proof points", items: [
100
+ 'Catalog status reports 6 loaded items across 4 kinds.',
101
+ 'Doctor now hard-fails when skill.sh is not installed.',
102
+ 'Recommendations return trust/risk tables and safe filters.',
103
+ 'Risk assessment correctly blocks high-risk installs.'
104
+ ] }), footer: ['Outcome: source-of-truth catalog', 'Audience: platform + app teams', 'Mode: automation-safe + human-friendly'] }));
105
+ const SceneInit = () => (_jsx(SceneShell, { sceneNumber: 2, title: "1) First-Run Init Wizard", subtitle: "Local project defaults are generated automatically", kpis: ['Writes .skills-mcps.json', 'Provider defaults', 'Risk posture preset', 'Optional initial sync'], left: _jsx(TerminalBlock, { command: "npm run init -- --project .", tone: "success", lines: [
106
+ '? kinds: skill,mcp,claude-plugin,copilot-extension',
107
+ '? providers: anthropic,github,mcp,openai',
108
+ '? risk posture: strict',
109
+ 'Created .skills-mcps.json and initialized onboarding stamp'
110
+ ] }), right: _jsx(BulletBlock, { title: "Generated defaults", items: [
111
+ 'Preferred kinds and providers for this repository.',
112
+ 'Default output mode for human vs automation usage.',
113
+ 'Risk posture that controls installation policy.',
114
+ 'Onboarding state used by doctor and status.'
115
+ ], tone: "success" }), footer: ['Example file: .skills-mcps.json', 'No server required', 'Project-local and portable'] }));
116
+ const SceneDoctor = () => (_jsx(SceneShell, { sceneNumber: 3, title: "2) Doctor Diagnostics", subtitle: "Real output with required skill.sh enforcement", kpis: ['Pass/Warn/Fail summary', 'CLI dependency checks', 'Catalog freshness checks', 'Actionable fixes'], left: _jsx(TerminalBlock, { command: "npm run doctor", tone: "danger", lines: [
117
+ 'skill.sh fail skill.sh not found',
118
+ 'gh pass gh available',
119
+ 'Node version pass Node 22.16.0',
120
+ 'Catalog pass 6 items loaded',
121
+ 'Hint: Resolve failing checks before installation workflows.'
122
+ ] }), right: _jsx(BulletBlock, { title: "Required action", items: [
123
+ 'Install skill.sh and verify with skill.sh --version.',
124
+ 'Run doctor again until all required checks pass.',
125
+ 'Keep doctor as a preflight in team scripts/CI.',
126
+ 'Install command preflight also enforces binary presence.'
127
+ ], tone: "danger" }), footer: ['Command latency: seconds', 'Human-readable by default', 'Machine parsing available via json'] }));
128
+ const SceneSync = () => (_jsx(SceneShell, { sceneNumber: 4, title: "3) Catalog Sync Across Providers", subtitle: "Incremental ingestion, adapter normalization, deterministic merge", kpis: ['Remote + local fallback', 'Adapter-based mapping', 'Stable ID merge', 'Dual-write compatibility'], left: _jsx(TerminalBlock, { command: "npm run sync -- --dry-run", lines: [
129
+ 'community-skills-index (skill) entries=2 remote=yes',
130
+ 'public-mcp-directory (mcp) entries=2 remote=yes',
131
+ 'official-claude-plugins (claude-plugin) entries=1 remote=yes',
132
+ 'official-copilot-extensions (copilot-extension) entries=1 remote=yes',
133
+ 'Hint: Run without --dry-run to persist synced catalogs.'
134
+ ] }), right: _jsx(BulletBlock, { title: "Normalization pipeline", items: [
135
+ 'Resolve registry entries with updatedSince cursors.',
136
+ 'Apply provider adapter to common CatalogItem contract.',
137
+ 'Validate schema with Zod and reject malformed payloads.',
138
+ 'Persist unified catalog + compatibility views.'
139
+ ] }), footer: ['Schedule: daily via Actions', 'Manual refresh: sync command', 'Stale registry threshold: 48h'] }));
140
+ const SceneBrowse = () => (_jsx(SceneShell, { sceneNumber: 5, title: "4) Discovery: List, Search, Show, Top", subtitle: "Browse broadly, filter hard, inspect deeply", kpis: ['Kind/provider filters', 'Search by capability', 'Detailed item view', 'Top picks by context'], left: _jsx(TerminalBlock, { command: "npm run list -- --kind mcp --limit 5", lines: [
141
+ 'ID TYPE PROVIDER RISK BLOCKED',
142
+ 'mcp:filesystem mcp mcp low(10) false',
143
+ 'mcp:remote-browser mcp mcp high(59) true',
144
+ 'Tip: npm run show -- --id mcp:filesystem'
145
+ ] }), right: _jsx(TerminalBlock, { command: "npm run search -- security && npm run show -- --id mcp:filesystem", tone: "info", lines: [
146
+ 'copilot-extension:repo-security match=140',
147
+ 'skill:secure-prompting match=30',
148
+ 'show output includes: provider=mcp, transport=stdio,',
149
+ 'risk.tier=low, policyStatus.approvedInWhitelist=true'
150
+ ] }), footer: ['Works for empty and mature repos', 'Consistent IDs across commands', 'Fast iteration for platform teams'] }));
151
+ const SceneRecommend = () => (_jsx(SceneShell, { sceneNumber: 6, title: "5) Trust-First Recommendation Engine", subtitle: "High-quality defaults with transparent scoring breakdown", kpis: ['rankScore + breakdown', 'blocked + reason flags', 'Sort and filter controls', 'Safe-mode recommendations'], left: _jsx(TerminalBlock, { command: "npm run recommend -- --project . --only-safe --sort trust --limit 5", tone: "success", lines: [
152
+ 'mcp:filesystem rank=51.7 trust=41.8 low(10) false',
153
+ 'copilot-extension:repo-security rank=50.6 trust=38.6 low(0) false',
154
+ 'skill:secure-prompting rank=50.9 trust=37.8 low(0) false',
155
+ 'claude-plugin:workspace-ops rank=42.6 trust=37.2 low(0) false'
156
+ ] }), right: _jsx(BulletBlock, { title: "Score composition example", items: [
157
+ 'Base fit: compatibility + capability coverage.',
158
+ 'Trust core: provenance + maintenance + adoption.',
159
+ 'Security penalty: strongest weight, hard blocks for risky tiers.',
160
+ 'Freshness bonus: recently validated metadata scores higher.'
161
+ ], tone: "success" }), footer: ['Use --format json for automation', 'Use --kind and --provider for slicing', 'Use --only-safe in production'] }));
162
+ const SceneExport = () => (_jsx(SceneShell, { sceneNumber: 7, title: "6) Export and Governance Reporting", subtitle: "Generate decision artifacts for docs, reviews, and policy audits", kpis: ['CSV export', 'Markdown export', 'Deterministic columns', 'CI-friendly outputs'], left: _jsx(TerminalBlock, { command: "npm run dev -- recommend --project . --export csv --out recommendations.csv", tone: "success", lines: ['Exported 5 recommendations to recommendations.csv', 'Columns: id, kind, provider, rank, trust, fit, risk, blocked'] }), right: _jsx(TerminalBlock, { command: "npm run dev -- recommend --project . --export md --out recommendations.md", tone: "info", lines: [
163
+ 'Exported 5 recommendations to recommendations.md',
164
+ 'Share in architecture docs and security sign-off reviews',
165
+ 'Stable output schema keeps diffs clean across runs'
166
+ ] }), footer: ['Great for ADR attachments', 'No spreadsheet tooling needed', 'Output files are repo-friendly'] }));
167
+ const SceneAssessInstall = () => (_jsx(SceneShell, { sceneNumber: 8, title: "7) Risk Assessment + Install Gates", subtitle: "Install flow enforces policy before any side effects", kpis: ['Assessed risk tier', 'Block-by-default high risk', 'Explicit override support', 'Install audit trail'], left: _jsx(TerminalBlock, { command: "npm run dev -- assess --id mcp:remote-browser", tone: "warn", lines: [
168
+ 'id: mcp:remote-browser',
169
+ 'riskScore: 59',
170
+ 'riskTier: high',
171
+ 'scanner findings: vuln=1 suspicious=2 injection=1 exfiltration=1'
172
+ ] }), right: _jsx(TerminalBlock, { command: "npm run dev -- install --id mcp:remote-browser --yes", tone: "danger", lines: [
173
+ 'Blocked by security policy (high, score=59).',
174
+ 'Use --override-risk only with explicit acceptance.',
175
+ 'If installer is skill.sh, missing binary also fails preflight.'
176
+ ] }), footer: ['Safer defaults for platform teams', 'Override is explicit and auditable', 'Ideal for enterprise change control'] }));
177
+ const SceneSecurityOps = () => (_jsx(SceneShell, { sceneNumber: 9, title: "8) Whitelist + Quarantine Operations", subtitle: "Continuously contain risky catalog entries", kpis: ['Whitelist verification', 'Quarantine application', 'Daily security workflow', 'Stale-source visibility'], left: _jsx(TerminalBlock, { command: "npm run whitelist:verify", tone: "warn", lines: [
178
+ 'reportPath: data/security-reports/YYYY-MM-DD/report.json',
179
+ 'passed: 9 failed: 1 staleRegistries: 0',
180
+ 'Non-whitelisted risky entries are candidates for quarantine'
181
+ ] }), right: _jsx(TerminalBlock, { command: "npm run quarantine:apply -- --report data/security-reports/YYYY-MM-DD/report.json", tone: "danger", lines: ['Applied quarantine entries: 1', 'Catalog state updated to prevent accidental installation', 'Daily Security workflow can automate this path'] }), footer: ['Security posture stays current', 'Automates recurring policy enforcement', 'Visible in CLI status and reports'] }));
182
+ const SceneCI = () => (_jsx(SceneShell, { sceneNumber: 10, title: "9) CI/CD Security and Validation Stack", subtitle: "Hard gates on quality, dependency risk, secrets, and code scanning", kpis: ['CI lint/test/build', 'CodeQL', 'Dependency Review', 'Secrets + Trivy + SBOM'], left: _jsx(TerminalBlock, { command: "GitHub Actions required checks", tone: "success", lines: [
183
+ 'CI / build',
184
+ 'Security / CodeQL',
185
+ 'Security / Dependency Review',
186
+ 'Security / Secrets',
187
+ 'Security / SBOM + Trivy'
188
+ ] }), right: _jsx(BulletBlock, { title: "Release confidence gains", items: [
189
+ 'Strict PR gates block risky changes before merge.',
190
+ 'Scheduled scans catch drift on main branch.',
191
+ 'Catalog sync and daily security maintain freshness.',
192
+ 'Badges in README expose operational trust signals.'
193
+ ], tone: "success" }), footer: ['Goal: minimum risk to publish', 'No hidden security debt', 'Trust is visible to users immediately'] }));
194
+ const SceneOutro = () => (_jsx(SceneShell, { sceneNumber: 11, title: "Framework Ready for Real Projects", subtitle: "Real usage validated: command flow, policy gates, and outputs", kpis: ['Simple onboarding', 'Rich CLI visualization', 'Trust-first ranking', 'Production security gates'], left: _jsx(TerminalBlock, { command: "Operator flow (real session)", tone: "success", lines: [
195
+ '1) npm run about',
196
+ '2) npm run doctor',
197
+ '3) npm run list/search/show',
198
+ '4) npm run recommend',
199
+ '5) npm run dev -- assess/install'
200
+ ] }), right: _jsx(BulletBlock, { title: "Final operator flow", items: [
201
+ 'init once per repo',
202
+ 'doctor before high-impact operations',
203
+ 'sync daily and recommend safely',
204
+ 'assess/install with policy gates always on'
205
+ ], tone: "info" }), footer: ['Repository: github.com/amitrintzler/skills-and-mcps', 'Use README quick links for full command map', 'Toolkit: built for speed + safety'] }));
206
+ export const ExplainerVideo = () => {
207
+ const scenes = [
208
+ SceneIntro,
209
+ SceneInit,
210
+ SceneDoctor,
211
+ SceneSync,
212
+ SceneBrowse,
213
+ SceneRecommend,
214
+ SceneExport,
215
+ SceneAssessInstall,
216
+ SceneSecurityOps,
217
+ SceneCI,
218
+ SceneOutro
219
+ ];
220
+ return (_jsx(AbsoluteFill, { style: { backgroundColor: palette.bg }, children: scenes.map((Scene, index) => {
221
+ const from = index * sceneDuration;
222
+ return (_jsx(Sequence, { from: from, durationInFrames: sceneDuration, children: _jsx(FadeScene, { duration: sceneDuration, children: _jsx(Scene, {}) }) }, from));
223
+ }) }));
224
+ };
225
+ export const walkthroughDurationInFrames = sceneDuration * sceneCount;
@@ -0,0 +1,11 @@
1
+ import { logger } from '../lib/logger.js';
2
+ import { runCli } from '../interfaces/cli/index.js';
3
+ async function run() {
4
+ const args = process.argv.slice(2);
5
+ logger.warn(`Deprecated script "ingest". Forwarding to "sync" with args: ${args.join(' ')}`);
6
+ await runCli(['sync', ...args]);
7
+ }
8
+ run().catch((error) => {
9
+ logger.error('Ingestion failed', error);
10
+ process.exitCode = 1;
11
+ });
@@ -0,0 +1,10 @@
1
+ import { logger } from '../lib/logger.js';
2
+ import { runCli } from '../interfaces/cli/index.js';
3
+ async function run() {
4
+ logger.warn('Deprecated script "validate:data". Running "whitelist verify".');
5
+ await runCli(['whitelist', 'verify']);
6
+ }
7
+ run().catch((error) => {
8
+ logger.error(error instanceof Error ? error.message : 'Validation command failed');
9
+ process.exitCode = 1;
10
+ });
@@ -0,0 +1,51 @@
1
+ import { readJsonFile } from '../lib/json.js';
2
+ import { getPackagePath } from '../lib/paths.js';
3
+ import { ItemInsightsFileSchema, ProvidersFileSchema, RankingPolicySchema, RecommendationWeightsSchema, RegistriesFileSchema, SecurityPolicySchema } from '../lib/validation/contracts.js';
4
+ const REGISTRIES_PATH = getPackagePath('config/registries.json');
5
+ const SECURITY_POLICY_PATH = getPackagePath('config/security-policy.json');
6
+ const RANKING_POLICY_PATH = getPackagePath('config/ranking-policy.json');
7
+ const RECOMMENDATION_WEIGHTS_PATH = getPackagePath('config/recommendation-weights.json');
8
+ const PROVIDERS_PATH = getPackagePath('config/providers.json');
9
+ const ITEM_INSIGHTS_PATH = getPackagePath('config/item-insights.json');
10
+ export async function loadRegistries() {
11
+ const raw = await readJsonFile(REGISTRIES_PATH);
12
+ const parsed = RegistriesFileSchema.parse(raw);
13
+ return parsed.registries.filter((registry) => registry.enabled);
14
+ }
15
+ export async function loadSecurityPolicy() {
16
+ const raw = await readJsonFile(SECURITY_POLICY_PATH);
17
+ return SecurityPolicySchema.parse(raw);
18
+ }
19
+ export async function loadRankingPolicy() {
20
+ try {
21
+ const raw = await readJsonFile(RANKING_POLICY_PATH);
22
+ return RankingPolicySchema.parse(raw);
23
+ }
24
+ catch {
25
+ const fallback = RecommendationWeightsSchema.parse(await readJsonFile(RECOMMENDATION_WEIGHTS_PATH));
26
+ return RankingPolicySchema.parse({
27
+ weights: {
28
+ compatibility: fallback.compatibility,
29
+ capabilityCoverage: fallback.capabilityCoverage,
30
+ maintenance: fallback.maintenance,
31
+ provenance: 18,
32
+ adoption: fallback.adoption,
33
+ freshnessBonusMax: 8,
34
+ securityPenaltyMax: fallback.securityPenaltyMax,
35
+ blockedPenalty: 40
36
+ },
37
+ tieBreakers: ['trust', 'risk', 'name'],
38
+ blockedFloorTier: 'high'
39
+ });
40
+ }
41
+ }
42
+ export async function loadProviders() {
43
+ const raw = await readJsonFile(PROVIDERS_PATH);
44
+ const parsed = ProvidersFileSchema.parse(raw);
45
+ return new Map(parsed.providers.filter((provider) => provider.enabled).map((provider) => [provider.id, provider]));
46
+ }
47
+ export async function loadItemInsights() {
48
+ const raw = await readJsonFile(ITEM_INSIGHTS_PATH);
49
+ const parsed = ItemInsightsFileSchema.parse(raw);
50
+ return new Map(parsed.insights.map((item) => [item.id, item]));
51
+ }
@@ -0,0 +1,21 @@
1
+ import fs from 'fs-extra';
2
+ import { getPackagePath } from '../lib/paths.js';
3
+ const CONFIG_PATH = getPackagePath('config/sources.json');
4
+ export async function loadSourcesConfig() {
5
+ const exists = await fs.pathExists(CONFIG_PATH);
6
+ if (!exists) {
7
+ throw new Error(`Missing config at ${CONFIG_PATH}`);
8
+ }
9
+ const raw = await fs.readJson(CONFIG_PATH);
10
+ return {
11
+ skills: normalize(raw.skills ?? []),
12
+ mcps: normalize(raw.mcps ?? [])
13
+ };
14
+ }
15
+ function normalize(list) {
16
+ return list.map((item) => ({
17
+ ...item,
18
+ file: getPackagePath(item.file),
19
+ priority: Number.isFinite(item.priority) ? item.priority : 0
20
+ }));
21
+ }
@@ -0,0 +1,77 @@
1
+ import { loadSourcesConfig } from '../config/sources.js';
2
+ import { readJsonFile, writeJsonFile } from '../lib/json.js';
3
+ import { logger } from '../lib/logger.js';
4
+ import { getStatePath } from '../lib/paths.js';
5
+ import { McpSchema } from '../models/records.js';
6
+ const OUTPUT_PATH = getStatePath('data/curated/mcps.json');
7
+ export async function ingestMcps() {
8
+ const { mcps } = await loadSourcesConfig();
9
+ const records = await loadMcpRecords(mcps);
10
+ const merged = mergeMcpRecords(records);
11
+ await writeJsonFile(OUTPUT_PATH, merged);
12
+ logger.info(`Wrote ${merged.length} MCP rows to ${OUTPUT_PATH}`);
13
+ return merged;
14
+ }
15
+ async function loadMcpRecords(descriptors) {
16
+ const perSource = await Promise.all(descriptors.map(async (descriptor) => {
17
+ const payload = await readJsonFile(descriptor.file);
18
+ return payload.map((record) => ({
19
+ source: descriptor,
20
+ record: McpSchema.parse(record)
21
+ }));
22
+ }));
23
+ return perSource.flat();
24
+ }
25
+ function mergeMcpRecords(records) {
26
+ const state = new Map();
27
+ for (const entry of records) {
28
+ const key = `${entry.record.jobFamily}:${entry.record.level}`;
29
+ const existing = state.get(key);
30
+ if (!existing) {
31
+ state.set(key, { priority: entry.source.priority, record: entry.record });
32
+ continue;
33
+ }
34
+ const preferIncoming = shouldPreferIncoming(entry, existing);
35
+ if (preferIncoming) {
36
+ state.set(key, {
37
+ priority: entry.source.priority,
38
+ record: mergeMcp(entry.record, existing.record)
39
+ });
40
+ }
41
+ else {
42
+ state.set(key, {
43
+ priority: existing.priority,
44
+ record: mergeMcp(existing.record, entry.record)
45
+ });
46
+ }
47
+ }
48
+ return Array.from(state.values())
49
+ .map((entry) => entry.record)
50
+ .sort((a, b) => a.jobFamily.localeCompare(b.jobFamily) || a.level.localeCompare(b.level));
51
+ }
52
+ function shouldPreferIncoming(entry, existing) {
53
+ if (entry.source.priority !== existing.priority) {
54
+ return entry.source.priority > existing.priority;
55
+ }
56
+ return entry.record.lastBenchmark >= existing.record.lastBenchmark;
57
+ }
58
+ function mergeMcp(primary, secondary) {
59
+ const mergedSkillLinks = dedupeStrings([...primary.skillLinks, ...secondary.skillLinks]);
60
+ return {
61
+ ...primary,
62
+ baseRange: normalizeRange(primary.baseRange, secondary.baseRange),
63
+ geoModifier: { ...secondary.geoModifier, ...primary.geoModifier },
64
+ skillLinks: mergedSkillLinks.length > 0 ? mergedSkillLinks : primary.skillLinks,
65
+ lastBenchmark: primary.lastBenchmark >= secondary.lastBenchmark ? primary.lastBenchmark : secondary.lastBenchmark
66
+ };
67
+ }
68
+ function normalizeRange(preferred, secondary) {
69
+ const min = Math.min(preferred.min, secondary.min);
70
+ const max = Math.max(preferred.max, secondary.max);
71
+ const mid = Math.max(min, Math.min(preferred.mid, max));
72
+ return { min, mid, max };
73
+ }
74
+ function dedupeStrings(values) {
75
+ return Array.from(new Set(values)).sort((a, b) => a.localeCompare(b));
76
+ }
77
+ export { mergeMcpRecords, mergeMcp, dedupeStrings, normalizeRange };