@shnitzel/plugscout 0.3.34 → 0.3.35
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/README.md +4 -2
- package/config/providers.json +10 -0
- package/config/registries.json +15 -0
- package/dist/catalog/adapter.js +4 -0
- package/dist/catalog/adapters/agentskills-il-v1.js +180 -0
- package/dist/catalog/adapters/mcp-registry-v0.1.js +17 -0
- package/dist/catalog/remote-registry.js +7 -2
- package/dist/catalog/sync.js +1 -1
- package/dist/lib/validation/contracts.js +2 -1
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -78,7 +78,7 @@ npm install
|
|
|
78
78
|
npm run setup
|
|
79
79
|
```
|
|
80
80
|
|
|
81
|
-
## Your first scan
|
|
81
|
+
## Your first scan
|
|
82
82
|
|
|
83
83
|
```bash
|
|
84
84
|
plugscout setup # install deps, write config, sync catalogs
|
|
@@ -97,7 +97,7 @@ skill:secure-prompting skill openai low(0) fals
|
|
|
97
97
|
|
|
98
98
|
Review any result with `plugscout show --id <id>`, then install with `plugscout install --id <id> --yes`.
|
|
99
99
|
|
|
100
|
-
## Quick Start
|
|
100
|
+
## Quick Start
|
|
101
101
|
|
|
102
102
|
```bash
|
|
103
103
|
npm install -g @shnitzel/plugscout
|
|
@@ -197,6 +197,8 @@ plugscout sync # skip registries synced within the last 6 hours
|
|
|
197
197
|
plugscout sync --force # re-fetch everything regardless of cache age
|
|
198
198
|
```
|
|
199
199
|
|
|
200
|
+
**First sync timing:** The MCP registry (`registry.modelcontextprotocol.io`) contains 10,000+ servers and is fetched in paginated batches. The first full sync takes 5–8 minutes. Subsequent syncs are cached for 6 hours and complete in seconds. Progress is printed per page so you can see it working.
|
|
201
|
+
|
|
200
202
|
Cursor and Gemini extension lists are served from `raw.githubusercontent.com` and auto-update on each sync.
|
|
201
203
|
|
|
202
204
|
## Core Commands
|
package/config/providers.json
CHANGED
package/config/registries.json
CHANGED
|
@@ -3363,6 +3363,21 @@
|
|
|
3363
3363
|
"official": true,
|
|
3364
3364
|
"licenseHint": "community"
|
|
3365
3365
|
}
|
|
3366
|
+
},
|
|
3367
|
+
{
|
|
3368
|
+
"id": "agentskills-il",
|
|
3369
|
+
"kind": "skill",
|
|
3370
|
+
"adapter": "agentskills-il-v1",
|
|
3371
|
+
"sourceType": "vendor-feed",
|
|
3372
|
+
"officialOnly": false,
|
|
3373
|
+
"entries": [],
|
|
3374
|
+
"remote": {
|
|
3375
|
+
"url": "https://raw.githubusercontent.com/amitrintzler/plugscout/main/assets/registries/agentskills-il.json",
|
|
3376
|
+
"format": "json-array",
|
|
3377
|
+
"timeoutMs": 15000,
|
|
3378
|
+
"fallbackToLocal": true,
|
|
3379
|
+
"provider": "skills-il"
|
|
3380
|
+
}
|
|
3366
3381
|
}
|
|
3367
3382
|
]
|
|
3368
3383
|
}
|
package/dist/catalog/adapter.js
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import { adaptAwesomeClaudeCodeEntries } from './adapters/awesome-claude-code-v1.js';
|
|
2
2
|
import { adaptCursorExtensionsEntries } from './adapters/cursor-extensions-v1.js';
|
|
3
3
|
import { adaptGeminiExtensionsEntries } from './adapters/gemini-extensions-v1.js';
|
|
4
|
+
import { adaptAgentskillsIlEntries } from './adapters/agentskills-il-v1.js';
|
|
4
5
|
import { adaptClaudeConnectorsScrapeEntries } from './adapters/claude-connectors-scrape-v1.js';
|
|
5
6
|
import { adaptClaudeCodeMarketplaceEntries } from './adapters/claude-code-marketplace-v1.js';
|
|
6
7
|
import { adaptClaudePluginsEntries } from './adapters/claude-plugins-v0.1.js';
|
|
@@ -47,5 +48,8 @@ export function adaptRegistryEntries(registry, entries) {
|
|
|
47
48
|
if (registry.adapter === 'gemini-extensions-v1') {
|
|
48
49
|
return adaptGeminiExtensionsEntries(registry.id, entries);
|
|
49
50
|
}
|
|
51
|
+
if (registry.adapter === 'agentskills-il-v1') {
|
|
52
|
+
return adaptAgentskillsIlEntries(registry.id, entries);
|
|
53
|
+
}
|
|
50
54
|
return entries;
|
|
51
55
|
}
|
|
@@ -0,0 +1,180 @@
|
|
|
1
|
+
import { dedupe, readString } from './shared.js';
|
|
2
|
+
export function adaptAgentskillsIlEntries(sourceId, entries) {
|
|
3
|
+
return entries
|
|
4
|
+
.map((entry) => mapEntry(sourceId, entry))
|
|
5
|
+
.filter((entry) => entry !== null);
|
|
6
|
+
}
|
|
7
|
+
function mapEntry(sourceId, entry) {
|
|
8
|
+
if (!entry || typeof entry !== 'object' || Array.isArray(entry)) {
|
|
9
|
+
return null;
|
|
10
|
+
}
|
|
11
|
+
const record = entry;
|
|
12
|
+
const source = readString(record, ['_source']);
|
|
13
|
+
if (source === 'mcp') {
|
|
14
|
+
return mapMcpEntry(sourceId, record);
|
|
15
|
+
}
|
|
16
|
+
return mapSkillEntry(sourceId, record);
|
|
17
|
+
}
|
|
18
|
+
function mapSkillEntry(sourceId, record) {
|
|
19
|
+
const slug = readString(record, ['_slug']);
|
|
20
|
+
if (!slug)
|
|
21
|
+
return null;
|
|
22
|
+
const displayName = resolveLocalized(record['display_name'], 'en') ?? slug;
|
|
23
|
+
const displayDescription = resolveLocalized(record['display_description'], 'en') ?? `AgentSkills IL skill: ${slug}`;
|
|
24
|
+
const tagsEn = extractStringArray(resolveLocalized(record['tags'], 'en'));
|
|
25
|
+
const tagsHe = extractStringArray(resolveLocalized(record['tags'], 'he'));
|
|
26
|
+
const category = readString(record, ['category']) ?? readString(record, ['_repo']) ?? 'general';
|
|
27
|
+
const repoUrl = readString(record, ['_repoUrl']);
|
|
28
|
+
const supportedAgents = extractStringArray(record['supported_agents']);
|
|
29
|
+
const compatibility = dedupe(['general', ...agentsToCompatibility(supportedAgents)]);
|
|
30
|
+
const capabilities = dedupe([...tagsEn, categoryToCapability(category)].filter(Boolean));
|
|
31
|
+
return {
|
|
32
|
+
id: `skill:agentskills-il/${slug}`,
|
|
33
|
+
kind: 'skill',
|
|
34
|
+
provider: 'skills-il',
|
|
35
|
+
name: displayName,
|
|
36
|
+
description: displayDescription,
|
|
37
|
+
capabilities,
|
|
38
|
+
compatibility,
|
|
39
|
+
source: sourceId,
|
|
40
|
+
install: {
|
|
41
|
+
kind: 'manual',
|
|
42
|
+
instructions: `npx skills-il ${slug}`,
|
|
43
|
+
url: repoUrl ?? `https://agentskills.co.il/skills/${slug}`
|
|
44
|
+
},
|
|
45
|
+
adoptionSignal: 55,
|
|
46
|
+
maintenanceSignal: 72,
|
|
47
|
+
provenanceSignal: 85,
|
|
48
|
+
freshnessSignal: 75,
|
|
49
|
+
securitySignals: {
|
|
50
|
+
knownVulnerabilities: 0,
|
|
51
|
+
suspiciousPatterns: 0,
|
|
52
|
+
injectionFindings: 0,
|
|
53
|
+
exfiltrationSignals: 0,
|
|
54
|
+
integrityAlerts: 0
|
|
55
|
+
},
|
|
56
|
+
metadata: {
|
|
57
|
+
repositoryUrl: repoUrl,
|
|
58
|
+
category,
|
|
59
|
+
tagsHe,
|
|
60
|
+
descriptionHe: resolveLocalized(record['display_description'], 'he'),
|
|
61
|
+
version: readString(record, ['version']),
|
|
62
|
+
sourceType: 'vendor-feed',
|
|
63
|
+
sourceConfidence: 'official'
|
|
64
|
+
}
|
|
65
|
+
};
|
|
66
|
+
}
|
|
67
|
+
function mapMcpEntry(sourceId, record) {
|
|
68
|
+
const slug = readString(record, ['_slug']);
|
|
69
|
+
const pkgName = readString(record, ['name']);
|
|
70
|
+
if (!slug && !pkgName)
|
|
71
|
+
return null;
|
|
72
|
+
const id = slug ?? pkgName ?? '';
|
|
73
|
+
const name = toTitle(slug ?? pkgName?.replace(/^@[^/]+\//, '') ?? id);
|
|
74
|
+
const description = readString(record, ['description']) ?? `AgentSkills IL MCP server: ${id}`;
|
|
75
|
+
const keywords = extractStringArray(record['keywords']);
|
|
76
|
+
const repoUrl = readString(record, ['_repoUrl']);
|
|
77
|
+
const capabilities = dedupe(keywords.length > 0 ? keywords : ['israel']);
|
|
78
|
+
return {
|
|
79
|
+
id: `mcp:agentskills-il/${slug ?? id}`,
|
|
80
|
+
kind: 'mcp',
|
|
81
|
+
provider: 'skills-il',
|
|
82
|
+
name,
|
|
83
|
+
description,
|
|
84
|
+
transport: 'stdio',
|
|
85
|
+
authModel: 'none',
|
|
86
|
+
capabilities,
|
|
87
|
+
compatibility: ['general', 'node'],
|
|
88
|
+
source: sourceId,
|
|
89
|
+
install: {
|
|
90
|
+
kind: 'skill.sh',
|
|
91
|
+
target: pkgName ?? `@skills-il/${slug}`,
|
|
92
|
+
args: []
|
|
93
|
+
},
|
|
94
|
+
adoptionSignal: 50,
|
|
95
|
+
maintenanceSignal: 70,
|
|
96
|
+
provenanceSignal: 85,
|
|
97
|
+
freshnessSignal: 75,
|
|
98
|
+
securitySignals: {
|
|
99
|
+
knownVulnerabilities: 0,
|
|
100
|
+
suspiciousPatterns: 0,
|
|
101
|
+
injectionFindings: 0,
|
|
102
|
+
exfiltrationSignals: 0,
|
|
103
|
+
integrityAlerts: 0
|
|
104
|
+
},
|
|
105
|
+
metadata: {
|
|
106
|
+
repositoryUrl: repoUrl,
|
|
107
|
+
packageIdentifier: pkgName,
|
|
108
|
+
packageRegistryType: 'npm',
|
|
109
|
+
packageRuntime: 'node',
|
|
110
|
+
version: readString(record, ['version']),
|
|
111
|
+
sourceType: 'vendor-feed',
|
|
112
|
+
sourceConfidence: 'official'
|
|
113
|
+
}
|
|
114
|
+
};
|
|
115
|
+
}
|
|
116
|
+
function resolveLocalized(value, lang) {
|
|
117
|
+
if (!value || typeof value !== 'object' || Array.isArray(value)) {
|
|
118
|
+
if (typeof value === 'string')
|
|
119
|
+
return value;
|
|
120
|
+
if (Array.isArray(value))
|
|
121
|
+
return value;
|
|
122
|
+
return undefined;
|
|
123
|
+
}
|
|
124
|
+
const rec = value;
|
|
125
|
+
const v = rec[lang];
|
|
126
|
+
if (typeof v === 'string')
|
|
127
|
+
return v;
|
|
128
|
+
if (Array.isArray(v))
|
|
129
|
+
return v;
|
|
130
|
+
return undefined;
|
|
131
|
+
}
|
|
132
|
+
function extractStringArray(value) {
|
|
133
|
+
if (Array.isArray(value)) {
|
|
134
|
+
return value.filter((v) => typeof v === 'string' && v.trim().length > 0);
|
|
135
|
+
}
|
|
136
|
+
if (typeof value === 'string' && value.trim().length > 0) {
|
|
137
|
+
return [value];
|
|
138
|
+
}
|
|
139
|
+
return [];
|
|
140
|
+
}
|
|
141
|
+
function agentsToCompatibility(agents) {
|
|
142
|
+
const map = {
|
|
143
|
+
'claude-code': 'claude',
|
|
144
|
+
cursor: 'cursor',
|
|
145
|
+
'github-copilot': 'github',
|
|
146
|
+
windsurf: 'windsurf',
|
|
147
|
+
'gemini-cli': 'gemini',
|
|
148
|
+
codex: 'openai',
|
|
149
|
+
opencode: 'general',
|
|
150
|
+
antigravity: 'general',
|
|
151
|
+
};
|
|
152
|
+
return agents.map((a) => map[a]).filter((v) => Boolean(v));
|
|
153
|
+
}
|
|
154
|
+
function categoryToCapability(category) {
|
|
155
|
+
const map = {
|
|
156
|
+
'tax-and-finance': 'finance',
|
|
157
|
+
localization: 'localization',
|
|
158
|
+
'government-services': 'government',
|
|
159
|
+
'security-compliance': 'security',
|
|
160
|
+
communication: 'communication',
|
|
161
|
+
'developer-tools': 'automation',
|
|
162
|
+
'food-and-dining': 'food',
|
|
163
|
+
'health-services': 'health',
|
|
164
|
+
'marketing-growth': 'marketing',
|
|
165
|
+
'legal-tech': 'legal',
|
|
166
|
+
education: 'education',
|
|
167
|
+
accounting: 'finance',
|
|
168
|
+
'design-systems': 'design',
|
|
169
|
+
courses: 'education',
|
|
170
|
+
};
|
|
171
|
+
return map[category] ?? category;
|
|
172
|
+
}
|
|
173
|
+
function toTitle(slug) {
|
|
174
|
+
return slug
|
|
175
|
+
.replace(/^@[^/]+\//, '')
|
|
176
|
+
.split(/[-_]/g)
|
|
177
|
+
.filter(Boolean)
|
|
178
|
+
.map((part) => part[0].toUpperCase() + part.slice(1))
|
|
179
|
+
.join(' ');
|
|
180
|
+
}
|
|
@@ -1,9 +1,26 @@
|
|
|
1
1
|
import { dedupe, extractStringArray, readNestedString, readString } from './shared.js';
|
|
2
2
|
export function adaptMcpRegistryEntries(sourceId, entries) {
|
|
3
3
|
return entries
|
|
4
|
+
.filter((entry) => isMcpEntryLatest(entry))
|
|
4
5
|
.map((entry) => mapMcpRegistryEntry(sourceId, entry))
|
|
5
6
|
.filter((entry) => entry !== null);
|
|
6
7
|
}
|
|
8
|
+
function isMcpEntryLatest(entry) {
|
|
9
|
+
if (!entry || typeof entry !== 'object' || Array.isArray(entry)) {
|
|
10
|
+
return true;
|
|
11
|
+
}
|
|
12
|
+
const record = entry;
|
|
13
|
+
const meta = record._meta;
|
|
14
|
+
if (!meta || typeof meta !== 'object' || Array.isArray(meta)) {
|
|
15
|
+
return true;
|
|
16
|
+
}
|
|
17
|
+
const official = meta['io.modelcontextprotocol.registry/official'];
|
|
18
|
+
if (!official || typeof official !== 'object' || Array.isArray(official)) {
|
|
19
|
+
return true;
|
|
20
|
+
}
|
|
21
|
+
const isLatest = official.isLatest;
|
|
22
|
+
return isLatest !== false;
|
|
23
|
+
}
|
|
7
24
|
function mapMcpRegistryEntry(sourceId, entry) {
|
|
8
25
|
if (!entry || typeof entry !== 'object' || Array.isArray(entry)) {
|
|
9
26
|
return null;
|
|
@@ -21,7 +21,7 @@ export async function resolveRegistryEntries(registry, options = {}, fetchImpl =
|
|
|
21
21
|
}
|
|
22
22
|
}
|
|
23
23
|
try {
|
|
24
|
-
const parsed = await fetchRemoteRegistryEntries(registry, options, fetchImpl);
|
|
24
|
+
const parsed = await fetchRemoteRegistryEntries(registry, options, fetchImpl, options.onProgress);
|
|
25
25
|
if (parsed.length === 0 && registry.entries.length > 0) {
|
|
26
26
|
const level = options.updatedSince ? 'info' : 'warn';
|
|
27
27
|
const reason = options.updatedSince ? 'returned no updates' : 'returned no entries';
|
|
@@ -38,14 +38,16 @@ export async function resolveRegistryEntries(registry, options = {}, fetchImpl =
|
|
|
38
38
|
throw error;
|
|
39
39
|
}
|
|
40
40
|
}
|
|
41
|
-
export async function fetchRemoteRegistryEntries(registry, options = {}, fetchImpl = fetch) {
|
|
41
|
+
export async function fetchRemoteRegistryEntries(registry, options = {}, fetchImpl = fetch, onProgress) {
|
|
42
42
|
if (!registry.remote) {
|
|
43
43
|
throw new Error(`Registry ${registry.id} has no remote definition`);
|
|
44
44
|
}
|
|
45
45
|
validateRemoteHost(registry);
|
|
46
46
|
const allEntries = [];
|
|
47
47
|
let cursor;
|
|
48
|
+
let page = 0;
|
|
48
49
|
do {
|
|
50
|
+
page++;
|
|
49
51
|
const payload = await fetchRemoteRegistryPayload(registry, fetchImpl, {
|
|
50
52
|
cursor,
|
|
51
53
|
updatedSince: options.updatedSince
|
|
@@ -53,6 +55,9 @@ export async function fetchRemoteRegistryEntries(registry, options = {}, fetchIm
|
|
|
53
55
|
const parsed = extractEntries(payload, registry.remote.format, registry.remote.entryPath, registry.kind);
|
|
54
56
|
allEntries.push(...parsed);
|
|
55
57
|
cursor = resolveNextCursor(payload, registry.remote.pagination?.nextCursorPath);
|
|
58
|
+
if (cursor && onProgress) {
|
|
59
|
+
onProgress(` ↓ ${registry.id} page ${page + 1}… (${allEntries.length} so far)`);
|
|
60
|
+
}
|
|
56
61
|
} while (cursor && registry.remote.pagination);
|
|
57
62
|
return allEntries;
|
|
58
63
|
}
|
package/dist/catalog/sync.js
CHANGED
|
@@ -50,7 +50,7 @@ export async function syncCatalogs(today = new Date().toISOString().slice(0, 10)
|
|
|
50
50
|
}
|
|
51
51
|
progress(` ↓ ${registry.id} (${registry.kind})…`);
|
|
52
52
|
const updatedSince = registry.remote?.supportsUpdatedSince ? getUpdatedSince(syncState, registry.id) : undefined;
|
|
53
|
-
const resolved = await resolveRegistryEntries(registry, { updatedSince });
|
|
53
|
+
const resolved = await resolveRegistryEntries(registry, { updatedSince, onProgress: progress });
|
|
54
54
|
const adaptedEntries = resolved.source === 'remote' ? adaptRegistryEntries(registry, resolved.entries) : resolved.entries;
|
|
55
55
|
// Always include curated local entries so they persist even when remote succeeds
|
|
56
56
|
const curatedEntries = resolved.source === 'remote' ? registry.entries : [];
|
|
@@ -139,7 +139,8 @@ export const RegistrySchema = z.object({
|
|
|
139
139
|
'claude-connectors-scrape-v1',
|
|
140
140
|
'awesome-claude-code-v1',
|
|
141
141
|
'cursor-extensions-v1',
|
|
142
|
-
'gemini-extensions-v1'
|
|
142
|
+
'gemini-extensions-v1',
|
|
143
|
+
'agentskills-il-v1'
|
|
143
144
|
])
|
|
144
145
|
.default('direct'),
|
|
145
146
|
enabled: z.boolean().default(true),
|
package/package.json
CHANGED