@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
|
@@ -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.
|
|
74
|
-
if (NPM_AI.has(name) || NPM_AI_PREFIX.some((p) => name.startsWith(p)))
|
|
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
|
|
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)
|
|
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
|
|
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
|
|
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: '
|
|
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: '
|
|
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);
|