@shomra/agent 0.3.16 → 0.3.18
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/NOTICE +1 -1
- package/README.md +57 -57
- package/package.json +3 -9
- package/shomra.mjs +9 -7168
- package/src/agents/hook-command.mjs +19 -0
- package/src/agents/hook-files.mjs +41 -0
- package/src/agents/installers.mjs +203 -0
- package/src/artifacts/matchers.mjs +59 -0
- package/src/artifacts/report.mjs +50 -0
- package/src/cli/flags.mjs +68 -0
- package/src/cli/help-sections.mjs +309 -0
- package/src/cli/help.mjs +27 -0
- package/src/cli/main.mjs +55 -0
- package/src/cli/registry.mjs +80 -0
- package/src/cli/suggestions.mjs +33 -0
- package/src/commands/add.mjs +149 -0
- package/src/commands/agent-identity.mjs +46 -0
- package/src/commands/check.mjs +194 -0
- package/src/commands/corpus.mjs +126 -0
- package/src/commands/design.mjs +168 -0
- package/src/commands/doctor.mjs +209 -0
- package/src/commands/fix.mjs +115 -0
- package/src/commands/gate.mjs +154 -0
- package/src/commands/git-hooks.mjs +163 -0
- package/src/commands/init.mjs +36 -0
- package/src/commands/install-hook.mjs +51 -0
- package/src/commands/llm-proxy.mjs +153 -0
- package/src/commands/mcp-add.mjs +185 -0
- package/src/commands/mcp.mjs +143 -0
- package/src/commands/memory-scan.mjs +181 -0
- package/src/commands/model-scan.mjs +99 -0
- package/src/commands/models.mjs +145 -0
- package/src/commands/new.mjs +64 -0
- package/src/commands/plan.mjs +87 -0
- package/src/commands/pr.mjs +249 -0
- package/src/commands/protect.mjs +38 -0
- package/src/commands/provenance.mjs +91 -0
- package/src/commands/redteam.mjs +166 -0
- package/src/commands/rules.mjs +220 -0
- package/src/commands/run.mjs +128 -0
- package/src/commands/scan-zip.mjs +118 -0
- package/src/commands/scan.mjs +102 -0
- package/src/commands/secrets.mjs +99 -0
- package/src/commands/status.mjs +50 -0
- package/src/commands/why.mjs +88 -0
- package/src/core/api-client.mjs +66 -0
- package/src/core/api-key.mjs +6 -0
- package/src/core/circuit-breaker.mjs +42 -0
- package/src/core/config.mjs +37 -0
- package/src/core/exit-codes.mjs +9 -0
- package/src/core/json-file.mjs +13 -0
- package/src/core/numbers.mjs +4 -0
- package/src/core/package-root.mjs +10 -0
- package/src/core/terminal.mjs +16 -0
- package/src/core/version.mjs +14 -0
- package/src/core/wire-limits.mjs +53 -0
- package/src/corpus/screening.mjs +127 -0
- package/{ai-usage.mjs → src/detect/ai-usage.mjs} +0 -27
- package/src/detect/code-sast.mjs +2 -0
- package/{design.mjs → src/detect/design.mjs} +18 -107
- package/src/detect/guard-signals.mjs +18 -0
- package/{model-refs.mjs → src/detect/model-refs.mjs} +18 -77
- package/src/detect/sast/chains.mjs +30 -0
- package/src/detect/sast/path-expressions.mjs +76 -0
- package/src/detect/sast/rules-chains.mjs +33 -0
- package/src/detect/sast/rules-config.mjs +51 -0
- package/src/detect/sast/rules-javascript.mjs +109 -0
- package/src/detect/sast/rules-python.mjs +292 -0
- package/src/detect/sast/scanner.mjs +104 -0
- package/src/detect/sast/source-lines.mjs +115 -0
- package/src/detect/sast/taint.mjs +71 -0
- package/src/detect/signals/artifacts.mjs +113 -0
- package/src/detect/signals/autonomy.mjs +55 -0
- package/src/detect/signals/config-markers.mjs +28 -0
- package/src/detect/signals/credential-harvest.mjs +64 -0
- package/src/detect/signals/durable-claims.mjs +73 -0
- package/src/detect/signals/egress.mjs +56 -0
- package/src/detect/signals/execution-hijack.mjs +128 -0
- package/src/detect/signals/gate.mjs +91 -0
- package/src/detect/signals/injection.mjs +55 -0
- package/src/detect/signals/lines.mjs +42 -0
- package/src/detect/signals/masking.mjs +99 -0
- package/src/detect/signals/memory.mjs +357 -0
- package/src/detect/signals/packages.mjs +45 -0
- package/src/detect/signals/propagation.mjs +86 -0
- package/src/detect/signals/prose-context.mjs +82 -0
- package/src/detect/signals/scan.mjs +91 -0
- package/src/detect/signals/secrets.mjs +85 -0
- package/src/detect/signals/sensitive.mjs +9 -0
- package/src/detect/signals/severity.mjs +10 -0
- package/src/detect/signals/shell.mjs +96 -0
- package/src/detect/signals/staged-fetch.mjs +66 -0
- package/src/detect/signals/text-match.mjs +35 -0
- package/src/gate/batch.mjs +157 -0
- package/src/gate/environment.mjs +122 -0
- package/src/gate/repo-policy.mjs +65 -0
- package/src/gate/result.mjs +53 -0
- package/src/gate/sarif.mjs +33 -0
- package/src/gate/sast.mjs +64 -0
- package/src/gate/suppressions.mjs +0 -0
- package/src/guard/classify.mjs +50 -0
- package/src/guard/emit.mjs +51 -0
- package/src/guard/ignore.mjs +24 -0
- package/src/guard/ledger.mjs +112 -0
- package/src/guard/model-load.mjs +50 -0
- package/src/guard/normalize.mjs +77 -0
- package/src/guard/options.mjs +10 -0
- package/src/guard/prompt-guard.mjs +184 -0
- package/src/guard/report.mjs +35 -0
- package/src/guard/result-guard.mjs +140 -0
- package/src/guard/tool-guard.mjs +166 -0
- package/src/inventory/agent-artifacts.mjs +5 -0
- package/src/inventory/agent-posture.mjs +249 -0
- package/src/inventory/artifacts/classify.mjs +27 -0
- package/src/inventory/artifacts/discover.mjs +187 -0
- package/src/inventory/artifacts/file-read.mjs +42 -0
- package/src/inventory/artifacts/hooks.mjs +14 -0
- package/src/inventory/artifacts/limits.mjs +37 -0
- package/src/inventory/artifacts/marketplaces.mjs +45 -0
- package/src/inventory/artifacts/roots.mjs +20 -0
- package/src/inventory/artifacts/walk.mjs +36 -0
- package/src/inventory/discovery/ai-dependencies.mjs +161 -0
- package/src/inventory/discovery/ai-tools.mjs +23 -0
- package/src/inventory/discovery/all.mjs +40 -0
- package/src/inventory/discovery/coding-agents.mjs +77 -0
- package/src/inventory/discovery/fs-read.mjs +36 -0
- package/src/inventory/discovery/local-runtimes.mjs +53 -0
- package/src/inventory/discovery/mcp-clients.mjs +67 -0
- package/src/inventory/discovery/mcp-servers.mjs +78 -0
- package/src/inventory/discovery/model-keys.mjs +97 -0
- package/src/inventory/discovery/platform.mjs +16 -0
- package/src/inventory/discovery/rules-files.mjs +25 -0
- package/src/inventory/discovery/vector-stores.mjs +176 -0
- package/src/inventory/discovery/workspace.mjs +124 -0
- package/src/inventory/discovery.mjs +10 -0
- package/src/mcp/child-process.mjs +50 -0
- package/src/mcp/config-wrapping.mjs +75 -0
- package/src/mcp/connect-gate.mjs +45 -0
- package/src/mcp/hosts.mjs +16 -0
- package/src/mcp/jsonrpc.mjs +48 -0
- package/src/mcp/lookup.mjs +50 -0
- package/src/mcp/screening.mjs +103 -0
- package/src/mcp/server-tools.mjs +97 -0
- package/src/mcp/server.mjs +102 -0
- package/src/mcp/shim.mjs +205 -0
- package/src/models/lookup.mjs +79 -0
- package/src/models/references.mjs +103 -0
- package/src/rules/context.mjs +98 -0
- package/src/rules/generate.mjs +103 -0
- package/src/rules/sections.mjs +145 -0
- package/src/scaffold/agent-project.mjs +185 -0
- package/src/scaffold/artifact-templates.mjs +35 -0
- package/code-sast.mjs +0 -1063
- package/discovery.mjs +0 -977
- package/guard-ledger.mjs +0 -239
- package/guard-signals.mjs +0 -1268
|
@@ -0,0 +1,78 @@
|
|
|
1
|
+
import path from 'node:path';
|
|
2
|
+
import { readJson } from './fs-read.mjs';
|
|
3
|
+
import { redactEnv } from './model-keys.mjs';
|
|
4
|
+
import { APPDATA, HOME, PLAT, vscodeUserDir } from './platform.mjs';
|
|
5
|
+
import { walkWorkspace } from './workspace.mjs';
|
|
6
|
+
|
|
7
|
+
export function vendorFromPath(file) {
|
|
8
|
+
if (/[\\/]\.cursor[\\/]/.test(file)) return 'cursor';
|
|
9
|
+
if (/[\\/]\.vscode[\\/]/.test(file)) return 'vscode';
|
|
10
|
+
if (/[\\/]\.gemini[\\/]/.test(file)) return 'gemini';
|
|
11
|
+
if (/[\\/]\.zed[\\/]/.test(file)) return 'zed';
|
|
12
|
+
return 'project';
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
function globalMcpCandidates() {
|
|
16
|
+
const c = [];
|
|
17
|
+
if (PLAT === 'win32') c.push({ vendor: 'claude', file: path.join(APPDATA, 'Claude', 'claude_desktop_config.json') });
|
|
18
|
+
else if (PLAT === 'darwin') c.push({ vendor: 'claude', file: path.join(HOME, 'Library', 'Application Support', 'Claude', 'claude_desktop_config.json') });
|
|
19
|
+
else c.push({ vendor: 'claude', file: path.join(HOME, '.config', 'Claude', 'claude_desktop_config.json') });
|
|
20
|
+
c.push({ vendor: 'cursor', file: path.join(HOME, '.cursor', 'mcp.json') });
|
|
21
|
+
c.push({ vendor: 'windsurf', file: path.join(HOME, '.codeium', 'windsurf', 'mcp_config.json') });
|
|
22
|
+
c.push({ vendor: 'continue', file: path.join(HOME, '.continue', 'config.json') });
|
|
23
|
+
c.push({ vendor: 'claude-code', file: path.join(HOME, '.claude.json') });
|
|
24
|
+
c.push({ vendor: 'gemini', file: path.join(HOME, '.gemini', 'settings.json') });
|
|
25
|
+
c.push({ vendor: 'cline', file: path.join(vscodeUserDir(), 'globalStorage', 'saoudrizwan.claude-dev', 'settings', 'cline_mcp_settings.json') });
|
|
26
|
+
c.push({ vendor: 'roo', file: path.join(vscodeUserDir(), 'globalStorage', 'rooveterinaryinc.roo-cline', 'settings', 'mcp_settings.json') });
|
|
27
|
+
|
|
28
|
+
c.push({ vendor: 'vscode', file: path.join(vscodeUserDir(), 'mcp.json') });
|
|
29
|
+
c.push({ vendor: 'vscode', file: path.join(vscodeUserDir(), 'settings.json') });
|
|
30
|
+
c.push({ vendor: 'cursor', file: path.join(vscodeUserDir('Cursor'), 'settings.json') });
|
|
31
|
+
c.push({ vendor: 'zed', file: PLAT === 'darwin' ? path.join(HOME, 'Library', 'Application Support', 'Zed', 'settings.json') : path.join(HOME, '.config', 'zed', 'settings.json') });
|
|
32
|
+
return c;
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
function extractServers(json) {
|
|
36
|
+
if (!json || typeof json !== 'object') return {};
|
|
37
|
+
return (
|
|
38
|
+
json.mcpServers ||
|
|
39
|
+
json.servers ||
|
|
40
|
+
json['mcp.servers'] ||
|
|
41
|
+
json.mcp?.servers ||
|
|
42
|
+
json.context_servers ||
|
|
43
|
+
{}
|
|
44
|
+
);
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
export function discoverMcpServers(roots = [process.cwd()], files = null) {
|
|
48
|
+
const walk = files || walkWorkspace(roots);
|
|
49
|
+
const candidates = [
|
|
50
|
+
...globalMcpCandidates(),
|
|
51
|
+
...walk.mcp.map(({ file }) => ({ vendor: vendorFromPath(file), file })),
|
|
52
|
+
];
|
|
53
|
+
const assets = [];
|
|
54
|
+
const seen = new Set();
|
|
55
|
+
for (const { vendor, file } of candidates) {
|
|
56
|
+
const json = readJson(file);
|
|
57
|
+
if (!json) continue;
|
|
58
|
+
const servers = extractServers(json);
|
|
59
|
+
for (const [name, cfg] of Object.entries(servers)) {
|
|
60
|
+
if (!cfg || typeof cfg !== 'object') continue;
|
|
61
|
+
const command = [cfg.command, ...(Array.isArray(cfg.args) ? cfg.args : [])].filter(Boolean).join(' ');
|
|
62
|
+
const identifier = cfg.url || cfg.serverUrl || command || name;
|
|
63
|
+
const key = `${name}:${identifier}`;
|
|
64
|
+
if (seen.has(key)) continue;
|
|
65
|
+
seen.add(key);
|
|
66
|
+
assets.push({
|
|
67
|
+
type: 'MCP_SERVER',
|
|
68
|
+
name,
|
|
69
|
+
identifier,
|
|
70
|
+
vendor,
|
|
71
|
+
metadata: { command, url: cfg.url || cfg.serverUrl || null, configFile: file, env: redactEnv(cfg.env) },
|
|
72
|
+
|
|
73
|
+
content: JSON.stringify({ command, url: cfg.url || cfg.serverUrl, env: cfg.env || {} }),
|
|
74
|
+
});
|
|
75
|
+
}
|
|
76
|
+
}
|
|
77
|
+
return assets;
|
|
78
|
+
}
|
|
@@ -0,0 +1,97 @@
|
|
|
1
|
+
import { readText } from './fs-read.mjs';
|
|
2
|
+
import { walkWorkspace } from './workspace.mjs';
|
|
3
|
+
|
|
4
|
+
const KEY_NAME_VENDOR = {
|
|
5
|
+
OPENAI_API_KEY: 'openai', AZURE_OPENAI_API_KEY: 'azure-openai', AZURE_OPENAI_KEY: 'azure-openai',
|
|
6
|
+
ANTHROPIC_API_KEY: 'anthropic', GOOGLE_API_KEY: 'google', GOOGLE_GENAI_API_KEY: 'google',
|
|
7
|
+
GEMINI_API_KEY: 'google', MISTRAL_API_KEY: 'mistral', GROQ_API_KEY: 'groq', COHERE_API_KEY: 'cohere',
|
|
8
|
+
HUGGINGFACE_API_KEY: 'huggingface', HUGGINGFACEHUB_API_TOKEN: 'huggingface', HF_TOKEN: 'huggingface',
|
|
9
|
+
OPENROUTER_API_KEY: 'openrouter', XAI_API_KEY: 'xai', DEEPSEEK_API_KEY: 'deepseek',
|
|
10
|
+
TOGETHER_API_KEY: 'together', TOGETHERAI_API_KEY: 'together', PERPLEXITY_API_KEY: 'perplexity',
|
|
11
|
+
REPLICATE_API_TOKEN: 'replicate', FIREWORKS_API_KEY: 'fireworks', DASHSCOPE_API_KEY: 'alibaba',
|
|
12
|
+
AI21_API_KEY: 'ai21', ANYSCALE_API_KEY: 'anyscale', VOYAGE_API_KEY: 'voyage', NVIDIA_API_KEY: 'nvidia',
|
|
13
|
+
CEREBRAS_API_KEY: 'cerebras', STABILITY_API_KEY: 'stability', ELEVENLABS_API_KEY: 'elevenlabs',
|
|
14
|
+
WATSONX_APIKEY: 'ibm', LANGCHAIN_API_KEY: 'langsmith', LANGSMITH_API_KEY: 'langsmith',
|
|
15
|
+
PINECONE_API_KEY: 'pinecone', WEAVIATE_API_KEY: 'weaviate',
|
|
16
|
+
};
|
|
17
|
+
|
|
18
|
+
const KEY_VALUE_PATTERNS = [
|
|
19
|
+
{ re: /^sk-ant-[A-Za-z0-9_-]{20,}/, vendor: 'anthropic' },
|
|
20
|
+
{ re: /^sk-or-[A-Za-z0-9_-]{20,}/, vendor: 'openrouter' },
|
|
21
|
+
{ re: /^sk-proj-[A-Za-z0-9_-]{20,}/, vendor: 'openai' },
|
|
22
|
+
{ re: /^sk-[A-Za-z0-9]{32,}/, vendor: 'openai' },
|
|
23
|
+
{ re: /^AIza[0-9A-Za-z_-]{30,}/, vendor: 'google' },
|
|
24
|
+
{ re: /^gsk_[A-Za-z0-9]{20,}/, vendor: 'groq' },
|
|
25
|
+
{ re: /^hf_[A-Za-z0-9]{20,}/, vendor: 'huggingface' },
|
|
26
|
+
{ re: /^xai-[A-Za-z0-9]{20,}/, vendor: 'xai' },
|
|
27
|
+
{ re: /^r8_[A-Za-z0-9]{20,}/, vendor: 'replicate' },
|
|
28
|
+
{ re: /^pplx-[A-Za-z0-9]{20,}/, vendor: 'perplexity' },
|
|
29
|
+
{ re: /^fw_[A-Za-z0-9]{20,}/, vendor: 'fireworks' },
|
|
30
|
+
];
|
|
31
|
+
|
|
32
|
+
function classifyKey(name, value) {
|
|
33
|
+
if (KEY_NAME_VENDOR[name]) return KEY_NAME_VENDOR[name];
|
|
34
|
+
for (const { re, vendor } of KEY_VALUE_PATTERNS) if (re.test(value)) return vendor;
|
|
35
|
+
|
|
36
|
+
if (/(_API_KEY|_API_TOKEN|_APIKEY)$/.test(name) && /(LLM|AI|GPT|CLAUDE|MODEL|OPENAI|ANTHROPIC|GEMINI)/.test(name)) return 'unknown';
|
|
37
|
+
return null;
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
export function discoverDotenvKeys(roots = [process.cwd()], files = null) {
|
|
41
|
+
const walk = files || walkWorkspace(roots);
|
|
42
|
+
const assets = [];
|
|
43
|
+
const seen = new Set();
|
|
44
|
+
for (const { file } of walk.env) {
|
|
45
|
+
const text = readText(file, 100_000);
|
|
46
|
+
if (text == null) continue;
|
|
47
|
+
for (const line of text.split(/\r?\n/)) {
|
|
48
|
+
const m = line.match(/^\s*(?:export\s+)?([A-Z0-9_]+)\s*=\s*(.*)$/);
|
|
49
|
+
if (!m) continue;
|
|
50
|
+
const name = m[1];
|
|
51
|
+
let value = m[2].trim().replace(/^["']|["']$/g, '');
|
|
52
|
+
if (!value || value.length < 8 || /^\$\{/.test(value) || /(your|xxx|placeholder|changeme|<|example)/i.test(value)) continue;
|
|
53
|
+
const vendor = classifyKey(name, value);
|
|
54
|
+
if (!vendor) continue;
|
|
55
|
+
const key = `${name}:${file}`;
|
|
56
|
+
if (seen.has(key)) continue;
|
|
57
|
+
seen.add(key);
|
|
58
|
+
assets.push({
|
|
59
|
+
type: 'MODEL_KEY',
|
|
60
|
+
name,
|
|
61
|
+
identifier: `dotenv:${file}:${name}`,
|
|
62
|
+
vendor,
|
|
63
|
+
metadata: { source: 'dotenv', file, fingerprint: `${value.slice(0, 3)}…${value.slice(-2)}` },
|
|
64
|
+
|
|
65
|
+
});
|
|
66
|
+
}
|
|
67
|
+
}
|
|
68
|
+
return assets;
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
export function discoverModelKeys() {
|
|
72
|
+
const assets = [];
|
|
73
|
+
for (const [name, v] of Object.entries(process.env)) {
|
|
74
|
+
if (!v || v.length < 8) continue;
|
|
75
|
+
const vendor = KEY_NAME_VENDOR[name] || (/(_API_KEY|_API_TOKEN|_APIKEY)$/.test(name) ? classifyKey(name, v) : null);
|
|
76
|
+
if (!vendor) continue;
|
|
77
|
+
assets.push({
|
|
78
|
+
type: 'MODEL_KEY',
|
|
79
|
+
name,
|
|
80
|
+
identifier: `env:${name}`,
|
|
81
|
+
vendor,
|
|
82
|
+
metadata: { source: 'environment', fingerprint: `${v.slice(0, 3)}…${v.slice(-2)}` },
|
|
83
|
+
|
|
84
|
+
});
|
|
85
|
+
}
|
|
86
|
+
return assets;
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
export function redactEnv(env) {
|
|
90
|
+
if (!env || typeof env !== 'object') return {};
|
|
91
|
+
const out = {};
|
|
92
|
+
for (const [k, v] of Object.entries(env)) {
|
|
93
|
+
const s = String(v ?? '');
|
|
94
|
+
out[k] = s.length > 8 ? `${s.slice(0, 3)}…${s.slice(-2)}` : s;
|
|
95
|
+
}
|
|
96
|
+
return out;
|
|
97
|
+
}
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
import os from 'node:os';
|
|
2
|
+
import path from 'node:path';
|
|
3
|
+
|
|
4
|
+
export const HOME = os.homedir();
|
|
5
|
+
|
|
6
|
+
export const APPDATA = process.env.APPDATA || path.join(HOME, 'AppData', 'Roaming');
|
|
7
|
+
|
|
8
|
+
export const LOCALAPPDATA = process.env.LOCALAPPDATA || path.join(HOME, 'AppData', 'Local');
|
|
9
|
+
|
|
10
|
+
export const PLAT = process.platform;
|
|
11
|
+
|
|
12
|
+
export function vscodeUserDir(variant = 'Code') {
|
|
13
|
+
if (PLAT === 'win32') return path.join(APPDATA, variant, 'User');
|
|
14
|
+
if (PLAT === 'darwin') return path.join(HOME, 'Library', 'Application Support', variant, 'User');
|
|
15
|
+
return path.join(HOME, '.config', variant, 'User');
|
|
16
|
+
}
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
import path from 'node:path';
|
|
2
|
+
import { readText } from './fs-read.mjs';
|
|
3
|
+
import { vendorFromPath } from './mcp-servers.mjs';
|
|
4
|
+
import { walkWorkspace } from './workspace.mjs';
|
|
5
|
+
|
|
6
|
+
export function discoverRulesFiles(roots = [process.cwd()], files = null) {
|
|
7
|
+
const walk = files || walkWorkspace(roots);
|
|
8
|
+
const assets = [];
|
|
9
|
+
const seen = new Set();
|
|
10
|
+
for (const { file } of walk.rules) {
|
|
11
|
+
if (seen.has(file)) continue;
|
|
12
|
+
seen.add(file);
|
|
13
|
+
const content = readText(file, 50_000);
|
|
14
|
+
if (content == null) continue;
|
|
15
|
+
assets.push({
|
|
16
|
+
type: 'AI_RULES',
|
|
17
|
+
name: path.basename(file),
|
|
18
|
+
identifier: file,
|
|
19
|
+
vendor: vendorFromPath(file) === 'project' ? 'rules' : vendorFromPath(file),
|
|
20
|
+
metadata: { bytes: content.length, dir: path.dirname(file) },
|
|
21
|
+
content: content.slice(0, 50_000),
|
|
22
|
+
});
|
|
23
|
+
}
|
|
24
|
+
return assets;
|
|
25
|
+
}
|
|
@@ -0,0 +1,176 @@
|
|
|
1
|
+
import path from 'node:path';
|
|
2
|
+
import { PY_VECTOR, VECTOR_ENV, vectorLibInfo } from './ai-dependencies.mjs';
|
|
3
|
+
import { readJson, readText } from './fs-read.mjs';
|
|
4
|
+
import { walkWorkspace } from './workspace.mjs';
|
|
5
|
+
|
|
6
|
+
const MAX_ENV_BYTES = 100_000;
|
|
7
|
+
const MAX_INDEX_FILES = 20;
|
|
8
|
+
const MAX_MANIFESTS = 10;
|
|
9
|
+
const MAX_HOSTS = 5;
|
|
10
|
+
const PLACEHOLDER_VALUE = /(your|xxx|placeholder|changeme|<|example)/i;
|
|
11
|
+
const ENV_ASSIGNMENT = /^\s*(?:export\s+)?([A-Z0-9_]+)\s*=\s*(.*)$/;
|
|
12
|
+
const URL_HOST = /^[a-z]+:\/\/([^/:?#\s]+)/i;
|
|
13
|
+
const BARE_HOST = /^[a-z0-9.-]+\.[a-z]{2,}(:\d+)?$/i;
|
|
14
|
+
|
|
15
|
+
function unquote(value) {
|
|
16
|
+
return String(value || '').trim().replace(/^["']|["']$/g, '');
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
function endpointHost(value) {
|
|
20
|
+
const raw = unquote(value);
|
|
21
|
+
if (!raw) return null;
|
|
22
|
+
const url = raw.match(URL_HOST);
|
|
23
|
+
if (url) return url[1];
|
|
24
|
+
return BARE_HOST.test(raw) ? raw.split(':')[0] : null;
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
function engineFromIndexFiles(names) {
|
|
28
|
+
if (names.includes('chroma.sqlite3') || names.some((name) => name.startsWith('chroma-'))) return 'chroma';
|
|
29
|
+
if (names.includes('index.faiss')) return 'faiss';
|
|
30
|
+
if (names.some((name) => name.endsWith('.lance'))) return 'lancedb';
|
|
31
|
+
if (names.some((name) => name.endsWith('.usearch'))) return 'usearch';
|
|
32
|
+
if (names.includes('docstore.json') || names.includes('default__vector_store.json')) return 'llamaindex';
|
|
33
|
+
return null;
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
function localIndexAssets(vectorFiles) {
|
|
37
|
+
const byDirectory = new Map();
|
|
38
|
+
for (const { file } of vectorFiles) {
|
|
39
|
+
const directory = path.dirname(file);
|
|
40
|
+
if (!byDirectory.has(directory)) byDirectory.set(directory, []);
|
|
41
|
+
byDirectory.get(directory).push(path.basename(file).toLowerCase());
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
const assets = [];
|
|
45
|
+
for (const [directory, names] of byDirectory) {
|
|
46
|
+
const engine = engineFromIndexFiles(names);
|
|
47
|
+
if (!engine) continue;
|
|
48
|
+
assets.push({
|
|
49
|
+
type: 'VECTOR_STORE',
|
|
50
|
+
name: `${engine} index (${path.basename(directory)})`,
|
|
51
|
+
identifier: `vector:local:${directory}`,
|
|
52
|
+
vendor: engine,
|
|
53
|
+
metadata: {
|
|
54
|
+
surface: 'local-index',
|
|
55
|
+
engine,
|
|
56
|
+
hosted: false,
|
|
57
|
+
pickleBacked: engine === 'faiss' && names.includes('index.pkl'),
|
|
58
|
+
dir: directory,
|
|
59
|
+
files: names.slice(0, MAX_INDEX_FILES),
|
|
60
|
+
},
|
|
61
|
+
});
|
|
62
|
+
}
|
|
63
|
+
return assets;
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
function npmVectorLibraries(file) {
|
|
67
|
+
const json = readJson(file);
|
|
68
|
+
if (!json) return [];
|
|
69
|
+
const dependencies = {
|
|
70
|
+
...(json.dependencies || {}),
|
|
71
|
+
...(json.devDependencies || {}),
|
|
72
|
+
...(json.peerDependencies || {}),
|
|
73
|
+
...(json.optionalDependencies || {}),
|
|
74
|
+
};
|
|
75
|
+
return Object.keys(dependencies).map(vectorLibInfo).filter(Boolean);
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
function pythonVectorLibraries(file) {
|
|
79
|
+
const text = readText(file, MAX_ENV_BYTES);
|
|
80
|
+
if (text == null) return [];
|
|
81
|
+
return PY_VECTOR
|
|
82
|
+
.filter((pkg) => {
|
|
83
|
+
const escaped = pkg.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
|
|
84
|
+
return new RegExp(`(^|[^a-z0-9_.-])${escaped}([^a-z0-9_.-]|$)`, 'im').test(text);
|
|
85
|
+
})
|
|
86
|
+
.map(vectorLibInfo)
|
|
87
|
+
.filter(Boolean);
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
function clientLibraryAssets(manifestFiles) {
|
|
91
|
+
const byEngine = new Map();
|
|
92
|
+
for (const { file } of manifestFiles) {
|
|
93
|
+
const libraries = path.basename(file) === 'package.json'
|
|
94
|
+
? npmVectorLibraries(file)
|
|
95
|
+
: pythonVectorLibraries(file);
|
|
96
|
+
for (const { engine, hosted } of libraries) {
|
|
97
|
+
if (!byEngine.has(engine)) byEngine.set(engine, { hosted, manifests: new Set() });
|
|
98
|
+
byEngine.get(engine).manifests.add(file);
|
|
99
|
+
}
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
return [...byEngine].map(([engine, { hosted, manifests }]) => {
|
|
103
|
+
const list = [...manifests];
|
|
104
|
+
return {
|
|
105
|
+
type: 'VECTOR_STORE',
|
|
106
|
+
name: `${engine} client`,
|
|
107
|
+
identifier: `vector:client:${engine}`,
|
|
108
|
+
vendor: engine,
|
|
109
|
+
metadata: {
|
|
110
|
+
surface: 'client-lib',
|
|
111
|
+
engine,
|
|
112
|
+
hosted,
|
|
113
|
+
usedInProjects: list.length,
|
|
114
|
+
manifests: list.slice(0, MAX_MANIFESTS),
|
|
115
|
+
},
|
|
116
|
+
};
|
|
117
|
+
});
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
function collectCloudSettings(envFiles) {
|
|
121
|
+
const byEngine = new Map();
|
|
122
|
+
for (const { file } of envFiles) {
|
|
123
|
+
const text = readText(file, MAX_ENV_BYTES);
|
|
124
|
+
if (text == null) continue;
|
|
125
|
+
|
|
126
|
+
for (const line of text.split(/\r?\n/)) {
|
|
127
|
+
const assignment = line.match(ENV_ASSIGNMENT);
|
|
128
|
+
if (!assignment) continue;
|
|
129
|
+
const setting = VECTOR_ENV[assignment[1]];
|
|
130
|
+
if (!setting) continue;
|
|
131
|
+
|
|
132
|
+
const value = unquote(assignment[2]);
|
|
133
|
+
if (!value || PLACEHOLDER_VALUE.test(value)) continue;
|
|
134
|
+
|
|
135
|
+
if (!byEngine.has(setting.engine)) {
|
|
136
|
+
byEngine.set(setting.engine, { hosts: new Set(), hasKey: false, hasEndpoint: false, files: new Set() });
|
|
137
|
+
}
|
|
138
|
+
const entry = byEngine.get(setting.engine);
|
|
139
|
+
entry.files.add(file);
|
|
140
|
+
if (setting.kind === 'key') {
|
|
141
|
+
entry.hasKey = true;
|
|
142
|
+
} else {
|
|
143
|
+
entry.hasEndpoint = true;
|
|
144
|
+
const host = endpointHost(value);
|
|
145
|
+
if (host) entry.hosts.add(host);
|
|
146
|
+
}
|
|
147
|
+
}
|
|
148
|
+
}
|
|
149
|
+
return byEngine;
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
function cloudEndpointAssets(envFiles) {
|
|
153
|
+
return [...collectCloudSettings(envFiles)].map(([engine, entry]) => ({
|
|
154
|
+
type: 'VECTOR_STORE',
|
|
155
|
+
name: `${engine} (cloud)`,
|
|
156
|
+
identifier: `vector:cloud:${engine}`,
|
|
157
|
+
vendor: engine,
|
|
158
|
+
metadata: {
|
|
159
|
+
surface: 'cloud-endpoint',
|
|
160
|
+
engine,
|
|
161
|
+
hosted: true,
|
|
162
|
+
hasApiKey: entry.hasKey,
|
|
163
|
+
hosts: [...entry.hosts].slice(0, MAX_HOSTS),
|
|
164
|
+
files: [...entry.files].slice(0, MAX_MANIFESTS),
|
|
165
|
+
},
|
|
166
|
+
}));
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
export function discoverVectorStores(roots = [process.cwd()], files = null) {
|
|
170
|
+
const walk = files || walkWorkspace(roots);
|
|
171
|
+
return [
|
|
172
|
+
...localIndexAssets(walk.vector),
|
|
173
|
+
...clientLibraryAssets(walk.manifests),
|
|
174
|
+
...cloudEndpointAssets(walk.env),
|
|
175
|
+
];
|
|
176
|
+
}
|
|
@@ -0,0 +1,124 @@
|
|
|
1
|
+
import fs from 'node:fs';
|
|
2
|
+
import path from 'node:path';
|
|
3
|
+
import { isAiUsageScannable } from '../../detect/ai-usage.mjs';
|
|
4
|
+
import { exists } from './fs-read.mjs';
|
|
5
|
+
import { HOME } from './platform.mjs';
|
|
6
|
+
|
|
7
|
+
const IGNORE_DIRS = new Set([
|
|
8
|
+
'node_modules', '.git', '.hg', '.svn', 'dist', 'build', 'out', '.next', '.nuxt',
|
|
9
|
+
'.cache', '.venv', 'venv', 'env', '__pycache__', '.tox', 'target', 'vendor',
|
|
10
|
+
'bin', 'obj', '.gradle', '.idea', 'coverage', '.pytest_cache', '.mypy_cache',
|
|
11
|
+
'Pods', '.terraform', '.expo', 'tmp', 'temp', '.turbo', '.parcel-cache',
|
|
12
|
+
'.svelte-kit', 'bower_components', '.pnpm-store', 'site-packages', '.yarn',
|
|
13
|
+
]);
|
|
14
|
+
|
|
15
|
+
function workspaceParents() {
|
|
16
|
+
const names = [
|
|
17
|
+
'Desktop', 'Documents', 'source', 'source/repos', 'repos', 'Repos',
|
|
18
|
+
'projects', 'Projects', 'dev', 'Dev', 'Developer', 'git', 'Git', 'code',
|
|
19
|
+
'Code', 'workspace', 'Workspace', 'work', 'src', 'go/src', 'ghq',
|
|
20
|
+
'OneDrive/Desktop', 'OneDrive/Documents',
|
|
21
|
+
];
|
|
22
|
+
return names.map((n) => path.join(HOME, n)).filter(exists);
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
export function resolveRoots(roots, autoExpand) {
|
|
26
|
+
const out = new Set();
|
|
27
|
+
for (const r of roots || []) if (r) out.add(path.resolve(r));
|
|
28
|
+
if (autoExpand) {
|
|
29
|
+
out.add(HOME);
|
|
30
|
+
for (const parent of workspaceParents()) {
|
|
31
|
+
out.add(parent);
|
|
32
|
+
try {
|
|
33
|
+
for (const e of fs.readdirSync(parent, { withFileTypes: true })) {
|
|
34
|
+
if (e.isDirectory() && !e.name.startsWith('.') && !IGNORE_DIRS.has(e.name)) {
|
|
35
|
+
out.add(path.join(parent, e.name));
|
|
36
|
+
}
|
|
37
|
+
}
|
|
38
|
+
} catch {
|
|
39
|
+
|
|
40
|
+
}
|
|
41
|
+
}
|
|
42
|
+
}
|
|
43
|
+
return [...out].slice(0, 500);
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
const RULE_NAMES = new Set([
|
|
47
|
+
'.cursorrules', '.windsurfrules', '.clinerules', '.roorules', '.aider.conf.yml',
|
|
48
|
+
'.aider.conf.yaml', 'AGENTS.md', 'CLAUDE.md', 'GEMINI.md', 'copilot-instructions.md',
|
|
49
|
+
]);
|
|
50
|
+
|
|
51
|
+
const MANIFEST_NAMES = new Set([
|
|
52
|
+
'package.json', 'requirements.txt', 'requirements-dev.txt', 'pyproject.toml',
|
|
53
|
+
'Pipfile', 'environment.yml', 'environment.yaml',
|
|
54
|
+
]);
|
|
55
|
+
|
|
56
|
+
const isEnvFile = (base) =>
|
|
57
|
+
/^\.env(\..+)?$/.test(base) && !/(example|sample|template|dist)/i.test(base);
|
|
58
|
+
|
|
59
|
+
const VECTOR_INDEX_BASENAMES = new Set([
|
|
60
|
+
'chroma.sqlite3',
|
|
61
|
+
'index.faiss', 'index.pkl',
|
|
62
|
+
'docstore.json', 'default__vector_store.json',
|
|
63
|
+
'chroma-embeddings.parquet', 'chroma-collections.parquet',
|
|
64
|
+
]);
|
|
65
|
+
|
|
66
|
+
const VECTOR_INDEX_EXTS = new Set(['faiss', 'lance', 'usearch']);
|
|
67
|
+
|
|
68
|
+
const isVectorIndex = (base) =>
|
|
69
|
+
VECTOR_INDEX_BASENAMES.has(base.toLowerCase()) ||
|
|
70
|
+
VECTOR_INDEX_EXTS.has((base.slice(base.lastIndexOf('.') + 1) || '').toLowerCase());
|
|
71
|
+
|
|
72
|
+
const MAX_SOURCE_FILES = 600;
|
|
73
|
+
|
|
74
|
+
export function walkWorkspace(roots) {
|
|
75
|
+
const found = { mcp: [], rules: [], manifests: [], env: [], vector: [], source: [] };
|
|
76
|
+
const seenDir = new Set();
|
|
77
|
+
let budget = 40_000;
|
|
78
|
+
const maxDepth = 6;
|
|
79
|
+
|
|
80
|
+
const consider = (base, full, parentBase) => {
|
|
81
|
+
if (base === '.mcp.json' || base === 'mcp.json') found.mcp.push({ file: full, parentBase });
|
|
82
|
+
else if (base === 'settings.json' && (parentBase === '.gemini' || parentBase === '.zed'))
|
|
83
|
+
found.mcp.push({ file: full, parentBase });
|
|
84
|
+
else if (RULE_NAMES.has(base)) {
|
|
85
|
+
if (base === 'copilot-instructions.md' && parentBase !== '.github') return;
|
|
86
|
+
found.rules.push({ file: full, parentBase });
|
|
87
|
+
} else if (MANIFEST_NAMES.has(base)) found.manifests.push({ file: full, parentBase });
|
|
88
|
+
else if (isEnvFile(base)) found.env.push({ file: full, parentBase });
|
|
89
|
+
else if (isVectorIndex(base)) found.vector.push({ file: full, parentBase });
|
|
90
|
+
|
|
91
|
+
else if (found.source.length < MAX_SOURCE_FILES && isAiUsageScannable(base)) found.source.push({ file: full, parentBase });
|
|
92
|
+
};
|
|
93
|
+
|
|
94
|
+
for (const root of roots) {
|
|
95
|
+
const queue = [{ dir: root, depth: 0 }];
|
|
96
|
+
while (queue.length && budget > 0) {
|
|
97
|
+
const { dir, depth } = queue.shift();
|
|
98
|
+
let real;
|
|
99
|
+
try {
|
|
100
|
+
real = fs.realpathSync(dir);
|
|
101
|
+
} catch {
|
|
102
|
+
continue;
|
|
103
|
+
}
|
|
104
|
+
if (seenDir.has(real)) continue;
|
|
105
|
+
seenDir.add(real);
|
|
106
|
+
budget--;
|
|
107
|
+
let entries;
|
|
108
|
+
try {
|
|
109
|
+
entries = fs.readdirSync(dir, { withFileTypes: true });
|
|
110
|
+
} catch {
|
|
111
|
+
continue;
|
|
112
|
+
}
|
|
113
|
+
for (const e of entries) {
|
|
114
|
+
const full = path.join(dir, e.name);
|
|
115
|
+
if (e.isDirectory()) {
|
|
116
|
+
if (depth < maxDepth && !IGNORE_DIRS.has(e.name)) queue.push({ dir: full, depth: depth + 1 });
|
|
117
|
+
} else if (e.isFile()) {
|
|
118
|
+
consider(e.name, full, path.basename(dir));
|
|
119
|
+
}
|
|
120
|
+
}
|
|
121
|
+
}
|
|
122
|
+
}
|
|
123
|
+
return found;
|
|
124
|
+
}
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
export { discoverAiDependencies, discoverAiUsageInCode } from './discovery/ai-dependencies.mjs';
|
|
2
|
+
export { discoverAiTools } from './discovery/ai-tools.mjs';
|
|
3
|
+
export { discoverAll } from './discovery/all.mjs';
|
|
4
|
+
export { discoverCodingAgents } from './discovery/coding-agents.mjs';
|
|
5
|
+
export { discoverLocalRuntimes } from './discovery/local-runtimes.mjs';
|
|
6
|
+
export { discoverMcpClients } from './discovery/mcp-clients.mjs';
|
|
7
|
+
export { discoverMcpServers } from './discovery/mcp-servers.mjs';
|
|
8
|
+
export { discoverDotenvKeys, discoverModelKeys } from './discovery/model-keys.mjs';
|
|
9
|
+
export { discoverRulesFiles } from './discovery/rules-files.mjs';
|
|
10
|
+
export { discoverVectorStores } from './discovery/vector-stores.mjs';
|
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
import fs from 'node:fs';
|
|
2
|
+
import path from 'node:path';
|
|
3
|
+
import { spawn } from 'node:child_process';
|
|
4
|
+
|
|
5
|
+
const CHILD_STDIO = ['pipe', 'pipe', 'inherit'];
|
|
6
|
+
const UNQUOTABLE_FOR_CMD = /[%!]/;
|
|
7
|
+
const CMD_WRAPPER = /\.(cmd|bat)$/i;
|
|
8
|
+
|
|
9
|
+
function quoteForCmd(argument) {
|
|
10
|
+
return `"${String(argument).replace(/"/g, '""')}"`;
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
function resolveWindowsExecutable(command) {
|
|
14
|
+
if (command.includes('\\') || command.includes('/')) return fs.existsSync(command) ? command : null;
|
|
15
|
+
const extensions = (process.env.PATHEXT || '.COM;.EXE;.BAT;.CMD').split(';').filter(Boolean);
|
|
16
|
+
for (const directory of (process.env.PATH || '').split(path.delimiter).filter(Boolean)) {
|
|
17
|
+
for (const extension of ['', ...extensions]) {
|
|
18
|
+
const candidate = path.join(directory, command + extension);
|
|
19
|
+
try {
|
|
20
|
+
if (fs.statSync(candidate).isFile()) return candidate;
|
|
21
|
+
} catch {
|
|
22
|
+
continue;
|
|
23
|
+
}
|
|
24
|
+
}
|
|
25
|
+
}
|
|
26
|
+
return null;
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
function spawnThroughCmd(executable, args) {
|
|
30
|
+
const hostile = [executable, ...args].find((argument) => UNQUOTABLE_FOR_CMD.test(String(argument)));
|
|
31
|
+
if (hostile) {
|
|
32
|
+
throw new Error(
|
|
33
|
+
`the launch line contains a character that cannot be safely quoted for cmd.exe (${hostile}). `
|
|
34
|
+
+ 'Point the server at its executable directly instead of a .cmd wrapper.',
|
|
35
|
+
);
|
|
36
|
+
}
|
|
37
|
+
const shell = process.env.ComSpec || 'cmd.exe';
|
|
38
|
+
const line = [executable, ...args].map(quoteForCmd).join(' ');
|
|
39
|
+
return spawn(shell, ['/d', '/s', '/c', `"${line}"`], { stdio: CHILD_STDIO, windowsVerbatimArguments: true });
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
export function spawnGuardedServer(command, args) {
|
|
43
|
+
if (process.platform !== 'win32') return spawn(command, args, { stdio: CHILD_STDIO });
|
|
44
|
+
|
|
45
|
+
const executable = resolveWindowsExecutable(command);
|
|
46
|
+
if (!executable || !CMD_WRAPPER.test(executable)) {
|
|
47
|
+
return spawn(executable || command, args, { stdio: CHILD_STDIO });
|
|
48
|
+
}
|
|
49
|
+
return spawnThroughCmd(executable, args);
|
|
50
|
+
}
|
|
@@ -0,0 +1,75 @@
|
|
|
1
|
+
import fs from 'node:fs';
|
|
2
|
+
import os from 'node:os';
|
|
3
|
+
import path from 'node:path';
|
|
4
|
+
|
|
5
|
+
function serversOf(config) {
|
|
6
|
+
const servers = config?.mcpServers || config?.servers;
|
|
7
|
+
return servers && typeof servers === 'object' ? servers : null;
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
function isShimmed(entry, selfPath) {
|
|
11
|
+
const args = entry.args ?? [];
|
|
12
|
+
return args.includes('mcp-guard') && args.some((argument) => String(argument) === selfPath);
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
function skipReason(entry) {
|
|
16
|
+
if (!entry.command) return entry.url ? 'http - mediated by the gateway' : 'no launch command';
|
|
17
|
+
return null;
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
export function wrapMcpConfig(config, selfPath, execPath) {
|
|
21
|
+
const servers = serversOf(config);
|
|
22
|
+
if (!servers) return { wrapped: [], skipped: [] };
|
|
23
|
+
|
|
24
|
+
const wrapped = [];
|
|
25
|
+
const skipped = [];
|
|
26
|
+
for (const [name, entry] of Object.entries(servers)) {
|
|
27
|
+
if (!entry || typeof entry !== 'object') continue;
|
|
28
|
+
const reason = skipReason(entry);
|
|
29
|
+
if (reason) {
|
|
30
|
+
skipped.push({ name, why: reason });
|
|
31
|
+
continue;
|
|
32
|
+
}
|
|
33
|
+
if (isShimmed(entry, selfPath)) {
|
|
34
|
+
skipped.push({ name, why: 'already guarded' });
|
|
35
|
+
continue;
|
|
36
|
+
}
|
|
37
|
+
servers[name] = {
|
|
38
|
+
...entry,
|
|
39
|
+
command: execPath,
|
|
40
|
+
args: [selfPath, 'mcp-guard', '--name', name, '--', entry.command, ...(entry.args ?? [])],
|
|
41
|
+
};
|
|
42
|
+
wrapped.push(name);
|
|
43
|
+
}
|
|
44
|
+
return { wrapped, skipped };
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
export function unwrapMcpConfig(config, selfPath) {
|
|
48
|
+
const servers = serversOf(config);
|
|
49
|
+
if (!servers) return { restored: [] };
|
|
50
|
+
|
|
51
|
+
const restored = [];
|
|
52
|
+
for (const [name, entry] of Object.entries(servers)) {
|
|
53
|
+
if (!entry || typeof entry !== 'object' || !isShimmed(entry, selfPath)) continue;
|
|
54
|
+
const args = entry.args ?? [];
|
|
55
|
+
const separator = args.indexOf('--');
|
|
56
|
+
if (separator === -1 || !args[separator + 1]) continue;
|
|
57
|
+
servers[name] = { ...entry, command: args[separator + 1], args: args.slice(separator + 2) };
|
|
58
|
+
if (!servers[name].args.length) delete servers[name].args;
|
|
59
|
+
restored.push(name);
|
|
60
|
+
}
|
|
61
|
+
return { restored };
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
export function mcpConfigCandidates() {
|
|
65
|
+
const home = os.homedir();
|
|
66
|
+
const cwd = process.cwd();
|
|
67
|
+
return [
|
|
68
|
+
{ label: 'Claude Code (project)', file: path.join(cwd, '.mcp.json') },
|
|
69
|
+
{ label: 'Claude Code (global)', file: path.join(home, '.claude.json') },
|
|
70
|
+
{ label: 'Cursor (project)', file: path.join(cwd, '.cursor', 'mcp.json') },
|
|
71
|
+
{ label: 'Cursor (global)', file: path.join(home, '.cursor', 'mcp.json') },
|
|
72
|
+
{ label: 'Windsurf', file: path.join(home, '.codeium', 'windsurf', 'mcp_config.json') },
|
|
73
|
+
{ label: 'Gemini CLI', file: path.join(home, '.gemini', 'settings.json') },
|
|
74
|
+
].filter((candidate) => fs.existsSync(candidate.file));
|
|
75
|
+
}
|