@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.
@@ -0,0 +1,60 @@
1
+ import { dedupe, extractStringArray, readString, toCount, toScore } from './shared.js';
2
+ export function adaptCursorExtensionsEntries(sourceId, entries) {
3
+ return entries
4
+ .map((entry) => mapCursorExtensionEntry(sourceId, entry))
5
+ .filter((entry) => entry !== null);
6
+ }
7
+ function mapCursorExtensionEntry(sourceId, entry) {
8
+ if (!entry || typeof entry !== 'object' || Array.isArray(entry)) {
9
+ return null;
10
+ }
11
+ const record = entry;
12
+ const slug = readString(record, ['slug', 'id', 'name']);
13
+ if (!slug) {
14
+ return null;
15
+ }
16
+ const name = readString(record, ['title', 'name']) ?? slug;
17
+ const description = readString(record, ['description', 'summary']) ?? `Cursor extension ${name}`;
18
+ const capabilities = dedupe(extractStringArray(record, ['capabilities', 'tools']).concat(extractStringArray(record, ['tags'])));
19
+ const compatibility = dedupe(extractStringArray(record, ['compatibility']).concat(['cursor']));
20
+ const metadata = record.metadata && typeof record.metadata === 'object' && !Array.isArray(record.metadata)
21
+ ? record.metadata
22
+ : {};
23
+ const vsixId = typeof record.vsixId === 'string' ? record.vsixId : typeof metadata.vsixId === 'string' ? metadata.vsixId : undefined;
24
+ const installInstructions = readString(record, ['install', 'instructions']) ??
25
+ (vsixId
26
+ ? `Install via Cursor Marketplace: open Extensions panel (Cmd+Shift+X) and search for ${name}.`
27
+ : `Install via Cursor Marketplace: open Extensions panel (Cmd+Shift+X) and search for ${name}.`);
28
+ const installUrl = readString(record, ['install', 'url']) ??
29
+ (vsixId ? `https://marketplace.visualstudio.com/items?itemName=${vsixId}` : undefined);
30
+ return {
31
+ id: slug.startsWith('cursor-extension:') ? slug : `cursor-extension:${slug}`,
32
+ kind: 'cursor-extension',
33
+ provider: readString(record, ['provider']) ?? 'cursor',
34
+ name,
35
+ description,
36
+ capabilities,
37
+ compatibility,
38
+ source: sourceId,
39
+ install: {
40
+ kind: 'manual',
41
+ instructions: installInstructions,
42
+ ...(installUrl ? { url: installUrl } : {})
43
+ },
44
+ adoptionSignal: toScore(record.adoptionSignal, 50),
45
+ maintenanceSignal: toScore(record.maintenanceSignal, 50),
46
+ provenanceSignal: toScore(record.provenanceSignal, 70),
47
+ freshnessSignal: toScore(record.freshnessSignal, 60),
48
+ securitySignals: {
49
+ knownVulnerabilities: toCount(record.knownVulnerabilities),
50
+ suspiciousPatterns: toCount(record.suspiciousPatterns),
51
+ injectionFindings: toCount(record.injectionFindings),
52
+ exfiltrationSignals: toCount(record.exfiltrationSignals),
53
+ integrityAlerts: toCount(record.integrityAlerts)
54
+ },
55
+ metadata: {
56
+ ...metadata,
57
+ ...(vsixId ? { vsixId } : {})
58
+ }
59
+ };
60
+ }
@@ -0,0 +1,64 @@
1
+ import { dedupe, extractStringArray, readString, toCount, toScore } from './shared.js';
2
+ export function adaptGeminiExtensionsEntries(sourceId, entries) {
3
+ return entries
4
+ .map((entry) => mapGeminiExtensionEntry(sourceId, entry))
5
+ .filter((entry) => entry !== null);
6
+ }
7
+ function mapGeminiExtensionEntry(sourceId, entry) {
8
+ if (!entry || typeof entry !== 'object' || Array.isArray(entry)) {
9
+ return null;
10
+ }
11
+ const record = entry;
12
+ const slug = readString(record, ['slug', 'id', 'name']);
13
+ if (!slug) {
14
+ return null;
15
+ }
16
+ const name = readString(record, ['title', 'name']) ?? slug;
17
+ const description = readString(record, ['description', 'summary']) ?? `Gemini CLI extension ${name}`;
18
+ const capabilities = dedupe(extractStringArray(record, ['capabilities', 'tools']).concat(extractStringArray(record, ['tags'])));
19
+ const compatibility = dedupe(extractStringArray(record, ['compatibility']).concat(['gemini', 'gemini-cli']));
20
+ const metadata = record.metadata && typeof record.metadata === 'object' && !Array.isArray(record.metadata)
21
+ ? record.metadata
22
+ : {};
23
+ const npmPkg = typeof record.npmPackage === 'string'
24
+ ? record.npmPackage
25
+ : typeof metadata.npmPackage === 'string'
26
+ ? metadata.npmPackage
27
+ : undefined;
28
+ const installInstructions = readString(record, ['install', 'instructions']) ??
29
+ (npmPkg
30
+ ? `Add to ~/.gemini/settings.json under mcpServers: { "${slug.replace('gemini-extension:', '')}": { "command": "npx", "args": ["-y", "${npmPkg}"] } }`
31
+ : `Configure in ~/.gemini/settings.json under mcpServers.`);
32
+ const installUrl = readString(record, ['install', 'url']) ??
33
+ (npmPkg ? `https://www.npmjs.com/package/${npmPkg}` : undefined);
34
+ return {
35
+ id: slug.startsWith('gemini-extension:') ? slug : `gemini-extension:${slug}`,
36
+ kind: 'gemini-extension',
37
+ provider: readString(record, ['provider']) ?? 'google',
38
+ name,
39
+ description,
40
+ capabilities,
41
+ compatibility,
42
+ source: sourceId,
43
+ install: {
44
+ kind: 'manual',
45
+ instructions: installInstructions,
46
+ ...(installUrl ? { url: installUrl } : {})
47
+ },
48
+ adoptionSignal: toScore(record.adoptionSignal, 50),
49
+ maintenanceSignal: toScore(record.maintenanceSignal, 50),
50
+ provenanceSignal: toScore(record.provenanceSignal, 75),
51
+ freshnessSignal: toScore(record.freshnessSignal, 60),
52
+ securitySignals: {
53
+ knownVulnerabilities: toCount(record.knownVulnerabilities),
54
+ suspiciousPatterns: toCount(record.suspiciousPatterns),
55
+ injectionFindings: toCount(record.injectionFindings),
56
+ exfiltrationSignals: toCount(record.exfiltrationSignals),
57
+ integrityAlerts: toCount(record.integrityAlerts)
58
+ },
59
+ metadata: {
60
+ ...metadata,
61
+ ...(npmPkg ? { npmPackage: npmPkg } : {})
62
+ }
63
+ };
64
+ }
@@ -132,6 +132,12 @@ function defaultCatalogKeyByKind(kind) {
132
132
  if (kind === 'copilot-extension') {
133
133
  return 'extensions';
134
134
  }
135
+ if (kind === 'cursor-extension') {
136
+ return 'extensions';
137
+ }
138
+ if (kind === 'gemini-extension') {
139
+ return 'extensions';
140
+ }
135
141
  return 'skills';
136
142
  }
137
143
  function resolveByPath(value, path) {
@@ -192,5 +198,10 @@ function validateRemoteHost(registry) {
192
198
  }
193
199
  }
194
200
  function requiresSafeHostAllowlist(kind) {
195
- return kind === 'claude-plugin' || kind === 'claude-connector' || kind === 'copilot-extension' || kind === 'mcp';
201
+ return (kind === 'claude-plugin' ||
202
+ kind === 'claude-connector' ||
203
+ kind === 'copilot-extension' ||
204
+ kind === 'cursor-extension' ||
205
+ kind === 'gemini-extension' ||
206
+ kind === 'mcp');
196
207
  }
@@ -38,7 +38,7 @@ export async function syncCatalogs(today = new Date().toISOString().slice(0, 10)
38
38
  await Promise.all([saveCatalogItems(mergedItems), saveLegacyCatalogViews(mergedItems)]);
39
39
  await saveSyncState(syncState);
40
40
  const kindCounts = countByKind(mergedItems);
41
- logger.info(`Synced ${mergedItems.length} items (${kindCounts.skill} skills, ${kindCounts.mcp} MCPs, ${kindCounts['claude-plugin']} Claude plugins, ${kindCounts['claude-connector']} Claude connectors, ${kindCounts['copilot-extension']} Copilot extensions)`);
41
+ logger.info(`Synced ${mergedItems.length} items (${kindCounts.skill} skills, ${kindCounts.mcp} MCPs, ${kindCounts['claude-plugin']} Claude plugins, ${kindCounts['claude-connector']} Claude connectors, ${kindCounts['copilot-extension']} Copilot extensions, ${kindCounts['cursor-extension']} Cursor extensions, ${kindCounts['gemini-extension']} Gemini extensions)`);
42
42
  const staleRegistries = getStaleRegistries(syncState);
43
43
  if (staleRegistries.length > 0) {
44
44
  logger.warn(`Stale registries (>48h without success): ${staleRegistries.join(', ')}`);
@@ -84,7 +84,9 @@ function normalizeId(id, kind) {
84
84
  mcp: 'mcp',
85
85
  'claude-plugin': 'claude-plugin',
86
86
  'claude-connector': 'claude-connector',
87
- 'copilot-extension': 'copilot-extension'
87
+ 'copilot-extension': 'copilot-extension',
88
+ 'cursor-extension': 'cursor-extension',
89
+ 'gemini-extension': 'gemini-extension'
88
90
  };
89
91
  return `${prefixMap[kind]}:${id.replace(/^([a-z-]+):/, '')}`;
90
92
  }
@@ -101,6 +103,12 @@ function inferProviderFromKind(kind) {
101
103
  if (kind === 'copilot-extension') {
102
104
  return 'github';
103
105
  }
106
+ if (kind === 'cursor-extension') {
107
+ return 'cursor';
108
+ }
109
+ if (kind === 'gemini-extension') {
110
+ return 'google';
111
+ }
104
112
  return 'openai';
105
113
  }
106
114
  function mergeItemsById(items) {
@@ -142,7 +150,9 @@ function countByKind(items) {
142
150
  mcp: 0,
143
151
  'claude-plugin': 0,
144
152
  'claude-connector': 0,
145
- 'copilot-extension': 0
153
+ 'copilot-extension': 0,
154
+ 'cursor-extension': 0,
155
+ 'gemini-extension': 0
146
156
  });
147
157
  }
148
158
  function defaultSourceConfidence(sourceType) {
@@ -0,0 +1,60 @@
1
+ import fs from 'node:fs/promises';
2
+ import path from 'node:path';
3
+ import os from 'node:os';
4
+ const PLUGSCOUT_MCP_VALUE = { command: 'npx', args: ['plugscout', 'mcp'] };
5
+ export function getConfigPath(client, scope) {
6
+ if (client === 'cursor') {
7
+ if (scope === 'project') {
8
+ return path.join(process.cwd(), '.cursor', 'mcp.json');
9
+ }
10
+ return path.join(os.homedir(), '.cursor', 'mcp.json');
11
+ }
12
+ return path.join(os.homedir(), '.gemini', 'settings.json');
13
+ }
14
+ export async function getClientMcpConfigStatus(client, scope) {
15
+ const configPath = getConfigPath(client, scope);
16
+ try {
17
+ const raw = await fs.readFile(configPath, 'utf8');
18
+ const config = JSON.parse(raw);
19
+ const mcpServers = config.mcpServers;
20
+ const configured = !!(mcpServers?.plugscout);
21
+ return { configured, configPath };
22
+ }
23
+ catch {
24
+ return { configured: false, configPath };
25
+ }
26
+ }
27
+ export async function writeClientMcpConfig(options) {
28
+ const { client, force = false } = options;
29
+ const scope = client === 'gemini' ? 'user' : options.scope;
30
+ const configPath = getConfigPath(client, scope);
31
+ let existing = {};
32
+ try {
33
+ const raw = await fs.readFile(configPath, 'utf8');
34
+ existing = JSON.parse(raw);
35
+ }
36
+ catch {
37
+ // file doesn't exist yet — start empty
38
+ }
39
+ const mcpServers = existing.mcpServers ?? {};
40
+ const current = mcpServers.plugscout;
41
+ if (current !== undefined) {
42
+ const isSame = JSON.stringify(current) === JSON.stringify(PLUGSCOUT_MCP_VALUE);
43
+ if (isSame) {
44
+ return { status: 'already-configured', configPath };
45
+ }
46
+ if (!force) {
47
+ throw new Error(`plugscout already exists in ${configPath} with a different value. Use --force to overwrite.`);
48
+ }
49
+ }
50
+ const updated = {
51
+ ...existing,
52
+ mcpServers: { ...mcpServers, plugscout: PLUGSCOUT_MCP_VALUE }
53
+ };
54
+ const dir = path.dirname(configPath);
55
+ await fs.mkdir(dir, { recursive: true });
56
+ const tmpPath = `${configPath}.plugscout.tmp`;
57
+ await fs.writeFile(tmpPath, `${JSON.stringify(updated, null, 2)}\n`, 'utf8');
58
+ await fs.rename(tmpPath, configPath);
59
+ return { status: 'written', configPath };
60
+ }
@@ -1,9 +1,11 @@
1
1
  import { spawnSync } from 'node:child_process';
2
2
  import path from 'node:path';
3
+ import os from 'node:os';
3
4
  import fs from 'node:fs/promises';
4
5
  import { getStaleRegistries, loadSyncState } from '../../catalog/sync-state.js';
5
6
  import { loadCatalogItems } from '../../catalog/repository.js';
6
7
  import { hasLegacySkillSh, resolveSkillsRuntime } from '../../install/dependencies.js';
8
+ import { getClientMcpConfigStatus } from './client-setup.js';
7
9
  export async function runDoctorChecks(projectPath = '.') {
8
10
  const checks = [];
9
11
  checks.push(checkSkillsRuntime());
@@ -58,6 +60,34 @@ export async function runDoctorChecks(projectPath = '.') {
58
60
  suggestion: 'Run: npm run dev -- init'
59
61
  });
60
62
  }
63
+ // Cursor IDE check
64
+ const cursorInstalled = spawnSync('which', ['cursor'], { encoding: 'utf8' }).status === 0 ||
65
+ await fs.access(path.join(os.homedir(), '.cursor')).then(() => true).catch(() => false);
66
+ checks.push(cursorInstalled
67
+ ? { name: 'Cursor IDE', status: 'pass', message: 'Cursor detected' }
68
+ : { name: 'Cursor IDE', status: 'warn', message: 'Cursor not detected', suggestion: 'Install Cursor from https://cursor.sh' });
69
+ // Gemini CLI check
70
+ checks.push(checkBinary('gemini', { suggestion: 'Install Gemini CLI: npm install -g @google/gemini-cli' }));
71
+ // Cursor MCP config check
72
+ try {
73
+ const cursorStatus = await getClientMcpConfigStatus('cursor', 'user');
74
+ checks.push(cursorStatus.configured
75
+ ? { name: 'Cursor MCP config', status: 'pass', message: `plugscout wired in ${cursorStatus.configPath}` }
76
+ : { name: 'Cursor MCP config', status: 'warn', message: 'plugscout not in Cursor MCP config', suggestion: 'Run: plugscout client setup --client cursor' });
77
+ }
78
+ catch {
79
+ checks.push({ name: 'Cursor MCP config', status: 'warn', message: 'Could not read Cursor MCP config', suggestion: 'Run: plugscout client setup --client cursor' });
80
+ }
81
+ // Gemini MCP config check
82
+ try {
83
+ const geminiStatus = await getClientMcpConfigStatus('gemini', 'user');
84
+ checks.push(geminiStatus.configured
85
+ ? { name: 'Gemini MCP config', status: 'pass', message: `plugscout wired in ${geminiStatus.configPath}` }
86
+ : { name: 'Gemini MCP config', status: 'warn', message: 'plugscout not in Gemini MCP config', suggestion: 'Run: plugscout client setup --client gemini' });
87
+ }
88
+ catch {
89
+ checks.push({ name: 'Gemini MCP config', status: 'warn', message: 'Could not read Gemini MCP config', suggestion: 'Run: plugscout client setup --client gemini' });
90
+ }
61
91
  return checks;
62
92
  }
63
93
  function checkSkillsRuntime() {
@@ -62,6 +62,7 @@ const COMMAND_ALIASES = {
62
62
  setup: 'setup',
63
63
  help: 'help',
64
64
  mcp: 'mcp',
65
+ client: 'client',
65
66
  };
66
67
  export async function runCli(argv) {
67
68
  const noUpdateCheck = hasFlag(argv, '--no-update-check');
@@ -136,6 +137,9 @@ export async function runCli(argv) {
136
137
  case 'mcp':
137
138
  await handleMcp(rest);
138
139
  return; // intentional: MCP server is long-lived; update banner would corrupt JSON-RPC stream
140
+ case 'client':
141
+ await handleClient(rest);
142
+ break;
139
143
  case 'help':
140
144
  printHelp();
141
145
  break;
@@ -166,7 +170,7 @@ async function handleAbout() {
166
170
  if (pkg.author) {
167
171
  console.log(`Author: ${pkg.author}`);
168
172
  }
169
- console.log('Scope: Claude plugins, Claude connectors, Copilot extensions, Skills, MCP servers');
173
+ console.log('Scope: Claude plugins, Claude connectors, Copilot extensions, Cursor extensions, Gemini extensions, Skills, MCP servers');
170
174
  console.log('Ranking: trust-first (fit + trust - risk penalties + freshness bonus)');
171
175
  console.log('Meaning: top/recommend output is repo-aware guidance, not a global popularity leaderboard.');
172
176
  console.log('Install discipline: review each suggestion, check provenance and risk, and do not install blindly from rank alone.');
@@ -190,7 +194,7 @@ async function handleStatus(args) {
190
194
  });
191
195
  console.log('Catalog Status');
192
196
  console.log(`Items: ${items.length}`);
193
- console.log(`Kinds: skill=${kindCounts.get('skill') ?? 0}, mcp=${kindCounts.get('mcp') ?? 0}, claude-plugin=${kindCounts.get('claude-plugin') ?? 0}, claude-connector=${kindCounts.get('claude-connector') ?? 0}, copilot-extension=${kindCounts.get('copilot-extension') ?? 0}`);
197
+ console.log(`Kinds: skill=${kindCounts.get('skill') ?? 0}, mcp=${kindCounts.get('mcp') ?? 0}, claude-plugin=${kindCounts.get('claude-plugin') ?? 0}, claude-connector=${kindCounts.get('claude-connector') ?? 0}, copilot-extension=${kindCounts.get('copilot-extension') ?? 0}, cursor-extension=${kindCounts.get('cursor-extension') ?? 0}, gemini-extension=${kindCounts.get('gemini-extension') ?? 0}`);
194
198
  console.log(`Providers: ${Array.from(providerCounts.entries())
195
199
  .sort((a, b) => a[0].localeCompare(b[0]))
196
200
  .map(([name, count]) => `${name}=${count}`)
@@ -228,7 +232,7 @@ async function handleInit(args) {
228
232
  const root = path.resolve(project);
229
233
  const [items, policy] = await Promise.all([loadCatalogItems(), loadSecurityPolicy()]);
230
234
  const providers = Array.from(new Set(items.map((item) => item.provider))).sort((a, b) => a.localeCompare(b));
231
- const defaultKinds = ['skill', 'mcp', 'claude-plugin', 'claude-connector', 'copilot-extension'];
235
+ const defaultKinds = ['skill', 'mcp', 'claude-plugin', 'claude-connector', 'copilot-extension', 'cursor-extension', 'gemini-extension'];
232
236
  const defaults = {
233
237
  defaultKinds,
234
238
  defaultProviders: providers,
@@ -298,7 +302,7 @@ async function handleSetup(args) {
298
302
  console.log('Step 2/3: Initializing local config...');
299
303
  const [items, policy] = await Promise.all([loadCatalogItems(), loadSecurityPolicy()]);
300
304
  const providers = Array.from(new Set(items.map((item) => item.provider))).sort((a, b) => a.localeCompare(b));
301
- const defaultKinds = ['skill', 'mcp', 'claude-plugin', 'claude-connector', 'copilot-extension'];
305
+ const defaultKinds = ['skill', 'mcp', 'claude-plugin', 'claude-connector', 'copilot-extension', 'cursor-extension', 'gemini-extension'];
302
306
  const defaults = {
303
307
  defaultKinds,
304
308
  defaultProviders: providers,
@@ -853,6 +857,33 @@ async function handleQuarantine(args) {
853
857
  const result = await applyQuarantineFromReport(report);
854
858
  console.log(renderJson(result));
855
859
  }
860
+ async function handleClient(args) {
861
+ const subcommand = args[0];
862
+ if (subcommand !== 'setup') {
863
+ throw new Error('Usage: client setup --client cursor|gemini [--scope user|project] [--force]');
864
+ }
865
+ const clientFlag = readFlag(args, '--client');
866
+ if (clientFlag !== 'cursor' && clientFlag !== 'gemini') {
867
+ throw new Error('Usage: client setup --client cursor|gemini');
868
+ }
869
+ const scopeFlag = readFlag(args, '--scope') ?? 'user';
870
+ if (scopeFlag !== 'user' && scopeFlag !== 'project') {
871
+ throw new Error('--scope must be user or project');
872
+ }
873
+ if (clientFlag === 'gemini' && scopeFlag === 'project') {
874
+ logger.warn('Gemini CLI only supports user scope; falling back to user scope.');
875
+ }
876
+ const force = hasFlag(args, '--force');
877
+ const { writeClientMcpConfig } = await import('./client-setup.js');
878
+ const result = await writeClientMcpConfig({ client: clientFlag, scope: scopeFlag, force });
879
+ if (result.status === 'already-configured') {
880
+ console.log(`plugscout already configured in ${result.configPath}`);
881
+ }
882
+ else {
883
+ console.log(`plugscout MCP config written: ${result.configPath}`);
884
+ printHint(`Restart ${clientFlag === 'cursor' ? 'Cursor IDE' : 'Gemini CLI'} for the change to take effect.`);
885
+ }
886
+ }
856
887
  async function handleUpgrade(args) {
857
888
  const subcommand = args[0] ?? 'check';
858
889
  if (subcommand !== 'check') {
@@ -980,7 +1011,7 @@ function printHelp() {
980
1011
  console.log(' init [--project .]');
981
1012
  console.log(' doctor [--project .] [--install-deps]');
982
1013
  console.log(' status [--verbose]');
983
- console.log(' sync [--kind skill,mcp,claude-plugin,claude-connector,copilot-extension] [--dry-run]');
1014
+ console.log(' sync [--kind skill,mcp,claude-plugin,claude-connector,copilot-extension,cursor-extension,gemini-extension] [--dry-run]');
984
1015
  console.log('');
985
1016
  console.log('Explore');
986
1017
  console.log(' list [--kind ...] [--provider ...] [--risk-tier low|medium|high|critical] [--blocked true|false] [--search q] [--limit n] [--sort name|risk|trust] [--format json|table] [--readable] [--details]');
@@ -1005,6 +1036,7 @@ function printHelp() {
1005
1036
  console.log('Other');
1006
1037
  console.log(' about');
1007
1038
  console.log(' web [--out .plugscout/report.html] [--kind ...] [--limit n] [--open]');
1039
+ console.log(' client setup --client cursor|gemini [--scope user|project] [--force]');
1008
1040
  console.log(' upgrade check');
1009
1041
  console.log(' help');
1010
1042
  console.log('');
@@ -1014,11 +1046,18 @@ function printHelp() {
1014
1046
  console.log(' plugins -> claude-plugin');
1015
1047
  console.log(' connectors -> claude-connector');
1016
1048
  console.log(' extensions, copilot -> copilot-extension');
1049
+ console.log(' cursor, cursor-extensions -> cursor-extension');
1050
+ console.log(' gemini, gemini-extensions -> gemini-extension');
1017
1051
  console.log('');
1018
1052
  console.log('Examples');
1019
1053
  console.log(' plugscout recommend --project . --only-safe --limit 10');
1054
+ console.log(' plugscout list --kind cursor --limit 15');
1055
+ console.log(' plugscout list --kind gemini --limit 11');
1020
1056
  console.log(' plugscout list --kind connectors --limit 10');
1021
1057
  console.log(' plugscout show --id claude-connector:asana');
1058
+ console.log(' plugscout client setup --client cursor');
1059
+ console.log(' plugscout client setup --client gemini');
1060
+ console.log(' plugscout sync --kind cursor-extension,gemini-extension');
1022
1061
  console.log('');
1023
1062
  console.log('Global options');
1024
1063
  console.log(' --no-update-check');
@@ -1032,7 +1071,7 @@ async function loadLocalCliConfig(projectRoot) {
1032
1071
  return null;
1033
1072
  }
1034
1073
  return {
1035
- defaultKinds: Array.isArray(parsed.defaultKinds) ? parsed.defaultKinds : ['skill', 'mcp', 'claude-plugin', 'claude-connector', 'copilot-extension'],
1074
+ defaultKinds: Array.isArray(parsed.defaultKinds) ? parsed.defaultKinds : ['skill', 'mcp', 'claude-plugin', 'claude-connector', 'copilot-extension', 'cursor-extension', 'gemini-extension'],
1036
1075
  defaultProviders: Array.isArray(parsed.defaultProviders) ? parsed.defaultProviders : [],
1037
1076
  riskPosture: parsed.riskPosture,
1038
1077
  outputStyle: parsed.outputStyle === 'json' ? 'json' : 'rich-table',
@@ -18,7 +18,7 @@ export function createMcpServer(version = '0.0.0') {
18
18
  type: 'object',
19
19
  properties: {
20
20
  query: { type: 'string', description: 'Search term' },
21
- kind: { type: 'string', description: 'Filter by kind: skill, mcp, claude-plugin, claude-connector, copilot-extension' },
21
+ kind: { type: 'string', description: 'Filter by kind: skill, mcp, claude-plugin, claude-connector, copilot-extension, cursor-extension, gemini-extension' },
22
22
  provider: { type: 'string', description: 'Filter by provider' },
23
23
  limit: { type: 'number', description: 'Max results (default: 20)' },
24
24
  },
@@ -24,7 +24,13 @@ const KIND_ALIASES = {
24
24
  'copilot extensions': 'copilot-extension',
25
25
  extension: 'copilot-extension',
26
26
  extensions: 'copilot-extension',
27
- copilot: 'copilot-extension'
27
+ copilot: 'copilot-extension',
28
+ 'cursor-extension': 'cursor-extension',
29
+ 'cursor-extensions': 'cursor-extension',
30
+ cursor: 'cursor-extension',
31
+ 'gemini-extension': 'gemini-extension',
32
+ 'gemini-extensions': 'gemini-extension',
33
+ gemini: 'gemini-extension'
28
34
  };
29
35
  export function readFlag(args, flag) {
30
36
  const index = args.indexOf(flag);
@@ -57,7 +63,7 @@ export function normalizeKind(raw) {
57
63
  return CatalogKindSchema.parse(normalized);
58
64
  }
59
65
  catch {
60
- throw new Error(`Invalid --kind value: ${raw}. Expected one of: skill, mcp, claude-plugin, claude-connector, copilot-extension. Aliases also supported: skills, mcps, plugins, connectors, extensions.`);
66
+ throw new Error(`Invalid --kind value: ${raw}. Expected one of: skill, mcp, claude-plugin, claude-connector, copilot-extension, cursor-extension, gemini-extension. Aliases also supported: skills, mcps, plugins, connectors, extensions, cursor, gemini.`);
61
67
  }
62
68
  }
63
69
  export function readCsvList(args, flag) {
@@ -207,60 +207,74 @@ export async function renderInteractiveHome() {
207
207
  }
208
208
  }
209
209
  }
210
- process.stdout.write('\n');
211
- process.stdin.setRawMode(true);
212
- process.stdin.resume();
213
- process.stdin.setEncoding('utf8');
214
- render(true);
215
- await new Promise((resolve) => {
216
- process.stdin.on('data', async function onKey(key) {
217
- if (key === CTRL_C) {
218
- process.stdin.removeListener('data', onKey);
219
- process.stdin.setRawMode(false);
220
- process.stdin.pause();
221
- process.stdout.write('\n');
222
- resolve();
223
- return;
224
- }
225
- else if (key === ARROW_UP) {
226
- selected = (selected - 1 + menuItems.length) % menuItems.length;
227
- render(false);
228
- }
229
- else if (key === ARROW_DOWN) {
230
- selected = (selected + 1) % menuItems.length;
231
- render(false);
232
- }
233
- else if (key === ENTER) {
234
- process.stdin.removeListener('data', onKey);
235
- process.stdin.setRawMode(false);
236
- process.stdin.pause();
237
- process.stdout.write('\n');
238
- const item = menuItems[selected];
239
- if (!item.command) {
240
- resolve();
210
+ let running = true;
211
+ while (running) {
212
+ process.stdout.write('\n');
213
+ process.stdin.setRawMode(true);
214
+ process.stdin.resume();
215
+ process.stdin.setEncoding('utf8');
216
+ render(true);
217
+ const action = await new Promise((resolve) => {
218
+ process.stdin.on('data', async function onKey(key) {
219
+ if (key === CTRL_C) {
220
+ process.stdin.removeListener('data', onKey);
221
+ process.stdin.setRawMode(false);
222
+ process.stdin.pause();
223
+ process.stdout.write('\n');
224
+ resolve({ exit: true });
241
225
  return;
242
226
  }
243
- let args = [...item.command];
244
- if (item.needsId) {
245
- const rl = createInterface({ input: process.stdin, output: process.stdout });
246
- process.stdin.resume();
247
- const id = await new Promise((res) => {
248
- rl.question(' Enter catalog ID: ', (answer) => {
249
- rl.close();
250
- res(answer.trim());
251
- });
252
- });
253
- if (!id) {
254
- resolve();
227
+ else if (key === ARROW_UP) {
228
+ selected = (selected - 1 + menuItems.length) % menuItems.length;
229
+ render(false);
230
+ }
231
+ else if (key === ARROW_DOWN) {
232
+ selected = (selected + 1) % menuItems.length;
233
+ render(false);
234
+ }
235
+ else if (key === ENTER) {
236
+ process.stdin.removeListener('data', onKey);
237
+ process.stdin.setRawMode(false);
238
+ process.stdin.pause();
239
+ process.stdout.write('\n');
240
+ const item = menuItems[selected];
241
+ if (!item.command) {
242
+ resolve({ exit: true });
255
243
  return;
256
244
  }
257
- args = [...args, '--id', id];
245
+ let args = [...item.command];
246
+ if (item.needsId) {
247
+ const rl = createInterface({ input: process.stdin, output: process.stdout });
248
+ process.stdin.resume();
249
+ const id = await new Promise((res) => {
250
+ rl.question(' Enter catalog ID: ', (answer) => {
251
+ rl.close();
252
+ res(answer.trim());
253
+ });
254
+ });
255
+ if (!id) {
256
+ resolve({ exit: false });
257
+ return;
258
+ }
259
+ args = [...args, '--id', id];
260
+ }
261
+ resolve({ exit: false, args });
258
262
  }
263
+ });
264
+ });
265
+ if (action.exit) {
266
+ running = false;
267
+ }
268
+ else {
269
+ if (action.args) {
259
270
  const cliPath = getPackagePath('dist/cli.js');
260
- const child = spawn(process.execPath, [cliPath, ...args], { stdio: 'inherit' });
261
- child.on('close', () => resolve());
262
- child.on('error', () => resolve());
271
+ await new Promise((done) => {
272
+ const child = spawn(process.execPath, [cliPath, ...action.args], { stdio: 'inherit' });
273
+ child.on('close', () => done());
274
+ child.on('error', () => done());
275
+ });
263
276
  }
264
- });
265
- });
277
+ process.stdout.write('\n');
278
+ }
279
+ }
266
280
  }