@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.
Files changed (84) hide show
  1. package/package.json +1 -1
  2. package/src/agents/hook-command.mjs +1 -1
  3. package/src/artifacts/matchers.mjs +7 -0
  4. package/src/cli/flags.mjs +2 -2
  5. package/src/cli/help-sections.mjs +7 -0
  6. package/src/cli/help.mjs +1 -1
  7. package/src/commands/check.mjs +3 -11
  8. package/src/commands/gate.mjs +26 -4
  9. package/src/commands/git-hooks.mjs +2 -2
  10. package/src/commands/ledger.mjs +0 -1
  11. package/src/commands/mcp-add.mjs +2 -1
  12. package/src/commands/memory-scan.mjs +135 -47
  13. package/src/commands/pr.mjs +7 -10
  14. package/src/commands/provenance.mjs +8 -13
  15. package/src/commands/scan.mjs +7 -1
  16. package/src/commands/secrets.mjs +4 -5
  17. package/src/core/git-exec.mjs +79 -0
  18. package/src/core/yaml-lite.mjs +300 -0
  19. package/src/core/zip-lite.mjs +37 -0
  20. package/src/detect/local-redact.mjs +1 -3
  21. package/src/detect/sast/rules-config.mjs +1 -1
  22. package/src/detect/sast/rules-javascript.mjs +16 -3
  23. package/src/detect/sast/rules-python.mjs +8 -6
  24. package/src/detect/sast/scanner.mjs +1 -1
  25. package/src/detect/signals/agent-frameworks.mjs +231 -0
  26. package/src/detect/signals/agent-graph-surface.mjs +113 -0
  27. package/src/detect/signals/agentic-ci-surface.mjs +314 -0
  28. package/src/detect/signals/agentic-shim.mjs +82 -0
  29. package/src/detect/signals/artifacts.mjs +8 -36
  30. package/src/detect/signals/autonomy.mjs +9 -1
  31. package/src/detect/signals/chat-template.mjs +211 -0
  32. package/src/detect/signals/ci-workflow.mjs +169 -0
  33. package/src/detect/signals/credential-harvest.mjs +1 -1
  34. package/src/detect/signals/execution-hijack.mjs +4 -2
  35. package/src/detect/signals/gate.mjs +84 -11
  36. package/src/detect/signals/guardrail-shape.mjs +564 -0
  37. package/src/detect/signals/guardrail-surface.mjs +221 -0
  38. package/src/detect/signals/injection.mjs +8 -0
  39. package/src/detect/signals/inspect-shim.mjs +7 -0
  40. package/src/detect/signals/instruction-paths.mjs +60 -0
  41. package/src/detect/signals/manifests.mjs +302 -0
  42. package/src/detect/signals/masking.mjs +14 -1
  43. package/src/detect/signals/mcp-advisories.mjs +109 -0
  44. package/src/detect/signals/mcp-config.mjs +598 -0
  45. package/src/detect/signals/memory-directives.mjs +661 -0
  46. package/src/detect/signals/memory-locations.mjs +158 -0
  47. package/src/detect/signals/memory.mjs +56 -31
  48. package/src/detect/signals/model-config-rules.mjs +655 -0
  49. package/src/detect/signals/model-config.mjs +61 -0
  50. package/src/detect/signals/packages.mjs +2 -2
  51. package/src/detect/signals/prose-context.mjs +6 -9
  52. package/src/detect/signals/scan.mjs +4 -4
  53. package/src/detect/signals/secret-scanner.mjs +241 -0
  54. package/src/detect/signals/secrets.mjs +1 -48
  55. package/src/detect/signals/shell.mjs +10 -10
  56. package/src/gate/advisories.mjs +16 -0
  57. package/src/gate/batch.mjs +10 -0
  58. package/src/gate/environment.mjs +8 -53
  59. package/src/guard/artifact-paths.mjs +107 -0
  60. package/src/guard/classify.mjs +165 -7
  61. package/src/guard/command-resolve.mjs +35 -5
  62. package/src/guard/memory-write.mjs +218 -0
  63. package/src/guard/prompt-guard.mjs +0 -1
  64. package/src/guard/tool-guard.mjs +52 -77
  65. package/src/inventory/agent-posture.mjs +236 -57
  66. package/src/inventory/artifacts/classify.mjs +10 -1
  67. package/src/inventory/artifacts/discover.mjs +113 -3
  68. package/src/inventory/artifacts/extensions.mjs +70 -0
  69. package/src/inventory/artifacts/hook-scripts.mjs +128 -0
  70. package/src/inventory/artifacts/limits.mjs +1 -1
  71. package/src/inventory/artifacts/plugins.mjs +105 -0
  72. package/src/inventory/artifacts/roots.mjs +40 -0
  73. package/src/inventory/discovery/ai-dependencies.mjs +39 -12
  74. package/src/inventory/discovery/all.mjs +4 -0
  75. package/src/inventory/discovery/cloud-clis.mjs +472 -0
  76. package/src/inventory/discovery/coding-agents.mjs +19 -4
  77. package/src/inventory/discovery/mcp-clients.mjs +16 -10
  78. package/src/inventory/discovery/mcp-servers.mjs +125 -35
  79. package/src/inventory/discovery/mcp-stores.mjs +207 -0
  80. package/src/inventory/env-redirect.mjs +148 -0
  81. package/src/inventory/grant-extract.mjs +463 -0
  82. package/src/inventory/project-roots.mjs +108 -0
  83. package/src/inventory/vscode-state.mjs +153 -0
  84. 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 { readJson } from './fs-read.mjs';
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
- 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') });
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: 'claude-code', file: path.join(HOME, '.claude.json') });
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: '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') });
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
- 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') });
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
- 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
- );
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
- 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}`;
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: { 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 || {} }),
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(/&quot;/g, '"').replace(/&apos;/g, "'").replace(/&lt;/g, '<').replace(/&gt;/g, '>').replace(/&#10;/g, '\n').replace(/&#13;/g, '\r').replace(/&#(\d+);/g, (_, n) => String.fromCharCode(Number(n))).replace(/&amp;/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
+ }