@shnitzel/plugscout 0.3.8 → 0.3.10
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/config/providers.json +20 -0
- package/config/registries.json +644 -0
- package/dist/catalog/adapter.js +8 -0
- package/dist/catalog/adapters/cursor-extensions-v1.js +60 -0
- package/dist/catalog/adapters/gemini-extensions-v1.js +64 -0
- package/dist/catalog/remote-registry.js +12 -1
- package/dist/catalog/sync.js +13 -3
- package/dist/interfaces/cli/client-setup.js +60 -0
- package/dist/interfaces/cli/doctor.js +30 -0
- package/dist/interfaces/cli/index.js +45 -6
- package/dist/interfaces/cli/mcp.js +1 -1
- package/dist/interfaces/cli/options.js +8 -2
- package/dist/interfaces/cli/ui/home.js +63 -49
- package/dist/interfaces/cli/ui/web-report.js +231 -164
- package/dist/lib/validation/contracts.js +4 -2
- package/package.json +1 -1
|
@@ -38,12 +38,8 @@ function filterByKinds(items, kinds) {
|
|
|
38
38
|
}
|
|
39
39
|
function renderHtml(rows, stats, policy) {
|
|
40
40
|
const kindCounts = countByKind(rows.map((entry) => entry.item));
|
|
41
|
-
const topClaude = rows.filter((entry) => entry.item.kind === 'claude-plugin').slice(0, 15);
|
|
42
|
-
const topConnectors = rows.filter((entry) => entry.item.kind === 'claude-connector').slice(0, 15);
|
|
43
|
-
const topCopilot = rows.filter((entry) => entry.item.kind === 'copilot-extension').slice(0, 15);
|
|
44
|
-
const allRows = rows.slice(0, 120);
|
|
45
|
-
const detailRows = rows.slice(0, 80);
|
|
46
41
|
const riskScale = escapeHtml(formatRiskScale(policy));
|
|
42
|
+
const cardsJson = rows.map((entry) => renderDetailCard(entry, policy)).join('\n');
|
|
47
43
|
return `<!doctype html>
|
|
48
44
|
<html lang="en">
|
|
49
45
|
<head>
|
|
@@ -74,13 +70,13 @@ function renderHtml(rows, stats, policy) {
|
|
|
74
70
|
.wrap { max-width: 1460px; margin: 0 auto; }
|
|
75
71
|
h1 { margin: 0 0 8px; font-size: 34px; }
|
|
76
72
|
.sub { color: var(--muted); margin: 0 0 22px; }
|
|
77
|
-
.cards {
|
|
73
|
+
.stat-cards {
|
|
78
74
|
display: grid;
|
|
79
|
-
grid-template-columns: repeat(
|
|
75
|
+
grid-template-columns: repeat(auto-fit, minmax(140px, 1fr));
|
|
80
76
|
gap: 12px;
|
|
81
77
|
margin-bottom: 18px;
|
|
82
78
|
}
|
|
83
|
-
.card {
|
|
79
|
+
.stat-card {
|
|
84
80
|
background: var(--panel);
|
|
85
81
|
border: 1px solid var(--line);
|
|
86
82
|
border-radius: 12px;
|
|
@@ -88,75 +84,84 @@ function renderHtml(rows, stats, policy) {
|
|
|
88
84
|
}
|
|
89
85
|
.k { color: var(--muted); font-size: 12px; text-transform: uppercase; letter-spacing: 0.05em; }
|
|
90
86
|
.v { font-size: 26px; font-weight: 700; margin-top: 4px; }
|
|
91
|
-
.
|
|
92
|
-
margin-
|
|
87
|
+
.legend {
|
|
88
|
+
margin-bottom: 18px;
|
|
93
89
|
background: var(--panel);
|
|
94
90
|
border: 1px solid var(--line);
|
|
95
91
|
border-radius: 12px;
|
|
96
|
-
overflow: hidden;
|
|
97
|
-
}
|
|
98
|
-
.section-body { padding: 14px 16px; }
|
|
99
|
-
.section h2 {
|
|
100
|
-
margin: 0;
|
|
101
92
|
padding: 14px 16px;
|
|
102
|
-
font-size:
|
|
103
|
-
|
|
104
|
-
color: var(--accent);
|
|
93
|
+
font-size: 13px;
|
|
94
|
+
color: var(--muted);
|
|
105
95
|
}
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
border-
|
|
111
|
-
|
|
112
|
-
|
|
96
|
+
/* Filter bar */
|
|
97
|
+
.filter-bar {
|
|
98
|
+
background: var(--panel);
|
|
99
|
+
border: 1px solid var(--line);
|
|
100
|
+
border-radius: 12px;
|
|
101
|
+
padding: 14px 16px;
|
|
102
|
+
margin-bottom: 18px;
|
|
103
|
+
display: flex;
|
|
104
|
+
flex-wrap: wrap;
|
|
105
|
+
gap: 10px;
|
|
106
|
+
align-items: center;
|
|
113
107
|
}
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
border-radius:
|
|
118
|
-
border: 1px solid #314664;
|
|
119
|
-
padding: 2px 9px;
|
|
120
|
-
font-size: 12px;
|
|
108
|
+
.filter-bar input, .filter-bar select {
|
|
109
|
+
background: #060f1d;
|
|
110
|
+
border: 1px solid #2a3d5c;
|
|
111
|
+
border-radius: 8px;
|
|
121
112
|
color: var(--text);
|
|
122
|
-
|
|
113
|
+
padding: 7px 10px;
|
|
114
|
+
font-size: 13px;
|
|
115
|
+
outline: none;
|
|
123
116
|
}
|
|
124
|
-
.
|
|
125
|
-
.
|
|
126
|
-
.
|
|
127
|
-
|
|
117
|
+
.filter-bar input { flex: 1; min-width: 160px; }
|
|
118
|
+
.filter-bar input::placeholder { color: var(--muted); }
|
|
119
|
+
.filter-bar select:focus, .filter-bar input:focus { border-color: var(--accent); }
|
|
120
|
+
#result-count { color: var(--muted); font-size: 13px; margin-left: auto; white-space: nowrap; }
|
|
121
|
+
/* Grid */
|
|
128
122
|
.detail-grid {
|
|
129
123
|
display: grid;
|
|
130
|
-
grid-template-columns: repeat(auto-
|
|
124
|
+
grid-template-columns: repeat(auto-fill, minmax(360px, 1fr));
|
|
131
125
|
gap: 12px;
|
|
132
|
-
padding: 14px;
|
|
133
126
|
}
|
|
127
|
+
/* Cards */
|
|
134
128
|
.detail-card {
|
|
135
129
|
border: 1px solid #243654;
|
|
136
130
|
border-radius: 10px;
|
|
137
131
|
background: #081121;
|
|
138
|
-
|
|
132
|
+
overflow: hidden;
|
|
133
|
+
transition: border-color 0.15s;
|
|
134
|
+
}
|
|
135
|
+
.detail-card:hover { border-color: #3a5a8a; }
|
|
136
|
+
.card-header {
|
|
137
|
+
padding: 14px;
|
|
138
|
+
cursor: pointer;
|
|
139
|
+
user-select: none;
|
|
139
140
|
}
|
|
140
|
-
.
|
|
141
|
+
.card-header:hover { background: rgba(255,255,255,0.03); }
|
|
142
|
+
.card-title-row {
|
|
141
143
|
display: flex;
|
|
142
144
|
align-items: flex-start;
|
|
143
145
|
justify-content: space-between;
|
|
144
|
-
gap:
|
|
145
|
-
margin-bottom:
|
|
146
|
-
}
|
|
147
|
-
.title {
|
|
148
|
-
margin: 0;
|
|
149
|
-
font-size: 16px;
|
|
150
|
-
line-height: 1.3;
|
|
146
|
+
gap: 8px;
|
|
147
|
+
margin-bottom: 4px;
|
|
151
148
|
}
|
|
152
|
-
.
|
|
153
|
-
|
|
149
|
+
.title { margin: 0; font-size: 16px; line-height: 1.3; }
|
|
150
|
+
.meta { color: var(--muted); font-size: 12px; font-family: ui-monospace, SFMono-Regular, Menlo, Monaco, Consolas, monospace; margin-bottom: 8px; }
|
|
151
|
+
.pill-row { display: flex; flex-wrap: wrap; gap: 6px; margin-bottom: 8px; }
|
|
152
|
+
.pill {
|
|
153
|
+
display: inline-block;
|
|
154
|
+
border-radius: 999px;
|
|
155
|
+
border: 1px solid #314664;
|
|
156
|
+
padding: 2px 9px;
|
|
154
157
|
font-size: 12px;
|
|
155
|
-
|
|
158
|
+
color: var(--text);
|
|
159
|
+
white-space: nowrap;
|
|
156
160
|
}
|
|
157
|
-
.
|
|
158
|
-
.
|
|
159
|
-
.
|
|
161
|
+
.ok { color: var(--ok); border-color: #166534; }
|
|
162
|
+
.warn { color: var(--warn); border-color: #854d0e; }
|
|
163
|
+
.bad { color: var(--bad); border-color: #7f1d1d; }
|
|
164
|
+
.chips { display: flex; flex-wrap: wrap; gap: 5px; }
|
|
160
165
|
.chip {
|
|
161
166
|
border: 1px solid #2f476b;
|
|
162
167
|
border-radius: 999px;
|
|
@@ -164,102 +169,148 @@ function renderHtml(rows, stats, policy) {
|
|
|
164
169
|
color: #c9dbf5;
|
|
165
170
|
font-size: 12px;
|
|
166
171
|
}
|
|
172
|
+
.expand-hint {
|
|
173
|
+
font-size: 11px;
|
|
174
|
+
color: #4a6a94;
|
|
175
|
+
margin-top: 8px;
|
|
176
|
+
text-align: right;
|
|
177
|
+
}
|
|
178
|
+
.detail-card.expanded .expand-hint { display: none; }
|
|
179
|
+
/* Expanded body */
|
|
180
|
+
.card-body {
|
|
181
|
+
display: none;
|
|
182
|
+
padding: 0 14px 14px;
|
|
183
|
+
border-top: 1px solid #1a2c44;
|
|
184
|
+
}
|
|
185
|
+
.detail-card.expanded .card-body { display: block; }
|
|
186
|
+
.line { margin-top: 9px; color: var(--text); font-size: 14px; }
|
|
187
|
+
.line .label { color: var(--muted); }
|
|
167
188
|
.link { color: #93c5fd; text-decoration: none; }
|
|
168
189
|
.link:hover { text-decoration: underline; }
|
|
190
|
+
.mono { font-family: ui-monospace, SFMono-Regular, Menlo, Monaco, Consolas, monospace; font-size: 13px; }
|
|
169
191
|
pre {
|
|
170
|
-
margin:
|
|
192
|
+
margin: 10px 0 0;
|
|
171
193
|
padding: 9px 10px;
|
|
172
194
|
border: 1px solid #243654;
|
|
173
195
|
border-radius: 8px;
|
|
174
196
|
background: #060f1d;
|
|
175
197
|
overflow-x: auto;
|
|
176
198
|
color: #dbeafe;
|
|
199
|
+
font-family: ui-monospace, SFMono-Regular, Menlo, Monaco, Consolas, monospace;
|
|
200
|
+
font-size: 13px;
|
|
177
201
|
}
|
|
202
|
+
.hidden { display: none !important; }
|
|
178
203
|
</style>
|
|
179
204
|
</head>
|
|
180
205
|
<body>
|
|
181
206
|
<div class="wrap">
|
|
182
207
|
<h1>PlugScout Web Report</h1>
|
|
183
|
-
<p class="sub">
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
<div class="card"><div class="k">
|
|
187
|
-
<div class="card"><div class="k">
|
|
188
|
-
<div class="card"><div class="k">
|
|
189
|
-
<div class="card"><div class="k">
|
|
190
|
-
<div class="card"><div class="k">
|
|
191
|
-
<div class="card"><div class="k">
|
|
208
|
+
<p class="sub">Claude plugins · Claude connectors · Copilot extensions · Cursor extensions · Gemini extensions · Skills · MCP servers</p>
|
|
209
|
+
|
|
210
|
+
<div class="stat-cards">
|
|
211
|
+
<div class="stat-card"><div class="k">Total</div><div class="v">${stats.totalItems}</div></div>
|
|
212
|
+
<div class="stat-card"><div class="k">Plugins</div><div class="v">${kindCounts['claude-plugin']}</div></div>
|
|
213
|
+
<div class="stat-card"><div class="k">Connectors</div><div class="v">${kindCounts['claude-connector']}</div></div>
|
|
214
|
+
<div class="stat-card"><div class="k">Copilot Ext</div><div class="v">${kindCounts['copilot-extension']}</div></div>
|
|
215
|
+
<div class="stat-card"><div class="k">Cursor Ext</div><div class="v">${kindCounts['cursor-extension']}</div></div>
|
|
216
|
+
<div class="stat-card"><div class="k">Gemini Ext</div><div class="v">${kindCounts['gemini-extension']}</div></div>
|
|
217
|
+
<div class="stat-card"><div class="k">Skills</div><div class="v">${kindCounts.skill}</div></div>
|
|
218
|
+
<div class="stat-card"><div class="k">MCP Servers</div><div class="v">${kindCounts.mcp}</div></div>
|
|
219
|
+
<div class="stat-card"><div class="k">Whitelist / Quarantine</div><div class="v">${stats.whitelist} / ${stats.quarantined}</div></div>
|
|
220
|
+
</div>
|
|
221
|
+
|
|
222
|
+
<div class="legend">
|
|
223
|
+
<strong>Scores:</strong>
|
|
224
|
+
Trust 0–100 (higher = more trustworthy) ·
|
|
225
|
+
Risk 0–100 (lower = safer) ·
|
|
226
|
+
${riskScale} ·
|
|
227
|
+
<span style="color:var(--bad)">blocked</span> = high/critical risk or quarantined
|
|
228
|
+
</div>
|
|
229
|
+
|
|
230
|
+
<div class="filter-bar">
|
|
231
|
+
<input id="search" type="text" placeholder="Search by name or ID…" oninput="applyFilters()" />
|
|
232
|
+
<select id="kind-filter" onchange="applyFilters()">
|
|
233
|
+
<option value="">All kinds</option>
|
|
234
|
+
<option value="claude-plugin">Claude plugin</option>
|
|
235
|
+
<option value="claude-connector">Claude connector</option>
|
|
236
|
+
<option value="copilot-extension">Copilot extension</option>
|
|
237
|
+
<option value="cursor-extension">Cursor extension</option>
|
|
238
|
+
<option value="gemini-extension">Gemini extension</option>
|
|
239
|
+
<option value="skill">Skill</option>
|
|
240
|
+
<option value="mcp">MCP server</option>
|
|
241
|
+
</select>
|
|
242
|
+
<select id="risk-filter" onchange="applyFilters()">
|
|
243
|
+
<option value="">All risk tiers</option>
|
|
244
|
+
<option value="low">Low</option>
|
|
245
|
+
<option value="medium">Medium</option>
|
|
246
|
+
<option value="high">High</option>
|
|
247
|
+
<option value="critical">Critical</option>
|
|
248
|
+
</select>
|
|
249
|
+
<select id="status-filter" onchange="applyFilters()">
|
|
250
|
+
<option value="">All statuses</option>
|
|
251
|
+
<option value="allowed">Allowed</option>
|
|
252
|
+
<option value="approved">Approved</option>
|
|
253
|
+
<option value="blocked">Blocked</option>
|
|
254
|
+
</select>
|
|
255
|
+
<select id="sort-by" onchange="applyFilters()">
|
|
256
|
+
<option value="name">Sort: Name A–Z</option>
|
|
257
|
+
<option value="trust-desc">Sort: Trust ↓</option>
|
|
258
|
+
<option value="risk-asc">Sort: Risk ↑ (safest first)</option>
|
|
259
|
+
<option value="risk-desc">Sort: Risk ↓ (riskiest first)</option>
|
|
260
|
+
</select>
|
|
261
|
+
<span id="result-count"></span>
|
|
262
|
+
</div>
|
|
263
|
+
|
|
264
|
+
<div class="detail-grid" id="card-grid">
|
|
265
|
+
${cardsJson}
|
|
192
266
|
</div>
|
|
193
|
-
<section class="section">
|
|
194
|
-
<h2>How to read scores</h2>
|
|
195
|
-
<div class="section-body">
|
|
196
|
-
<div><span class="pill ok">trust</span> 0-100, higher is better.</div>
|
|
197
|
-
<div style="margin-top:6px;"><span class="pill warn">risk</span> 0-100, lower is safer. ${riskScale}</div>
|
|
198
|
-
<div style="margin-top:6px;"><span class="pill bad">blocked</span> means policy high/critical risk or quarantined.</div>
|
|
199
|
-
</div>
|
|
200
|
-
</section>
|
|
201
|
-
${renderTableSection('Top Claude Plugins', topClaude)}
|
|
202
|
-
${renderTableSection('Top Claude Connectors', topConnectors)}
|
|
203
|
-
${renderTableSection('Top Copilot Extensions', topCopilot)}
|
|
204
|
-
${renderTableSection('Catalog Snapshot', allRows)}
|
|
205
|
-
${renderDetailSection('Decision details per item', detailRows, policy)}
|
|
206
267
|
</div>
|
|
268
|
+
|
|
269
|
+
<script>
|
|
270
|
+
function toggleCard(header) {
|
|
271
|
+
header.closest('.detail-card').classList.toggle('expanded');
|
|
272
|
+
}
|
|
273
|
+
|
|
274
|
+
function applyFilters() {
|
|
275
|
+
const search = document.getElementById('search').value.toLowerCase().trim();
|
|
276
|
+
const kind = document.getElementById('kind-filter').value;
|
|
277
|
+
const risk = document.getElementById('risk-filter').value;
|
|
278
|
+
const status = document.getElementById('status-filter').value;
|
|
279
|
+
const sort = document.getElementById('sort-by').value;
|
|
280
|
+
|
|
281
|
+
const grid = document.getElementById('card-grid');
|
|
282
|
+
const cards = Array.from(grid.querySelectorAll('.detail-card'));
|
|
283
|
+
|
|
284
|
+
let visible = 0;
|
|
285
|
+
cards.forEach(card => {
|
|
286
|
+
const matchSearch = !search || card.dataset.search.includes(search);
|
|
287
|
+
const matchKind = !kind || card.dataset.kind === kind;
|
|
288
|
+
const matchRisk = !risk || card.dataset.risk === risk;
|
|
289
|
+
const matchStatus = !status || card.dataset.status === status;
|
|
290
|
+
const show = matchSearch && matchKind && matchRisk && matchStatus;
|
|
291
|
+
card.classList.toggle('hidden', !show);
|
|
292
|
+
if (show) visible++;
|
|
293
|
+
});
|
|
294
|
+
|
|
295
|
+
document.getElementById('result-count').textContent = 'Showing ' + visible + ' of ' + cards.length;
|
|
296
|
+
|
|
297
|
+
const shown = cards.filter(c => !c.classList.contains('hidden'));
|
|
298
|
+
shown.sort((a, b) => {
|
|
299
|
+
if (sort === 'name') return a.dataset.name.localeCompare(b.dataset.name);
|
|
300
|
+
if (sort === 'trust-desc') return Number(b.dataset.trust) - Number(a.dataset.trust);
|
|
301
|
+
if (sort === 'risk-asc') return Number(a.dataset.riskscore) - Number(b.dataset.riskscore);
|
|
302
|
+
if (sort === 'risk-desc') return Number(b.dataset.riskscore) - Number(a.dataset.riskscore);
|
|
303
|
+
return a.dataset.name.localeCompare(b.dataset.name);
|
|
304
|
+
});
|
|
305
|
+
shown.forEach(card => grid.appendChild(card));
|
|
306
|
+
}
|
|
307
|
+
|
|
308
|
+
// Initial count
|
|
309
|
+
applyFilters();
|
|
310
|
+
</script>
|
|
207
311
|
</body>
|
|
208
312
|
</html>`;
|
|
209
313
|
}
|
|
210
|
-
function renderTableSection(title, rows) {
|
|
211
|
-
return `<section class="section">
|
|
212
|
-
<h2>${escapeHtml(title)}</h2>
|
|
213
|
-
<table>
|
|
214
|
-
<thead>
|
|
215
|
-
<tr>
|
|
216
|
-
<th>ID</th>
|
|
217
|
-
<th>Name</th>
|
|
218
|
-
<th>Kind</th>
|
|
219
|
-
<th>Provider</th>
|
|
220
|
-
<th>Trust</th>
|
|
221
|
-
<th>Source</th>
|
|
222
|
-
<th>Confidence</th>
|
|
223
|
-
<th>Risk</th>
|
|
224
|
-
<th>Status</th>
|
|
225
|
-
</tr>
|
|
226
|
-
</thead>
|
|
227
|
-
<tbody>
|
|
228
|
-
${rows
|
|
229
|
-
.map((entry) => {
|
|
230
|
-
const metadata = asMetadata(entry.item.metadata);
|
|
231
|
-
const confidence = stringOr(metadata.sourceConfidence, 'official');
|
|
232
|
-
const trustScore = computeTrustScore(entry.item);
|
|
233
|
-
const riskClass = entry.assessment.riskTier === 'low'
|
|
234
|
-
? 'ok'
|
|
235
|
-
: entry.assessment.riskTier === 'medium'
|
|
236
|
-
? 'warn'
|
|
237
|
-
: 'bad';
|
|
238
|
-
return `<tr>
|
|
239
|
-
<td class="mono">${escapeHtml(entry.item.id)}</td>
|
|
240
|
-
<td>${escapeHtml(entry.item.name)}</td>
|
|
241
|
-
<td>${escapeHtml(entry.item.kind)}</td>
|
|
242
|
-
<td>${escapeHtml(entry.item.provider)}</td>
|
|
243
|
-
<td><span class="pill">${trustScore.toFixed(0)}</span></td>
|
|
244
|
-
<td>${escapeHtml(entry.item.source)}</td>
|
|
245
|
-
<td><span class="pill">${escapeHtml(confidence)}</span></td>
|
|
246
|
-
<td><span class="pill ${riskClass}">${escapeHtml(entry.assessment.riskTier)} (${entry.assessment.riskScore.toFixed(0)})</span></td>
|
|
247
|
-
<td>${entry.blocked ? '<span class="pill bad">blocked</span>' : entry.approved ? '<span class="pill ok">approved</span>' : '<span class="pill ok">allowed</span>'}</td>
|
|
248
|
-
</tr>`;
|
|
249
|
-
})
|
|
250
|
-
.join('\n')}
|
|
251
|
-
</tbody>
|
|
252
|
-
</table>
|
|
253
|
-
</section>`;
|
|
254
|
-
}
|
|
255
|
-
function renderDetailSection(title, rows, policy) {
|
|
256
|
-
return `<section class="section">
|
|
257
|
-
<h2>${escapeHtml(title)}</h2>
|
|
258
|
-
<div class="detail-grid">
|
|
259
|
-
${rows.map((entry) => renderDetailCard(entry, policy)).join('\n')}
|
|
260
|
-
</div>
|
|
261
|
-
</section>`;
|
|
262
|
-
}
|
|
263
314
|
function renderDetailCard(entry, policy) {
|
|
264
315
|
const metadata = asMetadata(entry.item.metadata);
|
|
265
316
|
const trustScore = computeTrustScore(entry.item);
|
|
@@ -268,48 +319,62 @@ function renderDetailCard(entry, policy) {
|
|
|
268
319
|
const sourceRepo = typeof metadata.sourceRepo === 'string' ? metadata.sourceRepo : '';
|
|
269
320
|
const sourcePage = typeof metadata.sourcePage === 'string' ? metadata.sourcePage : '';
|
|
270
321
|
const status = entry.blocked ? 'blocked' : entry.approved ? 'approved' : 'allowed';
|
|
322
|
+
const statusClass = entry.blocked ? 'bad' : 'ok';
|
|
271
323
|
const installHint = buildInstallHint(entry.item);
|
|
272
324
|
const bestFor = entry.insight?.bestFor ?? [];
|
|
273
325
|
const tradeoffs = entry.insight?.tradeoffs ?? [];
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
|
|
326
|
+
const benefitSummary = entry.insight?.benefitSummary ?? '';
|
|
327
|
+
const riskClass = entry.assessment.riskTier === 'low'
|
|
328
|
+
? 'ok'
|
|
329
|
+
: entry.assessment.riskTier === 'medium'
|
|
330
|
+
? 'warn'
|
|
331
|
+
: 'bad';
|
|
332
|
+
const searchKey = escapeHtml(`${entry.item.id} ${entry.item.name} ${entry.item.capabilities.join(' ')}`.toLowerCase());
|
|
333
|
+
const previewChips = entry.item.capabilities
|
|
334
|
+
.slice(0, 3)
|
|
335
|
+
.map((cap) => `<span class="chip">${escapeHtml(cap)}</span>`)
|
|
336
|
+
.join('');
|
|
337
|
+
const allChips = entry.item.capabilities.length > 0
|
|
338
|
+
? entry.item.capabilities.map((cap) => `<span class="chip">${escapeHtml(cap)}</span>`).join('')
|
|
339
|
+
: '<span class="chip">no capability tags</span>';
|
|
340
|
+
return `<article class="detail-card"
|
|
341
|
+
data-search="${searchKey}"
|
|
342
|
+
data-kind="${escapeHtml(entry.item.kind)}"
|
|
343
|
+
data-risk="${escapeHtml(entry.assessment.riskTier)}"
|
|
344
|
+
data-status="${escapeHtml(status)}"
|
|
345
|
+
data-name="${escapeHtml(entry.item.name.toLowerCase())}"
|
|
346
|
+
data-trust="${trustScore.toFixed(0)}"
|
|
347
|
+
data-riskscore="${entry.assessment.riskScore.toFixed(0)}">
|
|
348
|
+
<div class="card-header" onclick="toggleCard(this)">
|
|
349
|
+
<div class="card-title-row">
|
|
277
350
|
<h3 class="title">${escapeHtml(entry.item.name)}</h3>
|
|
278
|
-
<div class="meta mono">${escapeHtml(entry.item.id)}</div>
|
|
279
|
-
</div>
|
|
280
|
-
<div>
|
|
281
351
|
<span class="pill">${escapeHtml(entry.item.kind)}</span>
|
|
282
352
|
</div>
|
|
353
|
+
<div class="meta">${escapeHtml(entry.item.id)}</div>
|
|
354
|
+
<div class="pill-row">
|
|
355
|
+
<span class="pill">trust: ${trustScore.toFixed(0)}</span>
|
|
356
|
+
<span class="pill ${riskClass}">risk: ${escapeHtml(entry.assessment.riskTier)} (${entry.assessment.riskScore.toFixed(0)})</span>
|
|
357
|
+
<span class="pill ${statusClass}">${escapeHtml(status)}</span>
|
|
358
|
+
</div>
|
|
359
|
+
${previewChips ? `<div class="chips">${previewChips}</div>` : ''}
|
|
360
|
+
<div class="expand-hint">▼ click for details</div>
|
|
361
|
+
</div>
|
|
362
|
+
<div class="card-body">
|
|
363
|
+
<div class="line">${escapeHtml(entry.item.description)}</div>
|
|
364
|
+
${benefitSummary ? `<div class="line"><span class="label">What it does:</span> ${escapeHtml(benefitSummary)}</div>` : ''}
|
|
365
|
+
${bestFor.length > 0 ? `<div class="line"><span class="label">Best for:</span> ${escapeHtml(bestFor.join('; '))}</div>` : ''}
|
|
366
|
+
${tradeoffs.length > 0 ? `<div class="line"><span class="label">Tradeoffs:</span> ${escapeHtml(tradeoffs.join('; '))}</div>` : ''}
|
|
367
|
+
<div class="line"><span class="label">Capabilities:</span></div>
|
|
368
|
+
<div class="chips" style="margin-top:6px">${allChips}</div>
|
|
369
|
+
<div class="line"><span class="label">Decision:</span> trust ${trustScore.toFixed(0)}/100 (${escapeHtml(describeTrustBand(trustScore))}), risk ${entry.assessment.riskScore.toFixed(0)}/100 (${escapeHtml(entry.assessment.riskTier)}; ${escapeHtml(describeRiskBand(entry.assessment.riskScore, policy))}), status ${escapeHtml(status)}.</div>
|
|
370
|
+
<div class="line"><span class="label">Risk reasons:</span> ${escapeHtml(entry.assessment.reasons.join('; '))}</div>
|
|
371
|
+
<div class="line"><span class="label">Provenance:</span> provider=${escapeHtml(entry.item.provider)} source=${escapeHtml(entry.item.source)} confidence=${escapeHtml(confidence)} catalog=${escapeHtml(catalogType)}</div>
|
|
372
|
+
${sourceRepo ? `<div class="line"><span class="label">Source repo:</span> <a class="link" href="${escapeHtml(sourceRepo)}" target="_blank" rel="noopener">${escapeHtml(sourceRepo)}</a></div>` : ''}
|
|
373
|
+
${sourcePage ? `<div class="line"><span class="label">Source page:</span> <a class="link" href="${escapeHtml(sourcePage)}" target="_blank" rel="noopener">${escapeHtml(sourcePage)}</a></div>` : ''}
|
|
374
|
+
<pre class="mono">${escapeHtml(installHint)}</pre>
|
|
283
375
|
</div>
|
|
284
|
-
<div class="line">${escapeHtml(entry.item.description)}</div>
|
|
285
|
-
<div class="line"><span class="label">Decision:</span> trust ${trustScore.toFixed(0)}/100 (${escapeHtml(describeTrustBand(trustScore))}), risk ${entry.assessment.riskScore.toFixed(0)}/100 (${escapeHtml(entry.assessment.riskTier)}; ${escapeHtml(describeRiskBand(entry.assessment.riskScore, policy))}), status ${escapeHtml(status)}.</div>
|
|
286
|
-
<div class="line"><span class="label">Risk reasons:</span> ${escapeHtml(entry.assessment.reasons.join('; '))}</div>
|
|
287
|
-
<div class="line"><span class="label">Provenance:</span> provider=${escapeHtml(entry.item.provider)} source=${escapeHtml(entry.item.source)} confidence=${escapeHtml(confidence)} catalog=${escapeHtml(catalogType)}</div>
|
|
288
|
-
${sourceRepo
|
|
289
|
-
? `<div class="line"><span class="label">Source repo:</span> <a class="link" href="${escapeHtml(sourceRepo)}">${escapeHtml(sourceRepo)}</a></div>`
|
|
290
|
-
: ''}
|
|
291
|
-
${sourcePage
|
|
292
|
-
? `<div class="line"><span class="label">Source page:</span> <a class="link" href="${escapeHtml(sourcePage)}">${escapeHtml(sourcePage)}</a></div>`
|
|
293
|
-
: ''}
|
|
294
|
-
${bestFor.length > 0
|
|
295
|
-
? `<div class="line"><span class="label">Best for:</span> ${escapeHtml(bestFor.join('; '))}</div>`
|
|
296
|
-
: ''}
|
|
297
|
-
${tradeoffs.length > 0
|
|
298
|
-
? `<div class="line"><span class="label">Tradeoffs:</span> ${escapeHtml(tradeoffs.join('; '))}</div>`
|
|
299
|
-
: ''}
|
|
300
|
-
<div class="chips">${renderChips(entry.item.capabilities)}</div>
|
|
301
|
-
<pre class="mono">${escapeHtml(installHint)}</pre>
|
|
302
376
|
</article>`;
|
|
303
377
|
}
|
|
304
|
-
function renderChips(values) {
|
|
305
|
-
if (values.length === 0) {
|
|
306
|
-
return '<span class="chip">no capability tags</span>';
|
|
307
|
-
}
|
|
308
|
-
return values
|
|
309
|
-
.slice(0, 8)
|
|
310
|
-
.map((value) => `<span class="chip">${escapeHtml(value)}</span>`)
|
|
311
|
-
.join('');
|
|
312
|
-
}
|
|
313
378
|
function buildInstallHint(item) {
|
|
314
379
|
if (item.install.kind === 'manual') {
|
|
315
380
|
if (item.install.url) {
|
|
@@ -379,6 +444,8 @@ function countByKind(items) {
|
|
|
379
444
|
mcp: 0,
|
|
380
445
|
'claude-plugin': 0,
|
|
381
446
|
'claude-connector': 0,
|
|
382
|
-
'copilot-extension': 0
|
|
447
|
+
'copilot-extension': 0,
|
|
448
|
+
'cursor-extension': 0,
|
|
449
|
+
'gemini-extension': 0
|
|
383
450
|
});
|
|
384
451
|
}
|
|
@@ -3,7 +3,7 @@ const isoDate = z
|
|
|
3
3
|
.string()
|
|
4
4
|
.regex(/^(19|20|21)\d{2}-[01]\d-[0-3]\d$/, 'Expected ISO date (YYYY-MM-DD)');
|
|
5
5
|
export const RiskTierSchema = z.enum(['low', 'medium', 'high', 'critical']);
|
|
6
|
-
export const CatalogKindSchema = z.enum(['skill', 'mcp', 'claude-plugin', 'claude-connector', 'copilot-extension']);
|
|
6
|
+
export const CatalogKindSchema = z.enum(['skill', 'mcp', 'claude-plugin', 'claude-connector', 'copilot-extension', 'cursor-extension', 'gemini-extension']);
|
|
7
7
|
const SecuritySignalsSchema = z
|
|
8
8
|
.object({
|
|
9
9
|
knownVulnerabilities: z.number().int().min(0).default(0),
|
|
@@ -137,7 +137,9 @@ export const RegistrySchema = z.object({
|
|
|
137
137
|
'copilot-extensions-v0.1',
|
|
138
138
|
'copilot-plugin-marketplace-v1',
|
|
139
139
|
'claude-connectors-scrape-v1',
|
|
140
|
-
'awesome-claude-code-v1'
|
|
140
|
+
'awesome-claude-code-v1',
|
|
141
|
+
'cursor-extensions-v1',
|
|
142
|
+
'gemini-extensions-v1'
|
|
141
143
|
])
|
|
142
144
|
.default('direct'),
|
|
143
145
|
enabled: z.boolean().default(true),
|
package/package.json
CHANGED