@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.
@@ -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(3, minmax(180px, 1fr));
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
- .section {
92
- margin-top: 18px;
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: 18px;
103
- border-bottom: 1px solid var(--line);
104
- color: var(--accent);
93
+ font-size: 13px;
94
+ color: var(--muted);
105
95
  }
106
- table { width: 100%; border-collapse: collapse; }
107
- th, td {
108
- text-align: left;
109
- padding: 10px 12px;
110
- border-bottom: 1px solid #1d2a41;
111
- vertical-align: top;
112
- word-break: break-word;
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
- th { color: var(--muted); font-size: 12px; text-transform: uppercase; letter-spacing: 0.04em; }
115
- .pill {
116
- display: inline-block;
117
- border-radius: 999px;
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
- white-space: nowrap;
113
+ padding: 7px 10px;
114
+ font-size: 13px;
115
+ outline: none;
123
116
  }
124
- .ok { color: var(--ok); border-color: #166534; }
125
- .warn { color: var(--warn); border-color: #854d0e; }
126
- .bad { color: var(--bad); border-color: #7f1d1d; }
127
- .mono { font-family: ui-monospace, SFMono-Regular, Menlo, Monaco, Consolas, "Liberation Mono", "Courier New", monospace; font-size: 13px; }
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-fit, minmax(360px, 1fr));
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
- padding: 12px;
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
- .detail-head {
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: 10px;
145
- margin-bottom: 8px;
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
- .meta {
153
- color: var(--muted);
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
- margin-top: 4px;
158
+ color: var(--text);
159
+ white-space: nowrap;
156
160
  }
157
- .line { margin-top: 8px; color: var(--text); }
158
- .line .label { color: var(--muted); }
159
- .chips { margin-top: 8px; display: flex; flex-wrap: wrap; gap: 6px; }
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: 8px 0 0;
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">Readable catalog view for Claude plugins, Claude connectors, Copilot extensions, Skills, and MCP servers.</p>
184
- <div class="cards">
185
- <div class="card"><div class="k">Items</div><div class="v">${stats.totalItems}</div></div>
186
- <div class="card"><div class="k">Claude Plugins</div><div class="v">${kindCounts['claude-plugin']}</div></div>
187
- <div class="card"><div class="k">Claude Connectors</div><div class="v">${kindCounts['claude-connector']}</div></div>
188
- <div class="card"><div class="k">Copilot Extensions</div><div class="v">${kindCounts['copilot-extension']}</div></div>
189
- <div class="card"><div class="k">Skills</div><div class="v">${kindCounts.skill}</div></div>
190
- <div class="card"><div class="k">MCP Servers</div><div class="v">${kindCounts.mcp}</div></div>
191
- <div class="card"><div class="k">Whitelist / Quarantine</div><div class="v">${stats.whitelist} / ${stats.quarantined}</div></div>
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) &nbsp;·&nbsp;
225
+ Risk 0–100 (lower = safer) &nbsp;·&nbsp;
226
+ ${riskScale} &nbsp;·&nbsp;
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
- return `<article class="detail-card">
275
- <div class="detail-head">
276
- <div>
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
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@shnitzel/plugscout",
3
- "version": "0.3.8",
3
+ "version": "0.3.10",
4
4
  "description": "Claude plugins + Claude connectors + Copilot extensions + Skills + MCP security intelligence framework",
5
5
  "private": false,
6
6
  "type": "module",