@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.
- package/LICENSE +21 -0
- package/README.md +228 -0
- package/assets/cli/logo.txt +24 -0
- package/config/item-insights.json +316 -0
- package/config/providers.json +46 -0
- package/config/ranking-policy.json +14 -0
- package/config/recommendation-weights.json +7 -0
- package/config/registries.json +1423 -0
- package/config/security-policy.json +19 -0
- package/config/sources.json +30 -0
- package/data/catalog/items.json +182109 -0
- package/data/catalog/mcps.json +163843 -0
- package/data/catalog/skills.json +4768 -0
- package/data/catalog/sync-state.json +62 -0
- package/data/curated/mcps.json +78 -0
- package/data/curated/skills.json +174 -0
- package/data/quarantine/quarantined.json +3 -0
- package/data/raw/2024-05-15/mcps.json +20 -0
- package/data/raw/2024-05-20/skills.json +20 -0
- package/data/raw/2024-06-05/mcps.json +20 -0
- package/data/raw/2024-06-05/skills.json +29 -0
- package/data/security-reports/.gitkeep +0 -0
- package/data/security-reports/2026-02-06/report.json +8 -0
- package/data/security-reports/2026-02-10/report.json +9 -0
- package/data/security-reports/2026-02-11/report.json +9 -0
- package/data/security-reports/2026-02-12/report.json +9 -0
- package/data/security-reports/2026-02-13/report.json +8 -0
- package/data/security-reports/2026-02-14/report.json +8 -0
- package/data/security-reports/2026-02-23/report.json +8 -0
- package/data/security-reports/2026-02-25/report.json +8 -0
- package/data/security-reports/2026-02-26/report.json +8 -0
- package/data/security-reports/2026-03-10/report.json +8 -0
- package/data/security-reports/audits/.gitkeep +0 -0
- package/data/security-reports/audits/2026-02-06T10-17-33-872Z-mcp_remote-browser.json +8 -0
- package/data/security-reports/audits/2026-02-06T10-17-33-881Z-mcp_remote-browser.json +8 -0
- package/data/security-reports/audits/2026-02-10T20-22-24-474Z-mcp_remote-browser.json +8 -0
- package/data/security-reports/audits/2026-02-10T20-22-24-483Z-mcp_remote-browser.json +8 -0
- package/data/security-reports/audits/2026-02-10T20-42-12-305Z-mcp_remote-browser.json +8 -0
- package/data/security-reports/audits/2026-02-10T20-42-12-319Z-mcp_remote-browser.json +8 -0
- package/data/security-reports/audits/2026-02-10T20-43-15-728Z-mcp_remote-browser.json +8 -0
- package/data/security-reports/audits/2026-02-10T20-43-15-738Z-mcp_remote-browser.json +8 -0
- package/data/security-reports/audits/2026-02-10T21-22-14-047Z-mcp_remote-browser.json +8 -0
- package/data/security-reports/audits/2026-02-10T21-22-14-051Z-mcp_remote-browser.json +8 -0
- package/data/security-reports/audits/2026-02-10T21-29-59-237Z-mcp_remote-browser.json +8 -0
- package/data/security-reports/audits/2026-02-10T21-29-59-243Z-mcp_remote-browser.json +8 -0
- package/data/security-reports/audits/2026-02-11T20-21-51-074Z-mcp_remote-browser.json +8 -0
- package/data/security-reports/audits/2026-02-11T20-21-51-123Z-mcp_remote-browser.json +8 -0
- package/data/security-reports/audits/2026-02-11T20-28-33-021Z-mcp_remote-browser.json +8 -0
- package/data/security-reports/audits/2026-02-11T20-28-33-026Z-mcp_remote-browser.json +8 -0
- package/data/security-reports/audits/2026-02-11T20-34-43-623Z-mcp_remote-browser.json +8 -0
- package/data/security-reports/audits/2026-02-11T20-34-43-625Z-mcp_remote-browser.json +8 -0
- package/data/security-reports/audits/2026-02-11T21-06-33-281Z-mcp_remote-browser.json +8 -0
- package/data/security-reports/audits/2026-02-11T21-06-33-285Z-mcp_remote-browser.json +8 -0
- package/data/security-reports/audits/2026-02-11T21-08-58-836Z-mcp_remote-browser.json +8 -0
- package/data/security-reports/audits/2026-02-11T21-08-58-843Z-mcp_remote-browser.json +8 -0
- package/data/security-reports/audits/2026-02-12T12-26-07-150Z-mcp_remote-browser.json +8 -0
- package/data/security-reports/audits/2026-02-12T12-26-07-159Z-mcp_remote-browser.json +8 -0
- package/data/security-reports/audits/2026-02-12T14-37-36-565Z-mcp_remote-browser.json +8 -0
- package/data/security-reports/audits/2026-02-12T14-37-36-569Z-mcp_remote-browser.json +8 -0
- package/data/security-reports/audits/2026-02-12T14-47-32-103Z-mcp_remote-browser.json +8 -0
- package/data/security-reports/audits/2026-02-12T14-47-32-213Z-mcp_remote-browser.json +8 -0
- package/data/security-reports/audits/2026-02-12T14-47-47-769Z-mcp_filesystem.json +8 -0
- package/data/security-reports/audits/2026-02-12T15-05-49-085Z-mcp_remote-browser.json +8 -0
- package/data/security-reports/audits/2026-02-12T15-05-49-087Z-mcp_remote-browser.json +8 -0
- package/data/security-reports/audits/2026-02-12T16-37-42-204Z-mcp_remote-browser.json +8 -0
- package/data/security-reports/audits/2026-02-12T16-37-42-243Z-mcp_remote-browser.json +8 -0
- package/data/security-reports/audits/2026-02-12T16-47-16-589Z-mcp_remote-browser.json +8 -0
- package/data/security-reports/audits/2026-02-12T16-47-16-596Z-mcp_remote-browser.json +8 -0
- package/data/security-reports/audits/2026-02-12T17-38-24-899Z-mcp_remote-browser.json +8 -0
- package/data/security-reports/audits/2026-02-12T17-38-24-905Z-mcp_remote-browser.json +8 -0
- package/data/security-reports/audits/2026-02-12T17-56-00-835Z-mcp_remote-browser.json +8 -0
- package/data/security-reports/audits/2026-02-12T17-56-00-840Z-mcp_remote-browser.json +8 -0
- package/data/security-reports/audits/2026-02-12T18-19-26-005Z-mcp_remote-browser.json +8 -0
- package/data/security-reports/audits/2026-02-12T18-19-26-008Z-mcp_remote-browser.json +8 -0
- package/data/security-reports/audits/2026-02-12T18-34-38-642Z-mcp_remote-browser.json +8 -0
- package/data/security-reports/audits/2026-02-12T18-34-38-645Z-mcp_remote-browser.json +8 -0
- package/data/security-reports/audits/2026-02-13T05-44-27-648Z-mcp_remote-browser.json +8 -0
- package/data/security-reports/audits/2026-02-13T05-44-27-656Z-mcp_remote-browser.json +8 -0
- package/data/security-reports/audits/2026-02-13T05-48-50-827Z-mcp_remote-browser.json +8 -0
- package/data/security-reports/audits/2026-02-13T05-48-50-900Z-mcp_remote-browser.json +8 -0
- package/data/security-reports/audits/2026-02-13T10-53-33-850Z-mcp_remote-browser.json +8 -0
- package/data/security-reports/audits/2026-02-13T10-53-33-853Z-mcp_remote-browser.json +8 -0
- package/data/security-reports/audits/2026-02-14T17-51-27-279Z-mcp_remote-browser.json +8 -0
- package/data/security-reports/audits/2026-02-14T17-51-27-282Z-mcp_remote-browser.json +8 -0
- package/data/security-reports/audits/2026-02-14T19-43-39-991Z-mcp_remote-browser.json +8 -0
- package/data/security-reports/audits/2026-02-14T19-43-39-997Z-mcp_remote-browser.json +8 -0
- package/data/security-reports/audits/2026-02-23T19-24-43-515Z-mcp_remote-browser.json +8 -0
- package/data/security-reports/audits/2026-02-23T19-24-43-518Z-mcp_remote-browser.json +8 -0
- package/data/security-reports/audits/2026-02-25T14-45-02-763Z-mcp_remote-browser.json +8 -0
- package/data/security-reports/audits/2026-02-25T14-45-02-778Z-mcp_remote-browser.json +8 -0
- package/data/security-reports/audits/2026-02-25T14-46-58-957Z-mcp_remote-browser.json +8 -0
- package/data/security-reports/audits/2026-02-25T14-46-58-960Z-mcp_remote-browser.json +8 -0
- package/data/security-reports/audits/2026-02-25T14-57-37-133Z-mcp_remote-browser.json +8 -0
- package/data/security-reports/audits/2026-02-25T14-57-37-139Z-mcp_remote-browser.json +8 -0
- package/data/security-reports/audits/2026-02-25T15-03-23-507Z-mcp_remote-browser.json +8 -0
- package/data/security-reports/audits/2026-02-25T15-03-23-513Z-mcp_remote-browser.json +8 -0
- package/data/security-reports/audits/2026-02-25T15-03-41-157Z-mcp_remote-browser.json +8 -0
- package/data/security-reports/audits/2026-02-25T15-03-41-162Z-mcp_remote-browser.json +8 -0
- package/data/security-reports/audits/2026-02-25T15-05-18-042Z-mcp_remote-browser.json +8 -0
- package/data/security-reports/audits/2026-02-25T15-05-18-048Z-mcp_remote-browser.json +8 -0
- package/data/security-reports/audits/2026-02-25T15-39-08-519Z-mcp_remote-browser.json +8 -0
- package/data/security-reports/audits/2026-02-25T15-39-08-526Z-mcp_remote-browser.json +8 -0
- package/data/security-reports/audits/2026-02-25T18-35-54-463Z-mcp_remote-browser.json +8 -0
- package/data/security-reports/audits/2026-02-25T18-35-54-466Z-mcp_remote-browser.json +8 -0
- package/data/security-reports/audits/2026-02-26T05-52-21-092Z-mcp_remote-browser.json +8 -0
- package/data/security-reports/audits/2026-02-26T05-52-21-093Z-mcp_remote-browser.json +8 -0
- package/data/security-reports/audits/2026-02-26T05-52-27-076Z-mcp_remote-browser.json +8 -0
- package/data/security-reports/audits/2026-02-26T05-52-27-079Z-mcp_remote-browser.json +8 -0
- package/data/security-reports/audits/2026-02-26T05-52-27-084Z-mcp_remote-browser.json +8 -0
- package/data/security-reports/audits/2026-02-26T05-52-27-086Z-mcp_remote-browser.json +8 -0
- package/data/security-reports/audits/2026-02-26T05-52-37-249Z-mcp_remote-browser.json +8 -0
- package/data/security-reports/audits/2026-02-26T05-52-37-258Z-mcp_remote-browser.json +8 -0
- package/data/security-reports/audits/2026-02-26T05-52-37-259Z-mcp_remote-browser.json +8 -0
- package/data/security-reports/audits/2026-02-26T05-52-37-274Z-mcp_remote-browser.json +8 -0
- package/data/security-reports/audits/2026-02-26T05-53-28-389Z-mcp_remote-browser.json +8 -0
- package/data/security-reports/audits/2026-02-26T05-53-28-391Z-mcp_remote-browser.json +8 -0
- package/data/security-reports/audits/2026-02-26T05-53-33-868Z-mcp_remote-browser.json +8 -0
- package/data/security-reports/audits/2026-02-26T05-53-33-880Z-mcp_remote-browser.json +8 -0
- package/data/security-reports/audits/2026-02-26T05-53-33-892Z-mcp_remote-browser.json +8 -0
- package/data/security-reports/audits/2026-02-26T05-53-33-900Z-mcp_remote-browser.json +8 -0
- package/data/security-reports/audits/2026-02-26T05-53-43-064Z-mcp_remote-browser.json +8 -0
- package/data/security-reports/audits/2026-02-26T05-53-43-066Z-mcp_remote-browser.json +8 -0
- package/data/security-reports/audits/2026-02-26T05-53-43-068Z-mcp_remote-browser.json +8 -0
- package/data/security-reports/audits/2026-02-26T14-55-47-466Z-claude-plugin_workspace-ops.json +8 -0
- package/data/security-reports/audits/2026-02-26T14-55-47-468Z-copilot-extension_repo-security.json +8 -0
- package/data/security-reports/audits/2026-02-26T16-55-59-431Z-mcp_remote-browser.json +8 -0
- package/data/security-reports/audits/2026-02-26T16-55-59-432Z-mcp_remote-browser.json +8 -0
- package/data/security-reports/audits/2026-02-26T16-55-59-435Z-mcp_remote-browser.json +8 -0
- package/data/security-reports/audits/2026-02-26T16-55-59-439Z-mcp_remote-browser.json +8 -0
- package/data/security-reports/audits/2026-02-26T16-56-08-566Z-mcp_remote-browser.json +8 -0
- package/data/security-reports/audits/2026-02-26T16-56-08-570Z-mcp_remote-browser.json +8 -0
- package/data/security-reports/audits/2026-02-26T16-56-08-589Z-mcp_remote-browser.json +8 -0
- package/data/security-reports/audits/2026-02-26T16-56-08-591Z-mcp_remote-browser.json +8 -0
- package/data/security-reports/audits/2026-02-26T16-56-47-356Z-mcp_remote-browser.json +8 -0
- package/data/security-reports/audits/2026-02-26T16-56-47-358Z-mcp_remote-browser.json +8 -0
- package/data/security-reports/audits/2026-02-26T16-56-53-607Z-mcp_remote-browser.json +8 -0
- package/data/security-reports/audits/2026-02-26T16-56-53-612Z-mcp_remote-browser.json +8 -0
- package/data/security-reports/audits/2026-02-26T16-56-53-624Z-mcp_remote-browser.json +8 -0
- package/data/security-reports/audits/2026-02-26T16-56-53-628Z-mcp_remote-browser.json +8 -0
- package/data/security-reports/audits/2026-02-26T16-57-09-879Z-mcp_remote-browser.json +8 -0
- package/data/security-reports/audits/2026-02-26T16-57-09-881Z-mcp_remote-browser.json +8 -0
- package/data/security-reports/audits/2026-02-26T16-57-10-846Z-mcp_remote-browser.json +8 -0
- package/data/security-reports/audits/2026-02-26T16-57-10-848Z-mcp_remote-browser.json +8 -0
- package/data/security-reports/audits/2026-03-10T18-15-05-007Z-claude-plugin_playwright.json +8 -0
- package/data/security-reports/audits/2026-03-10T18-36-16-092Z-claude-plugin_playwright.json +8 -0
- package/data/whitelist/approved.json +5 -0
- package/dist/catalog/adapter.js +39 -0
- package/dist/catalog/adapters/claude-code-marketplace-v1.js +260 -0
- package/dist/catalog/adapters/claude-connectors-scrape-v1.js +107 -0
- package/dist/catalog/adapters/claude-plugins-scrape-v1.js +107 -0
- package/dist/catalog/adapters/claude-plugins-v0.1.js +48 -0
- package/dist/catalog/adapters/copilot-extensions-v0.1.js +48 -0
- package/dist/catalog/adapters/copilot-plugin-marketplace-v1.js +117 -0
- package/dist/catalog/adapters/mcp-registry-v0.1.js +211 -0
- package/dist/catalog/adapters/openai-skills-github-v1.js +100 -0
- package/dist/catalog/adapters/openai-skills-v1.js +48 -0
- package/dist/catalog/adapters/shared.js +94 -0
- package/dist/catalog/remote-registry.js +196 -0
- package/dist/catalog/repository.js +161 -0
- package/dist/catalog/sync-state.js +61 -0
- package/dist/catalog/sync.js +153 -0
- package/dist/cli.js +25 -0
- package/dist/commands/ExplainerVideo.js +225 -0
- package/dist/commands/ingest.js +11 -0
- package/dist/commands/validate-data.js +10 -0
- package/dist/config/runtime.js +51 -0
- package/dist/config/sources.js +21 -0
- package/dist/ingestion/mcps.js +77 -0
- package/dist/ingestion/skills.js +76 -0
- package/dist/install/dependencies.js +58 -0
- package/dist/install/review-state.js +70 -0
- package/dist/install/skillsh.js +245 -0
- package/dist/interfaces/cli/doctor.js +90 -0
- package/dist/interfaces/cli/formatters/colors.js +24 -0
- package/dist/interfaces/cli/formatters/csv.js +10 -0
- package/dist/interfaces/cli/formatters/json.js +3 -0
- package/dist/interfaces/cli/formatters/markdown.js +6 -0
- package/dist/interfaces/cli/formatters/table.js +82 -0
- package/dist/interfaces/cli/index.js +1277 -0
- package/dist/interfaces/cli/options.js +93 -0
- package/dist/interfaces/cli/output.js +9 -0
- package/dist/interfaces/cli/types.js +1 -0
- package/dist/interfaces/cli/ui/home.js +114 -0
- package/dist/interfaces/cli/ui/web-report.js +384 -0
- package/dist/interfaces/cli/update-check.js +180 -0
- package/dist/lib/json.js +11 -0
- package/dist/lib/logger.js +13 -0
- package/dist/lib/paths.js +18 -0
- package/dist/lib/validation/contracts.js +245 -0
- package/dist/mcps/normalize.js +38 -0
- package/dist/models/records.js +31 -0
- package/dist/recommendation/engine.js +135 -0
- package/dist/recommendation/project-analysis.js +231 -0
- package/dist/recommendation/requirements.js +58 -0
- package/dist/security/assessment.js +56 -0
- package/dist/security/whitelist.js +70 -0
- package/dist/skills/normalize.js +39 -0
- package/dist/validation/curated.js +72 -0
- package/dist/video/Root.js +6 -0
- package/dist/video/index.js +3 -0
- package/package.json +102 -0
|
@@ -0,0 +1,180 @@
|
|
|
1
|
+
import fs from 'node:fs/promises';
|
|
2
|
+
import semver from 'semver';
|
|
3
|
+
import { readJsonFile, writeJsonFile } from '../../lib/json.js';
|
|
4
|
+
import { getPackagePath, getStatePath } from '../../lib/paths.js';
|
|
5
|
+
const UPDATE_CHECK_TTL_MS = 24 * 60 * 60 * 1000;
|
|
6
|
+
const FETCH_TIMEOUT_MS = 2500;
|
|
7
|
+
const RELEASE_REPO = 'amitrintzler/skills-and-mcps';
|
|
8
|
+
export const RELEASE_API_URL = `https://api.github.com/repos/${RELEASE_REPO}/releases/latest`;
|
|
9
|
+
export const RELEASE_DOWNLOAD_URL = `https://github.com/${RELEASE_REPO}/releases/latest`;
|
|
10
|
+
export function getUpdateCheckStatePath() {
|
|
11
|
+
return getStatePath('data/system/update-check.json');
|
|
12
|
+
}
|
|
13
|
+
export function normalizeReleaseVersion(tag) {
|
|
14
|
+
const clean = tag.trim().replace(/^v/i, '');
|
|
15
|
+
const strict = semver.valid(clean);
|
|
16
|
+
if (strict) {
|
|
17
|
+
return strict;
|
|
18
|
+
}
|
|
19
|
+
const coerced = semver.coerce(clean);
|
|
20
|
+
return coerced ? coerced.version : null;
|
|
21
|
+
}
|
|
22
|
+
export function isVersionNewer(candidate, current) {
|
|
23
|
+
const normalizedCurrent = normalizeReleaseVersion(current);
|
|
24
|
+
const normalizedCandidate = normalizeReleaseVersion(candidate);
|
|
25
|
+
if (!normalizedCurrent || !normalizedCandidate) {
|
|
26
|
+
return false;
|
|
27
|
+
}
|
|
28
|
+
return semver.gt(normalizedCandidate, normalizedCurrent);
|
|
29
|
+
}
|
|
30
|
+
export function isCacheFresh(lastCheckedAt, now = new Date()) {
|
|
31
|
+
if (!lastCheckedAt) {
|
|
32
|
+
return false;
|
|
33
|
+
}
|
|
34
|
+
const parsed = Date.parse(lastCheckedAt);
|
|
35
|
+
if (!Number.isFinite(parsed)) {
|
|
36
|
+
return false;
|
|
37
|
+
}
|
|
38
|
+
return now.getTime() - parsed < UPDATE_CHECK_TTL_MS;
|
|
39
|
+
}
|
|
40
|
+
export function isUpdateCheckDisabled(options) {
|
|
41
|
+
if (options.disableAutoCheck) {
|
|
42
|
+
return true;
|
|
43
|
+
}
|
|
44
|
+
if (process.env.PLUGSCOUT_DISABLE_UPDATE_CHECK === '1' || process.env.TOOLKIT_DISABLE_UPDATE_CHECK === '1') {
|
|
45
|
+
return true;
|
|
46
|
+
}
|
|
47
|
+
if (process.env.CI) {
|
|
48
|
+
return true;
|
|
49
|
+
}
|
|
50
|
+
return !process.stdout.isTTY;
|
|
51
|
+
}
|
|
52
|
+
export async function maybeNotifyAboutUpdate(options = {}) {
|
|
53
|
+
if (isUpdateCheckDisabled(options)) {
|
|
54
|
+
return;
|
|
55
|
+
}
|
|
56
|
+
const currentVersion = await loadCurrentVersion();
|
|
57
|
+
let state = await loadUpdateCheckState();
|
|
58
|
+
if (!isCacheFresh(state.lastCheckedAt) || !state.latestVersion) {
|
|
59
|
+
const fetched = await lookupLatestReleaseVersion();
|
|
60
|
+
state = {
|
|
61
|
+
...state,
|
|
62
|
+
lastCheckedAt: new Date().toISOString(),
|
|
63
|
+
source: 'github-releases'
|
|
64
|
+
};
|
|
65
|
+
if (fetched.status === 'ok') {
|
|
66
|
+
state.latestVersion = fetched.latestVersion;
|
|
67
|
+
}
|
|
68
|
+
if (fetched.status === 'no-release') {
|
|
69
|
+
delete state.latestVersion;
|
|
70
|
+
}
|
|
71
|
+
await saveUpdateCheckState(state);
|
|
72
|
+
if (fetched.status !== 'ok') {
|
|
73
|
+
return;
|
|
74
|
+
}
|
|
75
|
+
}
|
|
76
|
+
if (!state.latestVersion || !isVersionNewer(state.latestVersion, currentVersion)) {
|
|
77
|
+
return;
|
|
78
|
+
}
|
|
79
|
+
if (state.lastNotifiedVersion === state.latestVersion) {
|
|
80
|
+
return;
|
|
81
|
+
}
|
|
82
|
+
console.log(`New PlugScout version available: v${currentVersion} -> v${state.latestVersion}`);
|
|
83
|
+
console.log(`Download: ${RELEASE_DOWNLOAD_URL}`);
|
|
84
|
+
await saveUpdateCheckState({
|
|
85
|
+
...state,
|
|
86
|
+
source: 'github-releases',
|
|
87
|
+
lastNotifiedVersion: state.latestVersion
|
|
88
|
+
});
|
|
89
|
+
}
|
|
90
|
+
export async function checkForUpdateNow() {
|
|
91
|
+
const currentVersion = await loadCurrentVersion();
|
|
92
|
+
const result = await lookupLatestReleaseVersion();
|
|
93
|
+
if (result.status === 'error') {
|
|
94
|
+
return { status: 'error', currentVersion };
|
|
95
|
+
}
|
|
96
|
+
if (result.status === 'no-release') {
|
|
97
|
+
await saveUpdateCheckState({
|
|
98
|
+
...(await loadUpdateCheckState()),
|
|
99
|
+
source: 'github-releases',
|
|
100
|
+
lastCheckedAt: new Date().toISOString(),
|
|
101
|
+
latestVersion: undefined
|
|
102
|
+
});
|
|
103
|
+
return { status: 'no-release', currentVersion };
|
|
104
|
+
}
|
|
105
|
+
await saveUpdateCheckState({
|
|
106
|
+
...(await loadUpdateCheckState()),
|
|
107
|
+
source: 'github-releases',
|
|
108
|
+
lastCheckedAt: new Date().toISOString(),
|
|
109
|
+
latestVersion: result.latestVersion
|
|
110
|
+
});
|
|
111
|
+
if (isVersionNewer(result.latestVersion, currentVersion)) {
|
|
112
|
+
return { status: 'update-available', currentVersion, latestVersion: result.latestVersion };
|
|
113
|
+
}
|
|
114
|
+
return { status: 'up-to-date', currentVersion, latestVersion: result.latestVersion };
|
|
115
|
+
}
|
|
116
|
+
async function lookupLatestReleaseVersion() {
|
|
117
|
+
const controller = new AbortController();
|
|
118
|
+
const timeout = setTimeout(() => controller.abort(), FETCH_TIMEOUT_MS);
|
|
119
|
+
try {
|
|
120
|
+
const response = await fetch(RELEASE_API_URL, {
|
|
121
|
+
method: 'GET',
|
|
122
|
+
signal: controller.signal,
|
|
123
|
+
headers: {
|
|
124
|
+
Accept: 'application/vnd.github+json',
|
|
125
|
+
'User-Agent': 'plugscout-cli'
|
|
126
|
+
}
|
|
127
|
+
});
|
|
128
|
+
if (response.status === 404) {
|
|
129
|
+
return { status: 'no-release' };
|
|
130
|
+
}
|
|
131
|
+
if (!response.ok) {
|
|
132
|
+
return { status: 'error' };
|
|
133
|
+
}
|
|
134
|
+
const payload = (await response.json());
|
|
135
|
+
const normalized = normalizeReleaseVersion(payload.tag_name ?? '');
|
|
136
|
+
if (!normalized) {
|
|
137
|
+
return { status: 'error' };
|
|
138
|
+
}
|
|
139
|
+
return { status: 'ok', latestVersion: normalized };
|
|
140
|
+
}
|
|
141
|
+
catch {
|
|
142
|
+
return { status: 'error' };
|
|
143
|
+
}
|
|
144
|
+
finally {
|
|
145
|
+
clearTimeout(timeout);
|
|
146
|
+
}
|
|
147
|
+
}
|
|
148
|
+
async function loadCurrentVersion() {
|
|
149
|
+
try {
|
|
150
|
+
const raw = await fs.readFile(getPackagePath('package.json'), 'utf8');
|
|
151
|
+
const parsed = JSON.parse(raw);
|
|
152
|
+
const normalized = normalizeReleaseVersion(parsed.version ?? '');
|
|
153
|
+
return normalized ?? '0.0.0';
|
|
154
|
+
}
|
|
155
|
+
catch {
|
|
156
|
+
return '0.0.0';
|
|
157
|
+
}
|
|
158
|
+
}
|
|
159
|
+
async function loadUpdateCheckState() {
|
|
160
|
+
const filePath = getUpdateCheckStatePath();
|
|
161
|
+
try {
|
|
162
|
+
const raw = await readJsonFile(filePath);
|
|
163
|
+
if (!raw || typeof raw !== 'object' || Array.isArray(raw)) {
|
|
164
|
+
return {};
|
|
165
|
+
}
|
|
166
|
+
const typed = raw;
|
|
167
|
+
return {
|
|
168
|
+
lastCheckedAt: typeof typed.lastCheckedAt === 'string' ? typed.lastCheckedAt : undefined,
|
|
169
|
+
latestVersion: typeof typed.latestVersion === 'string' ? typed.latestVersion : undefined,
|
|
170
|
+
lastNotifiedVersion: typeof typed.lastNotifiedVersion === 'string' ? typed.lastNotifiedVersion : undefined,
|
|
171
|
+
source: typed.source === 'github-releases' ? 'github-releases' : undefined
|
|
172
|
+
};
|
|
173
|
+
}
|
|
174
|
+
catch {
|
|
175
|
+
return {};
|
|
176
|
+
}
|
|
177
|
+
}
|
|
178
|
+
async function saveUpdateCheckState(state) {
|
|
179
|
+
await writeJsonFile(getUpdateCheckStatePath(), state);
|
|
180
|
+
}
|
package/dist/lib/json.js
ADDED
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
import path from 'node:path';
|
|
2
|
+
import fs from 'fs-extra';
|
|
3
|
+
export async function readJsonFile(filePath) {
|
|
4
|
+
const fullPath = path.resolve(filePath);
|
|
5
|
+
return fs.readJson(fullPath);
|
|
6
|
+
}
|
|
7
|
+
export async function writeJsonFile(filePath, data) {
|
|
8
|
+
const fullPath = path.resolve(filePath);
|
|
9
|
+
await fs.ensureFile(fullPath);
|
|
10
|
+
await fs.writeJson(fullPath, data, { spaces: 2 });
|
|
11
|
+
}
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
function log(level, message, payload) {
|
|
2
|
+
const time = new Date().toISOString();
|
|
3
|
+
if (payload) {
|
|
4
|
+
console[level === 'info' ? 'log' : level](`[${time}] [${level.toUpperCase()}] ${message}`, payload);
|
|
5
|
+
return;
|
|
6
|
+
}
|
|
7
|
+
console[level === 'info' ? 'log' : level](`[${time}] [${level.toUpperCase()}] ${message}`);
|
|
8
|
+
}
|
|
9
|
+
export const logger = {
|
|
10
|
+
info: (message, payload) => log('info', message, payload),
|
|
11
|
+
warn: (message, payload) => log('warn', message, payload),
|
|
12
|
+
error: (message, payload) => log('error', message, payload)
|
|
13
|
+
};
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
import os from 'node:os';
|
|
2
|
+
import path from 'node:path';
|
|
3
|
+
import { fileURLToPath } from 'node:url';
|
|
4
|
+
const MODULE_DIR = path.dirname(fileURLToPath(import.meta.url));
|
|
5
|
+
const PACKAGE_ROOT = path.resolve(MODULE_DIR, '../..');
|
|
6
|
+
export function getPackageRoot() {
|
|
7
|
+
return PACKAGE_ROOT;
|
|
8
|
+
}
|
|
9
|
+
export function getPackagePath(...segments) {
|
|
10
|
+
return path.resolve(PACKAGE_ROOT, ...segments);
|
|
11
|
+
}
|
|
12
|
+
export function getToolkitHome() {
|
|
13
|
+
const root = process.env.PLUGSCOUT_HOME ?? process.env.TOOLKIT_HOME ?? path.join(os.homedir(), '.plugscout');
|
|
14
|
+
return path.resolve(root);
|
|
15
|
+
}
|
|
16
|
+
export function getStatePath(...segments) {
|
|
17
|
+
return path.resolve(getToolkitHome(), ...segments);
|
|
18
|
+
}
|
|
@@ -0,0 +1,245 @@
|
|
|
1
|
+
import { z } from 'zod';
|
|
2
|
+
const isoDate = z
|
|
3
|
+
.string()
|
|
4
|
+
.regex(/^(19|20|21)\d{2}-[01]\d-[0-3]\d$/, 'Expected ISO date (YYYY-MM-DD)');
|
|
5
|
+
export const RiskTierSchema = z.enum(['low', 'medium', 'high', 'critical']);
|
|
6
|
+
export const CatalogKindSchema = z.enum(['skill', 'mcp', 'claude-plugin', 'claude-connector', 'copilot-extension']);
|
|
7
|
+
const SecuritySignalsSchema = z
|
|
8
|
+
.object({
|
|
9
|
+
knownVulnerabilities: z.number().int().min(0).default(0),
|
|
10
|
+
suspiciousPatterns: z.number().int().min(0).default(0),
|
|
11
|
+
injectionFindings: z.number().int().min(0).default(0),
|
|
12
|
+
exfiltrationSignals: z.number().int().min(0).default(0),
|
|
13
|
+
integrityAlerts: z.number().int().min(0).default(0)
|
|
14
|
+
})
|
|
15
|
+
.default({});
|
|
16
|
+
export const InstallMethodSchema = z.discriminatedUnion('kind', [
|
|
17
|
+
z.object({
|
|
18
|
+
kind: z.literal('skill.sh'),
|
|
19
|
+
target: z.string().min(1),
|
|
20
|
+
args: z.array(z.string()).default([])
|
|
21
|
+
}),
|
|
22
|
+
z.object({
|
|
23
|
+
kind: z.literal('gh-cli'),
|
|
24
|
+
target: z.string().min(1),
|
|
25
|
+
args: z.array(z.string()).default([])
|
|
26
|
+
}),
|
|
27
|
+
z.object({
|
|
28
|
+
kind: z.literal('manual'),
|
|
29
|
+
instructions: z.string().min(1),
|
|
30
|
+
url: z.string().url().optional()
|
|
31
|
+
})
|
|
32
|
+
]);
|
|
33
|
+
export const CatalogItemSchema = z.object({
|
|
34
|
+
id: z.string().min(1),
|
|
35
|
+
kind: CatalogKindSchema,
|
|
36
|
+
name: z.string().min(1),
|
|
37
|
+
description: z.string().min(1),
|
|
38
|
+
provider: z.string().min(1),
|
|
39
|
+
capabilities: z.array(z.string().min(1)).default([]),
|
|
40
|
+
compatibility: z.array(z.string().min(1)).default([]),
|
|
41
|
+
source: z.string().min(1),
|
|
42
|
+
lastSeenAt: isoDate,
|
|
43
|
+
transport: z.enum(['stdio', 'http', 'sse', 'websocket']).optional(),
|
|
44
|
+
authModel: z.enum(['none', 'api_key', 'oauth', 'custom']).optional(),
|
|
45
|
+
install: InstallMethodSchema,
|
|
46
|
+
adoptionSignal: z.number().min(0).max(100).default(50),
|
|
47
|
+
maintenanceSignal: z.number().min(0).max(100).default(50),
|
|
48
|
+
provenanceSignal: z.number().min(0).max(100).default(80),
|
|
49
|
+
freshnessSignal: z.number().min(0).max(100).default(50),
|
|
50
|
+
securitySignals: SecuritySignalsSchema,
|
|
51
|
+
metadata: z.record(z.unknown()).default({})
|
|
52
|
+
});
|
|
53
|
+
export const CatalogSkillSchema = CatalogItemSchema.extend({
|
|
54
|
+
kind: z.literal('skill')
|
|
55
|
+
});
|
|
56
|
+
export const CatalogMcpServerSchema = CatalogItemSchema.extend({
|
|
57
|
+
kind: z.literal('mcp'),
|
|
58
|
+
transport: z.enum(['stdio', 'http', 'sse', 'websocket']).default('stdio'),
|
|
59
|
+
authModel: z.enum(['none', 'api_key', 'oauth', 'custom']).default('none')
|
|
60
|
+
});
|
|
61
|
+
export const RiskAssessmentSchema = z.object({
|
|
62
|
+
id: z.string().min(1),
|
|
63
|
+
riskScore: z.number().min(0).max(100),
|
|
64
|
+
riskTier: RiskTierSchema,
|
|
65
|
+
reasons: z.array(z.string().min(1)).nonempty(),
|
|
66
|
+
scannerResults: z.object({
|
|
67
|
+
packageIntegrity: z.object({ findings: z.number().int().min(0) }),
|
|
68
|
+
vulnerabilityIntel: z.object({ findings: z.number().int().min(0) }),
|
|
69
|
+
permissionPatterns: z.object({ findings: z.number().int().min(0) }),
|
|
70
|
+
injectionTests: z.object({ findings: z.number().int().min(0) }),
|
|
71
|
+
exfiltrationHeuristics: z.object({ findings: z.number().int().min(0) })
|
|
72
|
+
}),
|
|
73
|
+
assessedAt: z.string().datetime()
|
|
74
|
+
});
|
|
75
|
+
export const RecommendationSchema = z.object({
|
|
76
|
+
id: z.string().min(1),
|
|
77
|
+
kind: CatalogKindSchema,
|
|
78
|
+
provider: z.string().min(1),
|
|
79
|
+
rankScore: z.number().min(0).max(100),
|
|
80
|
+
fitReasons: z.array(z.string().min(1)).nonempty(),
|
|
81
|
+
scoreBreakdown: z.object({
|
|
82
|
+
fitScore: z.number().min(0).max(100),
|
|
83
|
+
trustScore: z.number().min(0).max(100),
|
|
84
|
+
securityPenalty: z.number().min(0).max(100),
|
|
85
|
+
freshnessBonus: z.number().min(0).max(100),
|
|
86
|
+
blockedPenalty: z.number().min(0).max(100)
|
|
87
|
+
}),
|
|
88
|
+
riskTier: RiskTierSchema,
|
|
89
|
+
riskScore: z.number().min(0).max(100),
|
|
90
|
+
blocked: z.boolean(),
|
|
91
|
+
blockReason: z.string().optional(),
|
|
92
|
+
installMethod: z.enum(['skill.sh', 'gh-cli', 'manual'])
|
|
93
|
+
});
|
|
94
|
+
export const InstallAuditSchema = z.object({
|
|
95
|
+
id: z.string().min(1),
|
|
96
|
+
requestedAt: z.string().datetime(),
|
|
97
|
+
policyDecision: z.enum(['allowed', 'blocked', 'override-allowed']),
|
|
98
|
+
overrideUsed: z.boolean(),
|
|
99
|
+
installer: z.enum(['skill.sh', 'gh-cli', 'manual', 'skills', 'npm', 'docker']),
|
|
100
|
+
exitCode: z.number().int()
|
|
101
|
+
});
|
|
102
|
+
export const RemoteRegistrySchema = z.object({
|
|
103
|
+
url: z.string().url(),
|
|
104
|
+
format: z.enum(['json-array', 'catalog-json', 'html']).default('json-array'),
|
|
105
|
+
entryPath: z.string().min(1).optional(),
|
|
106
|
+
supportsUpdatedSince: z.boolean().default(false),
|
|
107
|
+
updatedSinceParam: z.string().min(1).default('updated_since'),
|
|
108
|
+
pagination: z
|
|
109
|
+
.object({
|
|
110
|
+
mode: z.literal('cursor').default('cursor'),
|
|
111
|
+
cursorParam: z.string().min(1).default('cursor'),
|
|
112
|
+
nextCursorPath: z.string().min(1).default('next_cursor'),
|
|
113
|
+
limitParam: z.string().min(1).optional(),
|
|
114
|
+
limit: z.number().int().min(1).max(1000).optional()
|
|
115
|
+
})
|
|
116
|
+
.optional(),
|
|
117
|
+
timeoutMs: z.number().int().min(100).max(120000).default(10000),
|
|
118
|
+
authEnv: z.string().min(1).optional(),
|
|
119
|
+
fallbackToLocal: z.boolean().default(true),
|
|
120
|
+
provider: z.string().min(1).optional(),
|
|
121
|
+
official: z.boolean().default(true),
|
|
122
|
+
licenseHint: z.string().min(1).optional()
|
|
123
|
+
});
|
|
124
|
+
export const RegistrySchema = z.object({
|
|
125
|
+
id: z.string().min(1),
|
|
126
|
+
kind: CatalogKindSchema,
|
|
127
|
+
sourceType: z.enum(['public-index', 'vendor-feed', 'community-list']),
|
|
128
|
+
adapter: z
|
|
129
|
+
.enum([
|
|
130
|
+
'direct',
|
|
131
|
+
'mcp-registry-v0.1',
|
|
132
|
+
'openai-skills-v1',
|
|
133
|
+
'openai-skills-github-v1',
|
|
134
|
+
'claude-plugins-v0.1',
|
|
135
|
+
'claude-plugins-scrape-v1',
|
|
136
|
+
'claude-code-marketplace-v1',
|
|
137
|
+
'copilot-extensions-v0.1',
|
|
138
|
+
'copilot-plugin-marketplace-v1',
|
|
139
|
+
'claude-connectors-scrape-v1'
|
|
140
|
+
])
|
|
141
|
+
.default('direct'),
|
|
142
|
+
enabled: z.boolean().default(true),
|
|
143
|
+
officialOnly: z.boolean().default(true),
|
|
144
|
+
entries: z.array(z.unknown()).default([]),
|
|
145
|
+
remote: RemoteRegistrySchema.optional()
|
|
146
|
+
});
|
|
147
|
+
export const RegistriesFileSchema = z.object({
|
|
148
|
+
registries: z.array(RegistrySchema)
|
|
149
|
+
});
|
|
150
|
+
export const SecurityPolicySchema = z.object({
|
|
151
|
+
thresholds: z.object({
|
|
152
|
+
lowMax: z.number().int().min(0).max(100).default(24),
|
|
153
|
+
mediumMax: z.number().int().min(0).max(100).default(49),
|
|
154
|
+
highMax: z.number().int().min(0).max(100).default(74),
|
|
155
|
+
criticalMax: z.number().int().min(0).max(100).default(100)
|
|
156
|
+
}),
|
|
157
|
+
installGate: z.object({
|
|
158
|
+
blockTiers: z.array(RiskTierSchema).default(['high', 'critical']),
|
|
159
|
+
warnTiers: z.array(RiskTierSchema).default(['medium'])
|
|
160
|
+
}),
|
|
161
|
+
scoring: z.object({
|
|
162
|
+
vulnerabilityWeight: z.number().int().min(1).default(15),
|
|
163
|
+
suspiciousWeight: z.number().int().min(1).default(10),
|
|
164
|
+
injectionWeight: z.number().int().min(1).default(12),
|
|
165
|
+
exfiltrationWeight: z.number().int().min(1).default(12),
|
|
166
|
+
integrityWeight: z.number().int().min(1).default(10)
|
|
167
|
+
})
|
|
168
|
+
});
|
|
169
|
+
export const RankingPolicySchema = z.object({
|
|
170
|
+
weights: z.object({
|
|
171
|
+
compatibility: z.number().min(0).max(100).default(25),
|
|
172
|
+
capabilityCoverage: z.number().min(0).max(100).default(20),
|
|
173
|
+
maintenance: z.number().min(0).max(100).default(18),
|
|
174
|
+
provenance: z.number().min(0).max(100).default(18),
|
|
175
|
+
adoption: z.number().min(0).max(100).default(12),
|
|
176
|
+
freshnessBonusMax: z.number().min(0).max(100).default(8),
|
|
177
|
+
securityPenaltyMax: z.number().min(0).max(100).default(40),
|
|
178
|
+
blockedPenalty: z.number().min(0).max(100).default(40)
|
|
179
|
+
}),
|
|
180
|
+
tieBreakers: z.array(z.enum(['trust', 'risk', 'name'])).default(['trust', 'risk', 'name']),
|
|
181
|
+
blockedFloorTier: RiskTierSchema.default('high')
|
|
182
|
+
});
|
|
183
|
+
export const RecommendationWeightsSchema = z.object({
|
|
184
|
+
compatibility: z.number().min(0).max(100).default(40),
|
|
185
|
+
capabilityCoverage: z.number().min(0).max(100).default(25),
|
|
186
|
+
maintenance: z.number().min(0).max(100).default(15),
|
|
187
|
+
adoption: z.number().min(0).max(100).default(10),
|
|
188
|
+
securityPenaltyMax: z.number().min(0).max(100).default(30)
|
|
189
|
+
});
|
|
190
|
+
export const ProviderConfigSchema = z.object({
|
|
191
|
+
id: z.string().min(1),
|
|
192
|
+
enabled: z.boolean().default(true),
|
|
193
|
+
officialOnly: z.boolean().default(true),
|
|
194
|
+
trustLevel: z.enum(['high', 'medium', 'low']).default('high'),
|
|
195
|
+
authEnv: z.string().min(1).optional(),
|
|
196
|
+
poll: z.object({
|
|
197
|
+
mode: z.enum(['daily', 'manual', 'every-6h']).default('daily'),
|
|
198
|
+
rateLimitPerMinute: z.number().int().min(1).max(1000).default(60)
|
|
199
|
+
})
|
|
200
|
+
});
|
|
201
|
+
export const ProvidersFileSchema = z.object({
|
|
202
|
+
providers: z.array(ProviderConfigSchema)
|
|
203
|
+
});
|
|
204
|
+
export const ItemInsightSchema = z.object({
|
|
205
|
+
id: z.string().min(1),
|
|
206
|
+
benefitSummary: z.string().min(1),
|
|
207
|
+
bestFor: z.array(z.string().min(1)).default([]),
|
|
208
|
+
whenToUse: z.array(z.string().min(1)).default([]),
|
|
209
|
+
tradeoffs: z.array(z.string().min(1)).default([]),
|
|
210
|
+
usageNotes: z.array(z.string().min(1)).default([])
|
|
211
|
+
});
|
|
212
|
+
export const ItemInsightsFileSchema = z.object({
|
|
213
|
+
insights: z.array(ItemInsightSchema)
|
|
214
|
+
});
|
|
215
|
+
export const RequirementsProfileSchema = z.object({
|
|
216
|
+
useCase: z.string().default('general'),
|
|
217
|
+
stack: z.array(z.string()).default([]),
|
|
218
|
+
deployment: z.string().default('local'),
|
|
219
|
+
securityPosture: z.enum(['balanced', 'strict']).default('balanced'),
|
|
220
|
+
requiredCapabilities: z.array(z.string()).default([])
|
|
221
|
+
});
|
|
222
|
+
export const WhitelistFileSchema = z.object({
|
|
223
|
+
approved: z.array(z.string().min(1)).default([])
|
|
224
|
+
});
|
|
225
|
+
export const QuarantineEntrySchema = z.object({
|
|
226
|
+
id: z.string().min(1),
|
|
227
|
+
reason: z.string().min(1),
|
|
228
|
+
quarantinedAt: z.string().datetime()
|
|
229
|
+
});
|
|
230
|
+
export const QuarantineFileSchema = z.object({
|
|
231
|
+
quarantined: z.array(QuarantineEntrySchema).default([])
|
|
232
|
+
});
|
|
233
|
+
export const SecurityReportSchema = z.object({
|
|
234
|
+
generatedAt: z.string().datetime(),
|
|
235
|
+
staleRegistries: z.array(z.string().min(1)).default([]),
|
|
236
|
+
passed: z.array(z.string()).default([]),
|
|
237
|
+
failed: z
|
|
238
|
+
.array(z.object({
|
|
239
|
+
id: z.string().min(1),
|
|
240
|
+
riskTier: RiskTierSchema,
|
|
241
|
+
riskScore: z.number().min(0).max(100),
|
|
242
|
+
reasons: z.array(z.string()).nonempty()
|
|
243
|
+
}))
|
|
244
|
+
.default([])
|
|
245
|
+
});
|
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
import { CatalogMcpServerSchema } from '../lib/validation/contracts.js';
|
|
2
|
+
export function normalizeMcps(records, sourceId, today) {
|
|
3
|
+
return records
|
|
4
|
+
.map((entry) => CatalogMcpServerSchema.parse({
|
|
5
|
+
...ensureObject(entry),
|
|
6
|
+
source: ensureObject(entry).source ?? sourceId,
|
|
7
|
+
lastSeenAt: ensureObject(entry).lastSeenAt ?? today
|
|
8
|
+
}))
|
|
9
|
+
.sort((a, b) => a.id.localeCompare(b.id));
|
|
10
|
+
}
|
|
11
|
+
function ensureObject(value) {
|
|
12
|
+
if (!value || typeof value !== 'object' || Array.isArray(value)) {
|
|
13
|
+
throw new Error('Invalid MCP registry entry: expected object');
|
|
14
|
+
}
|
|
15
|
+
return value;
|
|
16
|
+
}
|
|
17
|
+
export function mergeMcpsById(mcps) {
|
|
18
|
+
const state = new Map();
|
|
19
|
+
for (const mcp of mcps) {
|
|
20
|
+
const existing = state.get(mcp.id);
|
|
21
|
+
if (!existing) {
|
|
22
|
+
state.set(mcp.id, mcp);
|
|
23
|
+
continue;
|
|
24
|
+
}
|
|
25
|
+
state.set(mcp.id, {
|
|
26
|
+
...existing,
|
|
27
|
+
capabilities: dedupe([...existing.capabilities, ...mcp.capabilities]),
|
|
28
|
+
compatibility: dedupe([...existing.compatibility, ...mcp.compatibility]),
|
|
29
|
+
maintenanceSignal: Math.max(existing.maintenanceSignal, mcp.maintenanceSignal),
|
|
30
|
+
adoptionSignal: Math.max(existing.adoptionSignal, mcp.adoptionSignal),
|
|
31
|
+
lastSeenAt: mcp.lastSeenAt >= existing.lastSeenAt ? mcp.lastSeenAt : existing.lastSeenAt
|
|
32
|
+
});
|
|
33
|
+
}
|
|
34
|
+
return Array.from(state.values()).sort((a, b) => a.id.localeCompare(b.id));
|
|
35
|
+
}
|
|
36
|
+
function dedupe(values) {
|
|
37
|
+
return Array.from(new Set(values)).sort((a, b) => a.localeCompare(b));
|
|
38
|
+
}
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
import { z } from 'zod';
|
|
2
|
+
const isoDate = z
|
|
3
|
+
.string()
|
|
4
|
+
.regex(/^(19|20|21)\d{2}-[01]\d-[0-3]\d$/, 'Must be an ISO date (YYYY-MM-DD)');
|
|
5
|
+
export const SkillSchema = z.object({
|
|
6
|
+
id: z.string().min(1),
|
|
7
|
+
name: z.string().min(1),
|
|
8
|
+
description: z.string().min(1),
|
|
9
|
+
taxonomyPath: z.array(z.string().min(1)).nonempty(),
|
|
10
|
+
aliases: z.array(z.string().min(1)).default([]),
|
|
11
|
+
proficiencyLevels: z.array(z.string().min(1)).nonempty(),
|
|
12
|
+
lastValidated: isoDate
|
|
13
|
+
});
|
|
14
|
+
const rangeSchema = z
|
|
15
|
+
.object({
|
|
16
|
+
min: z.number().nonnegative(),
|
|
17
|
+
mid: z.number().nonnegative(),
|
|
18
|
+
max: z.number().nonnegative()
|
|
19
|
+
})
|
|
20
|
+
.refine((value) => value.min <= value.mid && value.mid <= value.max, {
|
|
21
|
+
message: 'Expected min <= mid <= max'
|
|
22
|
+
});
|
|
23
|
+
export const McpSchema = z.object({
|
|
24
|
+
jobFamily: z.string().min(1),
|
|
25
|
+
level: z.string().min(1),
|
|
26
|
+
currency: z.string().min(1),
|
|
27
|
+
baseRange: rangeSchema,
|
|
28
|
+
geoModifier: z.record(z.number().positive()),
|
|
29
|
+
skillLinks: z.array(z.string().min(1)).nonempty(),
|
|
30
|
+
lastBenchmark: isoDate
|
|
31
|
+
});
|
|
@@ -0,0 +1,135 @@
|
|
|
1
|
+
import { loadRankingPolicy, loadSecurityPolicy } from '../config/runtime.js';
|
|
2
|
+
import { loadCatalogItems, loadQuarantine } from '../catalog/repository.js';
|
|
3
|
+
import { RecommendationSchema } from '../lib/validation/contracts.js';
|
|
4
|
+
import { buildAssessment, isBlockedTier } from '../security/assessment.js';
|
|
5
|
+
export async function recommend(options) {
|
|
6
|
+
const [items, quarantinedEntries, rankingPolicy, securityPolicy] = await Promise.all([
|
|
7
|
+
loadCatalogItems(),
|
|
8
|
+
loadQuarantine(),
|
|
9
|
+
loadRankingPolicy(),
|
|
10
|
+
loadSecurityPolicy()
|
|
11
|
+
]);
|
|
12
|
+
const kindFilter = options.kinds?.length ? new Set(options.kinds) : null;
|
|
13
|
+
const filteredItems = kindFilter ? items.filter((item) => kindFilter.has(item.kind)) : items;
|
|
14
|
+
const quarantinedIds = new Set(quarantinedEntries.map((entry) => entry.id));
|
|
15
|
+
return filteredItems
|
|
16
|
+
.map((candidate) => rankCandidate(candidate, options.projectSignals, options.requirements, rankingPolicy, securityPolicy, quarantinedIds))
|
|
17
|
+
.sort(sortRecommendations);
|
|
18
|
+
}
|
|
19
|
+
function rankCandidate(candidate, projectSignals, requirements, rankingPolicy, securityPolicy, quarantinedIds) {
|
|
20
|
+
const assessment = buildAssessment(candidate, securityPolicy);
|
|
21
|
+
const effectiveRequiredCapabilities = dedupe([
|
|
22
|
+
...requirements.requiredCapabilities,
|
|
23
|
+
...projectSignals.inferredCapabilities
|
|
24
|
+
]);
|
|
25
|
+
const compatibilityScore = overlapScore(candidate.compatibility, [
|
|
26
|
+
...projectSignals.compatibilityTags,
|
|
27
|
+
...requirements.stack
|
|
28
|
+
]);
|
|
29
|
+
const capabilityScore = overlapScore(candidate.capabilities, effectiveRequiredCapabilities);
|
|
30
|
+
const inferredCapabilityMatches = countMatches(candidate.capabilities, projectSignals.inferredCapabilities);
|
|
31
|
+
const fitScore = compatibilityScore * (rankingPolicy.weights.compatibility / 100) +
|
|
32
|
+
capabilityScore * (rankingPolicy.weights.capabilityCoverage / 100);
|
|
33
|
+
const trustScore = candidate.maintenanceSignal * (rankingPolicy.weights.maintenance / 100) +
|
|
34
|
+
candidate.provenanceSignal * (rankingPolicy.weights.provenance / 100) +
|
|
35
|
+
candidate.adoptionSignal * (rankingPolicy.weights.adoption / 100);
|
|
36
|
+
const freshnessBonus = (candidate.freshnessSignal / 100) * rankingPolicy.weights.freshnessBonusMax;
|
|
37
|
+
const baseSecurityPenalty = (assessment.riskScore / 100) * rankingPolicy.weights.securityPenaltyMax;
|
|
38
|
+
const sourcePenalty = computeSourcePenalty(candidate);
|
|
39
|
+
const securityPenalty = Math.min(100, baseSecurityPenalty + sourcePenalty);
|
|
40
|
+
const blockedByPolicy = isBlockedTier(assessment.riskTier, securityPolicy);
|
|
41
|
+
const blockedByQuarantine = quarantinedIds.has(candidate.id);
|
|
42
|
+
const blocked = blockedByPolicy || blockedByQuarantine;
|
|
43
|
+
const blockedPenalty = blocked ? rankingPolicy.weights.blockedPenalty : 0;
|
|
44
|
+
const rawRank = fitScore + trustScore + freshnessBonus - securityPenalty - blockedPenalty;
|
|
45
|
+
const rankScore = Math.max(0, Math.min(100, rawRank));
|
|
46
|
+
const fitReasons = [
|
|
47
|
+
`Project archetype: ${projectSignals.inferredArchetype} (${projectSignals.inferenceConfidence}% confidence)`,
|
|
48
|
+
`Compatibility overlap: ${compatibilityScore.toFixed(1)}`,
|
|
49
|
+
`Capability coverage: ${capabilityScore.toFixed(1)}`,
|
|
50
|
+
`Inferred capability matches: ${inferredCapabilityMatches}`,
|
|
51
|
+
`Repo evidence signals: ${projectSignals.scanEvidence.length}`,
|
|
52
|
+
`Maintenance signal: ${candidate.maintenanceSignal}`,
|
|
53
|
+
`Provenance signal: ${candidate.provenanceSignal}`,
|
|
54
|
+
`Adoption signal: ${candidate.adoptionSignal}`,
|
|
55
|
+
sourcePenalty > 0 ? `Source confidence penalty: ${round(sourcePenalty)}` : 'Source confidence penalty: 0'
|
|
56
|
+
];
|
|
57
|
+
const blockReason = blockedByQuarantine
|
|
58
|
+
? 'Quarantined by whitelist verification'
|
|
59
|
+
: blockedByPolicy
|
|
60
|
+
? `Blocked by security policy tier: ${assessment.riskTier}`
|
|
61
|
+
: undefined;
|
|
62
|
+
return RecommendationSchema.parse({
|
|
63
|
+
id: candidate.id,
|
|
64
|
+
kind: candidate.kind,
|
|
65
|
+
provider: candidate.provider,
|
|
66
|
+
rankScore,
|
|
67
|
+
fitReasons,
|
|
68
|
+
scoreBreakdown: {
|
|
69
|
+
fitScore: round(fitScore),
|
|
70
|
+
trustScore: round(trustScore),
|
|
71
|
+
securityPenalty: round(securityPenalty),
|
|
72
|
+
freshnessBonus: round(freshnessBonus),
|
|
73
|
+
blockedPenalty: round(blockedPenalty)
|
|
74
|
+
},
|
|
75
|
+
riskTier: assessment.riskTier,
|
|
76
|
+
riskScore: assessment.riskScore,
|
|
77
|
+
blocked,
|
|
78
|
+
blockReason,
|
|
79
|
+
installMethod: candidate.install.kind
|
|
80
|
+
});
|
|
81
|
+
}
|
|
82
|
+
function sortRecommendations(a, b) {
|
|
83
|
+
const primary = b.rankScore - a.rankScore;
|
|
84
|
+
if (primary !== 0) {
|
|
85
|
+
return primary;
|
|
86
|
+
}
|
|
87
|
+
const trustA = a.scoreBreakdown.trustScore;
|
|
88
|
+
const trustB = b.scoreBreakdown.trustScore;
|
|
89
|
+
const trustDiff = trustB - trustA;
|
|
90
|
+
if (trustDiff !== 0) {
|
|
91
|
+
return trustDiff;
|
|
92
|
+
}
|
|
93
|
+
const riskDiff = a.riskScore - b.riskScore;
|
|
94
|
+
if (riskDiff !== 0) {
|
|
95
|
+
return riskDiff;
|
|
96
|
+
}
|
|
97
|
+
return a.id.localeCompare(b.id);
|
|
98
|
+
}
|
|
99
|
+
function overlapScore(left, right) {
|
|
100
|
+
if (left.length === 0 || right.length === 0) {
|
|
101
|
+
return 0;
|
|
102
|
+
}
|
|
103
|
+
const rightSet = new Set(right.map((value) => value.toLowerCase()));
|
|
104
|
+
const matches = left.filter((value) => rightSet.has(value.toLowerCase())).length;
|
|
105
|
+
return (matches / Math.max(left.length, right.length)) * 100;
|
|
106
|
+
}
|
|
107
|
+
function countMatches(left, right) {
|
|
108
|
+
if (left.length === 0 || right.length === 0) {
|
|
109
|
+
return 0;
|
|
110
|
+
}
|
|
111
|
+
const rightSet = new Set(right.map((value) => value.toLowerCase()));
|
|
112
|
+
return left.filter((value) => rightSet.has(value.toLowerCase())).length;
|
|
113
|
+
}
|
|
114
|
+
function dedupe(values) {
|
|
115
|
+
return Array.from(new Set(values.map((value) => value.trim()).filter((value) => value.length > 0)));
|
|
116
|
+
}
|
|
117
|
+
function round(value) {
|
|
118
|
+
return Math.round(value * 10) / 10;
|
|
119
|
+
}
|
|
120
|
+
function computeSourcePenalty(candidate) {
|
|
121
|
+
const metadata = candidate.metadata && typeof candidate.metadata === 'object' && !Array.isArray(candidate.metadata)
|
|
122
|
+
? candidate.metadata
|
|
123
|
+
: {};
|
|
124
|
+
let penalty = 0;
|
|
125
|
+
const sourceType = typeof metadata.sourceType === 'string' ? metadata.sourceType : '';
|
|
126
|
+
if (sourceType === 'community-list') {
|
|
127
|
+
penalty += 6;
|
|
128
|
+
}
|
|
129
|
+
const catalogType = typeof metadata.catalogType === 'string' ? metadata.catalogType : '';
|
|
130
|
+
const confidence = typeof metadata.sourceConfidence === 'string' ? metadata.sourceConfidence : '';
|
|
131
|
+
if (catalogType === 'connector' && confidence === 'scraped') {
|
|
132
|
+
penalty += 8;
|
|
133
|
+
}
|
|
134
|
+
return penalty;
|
|
135
|
+
}
|