@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,76 @@
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 { SkillSchema } from '../models/records.js';
6
+ const OUTPUT_PATH = getStatePath('data/curated/skills.json');
7
+ export async function ingestSkills() {
8
+ const { skills } = await loadSourcesConfig();
9
+ const records = await loadSkillRecords(skills);
10
+ const merged = mergeSkillRecords(records);
11
+ await writeJsonFile(OUTPUT_PATH, merged);
12
+ logger.info(`Wrote ${merged.length} skills to ${OUTPUT_PATH}`);
13
+ return merged;
14
+ }
15
+ async function loadSkillRecords(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: SkillSchema.parse(record)
21
+ }));
22
+ }));
23
+ return perSource.flat();
24
+ }
25
+ function mergeSkillRecords(records) {
26
+ const state = new Map();
27
+ for (const entry of records) {
28
+ const existing = state.get(entry.record.id);
29
+ if (!existing) {
30
+ state.set(entry.record.id, { priority: entry.source.priority, record: entry.record });
31
+ continue;
32
+ }
33
+ const preferIncoming = shouldPreferIncoming(entry, existing);
34
+ if (preferIncoming) {
35
+ state.set(entry.record.id, {
36
+ priority: entry.source.priority,
37
+ record: mergeSkill(entry.record, existing.record)
38
+ });
39
+ }
40
+ else {
41
+ state.set(entry.record.id, {
42
+ priority: existing.priority,
43
+ record: mergeSkill(existing.record, entry.record)
44
+ });
45
+ }
46
+ }
47
+ return Array.from(state.values())
48
+ .map((entry) => entry.record)
49
+ .sort((a, b) => a.id.localeCompare(b.id));
50
+ }
51
+ function shouldPreferIncoming(entry, existing) {
52
+ if (entry.source.priority !== existing.priority) {
53
+ return entry.source.priority > existing.priority;
54
+ }
55
+ return entry.record.lastValidated >= existing.record.lastValidated;
56
+ }
57
+ function mergeSkill(primary, secondary) {
58
+ const mergedProficiencyLevels = dedupeStrings([
59
+ ...primary.proficiencyLevels,
60
+ ...secondary.proficiencyLevels
61
+ ]);
62
+ return {
63
+ ...primary,
64
+ description: secondary.description.length > primary.description.length ? secondary.description : primary.description,
65
+ aliases: dedupeStrings([...primary.aliases, ...secondary.aliases]),
66
+ proficiencyLevels: mergedProficiencyLevels.length > 0
67
+ ? mergedProficiencyLevels
68
+ : primary.proficiencyLevels,
69
+ taxonomyPath: primary.taxonomyPath.length ? primary.taxonomyPath : secondary.taxonomyPath,
70
+ lastValidated: primary.lastValidated >= secondary.lastValidated ? primary.lastValidated : secondary.lastValidated
71
+ };
72
+ }
73
+ function dedupeStrings(values) {
74
+ return Array.from(new Set(values)).sort((a, b) => a.localeCompare(b));
75
+ }
76
+ export { mergeSkillRecords, mergeSkill, dedupeStrings };
@@ -0,0 +1,58 @@
1
+ import { spawn } from 'node:child_process';
2
+ import { spawnSync } from 'node:child_process';
3
+ export function hasBinary(name) {
4
+ const result = spawnSync('which', [name], { encoding: 'utf8' });
5
+ return result.status === 0;
6
+ }
7
+ export function resolveSkillsRuntime() {
8
+ if (hasBinary('skills')) {
9
+ return {
10
+ binary: 'skills',
11
+ prefixArgs: [],
12
+ label: 'skills'
13
+ };
14
+ }
15
+ if (hasBinary('npx')) {
16
+ return {
17
+ binary: 'npx',
18
+ prefixArgs: ['-y', 'skills'],
19
+ label: 'npx skills'
20
+ };
21
+ }
22
+ return null;
23
+ }
24
+ export function hasLegacySkillSh() {
25
+ return hasBinary('skill.sh');
26
+ }
27
+ export async function installToolkitDependencies() {
28
+ const installed = [];
29
+ if (!hasBinary('skills')) {
30
+ if (!hasBinary('npm')) {
31
+ throw new Error('npm is required to install the skills CLI automatically.');
32
+ }
33
+ if (process.env.SKILLS_MCPS_DEP_INSTALL_DRY_RUN === '1') {
34
+ installed.push('skills');
35
+ return installed;
36
+ }
37
+ await runCommand('npm', ['install', '-g', 'skills'], 'npm install -g skills');
38
+ installed.push('skills');
39
+ }
40
+ return installed;
41
+ }
42
+ async function runCommand(binary, args, label) {
43
+ await new Promise((resolve, reject) => {
44
+ const child = spawn(binary, args, {
45
+ stdio: 'inherit'
46
+ });
47
+ child.on('error', (error) => {
48
+ reject(new Error(`Failed to execute ${label}: ${error.message}`));
49
+ });
50
+ child.on('close', (code) => {
51
+ if ((code ?? 1) !== 0) {
52
+ reject(new Error(`${label} exited with code ${code ?? 1}`));
53
+ return;
54
+ }
55
+ resolve();
56
+ });
57
+ });
58
+ }
@@ -0,0 +1,70 @@
1
+ import { readJsonFile, writeJsonFile } from '../lib/json.js';
2
+ import { getStatePath } from '../lib/paths.js';
3
+ const REVIEW_STATE_PATH = getStatePath('data/system/review-state.json');
4
+ const REVIEW_TTL_MS = 24 * 60 * 60 * 1000;
5
+ export async function recordItemReview(id, via) {
6
+ const state = await loadReviewState();
7
+ state.reviews[id] = {
8
+ reviewedAt: new Date().toISOString(),
9
+ via
10
+ };
11
+ await writeJsonFile(REVIEW_STATE_PATH, state);
12
+ }
13
+ export async function hasRecentReview(id, now = Date.now()) {
14
+ const receipt = await getReviewReceipt(id);
15
+ if (!receipt) {
16
+ return false;
17
+ }
18
+ const reviewedAtMs = Date.parse(receipt.reviewedAt);
19
+ if (Number.isNaN(reviewedAtMs)) {
20
+ return false;
21
+ }
22
+ return now - reviewedAtMs <= REVIEW_TTL_MS;
23
+ }
24
+ export async function assertRecentReview(id) {
25
+ if (await hasRecentReview(id)) {
26
+ return;
27
+ }
28
+ throw new Error(`Review required before install for ${id}. Run \`plugscout show --id ${id}\` or \`plugscout assess --id ${id}\`, then retry. Use --override-review only if you intentionally want to bypass this safeguard.`);
29
+ }
30
+ export function getReviewStatePath() {
31
+ return REVIEW_STATE_PATH;
32
+ }
33
+ async function getReviewReceipt(id) {
34
+ const state = await loadReviewState();
35
+ return state.reviews[id] ?? null;
36
+ }
37
+ async function loadReviewState() {
38
+ try {
39
+ const raw = await readJsonFile(REVIEW_STATE_PATH);
40
+ if (!raw || typeof raw !== 'object') {
41
+ return { reviews: {} };
42
+ }
43
+ const reviews = raw.reviews;
44
+ if (!reviews || typeof reviews !== 'object') {
45
+ return { reviews: {} };
46
+ }
47
+ const normalized = {};
48
+ Object.entries(reviews).forEach(([id, value]) => {
49
+ if (!value || typeof value !== 'object') {
50
+ return;
51
+ }
52
+ const reviewedAt = typeof value.reviewedAt === 'string'
53
+ ? value.reviewedAt
54
+ : null;
55
+ const via = value.via;
56
+ if (!reviewedAt || (via !== 'show' && via !== 'assess')) {
57
+ return;
58
+ }
59
+ normalized[id] = { reviewedAt, via };
60
+ });
61
+ return { reviews: normalized };
62
+ }
63
+ catch (error) {
64
+ const maybeFsError = error;
65
+ if (maybeFsError.code === 'ENOENT') {
66
+ return { reviews: {} };
67
+ }
68
+ throw error;
69
+ }
70
+ }
@@ -0,0 +1,245 @@
1
+ import { spawn } from 'node:child_process';
2
+ import { spawnSync } from 'node:child_process';
3
+ import { loadSecurityPolicy } from '../config/runtime.js';
4
+ import { loadCatalogItemById } from '../catalog/repository.js';
5
+ import { logger } from '../lib/logger.js';
6
+ import { writeJsonFile } from '../lib/json.js';
7
+ import { getStatePath } from '../lib/paths.js';
8
+ import { InstallAuditSchema } from '../lib/validation/contracts.js';
9
+ import { buildAssessment, isBlockedTier, isWarnTier } from '../security/assessment.js';
10
+ import { hasLegacySkillSh, resolveSkillsRuntime } from './dependencies.js';
11
+ import { assertRecentReview } from './review-state.js';
12
+ export async function installWithSkillSh(options) {
13
+ const record = await loadCatalogItemById(options.id);
14
+ if (!record) {
15
+ throw new Error(`Catalog entry not found: ${options.id}`);
16
+ }
17
+ if (!options.overrideReview) {
18
+ await assertRecentReview(options.id);
19
+ }
20
+ const policy = await loadSecurityPolicy();
21
+ const assessment = buildAssessment(record, policy);
22
+ if (isBlockedTier(assessment.riskTier, policy) && !options.overrideRisk) {
23
+ await persistAudit({
24
+ id: options.id,
25
+ requestedAt: new Date().toISOString(),
26
+ policyDecision: 'blocked',
27
+ overrideUsed: false,
28
+ installer: record.install.kind,
29
+ exitCode: 1
30
+ });
31
+ throw new Error(`Blocked by security policy (${assessment.riskTier}, score=${assessment.riskScore}). Use --override-risk to force.`);
32
+ }
33
+ if (isWarnTier(assessment.riskTier, policy)) {
34
+ logger.warn(`Security warning for ${options.id}: ${assessment.riskTier} (${assessment.riskScore})`);
35
+ }
36
+ const { exitCode, installer } = await executeInstall(record, options.yes);
37
+ return persistAudit({
38
+ id: options.id,
39
+ requestedAt: new Date().toISOString(),
40
+ policyDecision: options.overrideRisk ? 'override-allowed' : 'allowed',
41
+ overrideUsed: options.overrideRisk,
42
+ installer,
43
+ exitCode
44
+ });
45
+ }
46
+ async function executeInstall(item, yes) {
47
+ const install = item.install;
48
+ if (install.kind === 'manual') {
49
+ logger.info(`Manual install required: ${install.instructions}${install.url ? ` (${install.url})` : ''}`);
50
+ return { exitCode: 0, installer: 'manual' };
51
+ }
52
+ if (install.kind === 'skill.sh') {
53
+ const plan = resolvePreferredInstallPlan(item, yes);
54
+ if (plan) {
55
+ const exitCode = await executeCommand(plan.binary, plan.args, plan.label);
56
+ return { exitCode, installer: plan.installer };
57
+ }
58
+ ensureLegacySkillShAvailable(item.id);
59
+ const commandArgs = buildSkillShInstallArgs(install.target, install.args, yes);
60
+ const exitCode = await executeCommand('skill.sh', commandArgs, 'skill.sh');
61
+ return { exitCode, installer: 'skill.sh' };
62
+ }
63
+ ensureBinaryAvailable('gh', 'gh CLI is required for gh-cli installers. Install it and verify with: gh --version');
64
+ const commandArgs = buildGhInstallArgs(install.target, install.args, yes);
65
+ const exitCode = await executeCommand('gh', commandArgs, 'gh');
66
+ return { exitCode, installer: 'gh-cli' };
67
+ }
68
+ export function buildSkillShInstallArgs(target, args, yes) {
69
+ const commandArgs = ['install', target, ...args];
70
+ if (yes) {
71
+ commandArgs.push('--yes');
72
+ }
73
+ return commandArgs;
74
+ }
75
+ export function buildGhInstallArgs(target, args, yes) {
76
+ const commandArgs = [...args];
77
+ if (commandArgs.length === 0) {
78
+ commandArgs.push(target);
79
+ }
80
+ if (yes) {
81
+ commandArgs.push('--yes');
82
+ }
83
+ return commandArgs;
84
+ }
85
+ export function resolvePreferredInstallPlan(item, yes) {
86
+ if (item.install.kind !== 'skill.sh') {
87
+ return null;
88
+ }
89
+ const modernSkillPlan = buildModernSkillsInstallArgs(item, yes);
90
+ if (modernSkillPlan) {
91
+ return modernSkillPlan;
92
+ }
93
+ const directMcpPlan = buildDirectMcpInstallArgs(item);
94
+ if (directMcpPlan) {
95
+ return directMcpPlan;
96
+ }
97
+ return null;
98
+ }
99
+ function buildModernSkillsInstallArgs(item, yes) {
100
+ if (item.kind !== 'skill' || item.install.kind !== 'skill.sh') {
101
+ return null;
102
+ }
103
+ const runtime = resolveSkillsRuntime();
104
+ if (!runtime) {
105
+ return null;
106
+ }
107
+ const metadata = item.metadata;
108
+ const registryId = typeof metadata.sourceRegistryId === 'string'
109
+ ? metadata.sourceRegistryId
110
+ : typeof metadata.marketplaceRegistry === 'string'
111
+ ? metadata.marketplaceRegistry
112
+ : '';
113
+ const githubUrl = typeof metadata.githubUrl === 'string' ? metadata.githubUrl : '';
114
+ const repoUrl = resolveSkillsRepoUrl(registryId, githubUrl);
115
+ if (!repoUrl) {
116
+ return null;
117
+ }
118
+ const slug = resolveSkillSlug(item.id, metadata);
119
+ if (!slug) {
120
+ return null;
121
+ }
122
+ const args = runtime.prefixArgs.concat(['add', repoUrl, '--skill', slug, '--agent', '*']);
123
+ if (yes) {
124
+ args.push('--yes');
125
+ }
126
+ return {
127
+ binary: runtime.binary,
128
+ args,
129
+ label: runtime.label,
130
+ installer: 'skills'
131
+ };
132
+ }
133
+ function buildDirectMcpInstallArgs(item) {
134
+ if (item.kind !== 'mcp' || item.install.kind !== 'skill.sh') {
135
+ return null;
136
+ }
137
+ const target = item.install.target;
138
+ if (isDockerTarget(target)) {
139
+ return {
140
+ binary: 'docker',
141
+ args: ['pull', target],
142
+ label: 'docker',
143
+ installer: 'docker'
144
+ };
145
+ }
146
+ if (isNodePackageTarget(item, target)) {
147
+ return {
148
+ binary: 'npm',
149
+ args: ['install', '-g', target],
150
+ label: 'npm',
151
+ installer: 'npm'
152
+ };
153
+ }
154
+ return null;
155
+ }
156
+ function resolveSkillsRepoUrl(registryId, githubUrl) {
157
+ if (registryId === 'openai-skills-curated' || githubUrl.includes('github.com/openai/skills')) {
158
+ return 'https://github.com/openai/skills';
159
+ }
160
+ if (registryId === 'anthropic-skills' || githubUrl.includes('github.com/anthropics/skills')) {
161
+ return 'https://github.com/anthropics/skills';
162
+ }
163
+ return null;
164
+ }
165
+ function resolveSkillSlug(id, metadata) {
166
+ const prefixed = id.startsWith('skill:') ? id.slice('skill:'.length) : id;
167
+ if (prefixed.length > 0) {
168
+ return prefixed;
169
+ }
170
+ const githubPath = typeof metadata.githubPath === 'string' ? metadata.githubPath : '';
171
+ if (githubPath.length > 0) {
172
+ return githubPath.split('/').at(-1) ?? null;
173
+ }
174
+ const bundledSkillPath = typeof metadata.bundledSkillPath === 'string' ? metadata.bundledSkillPath : '';
175
+ if (bundledSkillPath.length > 0) {
176
+ return bundledSkillPath.split('/').at(-1) ?? null;
177
+ }
178
+ return null;
179
+ }
180
+ function isDockerTarget(target) {
181
+ return /^(docker\.io\/|ghcr\.io\/)/.test(target) || (/^[\w.-]+\/[\w./-]+:[\w.-]+$/.test(target) && target.includes('/'));
182
+ }
183
+ function isNodePackageTarget(item, target) {
184
+ if (!/^(@[a-z0-9_.-]+\/[a-z0-9_.-]+|[a-z0-9][a-z0-9._-]*)$/i.test(target)) {
185
+ return false;
186
+ }
187
+ const metadata = item.metadata;
188
+ const registryType = typeof metadata.packageRegistryType === 'string' ? metadata.packageRegistryType.toLowerCase() : '';
189
+ const runtime = typeof metadata.packageRuntime === 'string' ? metadata.packageRuntime.toLowerCase() : '';
190
+ if (registryType.includes('npm') || runtime.includes('node')) {
191
+ return true;
192
+ }
193
+ return item.compatibility.includes('node');
194
+ }
195
+ async function executeCommand(binary, args, label) {
196
+ if (process.env.SKILLS_MCPS_INSTALL_DRY_RUN === '1') {
197
+ logger.info(`Dry-run ${label} ${args.join(' ')}`);
198
+ return 0;
199
+ }
200
+ if (binary === 'docker') {
201
+ ensureBinaryAvailable('docker', 'Docker is required for container-backed MCP installs. Install Docker Desktop or a compatible docker runtime.');
202
+ }
203
+ if (binary === 'npm') {
204
+ ensureBinaryAvailable('npm', 'npm is required for Node package installs. Install Node.js and verify with: npm --version');
205
+ }
206
+ if (binary === 'skills') {
207
+ ensureBinaryAvailable('skills', 'skills CLI is required. Run `plugscout doctor --install-deps` to install it.');
208
+ }
209
+ return new Promise((resolve, reject) => {
210
+ const child = spawn(binary, args, {
211
+ stdio: 'inherit'
212
+ });
213
+ child.on('error', (error) => {
214
+ reject(new Error(`Failed to execute ${label}: ${error.message}`));
215
+ });
216
+ child.on('close', (code) => {
217
+ resolve(code ?? 1);
218
+ });
219
+ });
220
+ }
221
+ function ensureBinaryAvailable(binary, suggestion) {
222
+ if (process.env.SKILLS_MCPS_INSTALL_DRY_RUN === '1') {
223
+ return;
224
+ }
225
+ const result = spawnSync('which', [binary], { encoding: 'utf8' });
226
+ if (result.status !== 0) {
227
+ throw new Error(`${binary} is not available in PATH. ${suggestion}`);
228
+ }
229
+ }
230
+ function ensureLegacySkillShAvailable(id) {
231
+ if (process.env.SKILLS_MCPS_INSTALL_DRY_RUN === '1') {
232
+ return;
233
+ }
234
+ if (hasLegacySkillSh()) {
235
+ return;
236
+ }
237
+ throw new Error(`skill.sh is not available in PATH. This item (${id}) still requires the legacy skill.sh installer. Run \`plugscout doctor --install-deps\` to bootstrap the modern skills CLI for supported skill installs, or install skill.sh manually for legacy skill.sh items.`);
238
+ }
239
+ async function persistAudit(record) {
240
+ const parsed = InstallAuditSchema.parse(record);
241
+ const stamp = parsed.requestedAt.replace(/[:.]/g, '-');
242
+ const file = getStatePath(`data/security-reports/audits/${stamp}-${parsed.id.replace(/[^a-zA-Z0-9_-]/g, '_')}.json`);
243
+ await writeJsonFile(file, parsed);
244
+ return parsed;
245
+ }
@@ -0,0 +1,90 @@
1
+ import { spawnSync } from 'node:child_process';
2
+ import path from 'node:path';
3
+ import fs from 'node:fs/promises';
4
+ import { getStaleRegistries, loadSyncState } from '../../catalog/sync-state.js';
5
+ import { loadCatalogItems } from '../../catalog/repository.js';
6
+ import { hasLegacySkillSh, resolveSkillsRuntime } from '../../install/dependencies.js';
7
+ export async function runDoctorChecks(projectPath = '.') {
8
+ const checks = [];
9
+ checks.push(checkSkillsRuntime());
10
+ checks.push(hasLegacySkillSh()
11
+ ? { name: 'Legacy skill.sh', status: 'pass', message: 'skill.sh available' }
12
+ : {
13
+ name: 'Legacy skill.sh',
14
+ status: 'warn',
15
+ message: 'skill.sh not found',
16
+ suggestion: 'Optional: some legacy MCP installs still expect skill.sh. Official skills can install through the modern skills CLI.'
17
+ });
18
+ checks.push(checkBinary('gh'));
19
+ const nodeMajor = Number(process.versions.node.split('.')[0]);
20
+ checks.push(nodeMajor >= 18
21
+ ? { name: 'Node version', status: 'pass', message: `Node ${process.versions.node}` }
22
+ : {
23
+ name: 'Node version',
24
+ status: 'fail',
25
+ message: `Node ${process.versions.node}`,
26
+ suggestion: 'Upgrade to Node >=18.17'
27
+ });
28
+ try {
29
+ const items = await loadCatalogItems();
30
+ checks.push(items.length > 0
31
+ ? { name: 'Catalog', status: 'pass', message: `${items.length} items loaded` }
32
+ : { name: 'Catalog', status: 'warn', message: 'Catalog is empty', suggestion: 'Run: npm run sync' });
33
+ }
34
+ catch {
35
+ checks.push({ name: 'Catalog', status: 'fail', message: 'Catalog unreadable', suggestion: 'Run: npm run sync' });
36
+ }
37
+ const syncState = await loadSyncState();
38
+ const stale = getStaleRegistries(syncState);
39
+ checks.push(stale.length === 0
40
+ ? { name: 'Sync freshness', status: 'pass', message: 'No stale registries' }
41
+ : {
42
+ name: 'Sync freshness',
43
+ status: 'warn',
44
+ message: `${stale.length} stale registries`,
45
+ suggestion: 'Run: npm run sync'
46
+ });
47
+ const configPath = path.resolve(projectPath, '.skills-mcps.json');
48
+ try {
49
+ const raw = await fs.readFile(configPath, 'utf8');
50
+ JSON.parse(raw);
51
+ checks.push({ name: 'Local config', status: 'pass', message: '.skills-mcps.json is valid' });
52
+ }
53
+ catch {
54
+ checks.push({
55
+ name: 'Local config',
56
+ status: 'warn',
57
+ message: '.skills-mcps.json missing or invalid',
58
+ suggestion: 'Run: npm run dev -- init'
59
+ });
60
+ }
61
+ return checks;
62
+ }
63
+ function checkSkillsRuntime() {
64
+ const runtime = resolveSkillsRuntime();
65
+ if (runtime) {
66
+ return {
67
+ name: 'Skills CLI',
68
+ status: 'pass',
69
+ message: `${runtime.label} available`
70
+ };
71
+ }
72
+ return {
73
+ name: 'Skills CLI',
74
+ status: 'fail',
75
+ message: 'skills CLI not found',
76
+ suggestion: 'Run: plugscout doctor --install-deps'
77
+ };
78
+ }
79
+ function checkBinary(name, options = {}) {
80
+ const result = spawnSync('which', [name], { encoding: 'utf8' });
81
+ if (result.status === 0) {
82
+ return { name, status: 'pass', message: `${name} available` };
83
+ }
84
+ return {
85
+ name,
86
+ status: options.required ? 'fail' : 'warn',
87
+ message: `${name} not found`,
88
+ suggestion: options.suggestion ?? `Install ${name}`
89
+ };
90
+ }
@@ -0,0 +1,24 @@
1
+ const enabled = process.env.NO_COLOR !== '1';
2
+ function wrap(code, value) {
3
+ if (!enabled) {
4
+ return value;
5
+ }
6
+ return `\u001b[${code}m${value}\u001b[0m`;
7
+ }
8
+ export const colors = {
9
+ green: (value) => wrap(32, value),
10
+ yellow: (value) => wrap(33, value),
11
+ red: (value) => wrap(31, value),
12
+ cyan: (value) => wrap(36, value),
13
+ gray: (value) => wrap(90, value),
14
+ bold: (value) => wrap(1, value)
15
+ };
16
+ export function colorRisk(tier, value) {
17
+ if (tier === 'low') {
18
+ return colors.green(value);
19
+ }
20
+ if (tier === 'medium') {
21
+ return colors.yellow(value);
22
+ }
23
+ return colors.red(value);
24
+ }
@@ -0,0 +1,10 @@
1
+ export function renderCsv(headers, rows) {
2
+ const lines = [headers, ...rows].map((line) => line.map(escapeCell).join(','));
3
+ return `${lines.join('\n')}\n`;
4
+ }
5
+ function escapeCell(value) {
6
+ if (value.includes(',') || value.includes('"') || value.includes('\n')) {
7
+ return `"${value.replaceAll('"', '""')}"`;
8
+ }
9
+ return value;
10
+ }
@@ -0,0 +1,3 @@
1
+ export function renderJson(value) {
2
+ return JSON.stringify(value, null, 2);
3
+ }
@@ -0,0 +1,6 @@
1
+ export function renderMarkdown(headers, rows) {
2
+ const head = `| ${headers.join(' | ')} |`;
3
+ const divider = `| ${headers.map(() => '---').join(' | ')} |`;
4
+ const body = rows.map((row) => `| ${row.join(' | ')} |`).join('\n');
5
+ return `${head}\n${divider}\n${body}\n`;
6
+ }
@@ -0,0 +1,82 @@
1
+ import { colorRisk, colors } from './colors.js';
2
+ export function scoreBar(score, size = 10) {
3
+ const clamped = Math.max(0, Math.min(100, score));
4
+ const filled = Math.round((clamped / 100) * size);
5
+ return `${'█'.repeat(filled)}${'░'.repeat(size - filled)}`;
6
+ }
7
+ export function renderTable(columns, rows, options = {}) {
8
+ const header = columns
9
+ .map((column) => pad(column.header, column.width))
10
+ .join(' ');
11
+ const divider = columns
12
+ .map((column) => '-'.repeat(column.width))
13
+ .join(' ');
14
+ const body = rows.flatMap((row) => renderRow(columns, row, options));
15
+ return [colors.bold(header), colors.gray(divider), ...body].join('\n');
16
+ }
17
+ function renderRow(columns, row, options) {
18
+ if (!options.wrap) {
19
+ return [
20
+ columns
21
+ .map((column) => {
22
+ const value = String(row[column.key] ?? '');
23
+ const clipped = clip(value, column.width);
24
+ if (options.riskKey && column.key === options.riskKey) {
25
+ return pad(colorRisk(value.toLowerCase(), clipped), column.width);
26
+ }
27
+ return pad(clipped, column.width);
28
+ })
29
+ .join(' ')
30
+ ];
31
+ }
32
+ const wrappedCells = columns.map((column) => {
33
+ const value = String(row[column.key] ?? '');
34
+ return wrapToWidth(value, column.width);
35
+ });
36
+ const rowHeight = wrappedCells.reduce((max, cell) => Math.max(max, cell.length), 1);
37
+ const lines = [];
38
+ for (let line = 0; line < rowHeight; line += 1) {
39
+ lines.push(columns
40
+ .map((column, index) => {
41
+ const raw = wrappedCells[index][line] ?? '';
42
+ if (options.riskKey && column.key === options.riskKey) {
43
+ return pad(colorRisk(String(row[column.key] ?? '').toLowerCase(), raw), column.width);
44
+ }
45
+ return pad(raw, column.width);
46
+ })
47
+ .join(' '));
48
+ }
49
+ return lines;
50
+ }
51
+ function clip(value, width) {
52
+ if (value.length <= width) {
53
+ return value;
54
+ }
55
+ if (width <= 1) {
56
+ return value.slice(0, width);
57
+ }
58
+ return `${value.slice(0, width - 1)}…`;
59
+ }
60
+ function pad(value, width) {
61
+ if (value.length >= width) {
62
+ return value;
63
+ }
64
+ return `${value}${' '.repeat(width - value.length)}`;
65
+ }
66
+ function wrapToWidth(value, width) {
67
+ if (value.length <= width) {
68
+ return [value];
69
+ }
70
+ const output = [];
71
+ let remaining = value;
72
+ while (remaining.length > width) {
73
+ let cut = remaining.lastIndexOf(' ', width);
74
+ if (cut <= 0) {
75
+ cut = width;
76
+ }
77
+ output.push(remaining.slice(0, cut));
78
+ remaining = remaining.slice(cut).trimStart();
79
+ }
80
+ output.push(remaining);
81
+ return output;
82
+ }