@shomra/agent 0.3.28 → 0.3.30
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/package.json +1 -1
- package/src/agents/hook-command.mjs +1 -1
- package/src/artifacts/matchers.mjs +7 -0
- package/src/cli/flags.mjs +2 -2
- package/src/cli/help-sections.mjs +7 -0
- package/src/cli/help.mjs +1 -1
- package/src/commands/check.mjs +3 -11
- package/src/commands/gate.mjs +26 -4
- package/src/commands/git-hooks.mjs +2 -2
- package/src/commands/ledger.mjs +0 -1
- package/src/commands/mcp-add.mjs +2 -1
- package/src/commands/memory-scan.mjs +135 -47
- package/src/commands/pr.mjs +7 -10
- package/src/commands/provenance.mjs +8 -13
- package/src/commands/scan.mjs +7 -1
- package/src/commands/secrets.mjs +4 -5
- package/src/core/git-exec.mjs +79 -0
- package/src/core/yaml-lite.mjs +300 -0
- package/src/core/zip-lite.mjs +37 -0
- package/src/detect/local-redact.mjs +1 -3
- package/src/detect/sast/rules-config.mjs +1 -1
- package/src/detect/sast/rules-javascript.mjs +16 -3
- package/src/detect/sast/rules-python.mjs +8 -6
- package/src/detect/sast/scanner.mjs +1 -1
- package/src/detect/signals/agent-frameworks.mjs +231 -0
- package/src/detect/signals/agent-graph-surface.mjs +113 -0
- package/src/detect/signals/agentic-ci-surface.mjs +314 -0
- package/src/detect/signals/agentic-shim.mjs +82 -0
- package/src/detect/signals/artifacts.mjs +8 -36
- package/src/detect/signals/autonomy.mjs +9 -1
- package/src/detect/signals/chat-template.mjs +211 -0
- package/src/detect/signals/ci-workflow.mjs +169 -0
- package/src/detect/signals/credential-harvest.mjs +1 -1
- package/src/detect/signals/execution-hijack.mjs +4 -2
- package/src/detect/signals/gate.mjs +84 -11
- package/src/detect/signals/guardrail-shape.mjs +564 -0
- package/src/detect/signals/guardrail-surface.mjs +221 -0
- package/src/detect/signals/injection.mjs +8 -0
- package/src/detect/signals/inspect-shim.mjs +7 -0
- package/src/detect/signals/instruction-paths.mjs +60 -0
- package/src/detect/signals/manifests.mjs +302 -0
- package/src/detect/signals/masking.mjs +14 -1
- package/src/detect/signals/mcp-advisories.mjs +109 -0
- package/src/detect/signals/mcp-config.mjs +598 -0
- package/src/detect/signals/memory-directives.mjs +661 -0
- package/src/detect/signals/memory-locations.mjs +158 -0
- package/src/detect/signals/memory.mjs +56 -31
- package/src/detect/signals/model-config-rules.mjs +655 -0
- package/src/detect/signals/model-config.mjs +61 -0
- package/src/detect/signals/packages.mjs +2 -2
- package/src/detect/signals/prose-context.mjs +6 -9
- package/src/detect/signals/scan.mjs +4 -4
- package/src/detect/signals/secret-scanner.mjs +241 -0
- package/src/detect/signals/secrets.mjs +1 -48
- package/src/detect/signals/shell.mjs +10 -10
- package/src/gate/advisories.mjs +16 -0
- package/src/gate/batch.mjs +10 -0
- package/src/gate/environment.mjs +8 -53
- package/src/guard/artifact-paths.mjs +107 -0
- package/src/guard/classify.mjs +165 -7
- package/src/guard/command-resolve.mjs +35 -5
- package/src/guard/memory-write.mjs +218 -0
- package/src/guard/prompt-guard.mjs +0 -1
- package/src/guard/tool-guard.mjs +52 -77
- package/src/inventory/agent-posture.mjs +236 -57
- package/src/inventory/artifacts/classify.mjs +10 -1
- package/src/inventory/artifacts/discover.mjs +113 -3
- package/src/inventory/artifacts/extensions.mjs +70 -0
- package/src/inventory/artifacts/hook-scripts.mjs +128 -0
- package/src/inventory/artifacts/limits.mjs +1 -1
- package/src/inventory/artifacts/plugins.mjs +105 -0
- package/src/inventory/artifacts/roots.mjs +40 -0
- package/src/inventory/discovery/ai-dependencies.mjs +39 -12
- package/src/inventory/discovery/all.mjs +4 -0
- package/src/inventory/discovery/cloud-clis.mjs +472 -0
- package/src/inventory/discovery/coding-agents.mjs +19 -4
- package/src/inventory/discovery/mcp-clients.mjs +16 -10
- package/src/inventory/discovery/mcp-servers.mjs +125 -35
- package/src/inventory/discovery/mcp-stores.mjs +207 -0
- package/src/inventory/env-redirect.mjs +148 -0
- package/src/inventory/grant-extract.mjs +463 -0
- package/src/inventory/project-roots.mjs +108 -0
- package/src/inventory/vscode-state.mjs +153 -0
- package/src/mcp/server-tools.mjs +1 -1
|
@@ -1,76 +1,166 @@
|
|
|
1
|
+
import fs from 'node:fs';
|
|
1
2
|
import path from 'node:path';
|
|
2
|
-
import {
|
|
3
|
+
import { readText } from './fs-read.mjs';
|
|
3
4
|
import { redactEnv } from './model-keys.mjs';
|
|
4
|
-
import { APPDATA, HOME, PLAT, vscodeUserDir } from './platform.mjs';
|
|
5
|
+
import { APPDATA, HOME, LOCALAPPDATA, PLAT, vscodeUserDir } from './platform.mjs';
|
|
5
6
|
import { walkWorkspace } from './workspace.mjs';
|
|
7
|
+
import { parseConfigDoc, readMcpServers } from '../../detect/signals/mcp-config.mjs';
|
|
8
|
+
import { jetbrainsOptionFiles, readClaudeExtensions, readJetbrainsMcpServers, readSqliteMcpServers, warpDatabases } from './mcp-stores.mjs';
|
|
6
9
|
|
|
7
10
|
export function vendorFromPath(file) {
|
|
8
11
|
if (/[\\/]\.cursor[\\/]/.test(file)) return 'cursor';
|
|
9
12
|
if (/[\\/]\.vscode[\\/]/.test(file)) return 'vscode';
|
|
10
13
|
if (/[\\/]\.gemini[\\/]/.test(file)) return 'gemini';
|
|
11
14
|
if (/[\\/]\.zed[\\/]/.test(file)) return 'zed';
|
|
15
|
+
if (/[\\/]\.kiro[\\/]/.test(file)) return 'kiro';
|
|
16
|
+
if (/[\\/]\.amazonq[\\/]/.test(file)) return 'amazonq';
|
|
17
|
+
if (/[\\/]\.roo[\\/]/.test(file)) return 'roo';
|
|
18
|
+
if (/[\\/]\.windsurf[\\/]/.test(file)) return 'windsurf';
|
|
19
|
+
if (/[\\/]\.codex[\\/]/.test(file)) return 'codex';
|
|
12
20
|
return 'project';
|
|
13
21
|
}
|
|
14
22
|
|
|
23
|
+
function appSupport(...rel) {
|
|
24
|
+
if (PLAT === 'darwin') return path.join(HOME, 'Library', 'Application Support', ...rel);
|
|
25
|
+
if (PLAT === 'win32') return path.join(APPDATA, ...rel);
|
|
26
|
+
return path.join(HOME, '.config', ...rel);
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
function claudeMsixConfigs() {
|
|
30
|
+
if (PLAT !== 'win32') return [];
|
|
31
|
+
try {
|
|
32
|
+
return fs.readdirSync(path.join(LOCALAPPDATA, 'Packages'))
|
|
33
|
+
.filter((d) => /^(?:Claude|AnthropicPBC\.Claude)_/i.test(d))
|
|
34
|
+
.map((d) => path.join(LOCALAPPDATA, 'Packages', d, 'LocalCache', 'Roaming', 'Claude', 'claude_desktop_config.json'));
|
|
35
|
+
} catch {
|
|
36
|
+
return [];
|
|
37
|
+
}
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
function managedClaudeMcp() {
|
|
41
|
+
if (PLAT === 'darwin') return path.join('/Library', 'Application Support', 'ClaudeCode', 'managed-mcp.json');
|
|
42
|
+
if (PLAT === 'win32') return path.join(process.env.ProgramData || 'C:\\ProgramData', 'ClaudeCode', 'managed-mcp.json');
|
|
43
|
+
return path.join('/etc', 'claude-code', 'managed-mcp.json');
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
|
|
15
47
|
function globalMcpCandidates() {
|
|
16
48
|
const c = [];
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
49
|
+
c.push({ vendor: 'claude', file: appSupport('Claude', 'claude_desktop_config.json') });
|
|
50
|
+
for (const file of claudeMsixConfigs()) c.push({ vendor: 'claude', file });
|
|
51
|
+
c.push({ vendor: 'claude-code', file: path.join(HOME, '.claude.json') }); // user + per-project (projects.*.mcpServers)
|
|
52
|
+
c.push({ vendor: 'claude-code', file: managedClaudeMcp() });
|
|
20
53
|
c.push({ vendor: 'cursor', file: path.join(HOME, '.cursor', 'mcp.json') });
|
|
21
54
|
c.push({ vendor: 'windsurf', file: path.join(HOME, '.codeium', 'windsurf', 'mcp_config.json') });
|
|
22
55
|
c.push({ vendor: 'continue', file: path.join(HOME, '.continue', 'config.json') });
|
|
23
|
-
c.push({ vendor: '
|
|
56
|
+
c.push({ vendor: 'continue', file: path.join(HOME, '.continue', 'config.yaml') });
|
|
24
57
|
c.push({ vendor: 'gemini', file: path.join(HOME, '.gemini', 'settings.json') });
|
|
25
|
-
c.push({ vendor: '
|
|
26
|
-
c.push({ vendor: '
|
|
58
|
+
c.push({ vendor: 'qwen-code', file: path.join(HOME, '.qwen', 'settings.json') });
|
|
59
|
+
c.push({ vendor: 'codex', file: path.join(HOME, '.codex', 'config.toml') });
|
|
60
|
+
c.push({ vendor: 'copilot', file: path.join(HOME, '.copilot', 'mcp-config.json') });
|
|
61
|
+
c.push({ vendor: 'kiro', file: path.join(HOME, '.kiro', 'settings', 'mcp.json') });
|
|
62
|
+
c.push({ vendor: 'amazonq', file: path.join(HOME, '.aws', 'amazonq', 'mcp.json') });
|
|
63
|
+
c.push({ vendor: 'opencode', file: path.join(HOME, '.config', 'opencode', 'opencode.json') });
|
|
64
|
+
c.push({ vendor: 'crush', file: path.join(HOME, '.config', 'crush', 'crush.json') });
|
|
65
|
+
c.push({ vendor: 'goose', file: PLAT === 'win32' ? path.join(APPDATA, 'Block', 'goose', 'config', 'config.yaml') : path.join(HOME, '.config', 'goose', 'config.yaml') });
|
|
66
|
+
c.push({ vendor: 'lmstudio', file: path.join(HOME, '.lmstudio', 'mcp.json') });
|
|
67
|
+
c.push({ vendor: 'junie', file: path.join(HOME, '.junie', 'mcp', 'mcp.json') });
|
|
68
|
+
c.push({ vendor: 'visualstudio', file: path.join(HOME, '.mcp.json') }); // Visual Studio's user-scope config
|
|
69
|
+
c.push({ vendor: 'copilot', file: PLAT === 'win32' ? path.join(LOCALAPPDATA, 'github-copilot', 'intellij', 'mcp.json') : path.join(HOME, '.config', 'github-copilot', 'intellij', 'mcp.json') });
|
|
70
|
+
for (const file of warpDatabases()) c.push({ vendor: 'warp', file, store: 'sqlite' });
|
|
71
|
+
for (const file of jetbrainsOptionFiles()) c.push({ vendor: 'jetbrains', file, store: 'jetbrains-xml' });
|
|
27
72
|
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
73
|
+
const FORKS = [
|
|
74
|
+
{ variant: 'Code', vendor: 'vscode' },
|
|
75
|
+
{ variant: 'Code - Insiders', vendor: 'vscode' },
|
|
76
|
+
{ variant: 'VSCodium', vendor: 'vscode' },
|
|
77
|
+
{ variant: 'Cursor', vendor: 'cursor' },
|
|
78
|
+
{ variant: 'Windsurf', vendor: 'windsurf' },
|
|
79
|
+
];
|
|
80
|
+
for (const { variant, vendor } of FORKS) {
|
|
81
|
+
const user = vscodeUserDir(variant);
|
|
82
|
+
c.push({ vendor, file: path.join(user, 'mcp.json') });
|
|
83
|
+
c.push({ vendor, file: path.join(user, 'settings.json') });
|
|
84
|
+
c.push({ vendor: 'cline', file: path.join(user, 'globalStorage', 'saoudrizwan.claude-dev', 'settings', 'cline_mcp_settings.json') });
|
|
85
|
+
c.push({ vendor: 'roo', file: path.join(user, 'globalStorage', 'rooveterinaryinc.roo-cline', 'settings', 'mcp_settings.json') });
|
|
86
|
+
}
|
|
87
|
+
c.push({ vendor: 'zed', file: PLAT === 'darwin' ? path.join(HOME, 'Library', 'Application Support', 'Zed', 'settings.json') : PLAT === 'win32' ? path.join(APPDATA, 'Zed', 'settings.json') : path.join(HOME, '.config', 'zed', 'settings.json') });
|
|
32
88
|
return c;
|
|
33
89
|
}
|
|
34
90
|
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
json.
|
|
39
|
-
json.
|
|
40
|
-
json['mcp.
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
91
|
+
/** Project-scope configs in the directories being swept (the workspace walker finds `.mcp.json` / `mcp.json` itself). */
|
|
92
|
+
function projectMcpCandidates(roots) {
|
|
93
|
+
const rels = [
|
|
94
|
+
['.cursor', 'mcp.json'], ['.vscode', 'mcp.json'], ['.gemini', 'settings.json'], ['.kiro', 'settings', 'mcp.json'],
|
|
95
|
+
['.amazonq', 'mcp.json'], ['.roo', 'mcp.json'], ['.windsurf', 'mcp.json'], ['.codex', 'config.toml'],
|
|
96
|
+
['.junie', 'mcp', 'mcp.json'], ['opencode.json'], ['.zed', 'settings.json'], ['.vs', 'mcp.json'],
|
|
97
|
+
];
|
|
98
|
+
return roots.flatMap((root) => rels.map((rel) => ({ vendor: vendorFromPath(path.join(root, ...rel)), file: path.join(root, ...rel) })));
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
function readServers(file, store) {
|
|
102
|
+
if (store === 'sqlite') {
|
|
103
|
+
const { servers, state } = readSqliteMcpServers(file);
|
|
104
|
+
if (state !== 'read') unreadStores.push({ file, state });
|
|
105
|
+
return servers.length ? servers : null;
|
|
106
|
+
}
|
|
107
|
+
if (store === 'jetbrains-xml') {
|
|
108
|
+
const servers = readJetbrainsMcpServers(file);
|
|
109
|
+
return servers.length ? servers : null;
|
|
110
|
+
}
|
|
111
|
+
const text = readText(file, 1_000_000);
|
|
112
|
+
if (!text) return null;
|
|
113
|
+
const doc = parseConfigDoc(text, file);
|
|
114
|
+
return doc ? readMcpServers(doc) : null;
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
let unreadStores = [];
|
|
118
|
+
export function unreadMcpStores() {
|
|
119
|
+
return unreadStores.slice();
|
|
45
120
|
}
|
|
46
121
|
|
|
47
122
|
export function discoverMcpServers(roots = [process.cwd()], files = null) {
|
|
48
123
|
const walk = files || walkWorkspace(roots);
|
|
49
124
|
const candidates = [
|
|
50
125
|
...globalMcpCandidates(),
|
|
126
|
+
...projectMcpCandidates(roots),
|
|
51
127
|
...walk.mcp.map(({ file }) => ({ vendor: vendorFromPath(file), file })),
|
|
52
128
|
];
|
|
129
|
+
unreadStores = [];
|
|
53
130
|
const assets = [];
|
|
54
131
|
const seen = new Set();
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
const
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
132
|
+
const seenFile = new Set();
|
|
133
|
+
const sources = [];
|
|
134
|
+
for (const { vendor, file, store } of candidates) {
|
|
135
|
+
const key0 = path.resolve(file).toLowerCase();
|
|
136
|
+
if (seenFile.has(key0)) continue;
|
|
137
|
+
seenFile.add(key0);
|
|
138
|
+
const servers = readServers(file, store);
|
|
139
|
+
if (servers) sources.push({ vendor, file, servers });
|
|
140
|
+
}
|
|
141
|
+
for (const s of readClaudeExtensions()) sources.push({ vendor: 'claude', file: s.file, servers: [s] });
|
|
142
|
+
|
|
143
|
+
for (const { vendor, file, servers } of sources) {
|
|
144
|
+
for (const s of servers) {
|
|
145
|
+
const command = [s.command, ...(s.args ?? [])].filter(Boolean).join(' ');
|
|
146
|
+
const identifier = s.url || command || s.name;
|
|
147
|
+
const key = `${s.name}:${identifier}`;
|
|
64
148
|
if (seen.has(key)) continue;
|
|
65
149
|
seen.add(key);
|
|
66
150
|
assets.push({
|
|
67
151
|
type: 'MCP_SERVER',
|
|
68
|
-
name,
|
|
152
|
+
name: s.name,
|
|
69
153
|
identifier,
|
|
70
154
|
vendor,
|
|
71
|
-
metadata: {
|
|
72
|
-
|
|
73
|
-
|
|
155
|
+
metadata: {
|
|
156
|
+
command,
|
|
157
|
+
url: s.url || null,
|
|
158
|
+
configFile: file,
|
|
159
|
+
env: redactEnv(s.env),
|
|
160
|
+
...(s.scope ? { projectScope: s.scope } : {}),
|
|
161
|
+
...(s.disabled ? { disabled: true } : {}),
|
|
162
|
+
},
|
|
163
|
+
content: JSON.stringify({ command, url: s.url, env: s.env || {}, server: s }),
|
|
74
164
|
});
|
|
75
165
|
}
|
|
76
166
|
}
|
|
@@ -0,0 +1,207 @@
|
|
|
1
|
+
import fs from 'node:fs';
|
|
2
|
+
import os from 'node:os';
|
|
3
|
+
import path from 'node:path';
|
|
4
|
+
import { createRequire } from 'node:module';
|
|
5
|
+
import { extensionBundleServer, normalizeServerEntry, readMcpServers } from '../../detect/signals/mcp-config.mjs';
|
|
6
|
+
import { APPDATA, HOME, LOCALAPPDATA, PLAT } from './platform.mjs';
|
|
7
|
+
|
|
8
|
+
const require = createRequire(import.meta.url);
|
|
9
|
+
const MAX_DB_BYTES = 256 * 1024 * 1024;
|
|
10
|
+
const MAX_ROWS = 400;
|
|
11
|
+
const MAX_XML_BYTES = 2 * 1024 * 1024;
|
|
12
|
+
const MAX_EXTENSIONS = 200;
|
|
13
|
+
|
|
14
|
+
let sqlite;
|
|
15
|
+
function loadSqlite() {
|
|
16
|
+
if (sqlite !== undefined) return sqlite;
|
|
17
|
+
const original = process.emitWarning;
|
|
18
|
+
process.emitWarning = function (warning, ...rest) {
|
|
19
|
+
if (/sqlite/i.test(String(warning?.message ?? warning))) return;
|
|
20
|
+
return original.call(process, warning, ...rest);
|
|
21
|
+
};
|
|
22
|
+
try { sqlite = require('node:sqlite'); } catch { sqlite = null; } finally { process.emitWarning = original; }
|
|
23
|
+
return sqlite;
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
const isObj = (v) => !!v && typeof v === 'object' && !Array.isArray(v);
|
|
27
|
+
const looksLikeServer = (v) => isObj(v) && (typeof v.command === 'string' || Array.isArray(v.command) || typeof v.url === 'string' || typeof v.serverUrl === 'string' || typeof v.httpUrl === 'string');
|
|
28
|
+
|
|
29
|
+
export function serversInValue(value, fallbackName) {
|
|
30
|
+
if (value == null) return [];
|
|
31
|
+
let v = value;
|
|
32
|
+
if (typeof v === 'string' || Buffer.isBuffer(v) || v instanceof Uint8Array) {
|
|
33
|
+
const text = Buffer.from(v).toString('utf8').trim();
|
|
34
|
+
if (!/^[[{]/.test(text)) return [];
|
|
35
|
+
try { v = JSON.parse(text); } catch { return []; }
|
|
36
|
+
}
|
|
37
|
+
const mapped = readMcpServers(v);
|
|
38
|
+
if (mapped) return mapped;
|
|
39
|
+
if (looksLikeServer(v)) return [normalizeServerEntry(String(v.name ?? v.title ?? fallbackName ?? 'server'), v)];
|
|
40
|
+
if (Array.isArray(v)) return v.filter(looksLikeServer).slice(0, 100).map((s, i) => normalizeServerEntry(String(s.name ?? `${fallbackName ?? 'server'}-${i + 1}`), s));
|
|
41
|
+
return [];
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
/* ─── Warp ─────────────────────────────────────────────────────────────── */
|
|
45
|
+
|
|
46
|
+
export function warpDatabases() {
|
|
47
|
+
const out = [];
|
|
48
|
+
if (PLAT === 'darwin') {
|
|
49
|
+
const group = path.join(HOME, 'Library', 'Group Containers');
|
|
50
|
+
try {
|
|
51
|
+
for (const g of fs.readdirSync(group).filter((d) => /\.dev\.warp$/i.test(d))) {
|
|
52
|
+
const base = path.join(group, g, 'Library', 'Application Support');
|
|
53
|
+
for (const app of safeList(base).filter((d) => /^dev\.warp\.Warp/i.test(d))) out.push(path.join(base, app, 'warp.sqlite'));
|
|
54
|
+
}
|
|
55
|
+
} catch { /* no Warp */ }
|
|
56
|
+
} else if (PLAT === 'win32') {
|
|
57
|
+
for (const d of ['warp', 'Warp']) out.push(path.join(LOCALAPPDATA, d, 'Warp', 'data', 'warp.sqlite'));
|
|
58
|
+
} else {
|
|
59
|
+
for (const d of ['warp-terminal', 'warp-terminal-preview']) {
|
|
60
|
+
out.push(path.join(HOME, '.local', 'state', d, 'warp.sqlite'));
|
|
61
|
+
out.push(path.join(HOME, '.local', 'share', d, 'warp.sqlite'));
|
|
62
|
+
}
|
|
63
|
+
}
|
|
64
|
+
return [...new Set(out)].filter((f) => { try { return fs.statSync(f).isFile(); } catch { return false; } });
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
export function readSqliteMcpServers(file) {
|
|
68
|
+
const lib = loadSqlite();
|
|
69
|
+
if (!lib?.DatabaseSync) return { servers: [], state: 'no-sqlite' };
|
|
70
|
+
let st;
|
|
71
|
+
try { st = fs.statSync(file); } catch { return { servers: [], state: 'unreadable' }; }
|
|
72
|
+
if (st.size > MAX_DB_BYTES) return { servers: [], state: 'oversized' };
|
|
73
|
+
const dir = fs.mkdtempSync(path.join(os.tmpdir(), 'shomra-mcpdb-'));
|
|
74
|
+
const copy = path.join(dir, 'store.sqlite');
|
|
75
|
+
let db;
|
|
76
|
+
try {
|
|
77
|
+
fs.copyFileSync(file, copy);
|
|
78
|
+
for (const s of ['-wal', '-shm']) if (fs.existsSync(file + s)) fs.copyFileSync(file + s, copy + s);
|
|
79
|
+
db = new lib.DatabaseSync(copy, { readOnly: true });
|
|
80
|
+
const tables = db.prepare("SELECT name FROM sqlite_master WHERE type = 'table'").all().map((r) => String(r.name));
|
|
81
|
+
const servers = [];
|
|
82
|
+
for (const t of tables.filter((n) => /mcp/i.test(n)).slice(0, 20)) {
|
|
83
|
+
const quoted = `"${t.replace(/"/g, '""')}"`;
|
|
84
|
+
const cols = db.prepare(`PRAGMA table_info(${quoted})`).all().map((c) => String(c.name));
|
|
85
|
+
const nameCol = cols.find((c) => /^(?:name|title|display_name|server_name)$/i.test(c));
|
|
86
|
+
for (const row of db.prepare(`SELECT * FROM ${quoted} LIMIT ${MAX_ROWS}`).all()) {
|
|
87
|
+
const fallback = nameCol ? String(row[nameCol] ?? '') : undefined;
|
|
88
|
+
if (cols.some((c) => /^(?:command|url)$/i.test(c))) {
|
|
89
|
+
const entry = {};
|
|
90
|
+
for (const c of cols) {
|
|
91
|
+
const v = row[c];
|
|
92
|
+
if (v == null) continue;
|
|
93
|
+
if (/^(?:args|env|environment|headers)$/i.test(c) && typeof v === 'string' && /^[[{]/.test(v.trim())) { try { entry[c.toLowerCase()] = JSON.parse(v); continue; } catch { /* keep raw */ } }
|
|
94
|
+
entry[c.toLowerCase()] = v;
|
|
95
|
+
}
|
|
96
|
+
if (looksLikeServer(entry)) servers.push(normalizeServerEntry(String(entry.name ?? fallback ?? t), entry));
|
|
97
|
+
}
|
|
98
|
+
for (const c of cols) for (const s of serversInValue(row[c], fallback)) servers.push(s);
|
|
99
|
+
}
|
|
100
|
+
}
|
|
101
|
+
return { servers: dedupe(servers), state: 'read' };
|
|
102
|
+
} catch {
|
|
103
|
+
return { servers: [], state: 'unreadable' };
|
|
104
|
+
} finally {
|
|
105
|
+
try { db?.close(); } catch { /* ignore */ }
|
|
106
|
+
fs.rmSync(dir, { recursive: true, force: true });
|
|
107
|
+
}
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
/* ─── JetBrains ────────────────────────────────────────────────────────── */
|
|
111
|
+
|
|
112
|
+
export function jetbrainsOptionFiles() {
|
|
113
|
+
const roots = PLAT === 'win32' ? [path.join(APPDATA, 'JetBrains')] : PLAT === 'darwin' ? [path.join(HOME, 'Library', 'Application Support', 'JetBrains')] : [path.join(HOME, '.config', 'JetBrains')];
|
|
114
|
+
const out = [];
|
|
115
|
+
for (const root of roots) {
|
|
116
|
+
for (const product of safeList(root)) {
|
|
117
|
+
const options = path.join(root, product, 'options');
|
|
118
|
+
for (const f of safeList(options)) if (/mcp|llm|ai[-_]?assistant|junie/i.test(f) && /\.xml$/i.test(f)) out.push(path.join(options, f));
|
|
119
|
+
}
|
|
120
|
+
}
|
|
121
|
+
return out.slice(0, 200);
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
const unescapeXml = (s) => s.replace(/"/g, '"').replace(/'/g, "'").replace(/</g, '<').replace(/>/g, '>').replace(/ /g, '\n').replace(/ /g, '\r').replace(/&#(\d+);/g, (_, n) => String.fromCharCode(Number(n))).replace(/&/g, '&');
|
|
125
|
+
|
|
126
|
+
function jsonBlobs(text) {
|
|
127
|
+
const out = [];
|
|
128
|
+
for (let i = text.indexOf('{'); i !== -1 && out.length < 50; i = text.indexOf('{', i + 1)) {
|
|
129
|
+
let depth = 0, q = false, esc = false;
|
|
130
|
+
for (let j = i; j < text.length && j - i < 500_000; j++) {
|
|
131
|
+
const c = text[j];
|
|
132
|
+
if (q) { if (esc) esc = false; else if (c === '\\') esc = true; else if (c === '"') q = false; continue; }
|
|
133
|
+
if (c === '"') q = true;
|
|
134
|
+
else if (c === '{') depth++;
|
|
135
|
+
else if (c === '}' && --depth === 0) {
|
|
136
|
+
try { out.push(JSON.parse(text.slice(i, j + 1))); i = j; } catch { /* not JSON, keep scanning */ }
|
|
137
|
+
break;
|
|
138
|
+
}
|
|
139
|
+
}
|
|
140
|
+
}
|
|
141
|
+
return out;
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
export function readJetbrainsMcpServers(file) {
|
|
145
|
+
let xml;
|
|
146
|
+
try {
|
|
147
|
+
if (fs.statSync(file).size > MAX_XML_BYTES) return [];
|
|
148
|
+
xml = fs.readFileSync(file, 'utf8');
|
|
149
|
+
} catch { return []; }
|
|
150
|
+
const servers = [];
|
|
151
|
+
for (const blob of jsonBlobs(unescapeXml(xml))) servers.push(...serversInValue(blob));
|
|
152
|
+
const attrsOf = (s) => Object.fromEntries([...s.matchAll(/([\w:-]+)="([^"]*)"/g)].map(([, k, v]) => [k.toLowerCase(), unescapeXml(v)]));
|
|
153
|
+
const records = [];
|
|
154
|
+
for (const m of xml.matchAll(/<[\w:-]+\s[^<>]*\b(?:command|url)="[^"]*"[^<>]*>/g)) records.push(attrsOf(m[0]));
|
|
155
|
+
for (const m of xml.matchAll(/<[\w:-]+(\s[^<>]*)?>((?:\s*<option\s[^<>]*\/>)+)\s*<\/[\w:-]+>/g)) {
|
|
156
|
+
records.push({ ...Object.fromEntries([...m[2].matchAll(/<option\s+name="([^"]+)"\s+value="([^"]*)"/g)].map(([, k, v]) => [k.toLowerCase(), unescapeXml(v)])), ...attrsOf(m[1] ?? '') });
|
|
157
|
+
}
|
|
158
|
+
for (const e of records.slice(0, 200)) {
|
|
159
|
+
if (!(typeof e.command === 'string' || typeof e.url === 'string')) continue;
|
|
160
|
+
if (typeof e.args === 'string') e.args = /^\[/.test(e.args.trim()) ? safeJson(e.args) ?? e.args.split(/\s+/) : e.args.split(/\s+/).filter(Boolean);
|
|
161
|
+
if (typeof e.env === 'string' && /^\{/.test(e.env.trim())) e.env = safeJson(e.env);
|
|
162
|
+
servers.push(normalizeServerEntry(String(e.name ?? e.id ?? path.basename(file, '.xml')), e));
|
|
163
|
+
}
|
|
164
|
+
return dedupe(servers);
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
/* ─── Claude Desktop extensions ────────────────────────────────────────── */
|
|
168
|
+
|
|
169
|
+
function claudeDataDir() {
|
|
170
|
+
if (PLAT === 'darwin') return path.join(HOME, 'Library', 'Application Support', 'Claude');
|
|
171
|
+
if (PLAT === 'win32') return path.join(APPDATA, 'Claude');
|
|
172
|
+
return path.join(HOME, '.config', 'Claude');
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
export function readClaudeExtensions(dataDir = claudeDataDir()) {
|
|
176
|
+
const root = path.join(dataDir, 'Claude Extensions');
|
|
177
|
+
const settingsDir = path.join(dataDir, 'Claude Extensions Settings');
|
|
178
|
+
const out = [];
|
|
179
|
+
for (const id of safeList(root).slice(0, MAX_EXTENSIONS)) {
|
|
180
|
+
const manifestFile = path.join(root, id, 'manifest.json');
|
|
181
|
+
let json;
|
|
182
|
+
try { json = JSON.parse(fs.readFileSync(manifestFile, 'utf8')); } catch { continue; }
|
|
183
|
+
const s = extensionBundleServer(json);
|
|
184
|
+
if (!s) continue;
|
|
185
|
+
let settings = null;
|
|
186
|
+
try { settings = JSON.parse(fs.readFileSync(path.join(settingsDir, `${id}.json`), 'utf8')); } catch { /* none */ }
|
|
187
|
+
const resolve = (v) => (typeof v === 'string' ? v.replace(/\$\{__dirname\}/g, path.join(root, id)) : v);
|
|
188
|
+
out.push({ ...s, command: resolve(s.command), args: (s.args ?? []).map(resolve), disabled: s.disabled || settings?.isEnabled === false, file: manifestFile });
|
|
189
|
+
}
|
|
190
|
+
return out;
|
|
191
|
+
}
|
|
192
|
+
|
|
193
|
+
function safeList(dir) {
|
|
194
|
+
try { return fs.readdirSync(dir); } catch { return []; }
|
|
195
|
+
}
|
|
196
|
+
function safeJson(s) {
|
|
197
|
+
try { return JSON.parse(s); } catch { return null; }
|
|
198
|
+
}
|
|
199
|
+
function dedupe(servers) {
|
|
200
|
+
const seen = new Set();
|
|
201
|
+
return servers.filter((s) => {
|
|
202
|
+
const k = `${s.name}|${s.command ?? ''}|${(s.args ?? []).join(' ')}|${s.url ?? ''}`;
|
|
203
|
+
if (seen.has(k)) return false;
|
|
204
|
+
seen.add(k);
|
|
205
|
+
return true;
|
|
206
|
+
});
|
|
207
|
+
}
|
|
@@ -0,0 +1,148 @@
|
|
|
1
|
+
import fs from 'node:fs';
|
|
2
|
+
import path from 'node:path';
|
|
3
|
+
import os from 'node:os';
|
|
4
|
+
|
|
5
|
+
const HOME = os.homedir();
|
|
6
|
+
const PLAT = process.platform;
|
|
7
|
+
const MAX_BYTES = 256 * 1024;
|
|
8
|
+
const MAX_ITEMS = 10;
|
|
9
|
+
|
|
10
|
+
const ENDPOINT_KEY =
|
|
11
|
+
/^(?:ANTHROPIC_(?:BASE_URL|BEDROCK_BASE_URL|VERTEX_BASE_URL|FOUNDRY_BASE_URL)|OPENAI_(?:BASE_URL|API_BASE(?:_URL)?)|AZURE_OPENAI_ENDPOINT|GOOGLE_GEMINI_BASE_URL|GEMINI_(?:API_)?BASE_URL|CODEX_BASE_URL|LITELLM_(?:BASE_URL|PROXY_URL))$/i;
|
|
12
|
+
const PROXY_KEY = /^(?:HTTPS?_PROXY|ALL_PROXY)$/i;
|
|
13
|
+
const TLS_OFF = [
|
|
14
|
+
[/^NODE_TLS_REJECT_UNAUTHORIZED$/i, (v) => v.trim() === '0'],
|
|
15
|
+
[/^PYTHONHTTPSVERIFY$/i, (v) => v.trim() === '0'],
|
|
16
|
+
[/^GIT_SSL_NO_VERIFY$/i, (v) => /^(?:1|true|yes)$/i.test(v.trim())],
|
|
17
|
+
];
|
|
18
|
+
|
|
19
|
+
const PROCESS_KEYS = {
|
|
20
|
+
'claude-code': /^ANTHROPIC_/i,
|
|
21
|
+
codex: /^(?:OPENAI_|CODEX_)/i,
|
|
22
|
+
gemini: /^(?:GEMINI_|GOOGLE_GEMINI_)/i,
|
|
23
|
+
qwen: /^(?:OPENAI_|QWEN_)/i,
|
|
24
|
+
'qwen-code': /^(?:OPENAI_|QWEN_)/i,
|
|
25
|
+
};
|
|
26
|
+
|
|
27
|
+
function managedSettingsPath() {
|
|
28
|
+
if (PLAT === 'win32') return path.join(process.env.ProgramData || 'C:\\ProgramData', 'ClaudeCode', 'managed-settings.json');
|
|
29
|
+
if (PLAT === 'darwin') return '/Library/Application Support/ClaudeCode/managed-settings.json';
|
|
30
|
+
return '/etc/claude-code/managed-settings.json';
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
function sourcesFor(vendor, cwd) {
|
|
34
|
+
switch (vendor) {
|
|
35
|
+
case 'claude-code':
|
|
36
|
+
return [
|
|
37
|
+
{ file: path.join(HOME, '.claude', 'settings.json'), scope: 'user', shape: 'env-json' },
|
|
38
|
+
{ file: path.join(HOME, '.claude', 'settings.local.json'), scope: 'user', shape: 'env-json' },
|
|
39
|
+
{ file: path.join(cwd, '.claude', 'settings.json'), scope: 'project', shape: 'env-json' },
|
|
40
|
+
{ file: path.join(cwd, '.claude', 'settings.local.json'), scope: 'project', shape: 'env-json' },
|
|
41
|
+
{ file: managedSettingsPath(), scope: 'managed', shape: 'env-json' },
|
|
42
|
+
];
|
|
43
|
+
case 'gemini':
|
|
44
|
+
case 'qwen':
|
|
45
|
+
case 'qwen-code': {
|
|
46
|
+
const dir = vendor === 'gemini' ? '.gemini' : '.qwen';
|
|
47
|
+
return [
|
|
48
|
+
{ file: path.join(HOME, dir, '.env'), scope: 'user', shape: 'dotenv' },
|
|
49
|
+
{ file: path.join(cwd, dir, '.env'), scope: 'project', shape: 'dotenv' },
|
|
50
|
+
{ file: path.join(HOME, dir, 'settings.json'), scope: 'user', shape: 'gemini-settings' },
|
|
51
|
+
{ file: path.join(cwd, dir, 'settings.json'), scope: 'project', shape: 'gemini-settings' },
|
|
52
|
+
];
|
|
53
|
+
}
|
|
54
|
+
case 'codex':
|
|
55
|
+
return [
|
|
56
|
+
{ file: path.join(HOME, '.codex', 'config.toml'), scope: 'user', shape: 'codex-toml' },
|
|
57
|
+
{ file: path.join(cwd, '.codex', 'config.toml'), scope: 'project', shape: 'codex-toml' },
|
|
58
|
+
];
|
|
59
|
+
default:
|
|
60
|
+
return [];
|
|
61
|
+
}
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
function readSmall(file) {
|
|
65
|
+
try {
|
|
66
|
+
const st = fs.statSync(file);
|
|
67
|
+
if (!st.isFile() || st.size > MAX_BYTES) return null;
|
|
68
|
+
return fs.readFileSync(file, 'utf8');
|
|
69
|
+
} catch {
|
|
70
|
+
return null;
|
|
71
|
+
}
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
function parseJsonc(raw) {
|
|
75
|
+
try {
|
|
76
|
+
return JSON.parse(raw.replace(/\/\*[\s\S]*?\*\//g, '').replace(/(^|[^:"\\])\/\/.*$/gm, '$1'));
|
|
77
|
+
} catch {
|
|
78
|
+
return null;
|
|
79
|
+
}
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
function pairsOf(shape, raw) {
|
|
83
|
+
const pairs = [];
|
|
84
|
+
if (shape === 'dotenv') {
|
|
85
|
+
for (const line of raw.split(/\r?\n/)) {
|
|
86
|
+
const m = /^\s*(?:export\s+)?([A-Za-z_][A-Za-z0-9_]*)\s*=\s*(.*?)\s*$/.exec(line);
|
|
87
|
+
if (m) pairs.push([m[1], m[2].replace(/^(['"])(.*)\1$/, '$2')]);
|
|
88
|
+
}
|
|
89
|
+
return pairs;
|
|
90
|
+
}
|
|
91
|
+
if (shape === 'codex-toml') {
|
|
92
|
+
let section = '';
|
|
93
|
+
for (const line of raw.split(/\r?\n/)) {
|
|
94
|
+
const s = /^\s*\[([^\]]+)\]\s*$/.exec(line);
|
|
95
|
+
if (s) { section = s[1].trim(); continue; }
|
|
96
|
+
const m = /^\s*(base_url|openai_base_url)\s*=\s*["']([^"']*)["']/.exec(line);
|
|
97
|
+
if (m) pairs.push([section ? `${section}.${m[1]}` : m[1], m[2]]);
|
|
98
|
+
}
|
|
99
|
+
return pairs;
|
|
100
|
+
}
|
|
101
|
+
const doc = parseJsonc(raw);
|
|
102
|
+
if (!doc || typeof doc !== 'object') return pairs;
|
|
103
|
+
const env = doc.env && typeof doc.env === 'object' && !Array.isArray(doc.env) ? doc.env : {};
|
|
104
|
+
for (const [k, v] of Object.entries(env)) if (['string', 'number', 'boolean'].includes(typeof v)) pairs.push([k, String(v)]);
|
|
105
|
+
if (shape === 'gemini-settings' && typeof doc.proxy === 'string') pairs.push(['proxy', doc.proxy]);
|
|
106
|
+
return pairs;
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
function hostOf(value) {
|
|
110
|
+
const s = /^[a-z][\w+.-]*:\/\//i.test(value) ? value : `http://${value}`;
|
|
111
|
+
try {
|
|
112
|
+
const u = new URL(s);
|
|
113
|
+
return u.hostname ? { host: u.hostname.toLowerCase(), scheme: u.protocol.replace(':', '') } : null;
|
|
114
|
+
} catch {
|
|
115
|
+
return null;
|
|
116
|
+
}
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
export function classifyPair(key, value) {
|
|
120
|
+
const v = String(value ?? '');
|
|
121
|
+
if (TLS_OFF.some(([re, bad]) => re.test(key) && bad(v))) return { key, kind: 'tls-off' };
|
|
122
|
+
const endpoint = ENDPOINT_KEY.test(key) || /(?:^|\.)(?:base_url|openai_base_url)$/.test(key);
|
|
123
|
+
const proxy = PROXY_KEY.test(key) || key === 'proxy';
|
|
124
|
+
if (!endpoint && !proxy) return null;
|
|
125
|
+
const h = hostOf(v.trim());
|
|
126
|
+
return h ? { key, kind: proxy ? 'proxy' : 'model-endpoint', host: h.host, scheme: h.scheme } : null;
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
export function readEnvRedirects(vendor, cwd = process.cwd(), env = process.env) {
|
|
130
|
+
const out = [];
|
|
131
|
+
for (const src of sourcesFor(vendor, cwd)) {
|
|
132
|
+
const raw = readSmall(src.file);
|
|
133
|
+
if (raw == null) continue;
|
|
134
|
+
for (const [k, v] of pairsOf(src.shape, raw)) {
|
|
135
|
+
const hit = classifyPair(k, v);
|
|
136
|
+
if (hit && out.length < MAX_ITEMS) out.push({ ...hit, scope: src.scope, source: src.file });
|
|
137
|
+
}
|
|
138
|
+
}
|
|
139
|
+
const scoped = PROCESS_KEYS[vendor];
|
|
140
|
+
if (scoped) {
|
|
141
|
+
for (const [k, v] of Object.entries(env)) {
|
|
142
|
+
if (!scoped.test(k) && !TLS_OFF.some(([re]) => re.test(k))) continue;
|
|
143
|
+
const hit = classifyPair(k, v);
|
|
144
|
+
if (hit && hit.kind !== 'proxy' && out.length < MAX_ITEMS) out.push({ ...hit, scope: 'process', source: 'environment' });
|
|
145
|
+
}
|
|
146
|
+
}
|
|
147
|
+
return out;
|
|
148
|
+
}
|