@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
@@ -0,0 +1,70 @@
1
+ import fs from 'node:fs';
2
+ import path from 'node:path';
3
+ import { readText } from './file-read.mjs';
4
+ import { HOME } from './limits.mjs';
5
+
6
+ const MAX_SOURCE_FILES = 20;
7
+ const MAX_SOURCE_BYTES = 200_000;
8
+ const MAX_WALK_DEPTH = 4;
9
+ const CODE_RE = /\.(?:[cm]?js|jsx|ts|tsx|py|pyw)$/i;
10
+ const SKIP_DIRS = new Set(['node_modules', '.venv', 'venv', 'site-packages', '__pycache__', 'dist-packages', '.git']);
11
+
12
+ export const EXTENSIONS_DIR_NAME = 'Claude Extensions';
13
+
14
+ export function claudeDesktopDataDir(platform = process.platform) {
15
+ if (platform === 'darwin') return path.join(HOME, 'Library', 'Application Support', 'Claude');
16
+ if (platform === 'win32') return path.join(process.env.APPDATA || path.join(HOME, 'AppData', 'Roaming'), 'Claude');
17
+ return path.join(HOME, '.config', 'Claude');
18
+ }
19
+
20
+ export const EXTENSION_MANIFEST_RE = /^claude extensions\/[^/]+\/manifest\.json$/i;
21
+
22
+ function inside(root, target) {
23
+ const rel = path.relative(root, target);
24
+ return !!rel && !rel.startsWith('..') && !path.isAbsolute(rel);
25
+ }
26
+
27
+ function realInside(root, target) {
28
+ try { return inside(fs.realpathSync(root), fs.realpathSync(target)); } catch { return false; }
29
+ }
30
+
31
+ function sourceFiles(dir, depth = 0, out = []) {
32
+ if (depth > MAX_WALK_DEPTH || out.length >= MAX_SOURCE_FILES * 3) return out;
33
+ let entries = [];
34
+ try { entries = fs.readdirSync(dir, { withFileTypes: true }); } catch { return out; }
35
+ for (const e of entries) {
36
+ const full = path.join(dir, e.name);
37
+ if (e.isDirectory()) { if (!SKIP_DIRS.has(e.name)) sourceFiles(full, depth + 1, out); }
38
+ else if (e.isFile() && CODE_RE.test(e.name)) out.push(full);
39
+ }
40
+ return out;
41
+ }
42
+
43
+ export function bundleExtensionSource(artifact, absManifest, toRel, budget, capped) {
44
+ const root = path.dirname(absManifest);
45
+ let doc = null;
46
+ try { doc = JSON.parse(artifact.content); } catch { return; }
47
+ const entry = typeof doc?.server?.entry_point === 'string' ? doc.server.entry_point.replace(/^\$\{__dirname\}[\\/]?/, '') : null;
48
+ const entryAbs = entry ? path.resolve(root, entry) : null;
49
+ const candidates = [
50
+ ...(entryAbs && inside(root, entryAbs) ? [entryAbs] : []),
51
+ ...sourceFiles(root).filter((f) => f !== entryAbs),
52
+ ];
53
+ for (const abs of candidates) {
54
+ if (artifact.files.length >= MAX_SOURCE_FILES) { capped.push({ reason: 'bundle-cap', path: artifact.path }); break; }
55
+ if (!realInside(root, abs)) continue;
56
+ const read = readText(abs, MAX_SOURCE_BYTES);
57
+ if (!read || read.truncated) continue;
58
+ if (Buffer.byteLength(read.text) > budget.bytes) { capped.push({ reason: 'byte-budget', path: toRel(abs) }); continue; }
59
+ budget.bytes -= Buffer.byteLength(read.text);
60
+ artifact.files.push({ path: toRel(abs), content: read.text, binary: false });
61
+ }
62
+ artifact.metadata.bundledCount = artifact.files.length;
63
+
64
+ const id = path.basename(root);
65
+ try {
66
+ const dataDir = path.dirname(path.dirname(root));
67
+ const settings = JSON.parse(fs.readFileSync(path.join(dataDir, 'Claude Extensions Settings', `${id}.json`), 'utf8'));
68
+ if (settings?.isEnabled === false) artifact.metadata.activation = 'disabled';
69
+ } catch { }
70
+ }
@@ -0,0 +1,128 @@
1
+ import crypto from 'node:crypto';
2
+ import path from 'node:path';
3
+ import { loadConfig } from '../../core/config.mjs';
4
+ import { redactLocally } from '../../detect/local-redact.mjs';
5
+ import { readText } from './file-read.mjs';
6
+ import { HOME, MAX_BUNDLED } from './limits.mjs';
7
+
8
+ export const HOOK_SCRIPT_MODES = ['full', 'hash', 'off'];
9
+
10
+ const SCRIPT_EXT = /\.(?:sh|bash|zsh|ps1|psm1|bat|cmd|py|js|mjs|cjs|ts|rb|pl|php|lua)$/i;
11
+ const SECRET_PATH = /(?:^|[\\/])(?:\.env|\.ssh|\.aws|\.gnupg|\.netrc|\.npmrc|\.kube|\.docker)(?:[\\/]|$)|id_(?:rsa|dsa|ecdsa|ed25519)|secret|credential|password|\.pem$|\.key$/i;
12
+ const COMMAND_KEYS = new Set(['command', 'bash', 'sh', 'powershell', 'pwsh', 'windows', 'linux', 'osx']);
13
+ const EXTS = 'sh|bash|zsh|ps1|psm1|bat|cmd|py|js|mjs|cjs|ts|rb|pl|php|lua';
14
+ const REF_RE = new RegExp(`(?:^|["'\\s=;&|(\`])((?:\\$\\{?[A-Za-z_]\\w*\\}?|%[A-Za-z_]\\w*%|~|[A-Za-z]:)?[\\\\/]?(?:[\\w.@-]+[\\\\/])*[\\w.@-]+\\.(?:${EXTS}))(?=["'\\s;|&)\`]|$)`, 'g');
15
+ const QUOTED_REF_RE = new RegExp(`(["'])((?:\\$\\{?[A-Za-z_]\\w*\\}?|%[A-Za-z_]\\w*%|~|[A-Za-z]:)?[^"'\\n]*[\\\\/][^"'\\n]*?\\.(?:${EXTS}))\\1`, 'g');
16
+ const SHOMRA_GUARD_RE = /(?:^|[\\/])(?:shomra(?:-agent)?\.m?js|@shomra[\\/]agent[\\/].*)$/i;
17
+ const MAX_REFS = 40;
18
+
19
+ export function hookScriptMode(env = process.env, cfg = null) {
20
+ const raw = String(env.SHOMRA_HOOK_SCRIPTS ?? (cfg ?? safeConfig()).hookScripts ?? 'full').trim().toLowerCase();
21
+ return HOOK_SCRIPT_MODES.includes(raw) ? raw : 'full';
22
+ }
23
+
24
+ function safeConfig() {
25
+ try {
26
+ return loadConfig();
27
+ } catch {
28
+ return {};
29
+ }
30
+ }
31
+
32
+ export function hookCommands(canonical) {
33
+ let doc;
34
+ try {
35
+ doc = JSON.parse(canonical);
36
+ } catch {
37
+ return [];
38
+ }
39
+ const out = [];
40
+ const walk = (n, depth) => {
41
+ if (!n || depth > 12 || out.length > 200) return;
42
+ if (Array.isArray(n)) return n.forEach((v) => walk(v, depth + 1));
43
+ if (typeof n !== 'object') return;
44
+ for (const [k, v] of Object.entries(n)) {
45
+ if (COMMAND_KEYS.has(k) && typeof v === 'string') out.push(v);
46
+ else walk(v, depth + 1);
47
+ }
48
+ };
49
+ walk(doc?.hooks, 0);
50
+ return out;
51
+ }
52
+
53
+ export function scriptRefs(command) {
54
+ const out = new Set();
55
+ const cmd = String(command).replace(/(["'])(\$\{?[A-Za-z_]\w*\}?|%[A-Za-z_]\w*%)\1(?=[\\/])/g, '$2');
56
+ for (const m of cmd.matchAll(QUOTED_REF_RE)) if (out.size < MAX_REFS) out.add(m[2].trim());
57
+ const unquoted = cmd.replace(QUOTED_REF_RE, ' ');
58
+ for (const m of unquoted.matchAll(REF_RE)) {
59
+ if (out.size >= MAX_REFS) break;
60
+ out.add(m[1].trim());
61
+ }
62
+ return [...out];
63
+ }
64
+
65
+ export function pluginRootOf(hookFile) {
66
+ const norm = hookFile.replace(/\\/g, '/');
67
+ if (/\/hooks\/hooks\.json$/i.test(norm)) return path.dirname(path.dirname(hookFile));
68
+ if (/\/\.claude-plugin\/plugin\.json$/i.test(norm)) return path.dirname(path.dirname(hookFile));
69
+ return path.dirname(hookFile);
70
+ }
71
+
72
+ export function resolveRef(ref, { hookFile, projectDir }) {
73
+ const m = /^(\$\{?([A-Za-z_]\w*)\}?|%([A-Za-z_]\w*)%|~)([\\/].*)$/.exec(ref);
74
+ if (m) {
75
+ const name = (m[2] ?? m[3] ?? '').toUpperCase();
76
+ const rest = m[4].replace(/^[\\/]/, '');
77
+ const base =
78
+ m[1] === '~' || name === 'HOME' || name === 'USERPROFILE' ? HOME
79
+ : name === 'CLAUDE_PLUGIN_ROOT' || name === 'EXTENSIONPATH' ? pluginRootOf(hookFile)
80
+ : /^(?:CLAUDE|GEMINI|CURSOR|QWEN)_PROJECT_DIR$|^PWD$/.test(name) ? projectDir
81
+ : null;
82
+ return base ? [path.join(base, rest)] : [];
83
+ }
84
+ if (path.isAbsolute(ref) || /^[A-Za-z]:[\\/]/.test(ref)) return [ref];
85
+ return [...new Set([path.resolve(projectDir, ref), path.resolve(path.dirname(hookFile), ref)])];
86
+ }
87
+
88
+ const sha256 = (s) => crypto.createHash('sha256').update(s).digest('hex');
89
+
90
+ export function bundleHookScripts(artifact, hookFile, { projectDir, relPath, budget, capped, mode = hookScriptMode() }) {
91
+ if (mode === 'off') return;
92
+ const refs = [...new Set(hookCommands(artifact.content).flatMap(scriptRefs))].filter((r) => SCRIPT_EXT.test(r));
93
+ const scripts = [];
94
+ for (const ref of refs) {
95
+ if (SECRET_PATH.test(ref)) continue;
96
+ if (SHOMRA_GUARD_RE.test(ref)) {
97
+ scripts.push({ ref, state: 'shomra-guard' });
98
+ continue;
99
+ }
100
+ if (artifact.files.length >= MAX_BUNDLED) {
101
+ capped.push({ reason: 'bundle-cap', path: artifact.path });
102
+ break;
103
+ }
104
+ const target = resolveRef(ref, { hookFile, projectDir }).find((p) => readText(p) != null);
105
+ if (!target || SECRET_PATH.test(target)) {
106
+ scripts.push({ ref, state: 'missing' });
107
+ continue;
108
+ }
109
+ const read = readText(target);
110
+ const digest = sha256(read.text);
111
+ const wirePath = relPath(target) ?? ref.replace(/^(?:\$\{?\w+\}?|%\w+%|~)?[\\/]*/, '').replace(/\\/g, '/');
112
+ if (mode === 'hash') {
113
+ artifact.files.push({ path: wirePath, content: null, binary: false, sha256: digest, withheld: true });
114
+ scripts.push({ ref, path: wirePath, sha256: digest, bytes: read.bytes, state: 'withheld' });
115
+ continue;
116
+ }
117
+ if (read.bytes > budget.bytes) {
118
+ capped.push({ reason: 'byte-budget', path: wirePath });
119
+ scripts.push({ ref, path: wirePath, sha256: digest, bytes: read.bytes, state: 'over-budget' });
120
+ continue;
121
+ }
122
+ const masked = redactLocally(read.text, { categories: ['secret'] });
123
+ budget.bytes -= Buffer.byteLength(masked.text);
124
+ artifact.files.push({ path: wirePath, content: masked.text, binary: false, sha256: digest, ...(read.truncated ? { truncated: true } : {}) });
125
+ scripts.push({ ref, path: wirePath, sha256: digest, bytes: read.bytes, state: 'sent', maskedSecrets: masked.masked.length });
126
+ }
127
+ if (refs.length) artifact.metadata.hookScripts = { mode, scripts: scripts.slice(0, MAX_BUNDLED) };
128
+ }
@@ -3,7 +3,7 @@ import path from 'node:path';
3
3
 
4
4
  export const HOME = os.homedir();
5
5
 
6
- export const ARTIFACT_KINDS = ['skill', 'command', 'subagent', 'hook'];
6
+ export const ARTIFACT_KINDS = ['skill', 'command', 'subagent', 'hook', 'rules', 'plugin', 'extension'];
7
7
 
8
8
  export const MAX_DEPTH = 6;
9
9
 
@@ -0,0 +1,105 @@
1
+ import fs from 'node:fs';
2
+ import path from 'node:path';
3
+ import { readJsonAt, readText, stripJsonComments } from './file-read.mjs';
4
+ import { canonicalHooks } from './hooks.mjs';
5
+ import { HOME } from './limits.mjs';
6
+
7
+ export { PLUGIN_MANIFEST_RE } from '../../detect/signals/manifests.mjs';
8
+ export const MARKETPLACE_ROOT_RE = /(^|\/)plugins\/marketplaces\/[^/]+\/\.claude-plugin\/marketplace\.json$/i;
9
+ export const MAX_MANIFEST_BYTES = 200_000;
10
+
11
+ const MAX_COMPONENT_FILES = 12;
12
+ const MAX_BIN_NAMES = 40;
13
+
14
+ export function pluginRootOf(absManifest) {
15
+ const dir = path.dirname(absManifest);
16
+ const base = path.basename(dir).toLowerCase();
17
+ if (/^\.(claude|codex|cursor|copilot|github)-plugin$|^\.plugin$/.test(base)) return path.dirname(dir);
18
+ if (base === 'plugin' && path.basename(path.dirname(dir)).toLowerCase() === '.github') return path.dirname(path.dirname(dir));
19
+ if (base === 'plugins' && path.basename(path.dirname(dir)).toLowerCase() === '.agents') return path.dirname(path.dirname(dir));
20
+ return dir;
21
+ }
22
+
23
+ export function manifestName(text, fallback) {
24
+ try {
25
+ const doc = JSON.parse(stripJsonComments(text));
26
+ const n = doc?.name ?? doc?.id;
27
+ if (typeof n === 'string' && n.trim()) return n.trim().slice(0, 200);
28
+ } catch { }
29
+ return fallback;
30
+ }
31
+
32
+ function inside(root, target) {
33
+ const rel = path.relative(root, target);
34
+ return !!rel && !rel.startsWith('..') && !path.isAbsolute(rel);
35
+ }
36
+
37
+ function insideReal(root, target) {
38
+ try {
39
+ return inside(fs.realpathSync(root), fs.realpathSync(target));
40
+ } catch {
41
+ return false;
42
+ }
43
+ }
44
+
45
+ function declaredComponentPaths(text) {
46
+ let doc;
47
+ try { doc = JSON.parse(stripJsonComments(text)); } catch { return []; }
48
+ const out = [];
49
+ for (const key of ['hooks', 'mcpServers', 'lspServers', 'monitors']) {
50
+ const v = doc?.[key];
51
+ for (const p of typeof v === 'string' ? [v] : Array.isArray(v) ? v : []) {
52
+ if (typeof p === 'string' && p.trim()) out.push(p.trim().replace(/^\$\{?(?:CLAUDE_PLUGIN_ROOT|PLUGIN_ROOT)\}?\/?/, './'));
53
+ }
54
+ }
55
+ return out;
56
+ }
57
+
58
+ export function bundlePluginComponents(artifact, absManifest, toRel, budget, capped) {
59
+ const root = pluginRootOf(absManifest);
60
+ const wanted = new Set(['hooks/hooks.json', '.mcp.json', 'mcp.json', '.lsp.json', 'monitors/monitors.json']);
61
+ for (const p of declaredComponentPaths(artifact.content)) wanted.add(p.replace(/^\.\//, ''));
62
+
63
+ for (const rel of wanted) {
64
+ if (artifact.files.length >= MAX_COMPONENT_FILES) { capped.push({ reason: 'bundle-cap', path: artifact.path }); break; }
65
+ const abs = path.resolve(root, rel);
66
+ if (!inside(root, abs) || !/\.json$/i.test(abs) || !insideReal(root, abs)) continue;
67
+ const read = readText(abs, MAX_MANIFEST_BYTES);
68
+ if (!read || read.truncated) continue;
69
+ const content = /hooks[^/\\]*\.json$/i.test(abs) ? canonicalHooks(read.text) : read.text;
70
+ if (!content) continue;
71
+ if (Buffer.byteLength(content) > budget.bytes) { capped.push({ reason: 'byte-budget', path: toRel(abs) }); continue; }
72
+ budget.bytes -= Buffer.byteLength(content);
73
+ artifact.files.push({ path: toRel(abs), content, binary: false });
74
+ }
75
+
76
+ let bin = [];
77
+ try { bin = fs.readdirSync(path.join(root, 'bin'), { withFileTypes: true }).filter((e) => e.isFile()).map((e) => e.name); } catch { }
78
+ for (const name of bin.slice(0, MAX_BIN_NAMES)) artifact.files.push({ path: toRel(path.join(root, 'bin', name)), content: null, binary: true });
79
+
80
+ artifact.metadata.bundledCount = artifact.files.length;
81
+ }
82
+
83
+ export function installedPluginManifests(claudeDir) {
84
+ const doc = readJsonAt(path.join(claudeDir, 'plugins', 'installed_plugins.json'));
85
+ if (!doc || typeof doc !== 'object') return [];
86
+ const out = [];
87
+ const seen = new Set();
88
+ for (const [key, value] of Object.entries(doc.plugins ?? {})) {
89
+ const records = Array.isArray(value) ? value : [value];
90
+ for (const r of records) {
91
+ const installPath = typeof r?.installPath === 'string' ? r.installPath : null;
92
+ if (!installPath) continue;
93
+ const abs = path.resolve(installPath);
94
+ if (!inside(HOME, abs)) continue;
95
+ for (const candidate of [path.join(abs, '.claude-plugin', 'plugin.json'), path.join(abs, 'plugin.json')]) {
96
+ if (seen.has(candidate) || !fs.existsSync(candidate) || !insideReal(HOME, candidate)) continue;
97
+ seen.add(candidate);
98
+ const at = key.indexOf('@');
99
+ out.push({ manifest: candidate, marketplace: at > -1 ? key.slice(at + 1) : null });
100
+ break;
101
+ }
102
+ }
103
+ }
104
+ return out;
105
+ }
@@ -1,8 +1,45 @@
1
1
  import path from 'node:path';
2
2
  import { HOME } from './limits.mjs';
3
+ import { EXTENSIONS_DIR_NAME, claudeDesktopDataDir } from './extensions.mjs';
4
+
5
+ const PLAT = process.platform;
6
+ const APPDATA = process.env.APPDATA || path.join(HOME, 'AppData', 'Roaming');
7
+ const XDG = PLAT === 'win32' ? APPDATA : path.join(HOME, '.config');
8
+ const CLAUDE_MANAGED = PLAT === 'win32' ? 'C:\\Program Files\\ClaudeCode' : PLAT === 'darwin' ? '/Library/Application Support/ClaudeCode' : '/etc/claude-code';
9
+
10
+ export function rulesRoots() {
11
+ return [
12
+ { vendor: 'claude-code', scope: 'user', dir: CLAUDE_MANAGED, managed: true },
13
+ { vendor: 'qwen-code', scope: 'user', dir: path.join(HOME, '.qwen') },
14
+ { vendor: 'roo', scope: 'user', dir: path.join(HOME, '.roo') },
15
+ { vendor: 'kiro', scope: 'user', dir: path.join(HOME, '.kiro') },
16
+ { vendor: 'augment', scope: 'user', dir: path.join(HOME, '.augment') },
17
+ { vendor: 'junie', scope: 'user', dir: path.join(HOME, '.junie') },
18
+ { vendor: 'cline', scope: 'user', dir: path.join(HOME, 'Documents', 'Cline') },
19
+ { vendor: 'zed', scope: 'user', dir: path.join(XDG, PLAT === 'win32' ? 'Zed' : 'zed') },
20
+ { vendor: 'goose', scope: 'user', dir: PLAT === 'win32' ? path.join(APPDATA, 'Block', 'goose', 'config') : path.join(HOME, '.config', 'goose') },
21
+ { vendor: 'amp', scope: 'user', dir: path.join(HOME, '.config', 'amp') },
22
+ { vendor: 'opencode', scope: 'user', dir: path.join(HOME, '.config', 'opencode') },
23
+ { vendor: 'agents-md', scope: 'user', dir: path.join(HOME, '.agents') },
24
+ { vendor: 'continue', scope: 'user', dir: path.join(HOME, '.continue') },
25
+ ];
26
+ }
27
+
28
+ export function projectVendorRoots(cwd = process.cwd()) {
29
+ return [
30
+ { vendor: 'roo', scope: 'project', dir: path.join(cwd, '.roo') },
31
+ { vendor: 'qwen-code', scope: 'project', dir: path.join(cwd, '.qwen') },
32
+ { vendor: 'kiro', scope: 'project', dir: path.join(cwd, '.kiro') },
33
+ { vendor: 'cline', scope: 'project', dir: path.join(cwd, '.clinerules') },
34
+ { vendor: 'continue', scope: 'project', dir: path.join(cwd, '.continue') },
35
+ { vendor: 'agents-md', scope: 'project', dir: path.join(cwd, '.agents') },
36
+ ];
37
+ }
3
38
 
4
39
  export function artifactRoots(cwd = process.cwd()) {
5
40
  return [
41
+ ...rulesRoots(),
42
+ ...projectVendorRoots(cwd),
6
43
  { vendor: 'claude-code', scope: 'user', dir: path.join(HOME, '.claude') },
7
44
  { vendor: 'claude-code', scope: 'project', dir: path.join(cwd, '.claude') },
8
45
  { vendor: 'cursor', scope: 'user', dir: path.join(HOME, '.cursor') },
@@ -11,10 +48,13 @@ export function artifactRoots(cwd = process.cwd()) {
11
48
  { vendor: 'codex', scope: 'project', dir: path.join(cwd, '.codex') },
12
49
  { vendor: 'gemini', scope: 'user', dir: path.join(HOME, '.gemini') },
13
50
  { vendor: 'gemini', scope: 'project', dir: path.join(cwd, '.gemini') },
51
+ { vendor: 'windsurf', scope: 'user', dir: path.join(HOME, '.codeium', 'windsurf') },
14
52
  { vendor: 'windsurf', scope: 'project', dir: path.join(cwd, '.windsurf') },
15
53
  { vendor: 'opencode', scope: 'user', dir: path.join(HOME, '.opencode') },
16
54
  { vendor: 'opencode', scope: 'project', dir: path.join(cwd, '.opencode') },
17
55
 
56
+ { vendor: 'copilot', scope: 'user', dir: path.join(HOME, '.copilot') },
18
57
  { vendor: 'copilot', scope: 'project', dir: path.join(cwd, '.github') },
58
+ { vendor: 'claude-desktop', scope: 'user', dir: path.join(claudeDesktopDataDir(), EXTENSIONS_DIR_NAME) },
19
59
  ];
20
60
  }
@@ -67,11 +67,35 @@ export const VECTOR_ENV = {
67
67
  CHROMA_HOST: { engine: 'chroma', kind: 'endpoint' },
68
68
  };
69
69
 
70
+ export const MAX_SPECS = 8;
71
+
72
+ const escRe = (s) => s.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
73
+
74
+ const uncommented = (text) =>
75
+ text
76
+ .split(/\r?\n/)
77
+ .map((l) => l.replace(/(^|[ \t])#[^\n]*$/, '$1'))
78
+ .join('\n');
79
+
80
+
81
+ export function pySpecFor(text, pkg) {
82
+ const body = uncommented(text);
83
+ const name = escRe(pkg).replace(/[-_]/g, '[-_]');
84
+ const req = body.match(new RegExp(`(^|[^a-z0-9_.-])${name}\\s*(?:\\[[^\\]]*\\])?\\s*(===?|~=|>=|<=|!=|>|<)\\s*([\\w.*+!-]+)`, 'im'));
85
+ if (req) return `${req[2]}${req[3]}`;
86
+
87
+ const toml = body.match(new RegExp(`(^|\\n)\\s*["\']?${name}["\']?\\s*=\\s*(?:["\']([^"\']+)["\']|\\{[^}\\n]*version\\s*=\\s*["\']([^"\']+)["\'])`, 'i'));
88
+ const val = toml?.[2] ?? toml?.[3];
89
+ return val && val !== '*' ? val : null;
90
+ }
91
+
70
92
  function npmAiDeps(pkg) {
71
93
  const deps = { ...(pkg.dependencies || {}), ...(pkg.devDependencies || {}), ...(pkg.peerDependencies || {}), ...(pkg.optionalDependencies || {}) };
72
94
  const hits = [];
73
- for (const name of Object.keys(deps)) {
74
- if (NPM_AI.has(name) || NPM_AI_PREFIX.some((p) => name.startsWith(p))) hits.push(name);
95
+ for (const [name, spec] of Object.entries(deps)) {
96
+ if (NPM_AI.has(name) || NPM_AI_PREFIX.some((p) => name.startsWith(p))) {
97
+ hits.push({ name, spec: typeof spec === 'string' ? spec : null });
98
+ }
75
99
  }
76
100
  return hits;
77
101
  }
@@ -79,8 +103,8 @@ function npmAiDeps(pkg) {
79
103
  function pyAiDeps(text) {
80
104
  const hits = [];
81
105
  for (const pkg of PY_AI) {
82
- const re = new RegExp(`(^|[^a-z0-9_.-])${pkg.replace(/[.*+?^${}()|[\]\\]/g, '\\$&')}([^a-z0-9_.-]|$)`, 'im');
83
- if (re.test(text)) hits.push(pkg);
106
+ const re = new RegExp(`(^|[^a-z0-9_.-])${escRe(pkg)}([^a-z0-9_.-]|$)`, 'im');
107
+ if (re.test(text)) hits.push({ name: pkg, spec: pySpecFor(text, pkg) });
84
108
  }
85
109
  return hits;
86
110
  }
@@ -88,10 +112,12 @@ function pyAiDeps(text) {
88
112
  export function discoverAiDependencies(roots = [process.cwd()], files = null) {
89
113
  const walk = files || walkWorkspace(roots);
90
114
  const byPkg = new Map();
91
- const add = (eco, pkg, manifest) => {
115
+ const add = (eco, pkg, manifest, spec) => {
92
116
  const key = `${eco}:${pkg}`;
93
- if (!byPkg.has(key)) byPkg.set(key, { pkg, eco, manifests: new Set() });
94
- byPkg.get(key).manifests.add(manifest);
117
+ if (!byPkg.has(key)) byPkg.set(key, { pkg, eco, manifests: new Set(), specs: new Set() });
118
+ const row = byPkg.get(key);
119
+ row.manifests.add(manifest);
120
+ if (spec) row.specs.add(spec);
95
121
  };
96
122
  for (const { file } of walk.manifests) {
97
123
  const base = path.basename(file);
@@ -99,18 +125,18 @@ export function discoverAiDependencies(roots = [process.cwd()], files = null) {
99
125
  const json = readJson(file);
100
126
  if (!json) continue;
101
127
 
102
- for (const pkg of npmAiDeps(json)) if (!isVectorLib(pkg)) add('npm', pkg, file);
128
+ for (const hit of npmAiDeps(json)) if (!isVectorLib(hit.name)) add('npm', hit.name, file, hit.spec);
103
129
  } else {
104
130
  const text = readText(file, 100_000);
105
131
  if (text == null) continue;
106
- for (const pkg of pyAiDeps(text)) if (!isVectorLib(pkg)) add('pip', pkg, file);
132
+ for (const hit of pyAiDeps(text)) if (!isVectorLib(hit.name)) add('pip', hit.name, file, hit.spec);
107
133
  }
108
134
  }
109
135
  const assets = [];
110
- for (const { pkg, eco, manifests } of byPkg.values()) {
136
+ for (const { pkg, eco, manifests, specs } of byPkg.values()) {
111
137
  const list = [...manifests];
112
138
  assets.push({
113
- type: 'AI_TOOL',
139
+ type: 'AI_LIBRARY',
114
140
  name: `${pkg} (${eco})`,
115
141
  identifier: `dep:${eco}:${pkg}`,
116
142
  vendor: 'ai-sdk',
@@ -118,6 +144,7 @@ export function discoverAiDependencies(roots = [process.cwd()], files = null) {
118
144
  category: 'dependency',
119
145
  ecosystem: eco,
120
146
  package: pkg,
147
+ specs: [...specs].slice(0, MAX_SPECS),
121
148
  usedInProjects: list.length,
122
149
  manifests: list.slice(0, 10),
123
150
  },
@@ -139,7 +166,7 @@ export function discoverAiUsageInCode(roots = [process.cwd()], files = null) {
139
166
  for (const row of rollupAiUsage(usages)) {
140
167
  const site = row.firstSite;
141
168
  assets.push({
142
- type: 'AI_TOOL',
169
+ type: 'AI_LIBRARY',
143
170
  name: `${row.label} (in code)`,
144
171
  identifier: `ai-usage:${row.provider}`,
145
172
  vendor: 'ai-sdk',
@@ -1,6 +1,7 @@
1
1
  import { clampAsset } from '../../core/wire-limits.mjs';
2
2
  import { discoverAiDependencies, discoverAiUsageInCode } from './ai-dependencies.mjs';
3
3
  import { discoverAiTools } from './ai-tools.mjs';
4
+ import { discoverCloudClis } from './cloud-clis.mjs';
4
5
  import { discoverCodingAgents } from './coding-agents.mjs';
5
6
  import { discoverMcpClients } from './mcp-clients.mjs';
6
7
  import { discoverMcpServers } from './mcp-servers.mjs';
@@ -24,6 +25,9 @@ export function discoverAll(roots = [process.cwd()], opts = {}) {
24
25
  ...discoverAiTools(),
25
26
  ...discoverCodingAgents(scanRoots),
26
27
  ...discoverModelKeys(),
28
+ // ⚠ What this HOST is logged into - the reach an agent with a shell
29
+ // inherits and no agent policy granted. See `cloud-clis.mjs`.
30
+ ...discoverCloudClis(),
27
31
  ];
28
32
 
29
33
  const clamped = all.map(clampAsset);