@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,100 @@
1
+ import { dedupe, readString } from './shared.js';
2
+ export function adaptOpenAiSkillsGitHubEntries(sourceId, entries) {
3
+ return entries
4
+ .map((entry) => mapGitHubSkillEntry(sourceId, entry))
5
+ .filter((entry) => entry !== null);
6
+ }
7
+ function mapGitHubSkillEntry(sourceId, entry) {
8
+ if (!entry || typeof entry !== 'object' || Array.isArray(entry)) {
9
+ return null;
10
+ }
11
+ const record = entry;
12
+ const type = readString(record, ['type'])?.toLowerCase();
13
+ const path = readString(record, ['path']) ?? '';
14
+ const slug = readString(record, ['name']) ?? path.split('/').at(-1);
15
+ if (!slug || type !== 'dir') {
16
+ return null;
17
+ }
18
+ if (path && !/^skills\/\.curated\/[^/]+$/.test(path)) {
19
+ return null;
20
+ }
21
+ const normalizedSlug = slug.trim().toLowerCase();
22
+ const htmlUrl = readString(record, ['html_url']) ?? `https://github.com/openai/skills/tree/main/skills/.curated/${normalizedSlug}`;
23
+ const capabilityHints = inferCapabilitiesFromSlug(normalizedSlug);
24
+ return {
25
+ id: normalizedSlug.startsWith('skill:') ? normalizedSlug : `skill:${normalizedSlug}`,
26
+ kind: 'skill',
27
+ provider: 'openai',
28
+ name: toTitle(normalizedSlug),
29
+ description: `OpenAI curated Codex skill: ${normalizedSlug}.`,
30
+ capabilities: capabilityHints,
31
+ compatibility: inferCompatibilityFromSlug(normalizedSlug),
32
+ source: sourceId,
33
+ install: {
34
+ kind: 'manual',
35
+ instructions: `Install with skill-installer from openai/skills path skills/.curated/${normalizedSlug}`,
36
+ url: htmlUrl
37
+ },
38
+ adoptionSignal: 72,
39
+ maintenanceSignal: 82,
40
+ provenanceSignal: 96,
41
+ freshnessSignal: 80,
42
+ securitySignals: {
43
+ knownVulnerabilities: 0,
44
+ suspiciousPatterns: 0,
45
+ injectionFindings: 0,
46
+ exfiltrationSignals: 0,
47
+ integrityAlerts: 0
48
+ },
49
+ metadata: {
50
+ githubPath: path || `skills/.curated/${normalizedSlug}`,
51
+ githubUrl: htmlUrl
52
+ }
53
+ };
54
+ }
55
+ function inferCapabilitiesFromSlug(slug) {
56
+ const tokens = slug.split(/[-_/]/g).filter(Boolean);
57
+ const capabilities = [];
58
+ if (tokens.some((token) => ['security', 'threat', 'ownership'].includes(token))) {
59
+ capabilities.push('security');
60
+ }
61
+ if (tokens.some((token) => ['deploy', 'release', 'render', 'vercel', 'netlify', 'cloudflare'].includes(token))) {
62
+ capabilities.push('automation');
63
+ }
64
+ if (tokens.some((token) => ['docs', 'document', 'spec', 'meeting', 'knowledge', 'notion', 'linear'].includes(token))) {
65
+ capabilities.push('docs');
66
+ }
67
+ if (tokens.some((token) => ['playwright', 'screenshot', 'web', 'browser'].includes(token))) {
68
+ capabilities.push('browser-control');
69
+ }
70
+ if (tokens.some((token) => ['speech', 'transcribe', 'sora', 'imagegen', 'pdf', 'spreadsheet'].includes(token))) {
71
+ capabilities.push('content');
72
+ }
73
+ if (capabilities.length === 0) {
74
+ capabilities.push('automation');
75
+ }
76
+ return dedupe(capabilities);
77
+ }
78
+ function inferCompatibilityFromSlug(slug) {
79
+ const compatibility = ['general'];
80
+ if (slug.includes('github') || slug.includes('gh-')) {
81
+ compatibility.push('github');
82
+ }
83
+ if (slug.includes('notion')) {
84
+ compatibility.push('notion');
85
+ }
86
+ if (slug.includes('linear')) {
87
+ compatibility.push('linear');
88
+ }
89
+ if (slug.includes('playwright') || slug.includes('web-game')) {
90
+ compatibility.push('node');
91
+ }
92
+ return dedupe(compatibility);
93
+ }
94
+ function toTitle(slug) {
95
+ return slug
96
+ .split(/[-_/]/g)
97
+ .filter(Boolean)
98
+ .map((part) => part[0].toUpperCase() + part.slice(1))
99
+ .join(' ');
100
+ }
@@ -0,0 +1,48 @@
1
+ import { dedupe, extractStringArray, readNestedString, readNestedStringArray, readString, toCount, toScore } from './shared.js';
2
+ export function adaptOpenAiSkillsEntries(sourceId, entries) {
3
+ return entries
4
+ .map((entry) => mapOpenAiSkillEntry(sourceId, entry))
5
+ .filter((entry) => entry !== null);
6
+ }
7
+ function mapOpenAiSkillEntry(sourceId, entry) {
8
+ if (!entry || typeof entry !== 'object' || Array.isArray(entry)) {
9
+ return null;
10
+ }
11
+ const record = entry;
12
+ const slug = readString(record, ['slug', 'id', 'name']);
13
+ if (!slug) {
14
+ return null;
15
+ }
16
+ const name = readString(record, ['title', 'name']) ?? slug;
17
+ const description = readString(record, ['description', 'summary']) ?? `Skill ${name}`;
18
+ const capabilities = dedupe(extractStringArray(record, ['capabilities', 'tags']).concat(extractStringArray(record, ['features'])));
19
+ const compatibility = dedupe(extractStringArray(record, ['compatibility', 'runtimes']).concat(extractStringArray(record, ['frameworks'])));
20
+ const installTarget = readNestedString(record, ['install', 'target']) ?? readString(record, ['package', 'name']) ?? slug;
21
+ const installArgs = readNestedStringArray(record, ['install', 'args']);
22
+ return {
23
+ id: slug.startsWith('skill:') ? slug : `skill:${slug}`,
24
+ kind: 'skill',
25
+ provider: 'openai',
26
+ name,
27
+ description,
28
+ capabilities,
29
+ compatibility: compatibility.length > 0 ? compatibility : ['general'],
30
+ source: sourceId,
31
+ install: {
32
+ kind: 'skill.sh',
33
+ target: installTarget,
34
+ args: installArgs
35
+ },
36
+ adoptionSignal: toScore(record.adoptionSignal),
37
+ maintenanceSignal: toScore(record.maintenanceSignal),
38
+ provenanceSignal: toScore(record.provenanceSignal, 75),
39
+ freshnessSignal: toScore(record.freshnessSignal, 55),
40
+ securitySignals: {
41
+ knownVulnerabilities: toCount(record.knownVulnerabilities),
42
+ suspiciousPatterns: toCount(record.suspiciousPatterns),
43
+ injectionFindings: toCount(record.injectionFindings),
44
+ exfiltrationSignals: toCount(record.exfiltrationSignals),
45
+ integrityAlerts: toCount(record.integrityAlerts)
46
+ }
47
+ };
48
+ }
@@ -0,0 +1,94 @@
1
+ export function dedupe(values) {
2
+ return Array.from(new Set(values.filter((value) => value.trim().length > 0))).sort((a, b) => a.localeCompare(b));
3
+ }
4
+ export function extractStringArray(record, keys) {
5
+ for (const key of keys) {
6
+ const value = record[key];
7
+ if (!Array.isArray(value)) {
8
+ continue;
9
+ }
10
+ return value
11
+ .filter((item) => typeof item === 'string' && item.trim().length > 0)
12
+ .map((item) => item.trim());
13
+ }
14
+ return [];
15
+ }
16
+ export function readString(record, keys) {
17
+ for (const key of keys) {
18
+ const value = record[key];
19
+ if (typeof value === 'string' && value.trim().length > 0) {
20
+ return value.trim();
21
+ }
22
+ }
23
+ return undefined;
24
+ }
25
+ export function readNestedString(record, path) {
26
+ let current = record;
27
+ for (const key of path) {
28
+ if (!current || typeof current !== 'object' || Array.isArray(current)) {
29
+ return undefined;
30
+ }
31
+ current = current[key];
32
+ }
33
+ return typeof current === 'string' && current.trim().length > 0 ? current.trim() : undefined;
34
+ }
35
+ export function readNestedStringArray(record, path) {
36
+ let current = record;
37
+ for (const key of path) {
38
+ if (!current || typeof current !== 'object' || Array.isArray(current)) {
39
+ return [];
40
+ }
41
+ current = current[key];
42
+ }
43
+ if (!Array.isArray(current)) {
44
+ return [];
45
+ }
46
+ return current
47
+ .filter((value) => typeof value === 'string' && value.trim().length > 0)
48
+ .map((value) => value.trim());
49
+ }
50
+ export function toScore(value, fallback = 50) {
51
+ const parsed = typeof value === 'number' ? value : Number(value);
52
+ if (!Number.isFinite(parsed)) {
53
+ return fallback;
54
+ }
55
+ return Math.max(0, Math.min(100, parsed));
56
+ }
57
+ export function toCount(value) {
58
+ const parsed = typeof value === 'number' ? value : Number(value);
59
+ if (!Number.isFinite(parsed) || parsed < 0) {
60
+ return 0;
61
+ }
62
+ return Math.floor(parsed);
63
+ }
64
+ export function slugify(value) {
65
+ return value
66
+ .trim()
67
+ .toLowerCase()
68
+ .replace(/[^a-z0-9]+/g, '-')
69
+ .replace(/^-+|-+$/g, '');
70
+ }
71
+ export function stripHtml(value, maxLength = 240) {
72
+ const withoutTags = value.replace(/<script[\s\S]*?<\/script>/gi, ' ').replace(/<[^>]+>/g, ' ');
73
+ const normalized = withoutTags.replace(/&nbsp;/g, ' ').replace(/\s+/g, ' ').trim();
74
+ return normalized.slice(0, maxLength);
75
+ }
76
+ export function sanitizeUrl(value, allowedHosts) {
77
+ if (!value) {
78
+ return undefined;
79
+ }
80
+ try {
81
+ const parsed = new URL(value.trim());
82
+ if (parsed.protocol !== 'https:') {
83
+ return undefined;
84
+ }
85
+ const hostname = parsed.hostname.toLowerCase();
86
+ if (!allowedHosts.some((host) => hostname === host || hostname.endsWith(`.${host}`))) {
87
+ return undefined;
88
+ }
89
+ return parsed.toString();
90
+ }
91
+ catch {
92
+ return undefined;
93
+ }
94
+ }
@@ -0,0 +1,196 @@
1
+ import { logger } from '../lib/logger.js';
2
+ const SAFE_REMOTE_HOSTS = [
3
+ 'claude.com',
4
+ 'www.anthropic.com',
5
+ 'raw.githubusercontent.com',
6
+ 'github.com',
7
+ 'registry.modelcontextprotocol.io'
8
+ ];
9
+ export async function resolveRegistryEntries(registry, options = {}, fetchImpl = fetch) {
10
+ if (process.env.SKILLS_MCPS_SYNC_OFFLINE === '1') {
11
+ return { entries: registry.entries, source: 'local' };
12
+ }
13
+ if (!registry.remote) {
14
+ return { entries: registry.entries, source: 'local' };
15
+ }
16
+ if (registry.remote.authEnv) {
17
+ const token = process.env[registry.remote.authEnv];
18
+ if (!token && registry.entries.length > 0) {
19
+ logger.info(`Remote registry ${registry.id} requires ${registry.remote.authEnv}; using ${registry.entries.length} local fallback entries`);
20
+ return { entries: registry.entries, source: 'local' };
21
+ }
22
+ }
23
+ try {
24
+ const parsed = await fetchRemoteRegistryEntries(registry, options, fetchImpl);
25
+ if (parsed.length === 0 && registry.entries.length > 0) {
26
+ const level = options.updatedSince ? 'info' : 'warn';
27
+ const reason = options.updatedSince ? 'returned no updates' : 'returned no entries';
28
+ logger[level](`Remote registry ${registry.id} ${reason}; using ${registry.entries.length} local fallback entries`);
29
+ return { entries: registry.entries, source: 'local' };
30
+ }
31
+ return { entries: parsed, source: 'remote' };
32
+ }
33
+ catch (error) {
34
+ if (registry.remote.fallbackToLocal && registry.entries.length > 0) {
35
+ logger.warn(`Remote registry ${registry.id} fetch failed (${summarizeError(error)}); using ${registry.entries.length} local fallback entries`);
36
+ return { entries: registry.entries, source: 'local' };
37
+ }
38
+ throw error;
39
+ }
40
+ }
41
+ export async function fetchRemoteRegistryEntries(registry, options = {}, fetchImpl = fetch) {
42
+ if (!registry.remote) {
43
+ throw new Error(`Registry ${registry.id} has no remote definition`);
44
+ }
45
+ validateRemoteHost(registry);
46
+ const allEntries = [];
47
+ let cursor;
48
+ do {
49
+ const payload = await fetchRemoteRegistryPayload(registry, fetchImpl, {
50
+ cursor,
51
+ updatedSince: options.updatedSince
52
+ });
53
+ const parsed = extractEntries(payload, registry.remote.format, registry.remote.entryPath, registry.kind);
54
+ allEntries.push(...parsed);
55
+ cursor = resolveNextCursor(payload, registry.remote.pagination?.nextCursorPath);
56
+ } while (cursor && registry.remote.pagination);
57
+ return allEntries;
58
+ }
59
+ async function fetchRemoteRegistryPayload(registry, fetchImpl, params) {
60
+ const remote = registry.remote;
61
+ if (!remote) {
62
+ throw new Error(`Registry ${registry.id} has no remote definition`);
63
+ }
64
+ const headers = {
65
+ Accept: remote.format === 'html' ? 'text/html' : 'application/json'
66
+ };
67
+ if (remote.authEnv) {
68
+ const token = process.env[remote.authEnv];
69
+ if (token) {
70
+ headers.Authorization = `Bearer ${token}`;
71
+ }
72
+ }
73
+ const controller = new AbortController();
74
+ const timeoutMs = remote.timeoutMs;
75
+ const timer = setTimeout(() => controller.abort(), timeoutMs);
76
+ try {
77
+ const url = buildRemoteUrl(remote, params);
78
+ const response = await fetchImpl(url, {
79
+ method: 'GET',
80
+ headers,
81
+ signal: controller.signal
82
+ });
83
+ if (!response.ok) {
84
+ throw new Error(`Remote registry ${registry.id} request failed with ${response.status} ${response.statusText}`);
85
+ }
86
+ if (remote.format === 'html') {
87
+ if (!response.text) {
88
+ throw new Error(`Remote registry ${registry.id} expected text() response handler`);
89
+ }
90
+ return response.text();
91
+ }
92
+ return response.json();
93
+ }
94
+ finally {
95
+ clearTimeout(timer);
96
+ }
97
+ }
98
+ export function extractEntries(payload, format, entryPath, kind) {
99
+ if (format === 'json-array') {
100
+ if (!Array.isArray(payload)) {
101
+ throw new Error('Expected remote payload to be an array');
102
+ }
103
+ return payload;
104
+ }
105
+ if (format === 'html') {
106
+ if (typeof payload !== 'string') {
107
+ throw new Error('Expected remote payload to be a string for html format');
108
+ }
109
+ return [payload];
110
+ }
111
+ if (!payload || typeof payload !== 'object' || Array.isArray(payload)) {
112
+ throw new Error('Expected remote payload to be an object for catalog-json format');
113
+ }
114
+ const base = payload;
115
+ const defaultKey = defaultCatalogKeyByKind(kind);
116
+ const resolved = entryPath ? resolveByPath(base, entryPath) : base[defaultKey];
117
+ if (!Array.isArray(resolved)) {
118
+ throw new Error(`Expected resolved catalog entries to be an array at path: ${entryPath ?? defaultKey}`);
119
+ }
120
+ return resolved;
121
+ }
122
+ function defaultCatalogKeyByKind(kind) {
123
+ if (kind === 'mcp') {
124
+ return 'mcps';
125
+ }
126
+ if (kind === 'claude-plugin') {
127
+ return 'plugins';
128
+ }
129
+ if (kind === 'claude-connector') {
130
+ return 'connectors';
131
+ }
132
+ if (kind === 'copilot-extension') {
133
+ return 'extensions';
134
+ }
135
+ return 'skills';
136
+ }
137
+ function resolveByPath(value, path) {
138
+ const segments = path.split('.').filter((segment) => segment.length > 0);
139
+ let current = value;
140
+ for (const segment of segments) {
141
+ if (!current || typeof current !== 'object' || Array.isArray(current)) {
142
+ return undefined;
143
+ }
144
+ current = current[segment];
145
+ }
146
+ return current;
147
+ }
148
+ function resolveNextCursor(payload, path = 'next_cursor') {
149
+ if (!payload || typeof payload !== 'object' || Array.isArray(payload)) {
150
+ return undefined;
151
+ }
152
+ const resolved = resolveByPath(payload, path);
153
+ return typeof resolved === 'string' && resolved.trim().length > 0 ? resolved : undefined;
154
+ }
155
+ function buildRemoteUrl(remote, params) {
156
+ const url = new URL(remote.url);
157
+ if (remote.supportsUpdatedSince && params.updatedSince) {
158
+ url.searchParams.set(remote.updatedSinceParam, params.updatedSince);
159
+ }
160
+ if (remote.pagination?.limitParam && remote.pagination.limit) {
161
+ url.searchParams.set(remote.pagination.limitParam, String(remote.pagination.limit));
162
+ }
163
+ if (remote.pagination && params.cursor) {
164
+ url.searchParams.set(remote.pagination.cursorParam, params.cursor);
165
+ }
166
+ return url.toString();
167
+ }
168
+ function summarizeError(error) {
169
+ if (error instanceof Error && error.message.trim().length > 0) {
170
+ return error.message;
171
+ }
172
+ return 'unknown error';
173
+ }
174
+ function validateRemoteHost(registry) {
175
+ if (!registry.remote || !requiresSafeHostAllowlist(registry.kind)) {
176
+ return;
177
+ }
178
+ let url;
179
+ try {
180
+ url = new URL(registry.remote.url);
181
+ }
182
+ catch {
183
+ throw new Error(`Remote registry ${registry.id} has invalid URL: ${registry.remote.url}`);
184
+ }
185
+ if (url.protocol !== 'https:') {
186
+ throw new Error(`Remote registry ${registry.id} rejected non-HTTPS endpoint: ${registry.remote.url}`);
187
+ }
188
+ const hostname = url.hostname.toLowerCase();
189
+ const allowed = SAFE_REMOTE_HOSTS.includes(hostname);
190
+ if (!allowed) {
191
+ throw new Error(`Remote registry ${registry.id} host ${hostname} is not in safe host allowlist`);
192
+ }
193
+ }
194
+ function requiresSafeHostAllowlist(kind) {
195
+ return kind === 'claude-plugin' || kind === 'claude-connector' || kind === 'copilot-extension' || kind === 'mcp';
196
+ }
@@ -0,0 +1,161 @@
1
+ import fs from 'fs-extra';
2
+ import { readJsonFile, writeJsonFile } from '../lib/json.js';
3
+ import { getPackagePath, getStatePath } from '../lib/paths.js';
4
+ import { CatalogItemSchema, CatalogMcpServerSchema, CatalogSkillSchema, QuarantineFileSchema, WhitelistFileSchema } from '../lib/validation/contracts.js';
5
+ const ITEMS_REL_PATH = 'data/catalog/items.json';
6
+ const SKILLS_REL_PATH = 'data/catalog/skills.json';
7
+ const MCPS_REL_PATH = 'data/catalog/mcps.json';
8
+ const WHITELIST_REL_PATH = 'data/whitelist/approved.json';
9
+ const QUARANTINE_REL_PATH = 'data/quarantine/quarantined.json';
10
+ export function getItemsPath() {
11
+ return getStatePath(ITEMS_REL_PATH);
12
+ }
13
+ export function getSkillsPath() {
14
+ return getStatePath(SKILLS_REL_PATH);
15
+ }
16
+ export function getMcpsPath() {
17
+ return getStatePath(MCPS_REL_PATH);
18
+ }
19
+ export function getWhitelistPath() {
20
+ return getStatePath(WHITELIST_REL_PATH);
21
+ }
22
+ export function getQuarantinePath() {
23
+ return getStatePath(QUARANTINE_REL_PATH);
24
+ }
25
+ function getDefaultItemsPath() {
26
+ return getPackagePath(ITEMS_REL_PATH);
27
+ }
28
+ function getDefaultSkillsPath() {
29
+ return getPackagePath(SKILLS_REL_PATH);
30
+ }
31
+ function getDefaultMcpsPath() {
32
+ return getPackagePath(MCPS_REL_PATH);
33
+ }
34
+ function getDefaultWhitelistPath() {
35
+ return getPackagePath(WHITELIST_REL_PATH);
36
+ }
37
+ function getDefaultQuarantinePath() {
38
+ return getPackagePath(QUARANTINE_REL_PATH);
39
+ }
40
+ export async function loadCatalogItems() {
41
+ const primaryPath = getItemsPath();
42
+ if (await fs.pathExists(primaryPath)) {
43
+ return parseCatalogItems(await readJsonFile(primaryPath));
44
+ }
45
+ const fallbackPath = getDefaultItemsPath();
46
+ if (await fs.pathExists(fallbackPath)) {
47
+ return parseCatalogItems(await readJsonFile(fallbackPath));
48
+ }
49
+ return loadLegacyItems();
50
+ }
51
+ async function loadLegacyItems() {
52
+ const [skills, mcps] = await Promise.all([loadSkillsCatalog(), loadMcpsCatalog()]);
53
+ return [...skills, ...mcps];
54
+ }
55
+ export async function loadSkillsCatalog() {
56
+ const raw = await readArrayFromStateOrPackage(getSkillsPath(), getDefaultSkillsPath());
57
+ return raw.map((entry) => {
58
+ const value = ensureObject(entry);
59
+ return CatalogSkillSchema.parse({ ...value, kind: 'skill', provider: readProvider(entry, 'openai') });
60
+ });
61
+ }
62
+ export async function loadMcpsCatalog() {
63
+ const raw = await readArrayFromStateOrPackage(getMcpsPath(), getDefaultMcpsPath());
64
+ return raw.map((entry) => {
65
+ const value = ensureObject(entry);
66
+ return CatalogMcpServerSchema.parse({ ...value, kind: 'mcp', provider: readProvider(entry, 'mcp') });
67
+ });
68
+ }
69
+ function readProvider(value, fallback) {
70
+ if (!value || typeof value !== 'object' || Array.isArray(value)) {
71
+ return fallback;
72
+ }
73
+ const provider = value.provider;
74
+ return typeof provider === 'string' && provider.trim().length > 0 ? provider.trim() : fallback;
75
+ }
76
+ function ensureObject(value) {
77
+ if (!value || typeof value !== 'object' || Array.isArray(value)) {
78
+ return {};
79
+ }
80
+ return value;
81
+ }
82
+ export async function saveCatalogItems(records) {
83
+ await writeJsonFile(getItemsPath(), records);
84
+ }
85
+ export async function saveSkillsCatalog(records) {
86
+ await writeJsonFile(getSkillsPath(), records);
87
+ }
88
+ export async function saveMcpsCatalog(records) {
89
+ await writeJsonFile(getMcpsPath(), records);
90
+ }
91
+ export async function saveLegacyCatalogViews(records) {
92
+ const skills = records
93
+ .filter((item) => item.kind === 'skill')
94
+ .map((item) => CatalogSkillSchema.parse(item));
95
+ const mcps = records
96
+ .filter((item) => item.kind === 'mcp')
97
+ .map((item) => CatalogMcpServerSchema.parse(item));
98
+ await Promise.all([saveSkillsCatalog(skills), saveMcpsCatalog(mcps)]);
99
+ }
100
+ export async function loadWhitelist() {
101
+ const raw = await readObjectFromStateOrPackage(getWhitelistPath(), getDefaultWhitelistPath());
102
+ if (!raw) {
103
+ return new Set();
104
+ }
105
+ const parsed = WhitelistFileSchema.parse(raw);
106
+ return new Set(parsed.approved);
107
+ }
108
+ export async function saveWhitelist(ids) {
109
+ const approved = Array.from(new Set(ids)).sort((a, b) => a.localeCompare(b));
110
+ await writeJsonFile(getWhitelistPath(), { approved });
111
+ }
112
+ export async function loadQuarantine() {
113
+ const raw = await readObjectFromStateOrPackage(getQuarantinePath(), getDefaultQuarantinePath());
114
+ if (!raw) {
115
+ return [];
116
+ }
117
+ const parsed = QuarantineFileSchema.parse(raw);
118
+ return parsed.quarantined;
119
+ }
120
+ export async function saveQuarantine(entries) {
121
+ const deduped = new Map();
122
+ entries.forEach((entry) => deduped.set(entry.id, entry));
123
+ const sorted = Array.from(deduped.values()).sort((a, b) => a.id.localeCompare(b.id));
124
+ await writeJsonFile(getQuarantinePath(), { quarantined: sorted });
125
+ }
126
+ export async function loadCatalogItemById(id) {
127
+ const records = await loadCatalogItems();
128
+ return records.find((entry) => entry.id === id) ?? null;
129
+ }
130
+ export async function loadCatalogById(id) {
131
+ const found = await loadCatalogItemById(id);
132
+ if (!found) {
133
+ return null;
134
+ }
135
+ if (found.kind === 'skill') {
136
+ return { kind: 'skill', item: CatalogSkillSchema.parse(found) };
137
+ }
138
+ if (found.kind === 'mcp') {
139
+ return { kind: 'mcp', item: CatalogMcpServerSchema.parse(found) };
140
+ }
141
+ return { kind: found.kind, item: found };
142
+ }
143
+ async function readArrayFromStateOrPackage(statePath, packagePath) {
144
+ const preferred = await readObjectFromStateOrPackage(statePath, packagePath);
145
+ if (!preferred) {
146
+ return [];
147
+ }
148
+ return Array.isArray(preferred) ? preferred : [];
149
+ }
150
+ async function readObjectFromStateOrPackage(statePath, packagePath) {
151
+ if (await fs.pathExists(statePath)) {
152
+ return readJsonFile(statePath);
153
+ }
154
+ if (await fs.pathExists(packagePath)) {
155
+ return readJsonFile(packagePath);
156
+ }
157
+ return null;
158
+ }
159
+ function parseCatalogItems(raw) {
160
+ return raw.map((entry) => CatalogItemSchema.parse(entry));
161
+ }
@@ -0,0 +1,61 @@
1
+ import fs from 'fs-extra';
2
+ import { readJsonFile, writeJsonFile } from '../lib/json.js';
3
+ import { getStatePath } from '../lib/paths.js';
4
+ const SYNC_STATE_REL_PATH = 'data/catalog/sync-state.json';
5
+ export function getSyncStatePath() {
6
+ return getStatePath(SYNC_STATE_REL_PATH);
7
+ }
8
+ const EMPTY_STATE = { registries: {} };
9
+ export async function loadSyncState() {
10
+ const syncStatePath = getSyncStatePath();
11
+ if (!(await fs.pathExists(syncStatePath))) {
12
+ return EMPTY_STATE;
13
+ }
14
+ const raw = await readJsonFile(syncStatePath);
15
+ if (!raw || typeof raw !== 'object' || Array.isArray(raw)) {
16
+ return EMPTY_STATE;
17
+ }
18
+ const registries = raw.registries;
19
+ return { registries: registries ?? {} };
20
+ }
21
+ export async function saveSyncState(state) {
22
+ await writeJsonFile(getSyncStatePath(), state);
23
+ }
24
+ export function getUpdatedSince(state, registryId) {
25
+ return state.registries[registryId]?.lastUpdatedSince;
26
+ }
27
+ export function setUpdatedSince(state, registryId, timestamp) {
28
+ return {
29
+ registries: {
30
+ ...state.registries,
31
+ [registryId]: {
32
+ ...(state.registries[registryId] ?? {}),
33
+ lastUpdatedSince: timestamp
34
+ }
35
+ }
36
+ };
37
+ }
38
+ export function setSuccessfulSync(state, registryId, timestamp) {
39
+ return {
40
+ registries: {
41
+ ...state.registries,
42
+ [registryId]: {
43
+ ...(state.registries[registryId] ?? {}),
44
+ lastSuccessfulSyncAt: timestamp
45
+ }
46
+ }
47
+ };
48
+ }
49
+ export function getStaleRegistries(state, now = new Date(), staleAfterHours = 48) {
50
+ const staleCutoffMs = now.getTime() - staleAfterHours * 60 * 60 * 1000;
51
+ return Object.entries(state.registries)
52
+ .filter(([, value]) => {
53
+ if (!value.lastSuccessfulSyncAt) {
54
+ return true;
55
+ }
56
+ const stamp = Date.parse(value.lastSuccessfulSyncAt);
57
+ return !Number.isFinite(stamp) || stamp < staleCutoffMs;
58
+ })
59
+ .map(([registryId]) => registryId)
60
+ .sort((a, b) => a.localeCompare(b));
61
+ }