@shomra/agent 0.3.29 → 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/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 +7 -35
- package/src/detect/signals/chat-template.mjs +211 -0
- package/src/detect/signals/ci-workflow.mjs +169 -0
- package/src/detect/signals/gate.mjs +77 -9
- 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/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 +47 -29
- package/src/detect/signals/model-config-rules.mjs +655 -0
- package/src/detect/signals/model-config.mjs +61 -0
- 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 +3 -3
- 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,21 +1,30 @@
|
|
|
1
1
|
import path from 'node:path';
|
|
2
2
|
import { extOf } from './limits.mjs';
|
|
3
3
|
import { SETTINGS_BASENAMES } from './marketplaces.mjs';
|
|
4
|
+
import { isInstructionPath } from '../../detect/signals/instruction-paths.mjs';
|
|
5
|
+
import { PLUGIN_MANIFEST_RE } from './plugins.mjs';
|
|
6
|
+
import { EXTENSION_MANIFEST_RE } from './extensions.mjs';
|
|
7
|
+
|
|
8
|
+
export const COMMAND_PATH_RE = /(^|\/)commands?\/|(^|\/)\.[\w-]+\/prompts?\/|(^|\/)\.(?:windsurf|devin|clinerules)\/workflows\//;
|
|
4
9
|
|
|
5
10
|
export function classify(rel, vendor) {
|
|
6
11
|
const lower = rel.toLowerCase().replace(/\\/g, '/');
|
|
7
12
|
const base = path.basename(lower);
|
|
8
13
|
const ext = extOf(lower);
|
|
9
14
|
|
|
15
|
+
if (vendor === 'claude-desktop') return EXTENSION_MANIFEST_RE.test(lower) ? 'extension' : null;
|
|
16
|
+
if (PLUGIN_MANIFEST_RE.test(lower)) return 'plugin';
|
|
10
17
|
if (base === 'skill.md') return 'skill';
|
|
11
18
|
if (SETTINGS_BASENAMES.has(base)) return 'hook';
|
|
19
|
+
if (vendor === 'copilot' && ext === 'json' && /(^|\/)hooks\/[^/]+\.json$/.test(lower)) return 'hook';
|
|
12
20
|
if (/(^|\/)(sub)?agents?\//.test(lower) && ext === 'md') return 'subagent';
|
|
13
|
-
if (
|
|
21
|
+
if (COMMAND_PATH_RE.test(lower) && !/(^|\/)\.github\/workflows\//.test(lower) && (ext === 'md' || ext === 'toml')) {
|
|
14
22
|
|
|
15
23
|
if (vendor === 'copilot' && !/(^|\/)(prompts|chatmodes)\//.test(lower)) return null;
|
|
16
24
|
return 'command';
|
|
17
25
|
}
|
|
18
26
|
if (vendor === 'copilot' && /(^|\/)chatmodes\//.test(lower) && ext === 'md') return 'subagent';
|
|
27
|
+
if (lower.split('/').length <= 5 && !/(^|\/)(extensions|plugins|marketplaces|node_modules|cache|projects)\//.test(lower) && isInstructionPath(lower)) return 'rules';
|
|
19
28
|
return null;
|
|
20
29
|
}
|
|
21
30
|
|
|
@@ -4,8 +4,12 @@ import { clampArtifact } from '../../core/wire-limits.mjs';
|
|
|
4
4
|
import { classify, declaredName } from './classify.mjs';
|
|
5
5
|
import { readText } from './file-read.mjs';
|
|
6
6
|
import { canonicalHooks } from './hooks.mjs';
|
|
7
|
-
import {
|
|
7
|
+
import { bundleHookScripts } from './hook-scripts.mjs';
|
|
8
|
+
import { HOME, MAX_ARTIFACTS, MAX_BUNDLED, MAX_DIRS, MAX_TOTAL_BYTES, TEXT_EXTS, extOf } from './limits.mjs';
|
|
9
|
+
import { extractInstructionImports } from '../../detect/signals/instruction-paths.mjs';
|
|
8
10
|
import { CATALOGUE_DIR_RE, PLUGIN_PATH_RE, installedMarketplaces } from './marketplaces.mjs';
|
|
11
|
+
import { MARKETPLACE_ROOT_RE, MAX_MANIFEST_BYTES, bundlePluginComponents, installedPluginManifests, manifestName, pluginRootOf } from './plugins.mjs';
|
|
12
|
+
import { bundleExtensionSource } from './extensions.mjs';
|
|
9
13
|
import { artifactRoots } from './roots.mjs';
|
|
10
14
|
import { walkRoot } from './walk.mjs';
|
|
11
15
|
|
|
@@ -16,10 +20,13 @@ function relativeToSite(site, absolutePath) {
|
|
|
16
20
|
}
|
|
17
21
|
|
|
18
22
|
function artifactName(kind, content, absolutePath) {
|
|
23
|
+
if (kind === 'plugin') return manifestName(content, path.basename(pluginRootOf(absolutePath)) || path.basename(absolutePath));
|
|
24
|
+
if (kind === 'extension') return manifestName(content, path.basename(path.dirname(absolutePath)));
|
|
19
25
|
const declared = declaredName(content);
|
|
20
26
|
if (declared) return declared;
|
|
21
27
|
if (kind === 'skill') return path.basename(path.dirname(absolutePath));
|
|
22
28
|
if (kind === 'hook') return `${path.basename(absolutePath)} · hooks`;
|
|
29
|
+
if (kind === 'rules') return path.basename(absolutePath);
|
|
23
30
|
return path.basename(absolutePath).replace(/\.(md|toml)$/i, '');
|
|
24
31
|
}
|
|
25
32
|
|
|
@@ -30,8 +37,10 @@ function resolveActivation(marketplace, installed) {
|
|
|
30
37
|
}
|
|
31
38
|
|
|
32
39
|
function readArtifactContent(kind, absolutePath) {
|
|
33
|
-
const
|
|
40
|
+
const manifest = kind === 'plugin' || kind === 'extension';
|
|
41
|
+
const read = readText(absolutePath, manifest ? MAX_MANIFEST_BYTES : undefined);
|
|
34
42
|
if (!read) return null;
|
|
43
|
+
if (manifest && read.truncated) return { ...read, content: null, oversize: true };
|
|
35
44
|
if (kind !== 'hook') return { ...read, content: read.text };
|
|
36
45
|
const content = canonicalHooks(read.text);
|
|
37
46
|
return content ? { ...read, content } : null;
|
|
@@ -77,7 +86,12 @@ function collectFromSite({ site, files, state, project }) {
|
|
|
77
86
|
}
|
|
78
87
|
|
|
79
88
|
const marketplace = PLUGIN_PATH_RE.exec(relativePath)?.[2] ?? null;
|
|
80
|
-
|
|
89
|
+
|
|
90
|
+
if (kind === 'plugin' && marketplace && !MARKETPLACE_ROOT_RE.test(relativePath)) {
|
|
91
|
+
availableBy.set(marketplace, (availableBy.get(marketplace) ?? 0) + 1);
|
|
92
|
+
continue;
|
|
93
|
+
}
|
|
94
|
+
const activation = kind === 'plugin' && marketplace ? 'active' : resolveActivation(marketplace, installed);
|
|
81
95
|
if (activation === 'not-installed') {
|
|
82
96
|
availableBy.set(marketplace, (availableBy.get(marketplace) ?? 0) + 1);
|
|
83
97
|
continue;
|
|
@@ -85,6 +99,10 @@ function collectFromSite({ site, files, state, project }) {
|
|
|
85
99
|
|
|
86
100
|
const read = readArtifactContent(kind, absolutePath);
|
|
87
101
|
if (!read) continue;
|
|
102
|
+
if (read.oversize) {
|
|
103
|
+
capped.push({ reason: 'manifest-too-large', path: relativePath });
|
|
104
|
+
continue;
|
|
105
|
+
}
|
|
88
106
|
if (read.bytes > budget.bytes) {
|
|
89
107
|
capped.push({ reason: 'byte-budget', path: relativePath });
|
|
90
108
|
continue;
|
|
@@ -97,6 +115,76 @@ function collectFromSite({ site, files, state, project }) {
|
|
|
97
115
|
consumed.add(absolutePath);
|
|
98
116
|
artifacts.push(artifact);
|
|
99
117
|
if (kind === 'skill') skills.push({ artifact, dir: path.dirname(absolutePath), site });
|
|
118
|
+
if (kind === 'rules') state.rules.push({ artifact, absolutePath });
|
|
119
|
+
if (kind === 'plugin') state.plugins.push({ artifact, absolutePath, site });
|
|
120
|
+
if (kind === 'extension') state.extensions.push({ artifact, absolutePath, site });
|
|
121
|
+
if (kind === 'hook') (state.hooks ??= []).push({ artifact, absolutePath, site });
|
|
122
|
+
}
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
function collectInstalledPlugins({ sites, state, project }) {
|
|
126
|
+
const { artifacts, capped, consumed, budget } = state;
|
|
127
|
+
for (const site of sites) {
|
|
128
|
+
if (site.vendor !== 'claude-code' || site.scope !== 'user') continue;
|
|
129
|
+
for (const { manifest, marketplace } of installedPluginManifests(site.dir)) {
|
|
130
|
+
if (consumed.has(manifest)) continue;
|
|
131
|
+
if (artifacts.length >= MAX_ARTIFACTS) { capped.push({ reason: 'artifact-cap', path: manifest }); return; }
|
|
132
|
+
const relativePath = relativeToSite(site, manifest);
|
|
133
|
+
const read = readArtifactContent('plugin', manifest);
|
|
134
|
+
if (!read) continue;
|
|
135
|
+
if (read.oversize) { capped.push({ reason: 'manifest-too-large', path: relativePath }); continue; }
|
|
136
|
+
if (read.bytes > budget.bytes) { capped.push({ reason: 'byte-budget', path: relativePath }); continue; }
|
|
137
|
+
budget.bytes -= Buffer.byteLength(read.content);
|
|
138
|
+
const artifact = buildArtifact({
|
|
139
|
+
kind: 'plugin', content: read.content, read, relativePath, site, project, marketplace, activation: 'active', absolutePath: manifest,
|
|
140
|
+
});
|
|
141
|
+
consumed.add(manifest);
|
|
142
|
+
artifacts.push(artifact);
|
|
143
|
+
state.plugins.push({ artifact, absolutePath: manifest, site });
|
|
144
|
+
}
|
|
145
|
+
}
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
const IMPORT_READABLE_RE = /\.(?:md|mdx|markdown|mdc|rst|txt)$/i;
|
|
149
|
+
const IMPORT_SECRET_RE = /(?:^|[\\/])(?:\.env|\.ssh|\.aws|\.gnupg|\.netrc|\.npmrc)|id_(?:rsa|dsa|ecdsa|ed25519)|secret|credential|password|\.pem$|\.key$/i;
|
|
150
|
+
const MAX_IMPORT_DEPTH = 5;
|
|
151
|
+
|
|
152
|
+
function resolveImport(ref, fromFile) {
|
|
153
|
+
if (ref.startsWith('~/')) return path.join(HOME, ref.slice(2));
|
|
154
|
+
if (path.isAbsolute(ref)) return ref;
|
|
155
|
+
return path.resolve(path.dirname(fromFile), ref);
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
function bundleRuleImports(state) {
|
|
159
|
+
const { capped, budget } = state;
|
|
160
|
+
for (const { artifact, absolutePath } of state.rules) {
|
|
161
|
+
const seen = new Set([absolutePath]);
|
|
162
|
+
const queue = [{ file: absolutePath, text: artifact.content, depth: 0 }];
|
|
163
|
+
while (queue.length) {
|
|
164
|
+
const { file, text, depth } = queue.shift();
|
|
165
|
+
if (depth >= MAX_IMPORT_DEPTH) continue;
|
|
166
|
+
for (const ref of extractInstructionImports(text)) {
|
|
167
|
+
if (!IMPORT_READABLE_RE.test(ref) || IMPORT_SECRET_RE.test(ref)) continue;
|
|
168
|
+
const target = resolveImport(ref, file);
|
|
169
|
+
if (seen.has(target)) continue;
|
|
170
|
+
seen.add(target);
|
|
171
|
+
if (artifact.files.length >= MAX_BUNDLED) {
|
|
172
|
+
capped.push({ reason: 'bundle-cap', path: artifact.path });
|
|
173
|
+
queue.length = 0;
|
|
174
|
+
break;
|
|
175
|
+
}
|
|
176
|
+
const read = readText(target);
|
|
177
|
+
if (!read) continue;
|
|
178
|
+
if (read.bytes > budget.bytes) {
|
|
179
|
+
capped.push({ reason: 'byte-budget', path: ref });
|
|
180
|
+
continue;
|
|
181
|
+
}
|
|
182
|
+
budget.bytes -= Buffer.byteLength(read.text);
|
|
183
|
+
artifact.files.push({ path: ref.replace(/^[~.]*\//, ''), content: read.text, binary: false });
|
|
184
|
+
queue.push({ file: target, text: read.text, depth: depth + 1 });
|
|
185
|
+
}
|
|
186
|
+
}
|
|
187
|
+
if (artifact.files.length) artifact.metadata.importCount = artifact.files.length;
|
|
100
188
|
}
|
|
101
189
|
}
|
|
102
190
|
|
|
@@ -164,12 +252,34 @@ export function discoverAgentArtifacts(cwd = process.cwd(), roots = null) {
|
|
|
164
252
|
capped: [],
|
|
165
253
|
consumed: new Set(),
|
|
166
254
|
skills: [],
|
|
255
|
+
rules: [],
|
|
256
|
+
plugins: [],
|
|
257
|
+
extensions: [],
|
|
167
258
|
availableBy: new Map(),
|
|
168
259
|
};
|
|
169
260
|
|
|
170
261
|
const walked = walkSites(sites, state.budget);
|
|
171
262
|
for (const { site, files } of walked) collectFromSite({ site, files, state, project });
|
|
263
|
+
collectInstalledPlugins({ sites, state, project });
|
|
172
264
|
bundleSkillFiles({ skills: state.skills, walked, state });
|
|
265
|
+
bundleRuleImports(state);
|
|
266
|
+
for (const { artifact, absolutePath, site } of state.hooks ?? []) {
|
|
267
|
+
bundleHookScripts(artifact, absolutePath, {
|
|
268
|
+
projectDir: site.scope === 'project' ? path.dirname(site.dir) : cwd,
|
|
269
|
+
relPath: (abs) => {
|
|
270
|
+
const rel = relativeToSite(site, abs);
|
|
271
|
+
return rel.startsWith('..') || path.isAbsolute(rel) ? null : rel;
|
|
272
|
+
},
|
|
273
|
+
budget: state.budget,
|
|
274
|
+
capped: state.capped,
|
|
275
|
+
});
|
|
276
|
+
}
|
|
277
|
+
for (const { artifact, absolutePath, site } of state.extensions) {
|
|
278
|
+
bundleExtensionSource(artifact, absolutePath, (abs) => relativeToSite(site, abs), state.budget, state.capped);
|
|
279
|
+
}
|
|
280
|
+
for (const { artifact, absolutePath, site } of state.plugins) {
|
|
281
|
+
bundlePluginComponents(artifact, absolutePath, (abs) => relativeToSite(site, abs), state.budget, state.capped);
|
|
282
|
+
}
|
|
173
283
|
|
|
174
284
|
if (state.budget.dirs <= 0) state.capped.push({ reason: 'walk-budget', path: null });
|
|
175
285
|
|
|
@@ -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
|
}
|