@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.
- 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 +144 -0
- package/dist/interfaces/cli/doctor.js +88 -0
- package/dist/interfaces/cli/index.js +54 -6
- package/dist/interfaces/cli/mcp.js +1 -1
- package/dist/interfaces/cli/options.js +8 -2
- package/dist/interfaces/cli/ui/web-report.js +8 -2
- package/dist/lib/validation/contracts.js +4 -2
- package/package.json +1 -1
|
@@ -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' ||
|
|
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
|
}
|
package/dist/catalog/sync.js
CHANGED
|
@@ -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
|
}
|