@shnitzel/plugscout 0.3.10 → 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.
@@ -1,24 +1,111 @@
1
1
  import fs from 'node:fs/promises';
2
2
  import path from 'node:path';
3
3
  import os from 'node:os';
4
- const PLUGSCOUT_MCP_VALUE = { command: 'npx', args: ['plugscout', 'mcp'] };
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);
5
80
  export function getConfigPath(client, scope) {
6
- if (client === 'cursor') {
7
- if (scope === 'project') {
8
- return path.join(process.cwd(), '.cursor', 'mcp.json');
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] = {};
9
89
  }
10
- return path.join(os.homedir(), '.cursor', 'mcp.json');
90
+ current = current[key];
11
91
  }
12
- return path.join(os.homedir(), '.gemini', 'settings.json');
92
+ return current;
13
93
  }
14
94
  export async function getClientMcpConfigStatus(client, scope) {
15
- const configPath = getConfigPath(client, scope);
95
+ const def = CLIENT_DEFS[client];
96
+ const configPath = def.getConfigPath(scope);
16
97
  try {
17
98
  const raw = await fs.readFile(configPath, 'utf8');
18
99
  const config = JSON.parse(raw);
19
- const mcpServers = config.mcpServers;
20
- const configured = !!(mcpServers?.plugscout);
21
- return { configured, configPath };
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 };
22
109
  }
23
110
  catch {
24
111
  return { configured: false, configPath };
@@ -26,8 +113,9 @@ export async function getClientMcpConfigStatus(client, scope) {
26
113
  }
27
114
  export async function writeClientMcpConfig(options) {
28
115
  const { client, force = false } = options;
29
- const scope = client === 'gemini' ? 'user' : options.scope;
30
- const configPath = getConfigPath(client, scope);
116
+ const def = CLIENT_DEFS[client];
117
+ const scope = def.supportsProjectScope ? options.scope : 'user';
118
+ const configPath = def.getConfigPath(scope);
31
119
  let existing = {};
32
120
  try {
33
121
  const raw = await fs.readFile(configPath, 'utf8');
@@ -36,25 +124,21 @@ export async function writeClientMcpConfig(options) {
36
124
  catch {
37
125
  // file doesn't exist yet — start empty
38
126
  }
39
- const mcpServers = existing.mcpServers ?? {};
40
- const current = mcpServers.plugscout;
127
+ const container = navigateContainer(existing, def.containerPath);
128
+ const current = container['plugscout'];
41
129
  if (current !== undefined) {
42
- const isSame = JSON.stringify(current) === JSON.stringify(PLUGSCOUT_MCP_VALUE);
43
- if (isSame) {
130
+ const isSame = JSON.stringify(current) === JSON.stringify(def.entryValue);
131
+ if (isSame)
44
132
  return { status: 'already-configured', configPath };
45
- }
46
133
  if (!force) {
47
134
  throw new Error(`plugscout already exists in ${configPath} with a different value. Use --force to overwrite.`);
48
135
  }
49
136
  }
50
- const updated = {
51
- ...existing,
52
- mcpServers: { ...mcpServers, plugscout: PLUGSCOUT_MCP_VALUE }
53
- };
137
+ container['plugscout'] = def.entryValue;
54
138
  const dir = path.dirname(configPath);
55
139
  await fs.mkdir(dir, { recursive: true });
56
140
  const tmpPath = `${configPath}.plugscout.tmp`;
57
- await fs.writeFile(tmpPath, `${JSON.stringify(updated, null, 2)}\n`, 'utf8');
141
+ await fs.writeFile(tmpPath, `${JSON.stringify(existing, null, 2)}\n`, 'utf8');
58
142
  await fs.rename(tmpPath, configPath);
59
143
  return { status: 'written', configPath };
60
144
  }
@@ -5,7 +5,7 @@ import fs from 'node:fs/promises';
5
5
  import { getStaleRegistries, loadSyncState } from '../../catalog/sync-state.js';
6
6
  import { loadCatalogItems } from '../../catalog/repository.js';
7
7
  import { hasLegacySkillSh, resolveSkillsRuntime } from '../../install/dependencies.js';
8
- import { getClientMcpConfigStatus } from './client-setup.js';
8
+ import { getClientMcpConfigStatus, CLIENT_DEFS } from './client-setup.js';
9
9
  export async function runDoctorChecks(projectPath = '.') {
10
10
  const checks = [];
11
11
  checks.push(checkSkillsRuntime());
@@ -88,6 +88,64 @@ export async function runDoctorChecks(projectPath = '.') {
88
88
  catch {
89
89
  checks.push({ name: 'Gemini MCP config', status: 'warn', message: 'Could not read Gemini MCP config', suggestion: 'Run: plugscout client setup --client gemini' });
90
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
+ }
91
149
  return checks;
92
150
  }
93
151
  function checkSkillsRuntime() {
@@ -860,28 +860,33 @@ async function handleQuarantine(args) {
860
860
  async function handleClient(args) {
861
861
  const subcommand = args[0];
862
862
  if (subcommand !== 'setup') {
863
- throw new Error('Usage: client setup --client cursor|gemini [--scope user|project] [--force]');
863
+ throw new Error('Usage: client setup --client cursor|gemini|claude-desktop|windsurf|opencode|zed [--scope user|project] [--force]');
864
864
  }
865
+ const { writeClientMcpConfig, CLIENT_DEFS, VALID_CLIENT_KINDS } = await import('./client-setup.js');
865
866
  const clientFlag = readFlag(args, '--client');
866
- if (clientFlag !== 'cursor' && clientFlag !== 'gemini') {
867
- throw new Error('Usage: client setup --client cursor|gemini');
867
+ if (!clientFlag || !VALID_CLIENT_KINDS.includes(clientFlag)) {
868
+ throw new Error(`Usage: client setup --client ${VALID_CLIENT_KINDS.join('|')}`);
868
869
  }
869
870
  const scopeFlag = readFlag(args, '--scope') ?? 'user';
870
871
  if (scopeFlag !== 'user' && scopeFlag !== 'project') {
871
872
  throw new Error('--scope must be user or project');
872
873
  }
873
- if (clientFlag === 'gemini' && scopeFlag === 'project') {
874
- logger.warn('Gemini CLI only supports user scope; falling back to user scope.');
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.`);
875
877
  }
876
878
  const force = hasFlag(args, '--force');
877
- const { writeClientMcpConfig } = await import('./client-setup.js');
878
- const result = await writeClientMcpConfig({ client: clientFlag, scope: scopeFlag, force });
879
+ const result = await writeClientMcpConfig({
880
+ client: clientFlag,
881
+ scope: scopeFlag,
882
+ force
883
+ });
879
884
  if (result.status === 'already-configured') {
880
885
  console.log(`plugscout already configured in ${result.configPath}`);
881
886
  }
882
887
  else {
883
888
  console.log(`plugscout MCP config written: ${result.configPath}`);
884
- printHint(`Restart ${clientFlag === 'cursor' ? 'Cursor IDE' : 'Gemini CLI'} for the change to take effect.`);
889
+ printHint(`Restart ${def.label} for the change to take effect.`);
885
890
  }
886
891
  }
887
892
  async function handleUpgrade(args) {
@@ -1036,7 +1041,7 @@ function printHelp() {
1036
1041
  console.log('Other');
1037
1042
  console.log(' about');
1038
1043
  console.log(' web [--out .plugscout/report.html] [--kind ...] [--limit n] [--open]');
1039
- console.log(' client setup --client cursor|gemini [--scope user|project] [--force]');
1044
+ console.log(' client setup --client cursor|gemini|claude-desktop|windsurf|opencode|zed [--scope user|project] [--force]');
1040
1045
  console.log(' upgrade check');
1041
1046
  console.log(' help');
1042
1047
  console.log('');
@@ -1057,6 +1062,10 @@ function printHelp() {
1057
1062
  console.log(' plugscout show --id claude-connector:asana');
1058
1063
  console.log(' plugscout client setup --client cursor');
1059
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');
1060
1069
  console.log(' plugscout sync --kind cursor-extension,gemini-extension');
1061
1070
  console.log('');
1062
1071
  console.log('Global options');
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@shnitzel/plugscout",
3
- "version": "0.3.10",
3
+ "version": "0.3.11",
4
4
  "description": "Claude plugins + Claude connectors + Copilot extensions + Skills + MCP security intelligence framework",
5
5
  "private": false,
6
6
  "type": "module",