@shnitzel/plugscout 0.3.9 → 0.3.11

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,144 @@
1
+ import fs from 'node:fs/promises';
2
+ import path from 'node:path';
3
+ import os from 'node:os';
4
+ const PLUGSCOUT_MCP_STDIO = { command: 'npx', args: ['plugscout', 'mcp'] };
5
+ const PLUGSCOUT_MCP_ZED = { command: { path: 'npx', args: ['plugscout', 'mcp'] } };
6
+ function claudeDesktopConfigPath() {
7
+ if (process.platform === 'win32') {
8
+ return path.join(process.env['APPDATA'] ?? os.homedir(), 'Claude', 'claude_desktop_config.json');
9
+ }
10
+ if (process.platform === 'darwin') {
11
+ return path.join(os.homedir(), 'Library', 'Application Support', 'Claude', 'claude_desktop_config.json');
12
+ }
13
+ return path.join(os.homedir(), '.config', 'claude-desktop', 'claude_desktop_config.json');
14
+ }
15
+ function openCodeConfigPath() {
16
+ if (process.platform === 'win32') {
17
+ return path.join(process.env['APPDATA'] ?? os.homedir(), 'opencode', 'config.json');
18
+ }
19
+ return path.join(os.homedir(), '.config', 'opencode', 'config.json');
20
+ }
21
+ export const CLIENT_DEFS = {
22
+ cursor: {
23
+ label: 'Cursor IDE',
24
+ supportsProjectScope: true,
25
+ getConfigPath(scope) {
26
+ return scope === 'project'
27
+ ? path.join(process.cwd(), '.cursor', 'mcp.json')
28
+ : path.join(os.homedir(), '.cursor', 'mcp.json');
29
+ },
30
+ containerPath: ['mcpServers'],
31
+ entryValue: PLUGSCOUT_MCP_STDIO,
32
+ },
33
+ gemini: {
34
+ label: 'Gemini CLI',
35
+ supportsProjectScope: false,
36
+ getConfigPath() {
37
+ return path.join(os.homedir(), '.gemini', 'settings.json');
38
+ },
39
+ containerPath: ['mcpServers'],
40
+ entryValue: PLUGSCOUT_MCP_STDIO,
41
+ },
42
+ 'claude-desktop': {
43
+ label: 'Claude Desktop',
44
+ supportsProjectScope: false,
45
+ getConfigPath() {
46
+ return claudeDesktopConfigPath();
47
+ },
48
+ containerPath: ['mcpServers'],
49
+ entryValue: PLUGSCOUT_MCP_STDIO,
50
+ },
51
+ windsurf: {
52
+ label: 'Windsurf',
53
+ supportsProjectScope: false,
54
+ getConfigPath() {
55
+ return path.join(os.homedir(), '.codeium', 'windsurf', 'mcp_config.json');
56
+ },
57
+ containerPath: ['mcpServers'],
58
+ entryValue: PLUGSCOUT_MCP_STDIO,
59
+ },
60
+ opencode: {
61
+ label: 'OpenCode',
62
+ supportsProjectScope: false,
63
+ getConfigPath() {
64
+ return openCodeConfigPath();
65
+ },
66
+ containerPath: ['mcp'],
67
+ entryValue: PLUGSCOUT_MCP_STDIO,
68
+ },
69
+ zed: {
70
+ label: 'Zed',
71
+ supportsProjectScope: false,
72
+ getConfigPath() {
73
+ return path.join(os.homedir(), '.config', 'zed', 'settings.json');
74
+ },
75
+ containerPath: ['context_servers'],
76
+ entryValue: PLUGSCOUT_MCP_ZED,
77
+ },
78
+ };
79
+ export const VALID_CLIENT_KINDS = Object.keys(CLIENT_DEFS);
80
+ export function getConfigPath(client, scope) {
81
+ return CLIENT_DEFS[client].getConfigPath(scope);
82
+ }
83
+ function navigateContainer(obj, keys) {
84
+ let current = obj;
85
+ for (const key of keys) {
86
+ const next = current[key];
87
+ if (next === undefined || next === null || typeof next !== 'object' || Array.isArray(next)) {
88
+ current[key] = {};
89
+ }
90
+ current = current[key];
91
+ }
92
+ return current;
93
+ }
94
+ export async function getClientMcpConfigStatus(client, scope) {
95
+ const def = CLIENT_DEFS[client];
96
+ const configPath = def.getConfigPath(scope);
97
+ try {
98
+ const raw = await fs.readFile(configPath, 'utf8');
99
+ const config = JSON.parse(raw);
100
+ let container = config;
101
+ for (const key of def.containerPath) {
102
+ const next = container[key];
103
+ if (next === undefined || typeof next !== 'object' || next === null || Array.isArray(next)) {
104
+ return { configured: false, configPath };
105
+ }
106
+ container = next;
107
+ }
108
+ return { configured: !!container['plugscout'], configPath };
109
+ }
110
+ catch {
111
+ return { configured: false, configPath };
112
+ }
113
+ }
114
+ export async function writeClientMcpConfig(options) {
115
+ const { client, force = false } = options;
116
+ const def = CLIENT_DEFS[client];
117
+ const scope = def.supportsProjectScope ? options.scope : 'user';
118
+ const configPath = def.getConfigPath(scope);
119
+ let existing = {};
120
+ try {
121
+ const raw = await fs.readFile(configPath, 'utf8');
122
+ existing = JSON.parse(raw);
123
+ }
124
+ catch {
125
+ // file doesn't exist yet — start empty
126
+ }
127
+ const container = navigateContainer(existing, def.containerPath);
128
+ const current = container['plugscout'];
129
+ if (current !== undefined) {
130
+ const isSame = JSON.stringify(current) === JSON.stringify(def.entryValue);
131
+ if (isSame)
132
+ return { status: 'already-configured', configPath };
133
+ if (!force) {
134
+ throw new Error(`plugscout already exists in ${configPath} with a different value. Use --force to overwrite.`);
135
+ }
136
+ }
137
+ container['plugscout'] = def.entryValue;
138
+ const dir = path.dirname(configPath);
139
+ await fs.mkdir(dir, { recursive: true });
140
+ const tmpPath = `${configPath}.plugscout.tmp`;
141
+ await fs.writeFile(tmpPath, `${JSON.stringify(existing, null, 2)}\n`, 'utf8');
142
+ await fs.rename(tmpPath, configPath);
143
+ return { status: 'written', configPath };
144
+ }
@@ -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, CLIENT_DEFS } from './client-setup.js';
7
9
  export async function runDoctorChecks(projectPath = '.') {
8
10
  const checks = [];
9
11
  checks.push(checkSkillsRuntime());
@@ -58,6 +60,92 @@ 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
+ }
91
+ // Claude Desktop check
92
+ const claudeDesktopConfigPath = CLIENT_DEFS['claude-desktop'].getConfigPath('user');
93
+ const claudeDesktopPresent = spawnSync('which', ['claude'], { encoding: 'utf8' }).status === 0 ||
94
+ await fs.access(path.dirname(claudeDesktopConfigPath)).then(() => true).catch(() => false) ||
95
+ await fs.access(path.join('/', 'Applications', 'Claude.app')).then(() => true).catch(() => false);
96
+ checks.push(claudeDesktopPresent
97
+ ? { name: 'Claude Desktop', status: 'pass', message: 'Claude Desktop detected' }
98
+ : { name: 'Claude Desktop', status: 'warn', message: 'Claude Desktop not detected', suggestion: 'Install from https://claude.ai/download' });
99
+ // Claude Desktop MCP config check
100
+ try {
101
+ const claudeStatus = await getClientMcpConfigStatus('claude-desktop', 'user');
102
+ checks.push(claudeStatus.configured
103
+ ? { name: 'Claude Desktop MCP', status: 'pass', message: `plugscout wired in ${claudeStatus.configPath}` }
104
+ : { name: 'Claude Desktop MCP', status: 'warn', message: 'plugscout not in Claude Desktop config', suggestion: 'Run: plugscout client setup --client claude-desktop' });
105
+ }
106
+ catch {
107
+ checks.push({ name: 'Claude Desktop MCP', status: 'warn', message: 'Could not read Claude Desktop config', suggestion: 'Run: plugscout client setup --client claude-desktop' });
108
+ }
109
+ // Windsurf check
110
+ checks.push(checkBinary('windsurf', { suggestion: 'Install Windsurf from https://windsurf.ai' }));
111
+ // Windsurf MCP config check
112
+ try {
113
+ const windsurfStatus = await getClientMcpConfigStatus('windsurf', 'user');
114
+ checks.push(windsurfStatus.configured
115
+ ? { name: 'Windsurf MCP config', status: 'pass', message: `plugscout wired in ${windsurfStatus.configPath}` }
116
+ : { name: 'Windsurf MCP config', status: 'warn', message: 'plugscout not in Windsurf MCP config', suggestion: 'Run: plugscout client setup --client windsurf' });
117
+ }
118
+ catch {
119
+ checks.push({ name: 'Windsurf MCP config', status: 'warn', message: 'Could not read Windsurf MCP config', suggestion: 'Run: plugscout client setup --client windsurf' });
120
+ }
121
+ // OpenCode check
122
+ checks.push(checkBinary('opencode', { suggestion: 'Install OpenCode: npm install -g opencode-ai' }));
123
+ // OpenCode MCP config check
124
+ try {
125
+ const opencodeStatus = await getClientMcpConfigStatus('opencode', 'user');
126
+ checks.push(opencodeStatus.configured
127
+ ? { name: 'OpenCode MCP config', status: 'pass', message: `plugscout wired in ${opencodeStatus.configPath}` }
128
+ : { name: 'OpenCode MCP config', status: 'warn', message: 'plugscout not in OpenCode config', suggestion: 'Run: plugscout client setup --client opencode' });
129
+ }
130
+ catch {
131
+ checks.push({ name: 'OpenCode MCP config', status: 'warn', message: 'Could not read OpenCode config', suggestion: 'Run: plugscout client setup --client opencode' });
132
+ }
133
+ // Zed check
134
+ const zedInstalled = spawnSync('which', ['zed'], { encoding: 'utf8' }).status === 0 ||
135
+ await fs.access(path.join('/', 'Applications', 'Zed.app')).then(() => true).catch(() => false);
136
+ checks.push(zedInstalled
137
+ ? { name: 'Zed', status: 'pass', message: 'Zed detected' }
138
+ : { name: 'Zed', status: 'warn', message: 'Zed not detected', suggestion: 'Install Zed from https://zed.dev' });
139
+ // Zed MCP config check
140
+ try {
141
+ const zedStatus = await getClientMcpConfigStatus('zed', 'user');
142
+ checks.push(zedStatus.configured
143
+ ? { name: 'Zed MCP config', status: 'pass', message: `plugscout wired in ${zedStatus.configPath}` }
144
+ : { name: 'Zed MCP config', status: 'warn', message: 'plugscout not in Zed settings', suggestion: 'Run: plugscout client setup --client zed' });
145
+ }
146
+ catch {
147
+ checks.push({ name: 'Zed MCP config', status: 'warn', message: 'Could not read Zed settings', suggestion: 'Run: plugscout client setup --client zed' });
148
+ }
61
149
  return checks;
62
150
  }
63
151
  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,38 @@ 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|claude-desktop|windsurf|opencode|zed [--scope user|project] [--force]');
864
+ }
865
+ const { writeClientMcpConfig, CLIENT_DEFS, VALID_CLIENT_KINDS } = await import('./client-setup.js');
866
+ const clientFlag = readFlag(args, '--client');
867
+ if (!clientFlag || !VALID_CLIENT_KINDS.includes(clientFlag)) {
868
+ throw new Error(`Usage: client setup --client ${VALID_CLIENT_KINDS.join('|')}`);
869
+ }
870
+ const scopeFlag = readFlag(args, '--scope') ?? 'user';
871
+ if (scopeFlag !== 'user' && scopeFlag !== 'project') {
872
+ throw new Error('--scope must be user or project');
873
+ }
874
+ const def = CLIENT_DEFS[clientFlag];
875
+ if (!def.supportsProjectScope && scopeFlag === 'project') {
876
+ logger.warn(`${def.label} only supports user scope; falling back to user scope.`);
877
+ }
878
+ const force = hasFlag(args, '--force');
879
+ const result = await writeClientMcpConfig({
880
+ client: clientFlag,
881
+ scope: scopeFlag,
882
+ force
883
+ });
884
+ if (result.status === 'already-configured') {
885
+ console.log(`plugscout already configured in ${result.configPath}`);
886
+ }
887
+ else {
888
+ console.log(`plugscout MCP config written: ${result.configPath}`);
889
+ printHint(`Restart ${def.label} for the change to take effect.`);
890
+ }
891
+ }
856
892
  async function handleUpgrade(args) {
857
893
  const subcommand = args[0] ?? 'check';
858
894
  if (subcommand !== 'check') {
@@ -980,7 +1016,7 @@ function printHelp() {
980
1016
  console.log(' init [--project .]');
981
1017
  console.log(' doctor [--project .] [--install-deps]');
982
1018
  console.log(' status [--verbose]');
983
- console.log(' sync [--kind skill,mcp,claude-plugin,claude-connector,copilot-extension] [--dry-run]');
1019
+ console.log(' sync [--kind skill,mcp,claude-plugin,claude-connector,copilot-extension,cursor-extension,gemini-extension] [--dry-run]');
984
1020
  console.log('');
985
1021
  console.log('Explore');
986
1022
  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 +1041,7 @@ function printHelp() {
1005
1041
  console.log('Other');
1006
1042
  console.log(' about');
1007
1043
  console.log(' web [--out .plugscout/report.html] [--kind ...] [--limit n] [--open]');
1044
+ console.log(' client setup --client cursor|gemini|claude-desktop|windsurf|opencode|zed [--scope user|project] [--force]');
1008
1045
  console.log(' upgrade check');
1009
1046
  console.log(' help');
1010
1047
  console.log('');
@@ -1014,11 +1051,22 @@ function printHelp() {
1014
1051
  console.log(' plugins -> claude-plugin');
1015
1052
  console.log(' connectors -> claude-connector');
1016
1053
  console.log(' extensions, copilot -> copilot-extension');
1054
+ console.log(' cursor, cursor-extensions -> cursor-extension');
1055
+ console.log(' gemini, gemini-extensions -> gemini-extension');
1017
1056
  console.log('');
1018
1057
  console.log('Examples');
1019
1058
  console.log(' plugscout recommend --project . --only-safe --limit 10');
1059
+ console.log(' plugscout list --kind cursor --limit 15');
1060
+ console.log(' plugscout list --kind gemini --limit 11');
1020
1061
  console.log(' plugscout list --kind connectors --limit 10');
1021
1062
  console.log(' plugscout show --id claude-connector:asana');
1063
+ console.log(' plugscout client setup --client cursor');
1064
+ console.log(' plugscout client setup --client gemini');
1065
+ console.log(' plugscout client setup --client claude-desktop');
1066
+ console.log(' plugscout client setup --client windsurf');
1067
+ console.log(' plugscout client setup --client opencode');
1068
+ console.log(' plugscout client setup --client zed');
1069
+ console.log(' plugscout sync --kind cursor-extension,gemini-extension');
1022
1070
  console.log('');
1023
1071
  console.log('Global options');
1024
1072
  console.log(' --no-update-check');
@@ -1032,7 +1080,7 @@ async function loadLocalCliConfig(projectRoot) {
1032
1080
  return null;
1033
1081
  }
1034
1082
  return {
1035
- defaultKinds: Array.isArray(parsed.defaultKinds) ? parsed.defaultKinds : ['skill', 'mcp', 'claude-plugin', 'claude-connector', 'copilot-extension'],
1083
+ defaultKinds: Array.isArray(parsed.defaultKinds) ? parsed.defaultKinds : ['skill', 'mcp', 'claude-plugin', 'claude-connector', 'copilot-extension', 'cursor-extension', 'gemini-extension'],
1036
1084
  defaultProviders: Array.isArray(parsed.defaultProviders) ? parsed.defaultProviders : [],
1037
1085
  riskPosture: parsed.riskPosture,
1038
1086
  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) {
@@ -205,13 +205,15 @@ function renderHtml(rows, stats, policy) {
205
205
  <body>
206
206
  <div class="wrap">
207
207
  <h1>PlugScout Web Report</h1>
208
- <p class="sub">Claude plugins · Claude connectors · Copilot extensions · Skills · MCP servers</p>
208
+ <p class="sub">Claude plugins · Claude connectors · Copilot extensions · Cursor extensions · Gemini extensions · Skills · MCP servers</p>
209
209
 
210
210
  <div class="stat-cards">
211
211
  <div class="stat-card"><div class="k">Total</div><div class="v">${stats.totalItems}</div></div>
212
212
  <div class="stat-card"><div class="k">Plugins</div><div class="v">${kindCounts['claude-plugin']}</div></div>
213
213
  <div class="stat-card"><div class="k">Connectors</div><div class="v">${kindCounts['claude-connector']}</div></div>
214
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>
215
217
  <div class="stat-card"><div class="k">Skills</div><div class="v">${kindCounts.skill}</div></div>
216
218
  <div class="stat-card"><div class="k">MCP Servers</div><div class="v">${kindCounts.mcp}</div></div>
217
219
  <div class="stat-card"><div class="k">Whitelist / Quarantine</div><div class="v">${stats.whitelist} / ${stats.quarantined}</div></div>
@@ -232,6 +234,8 @@ function renderHtml(rows, stats, policy) {
232
234
  <option value="claude-plugin">Claude plugin</option>
233
235
  <option value="claude-connector">Claude connector</option>
234
236
  <option value="copilot-extension">Copilot extension</option>
237
+ <option value="cursor-extension">Cursor extension</option>
238
+ <option value="gemini-extension">Gemini extension</option>
235
239
  <option value="skill">Skill</option>
236
240
  <option value="mcp">MCP server</option>
237
241
  </select>
@@ -440,6 +444,8 @@ function countByKind(items) {
440
444
  mcp: 0,
441
445
  'claude-plugin': 0,
442
446
  'claude-connector': 0,
443
- 'copilot-extension': 0
447
+ 'copilot-extension': 0,
448
+ 'cursor-extension': 0,
449
+ 'gemini-extension': 0
444
450
  });
445
451
  }