@chimix/agentscan 0.6.0 → 0.7.0
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/README.md +31 -6
- package/dist/cli.js +319 -189
- package/package.json +2 -2
package/README.md
CHANGED
|
@@ -6,7 +6,7 @@
|
|
|
6
6
|
|
|
7
7
|
<p align="center">
|
|
8
8
|
<img src="https://img.shields.io/badge/checks-27-111111?style=flat-square" alt="27 checks">
|
|
9
|
-
<img src="https://img.shields.io/badge/tests-
|
|
9
|
+
<img src="https://img.shields.io/badge/tests-250%20passing-111111?style=flat-square" alt="250 tests">
|
|
10
10
|
<img src="https://img.shields.io/badge/network-none-111111?style=flat-square" alt="No network">
|
|
11
11
|
<img src="https://img.shields.io/badge/writes-none-111111?style=flat-square" alt="Writes nothing">
|
|
12
12
|
<img src="https://img.shields.io/badge/runs%20on-node%20%C2%B7%20bun-111111?style=flat-square" alt="Node or Bun">
|
|
@@ -62,7 +62,7 @@ of the filesystem**, where nothing else in your stack will ever tell you.
|
|
|
62
62
|
No AI, no network, no heuristics. Read the config, read the disk, compare:
|
|
63
63
|
|
|
64
64
|
```
|
|
65
|
-
1. Discover .claude/ .agents/ .mcp.json AGENTS.md skills-lock.json
|
|
65
|
+
1. Discover .claude/ .agents/ .mcp.json AGENTS.md skills-lock.json plugin hooks/
|
|
66
66
|
2. Extract immutable facts — never re-read during checking
|
|
67
67
|
3. Check 27 checks, each against one published spec line
|
|
68
68
|
4. Report text · --json · --output prompt (handoff for a fixing agent)
|
|
@@ -127,7 +127,9 @@ bunx @chimix/agentscan explain hook.missing-script:hook:PreToolUse:./x.js ~/your
|
|
|
127
127
|
```
|
|
128
128
|
|
|
129
129
|
`check` never opens a socket and never writes to the tree it scans. Contributing
|
|
130
|
-
needs **Bun** 1.
|
|
130
|
+
needs **Bun** 1.4+ — the version CI pins and every gate is verified on; the
|
|
131
|
+
text `bun.lock` this repo ships cannot be read by 1.1 at all. The repo runs
|
|
132
|
+
TypeScript directly and `bun run build`
|
|
131
133
|
bundles it for Node.
|
|
132
134
|
|
|
133
135
|
### In CI
|
|
@@ -191,7 +193,7 @@ Flags for `check`:
|
|
|
191
193
|
│ ⌒ │ 6 error · 2 warning · 11 info hidden (--verbose)
|
|
192
194
|
└─────┘
|
|
193
195
|
|
|
194
|
-
agentscan v0.
|
|
196
|
+
agentscan v0.7.0 — touchagency
|
|
195
197
|
|
|
196
198
|
Scanned: 46 deps · 108 skills · 1 mcp · 2 agents · packageManager=bun
|
|
197
199
|
|
|
@@ -223,7 +225,7 @@ JSON shape (abridged):
|
|
|
223
225
|
|
|
224
226
|
```json
|
|
225
227
|
{
|
|
226
|
-
"version": "0.
|
|
228
|
+
"version": "0.7.0",
|
|
227
229
|
"root": "/path/to/project",
|
|
228
230
|
"factsSummary": {
|
|
229
231
|
"packageManager": "bun",
|
|
@@ -417,7 +419,7 @@ Most validate one discovered item against its own file on disk. The four
|
|
|
417
419
|
|----|----------|---------|
|
|
418
420
|
| `config.unreadable` | error | A config file that is not valid JSON, so whatever it declares is silently not in effect |
|
|
419
421
|
| `scan.truncated` | info | A file past the scan cap, so the checks that read its body saw only a prefix — about this tool's reach, not about your project |
|
|
420
|
-
| `hook.missing-script` | error | A registered hook whose script does not exist — it never runs |
|
|
422
|
+
| `hook.missing-script` | error | A registered hook whose script does not exist — it never runs. Reads all four in-tree registration sites (see below) |
|
|
421
423
|
| `hook.unknown-event` | error | A hook registered under an event name that is never dispatched |
|
|
422
424
|
| `agent.missing-frontmatter` | error | An agent definition with no `---` block |
|
|
423
425
|
| `agent.missing-description` | error | Agent frontmatter has no `description` |
|
|
@@ -457,6 +459,24 @@ references measured across 17 projects, 1645 resolved skill-relative and 12 only
|
|
|
457
459
|
at the root. Fenced code blocks are stripped first: a path in an example is
|
|
458
460
|
illustration, not a pointer.
|
|
459
461
|
|
|
462
|
+
### Where it looks for hooks
|
|
463
|
+
|
|
464
|
+
The hooks reference lists seven places a hook can be registered. agentscan reads
|
|
465
|
+
the four that live inside the scanned project:
|
|
466
|
+
|
|
467
|
+
| Registered in | Script paths resolve against |
|
|
468
|
+
|---|---|
|
|
469
|
+
| `.claude/settings.json` · `.claude/settings.local.json` | project root, `${CLAUDE_PROJECT_DIR}` |
|
|
470
|
+
| A plugin's `hooks/hooks.json` (a directory with `.claude-plugin/plugin.json`, or the scan root itself) | the plugin root, `${CLAUDE_PLUGIN_ROOT}` |
|
|
471
|
+
| `SKILL.md` frontmatter | the skill's own directory, then the project root |
|
|
472
|
+
| `.claude/agents/*.md` frontmatter | the agent file's directory, then the project root |
|
|
473
|
+
|
|
474
|
+
`${CLAUDE_PLUGIN_ROOT}` is expanded **only** for a hook that came from a plugin.
|
|
475
|
+
In a settings file it names nothing, so the path is skipped rather than guessed
|
|
476
|
+
at — measured across 17 installed plugins, 31 of 33 plugin hook commands use it
|
|
477
|
+
and none of them are settings hooks. `hook.unknown-event` covers all four sites
|
|
478
|
+
at no extra cost. Sources and measurements: [docs/spec/hook-sources.md](docs/spec/hook-sources.md).
|
|
479
|
+
|
|
460
480
|
`hook.missing-script` and `mcp.command-missing` are deliberately conservative.
|
|
461
481
|
A value is only resolved when it is path-like and the answer is certain; shell
|
|
462
482
|
programs (`a && b`, `$(...)`, pipes), bare PATH binaries (`npx`, `uvx`), and
|
|
@@ -489,6 +509,11 @@ When adding a check, add its spec line there first.
|
|
|
489
509
|
- **Runtime split.** Source development and the composite Action use Bun; the
|
|
490
510
|
published `dist/cli.js` runs on Node 20.11+ or Bun. The source entrypoint uses
|
|
491
511
|
Bun's `import.meta.dir` / `import.meta.main` and is not the Node entrypoint.
|
|
512
|
+
- **Three hook locations are unread.** `~/.claude/settings.json`, managed policy
|
|
513
|
+
settings, and installed marketplace plugins under `~/.claude/plugins` all sit
|
|
514
|
+
outside the scanned project, and the docs say a plugin's install directory
|
|
515
|
+
changes on every update. A hook registered in one of those, pointing at a
|
|
516
|
+
missing script, is not reported.
|
|
492
517
|
- **Bounded reads.** A `SKILL.md` is read to 64 KB and a policy file to 100 KB,
|
|
493
518
|
so `skill.broken-reference` and `policyLines` see only that much of a larger
|
|
494
519
|
file. This is reported — `scan.truncated`, at `info` — rather than left
|
package/dist/cli.js
CHANGED
|
@@ -7259,10 +7259,11 @@ function checkHooks(facts) {
|
|
|
7259
7259
|
action: "warn",
|
|
7260
7260
|
severity: "error",
|
|
7261
7261
|
message: `${hook.event ?? hook.name} hook points at a script that does not exist: ${hook.scriptPath}`,
|
|
7262
|
-
reason: "The hook is registered but its script is missing, so it never runs. A guard hook that silently does nothing is worse than no hook — the config claims a protection that is not in effect.",
|
|
7262
|
+
reason: "The hook is registered but its script is missing, so it never runs. A guard hook that silently does nothing is worse than no hook — the config claims a protection that is not in effect. A relative path is looked for beside the file that declared it and at the project root; it is at neither.",
|
|
7263
7263
|
evidence: [
|
|
7264
7264
|
{ kind: "hook", value: `${hook.event ?? hook.name} @ ${hook.path}` },
|
|
7265
|
-
{ kind: "script", value: hook.scriptPath }
|
|
7265
|
+
{ kind: "script", value: hook.scriptPath },
|
|
7266
|
+
...hook.source === undefined || hook.source === "settings" ? [] : [{ kind: "source", value: hook.source }]
|
|
7266
7267
|
],
|
|
7267
7268
|
suggest: `Restore ${hook.scriptPath} or remove the hook from ${hook.path}`
|
|
7268
7269
|
}));
|
|
@@ -11703,11 +11704,16 @@ function loadConfig(root, configPath) {
|
|
|
11703
11704
|
|
|
11704
11705
|
// src/discover/index.ts
|
|
11705
11706
|
import { homedir as homedir3 } from "node:os";
|
|
11706
|
-
import { join as
|
|
11707
|
+
import { join as join9, resolve as resolve3 } from "node:path";
|
|
11707
11708
|
|
|
11708
11709
|
// src/discover/agents.ts
|
|
11709
|
-
import { existsSync as
|
|
11710
|
-
import { join as
|
|
11710
|
+
import { existsSync as existsSync4, readdirSync, statSync as statSync2 } from "node:fs";
|
|
11711
|
+
import { dirname as dirname2, join as join4 } from "node:path";
|
|
11712
|
+
|
|
11713
|
+
// src/discover/hooks.ts
|
|
11714
|
+
import { existsSync as existsSync3, statSync } from "node:fs";
|
|
11715
|
+
import { homedir } from "node:os";
|
|
11716
|
+
import { isAbsolute, join as join3 } from "node:path";
|
|
11711
11717
|
|
|
11712
11718
|
// src/discover/shared.ts
|
|
11713
11719
|
import { closeSync, existsSync as existsSync2, openSync, readFileSync as readFileSync2, readSync } from "node:fs";
|
|
@@ -11772,6 +11778,16 @@ function readCapped(path, cap) {
|
|
|
11772
11778
|
closeSync(fd);
|
|
11773
11779
|
}
|
|
11774
11780
|
}
|
|
11781
|
+
var NESTED_DISCOVERY_MAX_DEPTH = 8;
|
|
11782
|
+
var NESTED_DISCOVERY_SKIP = new Set([
|
|
11783
|
+
".git",
|
|
11784
|
+
"node_modules",
|
|
11785
|
+
".next",
|
|
11786
|
+
"dist",
|
|
11787
|
+
"build",
|
|
11788
|
+
"coverage",
|
|
11789
|
+
"vendor"
|
|
11790
|
+
]);
|
|
11775
11791
|
var AGENT_CONFIG_SIGNALS = [
|
|
11776
11792
|
".claude",
|
|
11777
11793
|
".agents",
|
|
@@ -11899,86 +11915,19 @@ function readFrontmatter(skillMdPath, errors3) {
|
|
|
11899
11915
|
if (description !== undefined) {
|
|
11900
11916
|
out.description = description;
|
|
11901
11917
|
}
|
|
11902
|
-
|
|
11903
|
-
|
|
11904
|
-
|
|
11905
|
-
// src/discover/agents.ts
|
|
11906
|
-
function agentFiles(dir, errors3) {
|
|
11907
|
-
let entries;
|
|
11908
|
-
try {
|
|
11909
|
-
entries = readdirSync(dir, { withFileTypes: true }).map((e) => e.isDirectory() ? `${e.name}/` : e.name);
|
|
11910
|
-
} catch (err) {
|
|
11911
|
-
errors3.push({
|
|
11912
|
-
path: dir,
|
|
11913
|
-
kind: "unreadable",
|
|
11914
|
-
detail: err instanceof Error ? err.message : String(err)
|
|
11915
|
-
});
|
|
11916
|
-
return [];
|
|
11917
|
-
}
|
|
11918
|
-
const out = [];
|
|
11919
|
-
for (const entry of entries) {
|
|
11920
|
-
if (entry.startsWith(".")) {
|
|
11921
|
-
continue;
|
|
11922
|
-
}
|
|
11923
|
-
if (entry.endsWith("/")) {
|
|
11924
|
-
out.push(...agentFiles(join3(dir, entry.slice(0, -1)), errors3).map((p) => join3(entry.slice(0, -1), p)));
|
|
11925
|
-
continue;
|
|
11926
|
-
}
|
|
11927
|
-
out.push(entry);
|
|
11918
|
+
if (record.hooks !== undefined) {
|
|
11919
|
+
out.hooks = record.hooks;
|
|
11928
11920
|
}
|
|
11929
11921
|
return out;
|
|
11930
11922
|
}
|
|
11931
|
-
function discoverAgents(root, errors3) {
|
|
11932
|
-
const dir = join3(root, ".claude", "agents");
|
|
11933
|
-
if (!existsSync3(dir)) {
|
|
11934
|
-
return [];
|
|
11935
|
-
}
|
|
11936
|
-
const entries = agentFiles(dir, errors3);
|
|
11937
|
-
const facts = [];
|
|
11938
|
-
for (const name of entries) {
|
|
11939
|
-
if (!name.endsWith(".md")) {
|
|
11940
|
-
continue;
|
|
11941
|
-
}
|
|
11942
|
-
const filePath = join3(dir, name);
|
|
11943
|
-
try {
|
|
11944
|
-
const st = statSync(filePath);
|
|
11945
|
-
if (!st.isFile()) {
|
|
11946
|
-
continue;
|
|
11947
|
-
}
|
|
11948
|
-
} catch {
|
|
11949
|
-
continue;
|
|
11950
|
-
}
|
|
11951
|
-
const fm = readFrontmatter(filePath, errors3);
|
|
11952
|
-
const fact = {
|
|
11953
|
-
name: name.slice(0, -".md".length),
|
|
11954
|
-
path: filePath,
|
|
11955
|
-
hasFrontmatter: fm.hasFrontmatter
|
|
11956
|
-
};
|
|
11957
|
-
if (fm.unreadable === true) {
|
|
11958
|
-
fact.unreadable = true;
|
|
11959
|
-
}
|
|
11960
|
-
if (fm.unparseable === true) {
|
|
11961
|
-
fact.unparseableFrontmatter = true;
|
|
11962
|
-
}
|
|
11963
|
-
if (fm.name !== undefined) {
|
|
11964
|
-
fact.frontmatterName = fm.name;
|
|
11965
|
-
}
|
|
11966
|
-
if (fm.description !== undefined) {
|
|
11967
|
-
fact.description = fm.description;
|
|
11968
|
-
}
|
|
11969
|
-
facts.push(fact);
|
|
11970
|
-
}
|
|
11971
|
-
return facts;
|
|
11972
|
-
}
|
|
11973
11923
|
|
|
11974
11924
|
// src/discover/hooks.ts
|
|
11975
|
-
import { existsSync as existsSync4, statSync as statSync2 } from "node:fs";
|
|
11976
|
-
import { homedir } from "node:os";
|
|
11977
|
-
import { isAbsolute, join as join4 } from "node:path";
|
|
11978
11925
|
var INTERPRETERS = /^(?:bash|sh|zsh|python3?|node|bun|deno)$/;
|
|
11979
11926
|
var INLINE_CODE_FLAGS = /^-(?:e|p|c)$/;
|
|
11980
11927
|
var SHELL_METACHARS = /[|;&`]|\$\(|\|\||&&/;
|
|
11981
|
-
|
|
11928
|
+
var PROJECT_DIR = /^\$(?:CLAUDE_PROJECT_DIR\b|\{CLAUDE_PROJECT_DIR\})/;
|
|
11929
|
+
var PLUGIN_ROOT = /^\$(?:CLAUDE_PLUGIN_ROOT\b|\{CLAUDE_PLUGIN_ROOT\})/;
|
|
11930
|
+
function hookScriptPath(command, bases = {}) {
|
|
11982
11931
|
const trimmed = command.trim();
|
|
11983
11932
|
if (trimmed.length === 0 || SHELL_METACHARS.test(trimmed)) {
|
|
11984
11933
|
return;
|
|
@@ -12013,39 +11962,106 @@ function hookScriptPath(command) {
|
|
|
12013
11962
|
return;
|
|
12014
11963
|
}
|
|
12015
11964
|
if (candidate.startsWith("$")) {
|
|
12016
|
-
|
|
11965
|
+
if (PROJECT_DIR.test(candidate)) {
|
|
11966
|
+
return candidate;
|
|
11967
|
+
}
|
|
11968
|
+
return bases.plugin !== undefined && PLUGIN_ROOT.test(candidate) ? candidate : undefined;
|
|
12017
11969
|
}
|
|
12018
11970
|
if (candidate.startsWith("/") || candidate.startsWith("./") || candidate.startsWith("../") || candidate.startsWith("~/") || candidate.startsWith(".")) {
|
|
12019
11971
|
return candidate;
|
|
12020
11972
|
}
|
|
12021
11973
|
return;
|
|
12022
11974
|
}
|
|
12023
|
-
function
|
|
12024
|
-
|
|
11975
|
+
function isFile(path) {
|
|
11976
|
+
try {
|
|
11977
|
+
return statSync(path).isFile();
|
|
11978
|
+
} catch {
|
|
11979
|
+
return false;
|
|
11980
|
+
}
|
|
11981
|
+
}
|
|
11982
|
+
function resolveHookScript(bases, raw) {
|
|
11983
|
+
const extracted = hookScriptPath(raw, bases);
|
|
12025
11984
|
if (extracted === undefined) {
|
|
12026
11985
|
return;
|
|
12027
11986
|
}
|
|
12028
|
-
|
|
12029
|
-
|
|
12030
|
-
|
|
12031
|
-
|
|
12032
|
-
|
|
11987
|
+
if (extracted.startsWith("~/")) {
|
|
11988
|
+
return {
|
|
11989
|
+
scriptPath: extracted,
|
|
11990
|
+
exists: isFile(join3(homedir(), extracted.slice(2)))
|
|
11991
|
+
};
|
|
12033
11992
|
}
|
|
12034
|
-
|
|
12035
|
-
|
|
12036
|
-
return { scriptPath: extracted, exists:
|
|
12037
|
-
}
|
|
12038
|
-
|
|
11993
|
+
if (PROJECT_DIR.test(extracted)) {
|
|
11994
|
+
const rel = extracted.replace(/^\$(?:CLAUDE_PROJECT_DIR|\{CLAUDE_PROJECT_DIR\})\/?/, "");
|
|
11995
|
+
return { scriptPath: extracted, exists: isFile(join3(bases.project, rel)) };
|
|
11996
|
+
}
|
|
11997
|
+
if (bases.plugin !== undefined && PLUGIN_ROOT.test(extracted)) {
|
|
11998
|
+
const rel = extracted.replace(/^\$(?:CLAUDE_PLUGIN_ROOT|\{CLAUDE_PLUGIN_ROOT\})\/?/, "");
|
|
11999
|
+
return { scriptPath: extracted, exists: isFile(join3(bases.plugin, rel)) };
|
|
12000
|
+
}
|
|
12001
|
+
if (isAbsolute(extracted)) {
|
|
12002
|
+
return { scriptPath: extracted, exists: isFile(extracted) };
|
|
12039
12003
|
}
|
|
12004
|
+
const candidates = [
|
|
12005
|
+
...bases.own === undefined ? [] : [join3(bases.own, extracted)],
|
|
12006
|
+
join3(bases.project, extracted)
|
|
12007
|
+
];
|
|
12008
|
+
return { scriptPath: extracted, exists: candidates.some(isFile) };
|
|
12009
|
+
}
|
|
12010
|
+
function hooksFromObject(hooks, filePath, source, bases, errors3) {
|
|
12011
|
+
if (hooks === null || typeof hooks !== "object" || Array.isArray(hooks)) {
|
|
12012
|
+
errors3.push({
|
|
12013
|
+
path: filePath,
|
|
12014
|
+
kind: "unexpected-shape",
|
|
12015
|
+
detail: "`hooks` is not an object"
|
|
12016
|
+
});
|
|
12017
|
+
return [];
|
|
12018
|
+
}
|
|
12019
|
+
const facts = [];
|
|
12020
|
+
for (const [event, groups] of Object.entries(hooks)) {
|
|
12021
|
+
const malformed = !Array.isArray(groups) || groups.some((group) => {
|
|
12022
|
+
if (group === null || typeof group !== "object" || Array.isArray(group)) {
|
|
12023
|
+
return true;
|
|
12024
|
+
}
|
|
12025
|
+
const inner = group.hooks;
|
|
12026
|
+
if (!Array.isArray(inner))
|
|
12027
|
+
return true;
|
|
12028
|
+
return inner.some((entry) => {
|
|
12029
|
+
if (entry === null || typeof entry !== "object" || Array.isArray(entry)) {
|
|
12030
|
+
return true;
|
|
12031
|
+
}
|
|
12032
|
+
const item = entry;
|
|
12033
|
+
return item.command !== undefined && (typeof item.command !== "string" || item.command.length === 0) || item.type === "command" && typeof item.command !== "string";
|
|
12034
|
+
});
|
|
12035
|
+
});
|
|
12036
|
+
if (malformed) {
|
|
12037
|
+
errors3.push({ path: filePath, kind: "unexpected-shape", detail: `invalid hook groups for ${event}` });
|
|
12038
|
+
continue;
|
|
12039
|
+
}
|
|
12040
|
+
const commands = collectHookCommands(groups);
|
|
12041
|
+
if (commands.length === 0) {
|
|
12042
|
+
facts.push({ name: event, path: filePath, event, source });
|
|
12043
|
+
continue;
|
|
12044
|
+
}
|
|
12045
|
+
for (const command of commands) {
|
|
12046
|
+
const fact = { name: event, path: filePath, event, source, command };
|
|
12047
|
+
const resolved = resolveHookScript(bases, command);
|
|
12048
|
+
if (resolved !== undefined) {
|
|
12049
|
+
fact.scriptPath = resolved.scriptPath;
|
|
12050
|
+
fact.scriptExists = resolved.exists;
|
|
12051
|
+
}
|
|
12052
|
+
facts.push(fact);
|
|
12053
|
+
}
|
|
12054
|
+
}
|
|
12055
|
+
return facts;
|
|
12040
12056
|
}
|
|
12041
12057
|
function discoverHooks(root, errors3) {
|
|
12042
12058
|
const files = [
|
|
12043
|
-
|
|
12044
|
-
|
|
12059
|
+
join3(root, ".claude", "settings.json"),
|
|
12060
|
+
join3(root, ".claude", "settings.local.json")
|
|
12045
12061
|
];
|
|
12046
12062
|
const facts = [];
|
|
12047
12063
|
for (const filePath of files) {
|
|
12048
|
-
if (!
|
|
12064
|
+
if (!existsSync3(filePath)) {
|
|
12049
12065
|
continue;
|
|
12050
12066
|
}
|
|
12051
12067
|
const raw = readJsonConfig(filePath, errors3);
|
|
@@ -12064,49 +12080,7 @@ function discoverHooks(root, errors3) {
|
|
|
12064
12080
|
if (hooks === undefined) {
|
|
12065
12081
|
continue;
|
|
12066
12082
|
}
|
|
12067
|
-
|
|
12068
|
-
errors3.push({
|
|
12069
|
-
path: filePath,
|
|
12070
|
-
kind: "unexpected-shape",
|
|
12071
|
-
detail: "`hooks` is not an object"
|
|
12072
|
-
});
|
|
12073
|
-
continue;
|
|
12074
|
-
}
|
|
12075
|
-
for (const [event, groups] of Object.entries(hooks)) {
|
|
12076
|
-
const malformed = !Array.isArray(groups) || groups.some((group) => {
|
|
12077
|
-
if (group === null || typeof group !== "object" || Array.isArray(group)) {
|
|
12078
|
-
return true;
|
|
12079
|
-
}
|
|
12080
|
-
const inner = group.hooks;
|
|
12081
|
-
if (!Array.isArray(inner))
|
|
12082
|
-
return true;
|
|
12083
|
-
return inner.some((entry) => {
|
|
12084
|
-
if (entry === null || typeof entry !== "object" || Array.isArray(entry)) {
|
|
12085
|
-
return true;
|
|
12086
|
-
}
|
|
12087
|
-
const item = entry;
|
|
12088
|
-
return item.command !== undefined && (typeof item.command !== "string" || item.command.length === 0) || item.type === "command" && typeof item.command !== "string";
|
|
12089
|
-
});
|
|
12090
|
-
});
|
|
12091
|
-
if (malformed) {
|
|
12092
|
-
errors3.push({ path: filePath, kind: "unexpected-shape", detail: `invalid hook groups for ${event}` });
|
|
12093
|
-
continue;
|
|
12094
|
-
}
|
|
12095
|
-
const commands = collectHookCommands(groups);
|
|
12096
|
-
if (commands.length === 0) {
|
|
12097
|
-
facts.push({ name: event, path: filePath, event });
|
|
12098
|
-
continue;
|
|
12099
|
-
}
|
|
12100
|
-
for (const command of commands) {
|
|
12101
|
-
const fact = { name: event, path: filePath, event, command };
|
|
12102
|
-
const resolved = resolveHookScript(root, command);
|
|
12103
|
-
if (resolved !== undefined) {
|
|
12104
|
-
fact.scriptPath = resolved.scriptPath;
|
|
12105
|
-
fact.scriptExists = resolved.exists;
|
|
12106
|
-
}
|
|
12107
|
-
facts.push(fact);
|
|
12108
|
-
}
|
|
12109
|
-
}
|
|
12083
|
+
facts.push(...hooksFromObject(hooks, filePath, "settings", { project: root }, errors3));
|
|
12110
12084
|
}
|
|
12111
12085
|
return facts;
|
|
12112
12086
|
}
|
|
@@ -12136,6 +12110,84 @@ function collectHookCommands(groups) {
|
|
|
12136
12110
|
return out;
|
|
12137
12111
|
}
|
|
12138
12112
|
|
|
12113
|
+
// src/discover/agents.ts
|
|
12114
|
+
function agentFiles(dir, errors3) {
|
|
12115
|
+
let entries;
|
|
12116
|
+
try {
|
|
12117
|
+
entries = readdirSync(dir, { withFileTypes: true }).map((e) => e.isDirectory() ? `${e.name}/` : e.name);
|
|
12118
|
+
} catch (err) {
|
|
12119
|
+
errors3.push({
|
|
12120
|
+
path: dir,
|
|
12121
|
+
kind: "unreadable",
|
|
12122
|
+
detail: err instanceof Error ? err.message : String(err)
|
|
12123
|
+
});
|
|
12124
|
+
return [];
|
|
12125
|
+
}
|
|
12126
|
+
const out = [];
|
|
12127
|
+
for (const entry of entries) {
|
|
12128
|
+
if (entry.startsWith(".")) {
|
|
12129
|
+
continue;
|
|
12130
|
+
}
|
|
12131
|
+
if (entry.endsWith("/")) {
|
|
12132
|
+
out.push(...agentFiles(join4(dir, entry.slice(0, -1)), errors3).map((p) => join4(entry.slice(0, -1), p)));
|
|
12133
|
+
continue;
|
|
12134
|
+
}
|
|
12135
|
+
out.push(entry);
|
|
12136
|
+
}
|
|
12137
|
+
return out;
|
|
12138
|
+
}
|
|
12139
|
+
function discoverAgents(root, errors3) {
|
|
12140
|
+
const dir = join4(root, ".claude", "agents");
|
|
12141
|
+
if (!existsSync4(dir)) {
|
|
12142
|
+
return [];
|
|
12143
|
+
}
|
|
12144
|
+
const entries = agentFiles(dir, errors3);
|
|
12145
|
+
const facts = [];
|
|
12146
|
+
for (const name of entries) {
|
|
12147
|
+
if (!name.endsWith(".md")) {
|
|
12148
|
+
continue;
|
|
12149
|
+
}
|
|
12150
|
+
const filePath = join4(dir, name);
|
|
12151
|
+
try {
|
|
12152
|
+
const st = statSync2(filePath);
|
|
12153
|
+
if (!st.isFile()) {
|
|
12154
|
+
continue;
|
|
12155
|
+
}
|
|
12156
|
+
} catch {
|
|
12157
|
+
continue;
|
|
12158
|
+
}
|
|
12159
|
+
const fm = readFrontmatter(filePath, errors3);
|
|
12160
|
+
const fact = {
|
|
12161
|
+
name: name.slice(0, -".md".length),
|
|
12162
|
+
path: filePath,
|
|
12163
|
+
hasFrontmatter: fm.hasFrontmatter
|
|
12164
|
+
};
|
|
12165
|
+
if (fm.unreadable === true) {
|
|
12166
|
+
fact.unreadable = true;
|
|
12167
|
+
}
|
|
12168
|
+
if (fm.unparseable === true) {
|
|
12169
|
+
fact.unparseableFrontmatter = true;
|
|
12170
|
+
}
|
|
12171
|
+
if (fm.name !== undefined) {
|
|
12172
|
+
fact.frontmatterName = fm.name;
|
|
12173
|
+
}
|
|
12174
|
+
if (fm.description !== undefined) {
|
|
12175
|
+
fact.description = fm.description;
|
|
12176
|
+
}
|
|
12177
|
+
if (fm.hooks !== undefined) {
|
|
12178
|
+
const hooks = hooksFromObject(fm.hooks, filePath, "agent", {
|
|
12179
|
+
project: root,
|
|
12180
|
+
own: dirname2(filePath)
|
|
12181
|
+
}, errors3);
|
|
12182
|
+
if (hooks.length > 0) {
|
|
12183
|
+
fact.frontmatterHooks = hooks;
|
|
12184
|
+
}
|
|
12185
|
+
}
|
|
12186
|
+
facts.push(fact);
|
|
12187
|
+
}
|
|
12188
|
+
return facts;
|
|
12189
|
+
}
|
|
12190
|
+
|
|
12139
12191
|
// src/discover/mcp.ts
|
|
12140
12192
|
import { existsSync as existsSync5, statSync as statSync3 } from "node:fs";
|
|
12141
12193
|
import { homedir as homedir2 } from "node:os";
|
|
@@ -12270,14 +12322,87 @@ function discoverMcp(root, mcpPaths, errors3) {
|
|
|
12270
12322
|
return facts;
|
|
12271
12323
|
}
|
|
12272
12324
|
|
|
12273
|
-
// src/discover/
|
|
12274
|
-
import { existsSync as existsSync6 } from "node:fs";
|
|
12325
|
+
// src/discover/plugins.ts
|
|
12326
|
+
import { existsSync as existsSync6, readdirSync as readdirSync2 } from "node:fs";
|
|
12275
12327
|
import { join as join6 } from "node:path";
|
|
12328
|
+
function findPluginRoots(root, errors3) {
|
|
12329
|
+
const found = [];
|
|
12330
|
+
if (existsSync6(join6(root, "hooks", "hooks.json"))) {
|
|
12331
|
+
found.push(root);
|
|
12332
|
+
}
|
|
12333
|
+
const walk = (dir, depth) => {
|
|
12334
|
+
let entries;
|
|
12335
|
+
try {
|
|
12336
|
+
entries = readdirSync2(dir, { withFileTypes: true });
|
|
12337
|
+
} catch {
|
|
12338
|
+
errors3.push({
|
|
12339
|
+
path: dir,
|
|
12340
|
+
kind: "unreadable",
|
|
12341
|
+
detail: "could not read directories while looking for plugins"
|
|
12342
|
+
});
|
|
12343
|
+
return;
|
|
12344
|
+
}
|
|
12345
|
+
for (const entry of entries) {
|
|
12346
|
+
if (!entry.isDirectory() || NESTED_DISCOVERY_SKIP.has(entry.name)) {
|
|
12347
|
+
continue;
|
|
12348
|
+
}
|
|
12349
|
+
if (entry.name.startsWith(".") && entry.name !== ".claude") {
|
|
12350
|
+
continue;
|
|
12351
|
+
}
|
|
12352
|
+
const child = join6(dir, entry.name);
|
|
12353
|
+
if (existsSync6(join6(child, ".claude-plugin", "plugin.json"))) {
|
|
12354
|
+
found.push(child);
|
|
12355
|
+
continue;
|
|
12356
|
+
}
|
|
12357
|
+
if (depth >= NESTED_DISCOVERY_MAX_DEPTH) {
|
|
12358
|
+
continue;
|
|
12359
|
+
}
|
|
12360
|
+
walk(child, depth + 1);
|
|
12361
|
+
}
|
|
12362
|
+
};
|
|
12363
|
+
walk(root, 0);
|
|
12364
|
+
return found;
|
|
12365
|
+
}
|
|
12366
|
+
function discoverPluginHooks(root, errors3) {
|
|
12367
|
+
const facts = [];
|
|
12368
|
+
for (const pluginRoot of findPluginRoots(root, errors3)) {
|
|
12369
|
+
const filePath = join6(pluginRoot, "hooks", "hooks.json");
|
|
12370
|
+
if (!existsSync6(filePath)) {
|
|
12371
|
+
continue;
|
|
12372
|
+
}
|
|
12373
|
+
const raw = readJsonConfig(filePath, errors3);
|
|
12374
|
+
if (raw === undefined) {
|
|
12375
|
+
continue;
|
|
12376
|
+
}
|
|
12377
|
+
if (raw === null || typeof raw !== "object" || Array.isArray(raw)) {
|
|
12378
|
+
errors3.push({
|
|
12379
|
+
path: filePath,
|
|
12380
|
+
kind: "unexpected-shape",
|
|
12381
|
+
detail: "hooks.json is not a JSON object"
|
|
12382
|
+
});
|
|
12383
|
+
continue;
|
|
12384
|
+
}
|
|
12385
|
+
const hooks = raw.hooks;
|
|
12386
|
+
if (hooks === undefined) {
|
|
12387
|
+
continue;
|
|
12388
|
+
}
|
|
12389
|
+
facts.push(...hooksFromObject(hooks, filePath, "plugin", {
|
|
12390
|
+
project: root,
|
|
12391
|
+
plugin: pluginRoot,
|
|
12392
|
+
own: pluginRoot
|
|
12393
|
+
}, errors3));
|
|
12394
|
+
}
|
|
12395
|
+
return facts;
|
|
12396
|
+
}
|
|
12397
|
+
|
|
12398
|
+
// src/discover/policy.ts
|
|
12399
|
+
import { existsSync as existsSync7 } from "node:fs";
|
|
12400
|
+
import { join as join7 } from "node:path";
|
|
12276
12401
|
function discoverPolicyFiles(root, policyFiles, errors3) {
|
|
12277
12402
|
const out = [];
|
|
12278
12403
|
for (const rel of policyFiles) {
|
|
12279
|
-
const filePath =
|
|
12280
|
-
if (!
|
|
12404
|
+
const filePath = join7(root, rel);
|
|
12405
|
+
if (!existsSync7(filePath)) {
|
|
12281
12406
|
continue;
|
|
12282
12407
|
}
|
|
12283
12408
|
try {
|
|
@@ -12297,8 +12422,8 @@ function discoverPolicyFiles(root, policyFiles, errors3) {
|
|
|
12297
12422
|
return out;
|
|
12298
12423
|
}
|
|
12299
12424
|
function discoverSkillsLock(root, errors3) {
|
|
12300
|
-
const filePath =
|
|
12301
|
-
if (!
|
|
12425
|
+
const filePath = join7(root, "skills-lock.json");
|
|
12426
|
+
if (!existsSync7(filePath)) {
|
|
12302
12427
|
return { locked: [], present: false };
|
|
12303
12428
|
}
|
|
12304
12429
|
const raw = readJsonConfig(filePath, errors3);
|
|
@@ -12343,8 +12468,8 @@ function discoverSkillsLock(root, errors3) {
|
|
|
12343
12468
|
}
|
|
12344
12469
|
|
|
12345
12470
|
// src/discover/skills.ts
|
|
12346
|
-
import { existsSync as
|
|
12347
|
-
import { join as
|
|
12471
|
+
import { existsSync as existsSync8, readdirSync as readdirSync3, statSync as statSync4 } from "node:fs";
|
|
12472
|
+
import { join as join8, resolve as resolve2 } from "node:path";
|
|
12348
12473
|
var BUNDLED = "scripts|references|assets|templates|examples";
|
|
12349
12474
|
var REFERENCE_RE = new RegExp(`(?<![\\w/.-])(?:\\./)?((?:${BUNDLED})/[A-Za-z0-9_./-]+\\.[A-Za-z0-9]{1,10})(?![A-Za-z0-9_.-])`, "g");
|
|
12350
12475
|
var FENCE_BLOCK = /^[ \t]*(`{3,}|~{3,})[^\n]*\n[\s\S]*?(?:^[ \t]*\1[ \t]*$|$)/gm;
|
|
@@ -12365,22 +12490,22 @@ function skillReferences(body) {
|
|
|
12365
12490
|
return out;
|
|
12366
12491
|
}
|
|
12367
12492
|
function brokenReferences(body, skillDir, root) {
|
|
12368
|
-
const
|
|
12493
|
+
const isFile2 = (p) => {
|
|
12369
12494
|
try {
|
|
12370
12495
|
return statSync4(p).isFile();
|
|
12371
12496
|
} catch {
|
|
12372
12497
|
return false;
|
|
12373
12498
|
}
|
|
12374
12499
|
};
|
|
12375
|
-
return skillReferences(body).filter((rel) => !
|
|
12500
|
+
return skillReferences(body).filter((rel) => !isFile2(join8(skillDir, rel)) && !isFile2(join8(root, rel)));
|
|
12376
12501
|
}
|
|
12377
12502
|
function discoverSkillsInDir(dir, source, errors3, root) {
|
|
12378
|
-
if (!
|
|
12503
|
+
if (!existsSync8(dir)) {
|
|
12379
12504
|
return [];
|
|
12380
12505
|
}
|
|
12381
12506
|
let entries;
|
|
12382
12507
|
try {
|
|
12383
|
-
entries =
|
|
12508
|
+
entries = readdirSync3(dir);
|
|
12384
12509
|
} catch (err) {
|
|
12385
12510
|
errors3.push({
|
|
12386
12511
|
path: dir,
|
|
@@ -12396,7 +12521,7 @@ function discoverSkillsInDir(dir, source, errors3, root) {
|
|
|
12396
12521
|
if (name.startsWith(".") || name === "node_modules") {
|
|
12397
12522
|
continue;
|
|
12398
12523
|
}
|
|
12399
|
-
const skillDir =
|
|
12524
|
+
const skillDir = join8(dir, name);
|
|
12400
12525
|
let st;
|
|
12401
12526
|
try {
|
|
12402
12527
|
st = statSync4(skillDir);
|
|
@@ -12406,10 +12531,10 @@ function discoverSkillsInDir(dir, source, errors3, root) {
|
|
|
12406
12531
|
if (!st.isDirectory()) {
|
|
12407
12532
|
continue;
|
|
12408
12533
|
}
|
|
12409
|
-
const skillMd =
|
|
12534
|
+
const skillMd = join8(skillDir, "SKILL.md");
|
|
12410
12535
|
let dirReadable = true;
|
|
12411
12536
|
try {
|
|
12412
|
-
|
|
12537
|
+
readdirSync3(skillDir);
|
|
12413
12538
|
} catch (err) {
|
|
12414
12539
|
dirReadable = false;
|
|
12415
12540
|
errors3.push({
|
|
@@ -12418,7 +12543,7 @@ function discoverSkillsInDir(dir, source, errors3, root) {
|
|
|
12418
12543
|
detail: err instanceof Error ? err.message : String(err)
|
|
12419
12544
|
});
|
|
12420
12545
|
}
|
|
12421
|
-
const hasSkillMd = dirReadable &&
|
|
12546
|
+
const hasSkillMd = dirReadable && existsSync8(skillMd);
|
|
12422
12547
|
const fm = hasSkillMd ? readFrontmatter(skillMd, errors3) : { hasFrontmatter: false };
|
|
12423
12548
|
const fact = {
|
|
12424
12549
|
id: name,
|
|
@@ -12446,26 +12571,25 @@ function discoverSkillsInDir(dir, source, errors3, root) {
|
|
|
12446
12571
|
if (fm.name !== undefined) {
|
|
12447
12572
|
fact.frontmatterName = fm.name;
|
|
12448
12573
|
}
|
|
12574
|
+
if (fm.hooks !== undefined) {
|
|
12575
|
+
const hooks = hooksFromObject(fm.hooks, skillMd, "skill", {
|
|
12576
|
+
project: root,
|
|
12577
|
+
own: skillDir
|
|
12578
|
+
}, errors3);
|
|
12579
|
+
if (hooks.length > 0) {
|
|
12580
|
+
fact.frontmatterHooks = hooks;
|
|
12581
|
+
}
|
|
12582
|
+
}
|
|
12449
12583
|
skills.push(fact);
|
|
12450
12584
|
}
|
|
12451
12585
|
return skills;
|
|
12452
12586
|
}
|
|
12453
|
-
var NESTED_DISCOVERY_MAX_DEPTH = 8;
|
|
12454
|
-
var NESTED_DISCOVERY_SKIP = new Set([
|
|
12455
|
-
".git",
|
|
12456
|
-
"node_modules",
|
|
12457
|
-
".next",
|
|
12458
|
-
"dist",
|
|
12459
|
-
"build",
|
|
12460
|
-
"coverage",
|
|
12461
|
-
"vendor"
|
|
12462
|
-
]);
|
|
12463
12587
|
function discoverNestedClaudeSkills(root, configuredRoots, errors3, options = {}) {
|
|
12464
12588
|
const out = [];
|
|
12465
12589
|
const read = (dir) => {
|
|
12466
12590
|
options.onDirectoryRead?.();
|
|
12467
12591
|
try {
|
|
12468
|
-
return
|
|
12592
|
+
return readdirSync3(dir, { withFileTypes: true });
|
|
12469
12593
|
} catch {
|
|
12470
12594
|
errors3.push({ path: dir, kind: "unreadable", detail: "could not read nested skill directories" });
|
|
12471
12595
|
return;
|
|
@@ -12478,7 +12602,7 @@ function discoverNestedClaudeSkills(root, configuredRoots, errors3, options = {}
|
|
|
12478
12602
|
for (const entry of entries) {
|
|
12479
12603
|
if (!entry.isDirectory() || entry.name !== "skills")
|
|
12480
12604
|
continue;
|
|
12481
|
-
const child =
|
|
12605
|
+
const child = join8(dir, entry.name);
|
|
12482
12606
|
if (configuredRoots.has(resolve2(child)))
|
|
12483
12607
|
continue;
|
|
12484
12608
|
out.push(...discoverSkillsInDir(child, "project", errors3, root));
|
|
@@ -12493,7 +12617,7 @@ function discoverNestedClaudeSkills(root, configuredRoots, errors3, options = {}
|
|
|
12493
12617
|
continue;
|
|
12494
12618
|
if (entry.name.startsWith(".") && entry.name !== ".claude")
|
|
12495
12619
|
continue;
|
|
12496
|
-
const child =
|
|
12620
|
+
const child = join8(dir, entry.name);
|
|
12497
12621
|
if (entry.name === ".claude") {
|
|
12498
12622
|
inspectClaude(child);
|
|
12499
12623
|
continue;
|
|
@@ -12523,7 +12647,7 @@ function discoverAgentSurface(root, config, opts) {
|
|
|
12523
12647
|
const configErrors = [];
|
|
12524
12648
|
const skills = [];
|
|
12525
12649
|
for (const rel of config.skillPaths) {
|
|
12526
|
-
skills.push(...discoverSkillsInDir(
|
|
12650
|
+
skills.push(...discoverSkillsInDir(join9(root, rel), "project", configErrors, root));
|
|
12527
12651
|
}
|
|
12528
12652
|
const configuredRoots = new Set(config.skillPaths.map((rel) => resolve3(root, rel)));
|
|
12529
12653
|
skills.push(...discoverNestedClaudeSkills(root, configuredRoots, configErrors, {
|
|
@@ -12531,14 +12655,20 @@ function discoverAgentSurface(root, config, opts) {
|
|
|
12531
12655
|
}));
|
|
12532
12656
|
if (opts.includeGlobal) {
|
|
12533
12657
|
const home = homedir3();
|
|
12534
|
-
skills.push(...discoverSkillsInDir(
|
|
12535
|
-
skills.push(...discoverSkillsInDir(
|
|
12658
|
+
skills.push(...discoverSkillsInDir(join9(home, ".claude", "skills"), "global", configErrors, root));
|
|
12659
|
+
skills.push(...discoverSkillsInDir(join9(home, ".codex", "skills"), "global", configErrors, root));
|
|
12536
12660
|
}
|
|
12537
12661
|
const lock = discoverSkillsLock(root, configErrors);
|
|
12662
|
+
const agents = discoverAgents(root, configErrors);
|
|
12538
12663
|
return {
|
|
12539
12664
|
skills: disambiguateSkills(skills, root),
|
|
12540
|
-
agents
|
|
12541
|
-
hooks:
|
|
12665
|
+
agents,
|
|
12666
|
+
hooks: [
|
|
12667
|
+
...discoverHooks(root, configErrors),
|
|
12668
|
+
...discoverPluginHooks(root, configErrors),
|
|
12669
|
+
...skills.flatMap((s) => s.frontmatterHooks ?? []),
|
|
12670
|
+
...agents.flatMap((a) => a.frontmatterHooks ?? [])
|
|
12671
|
+
],
|
|
12542
12672
|
mcp: discoverMcp(root, config.mcpPaths, configErrors),
|
|
12543
12673
|
policyFiles: discoverPolicyFiles(root, config.policyFiles, configErrors),
|
|
12544
12674
|
lockedSkills: lock.locked,
|
|
@@ -12549,11 +12679,11 @@ function discoverAgentSurface(root, config, opts) {
|
|
|
12549
12679
|
}
|
|
12550
12680
|
|
|
12551
12681
|
// src/facts/extract.ts
|
|
12552
|
-
import { existsSync as
|
|
12553
|
-
import { join as
|
|
12682
|
+
import { existsSync as existsSync9, readFileSync as readFileSync3 } from "node:fs";
|
|
12683
|
+
import { join as join10 } from "node:path";
|
|
12554
12684
|
function readPackageJson(root, errors3) {
|
|
12555
|
-
const path =
|
|
12556
|
-
if (!
|
|
12685
|
+
const path = join10(root, "package.json");
|
|
12686
|
+
if (!existsSync9(path)) {
|
|
12557
12687
|
return {};
|
|
12558
12688
|
}
|
|
12559
12689
|
let text;
|
|
@@ -12651,16 +12781,16 @@ function inferPackageManager(root, pkg) {
|
|
|
12651
12781
|
return prefix;
|
|
12652
12782
|
}
|
|
12653
12783
|
}
|
|
12654
|
-
if (
|
|
12784
|
+
if (existsSync9(join10(root, "bun.lock")) || existsSync9(join10(root, "bun.lockb"))) {
|
|
12655
12785
|
return "bun";
|
|
12656
12786
|
}
|
|
12657
|
-
if (
|
|
12787
|
+
if (existsSync9(join10(root, "pnpm-lock.yaml"))) {
|
|
12658
12788
|
return "pnpm";
|
|
12659
12789
|
}
|
|
12660
|
-
if (
|
|
12790
|
+
if (existsSync9(join10(root, "yarn.lock"))) {
|
|
12661
12791
|
return "yarn";
|
|
12662
12792
|
}
|
|
12663
|
-
if (
|
|
12793
|
+
if (existsSync9(join10(root, "package-lock.json"))) {
|
|
12664
12794
|
return "npm";
|
|
12665
12795
|
}
|
|
12666
12796
|
return "unknown";
|
|
@@ -13135,7 +13265,7 @@ function formatSummary(findings, verbose, ink) {
|
|
|
13135
13265
|
}
|
|
13136
13266
|
|
|
13137
13267
|
// src/version.ts
|
|
13138
|
-
var VERSION = "0.
|
|
13268
|
+
var VERSION = "0.7.0";
|
|
13139
13269
|
|
|
13140
13270
|
// src/commands/check.ts
|
|
13141
13271
|
async function runCheck(options) {
|
|
@@ -13182,13 +13312,13 @@ import {
|
|
|
13182
13312
|
writeFileSync
|
|
13183
13313
|
} from "node:fs";
|
|
13184
13314
|
import { tmpdir } from "node:os";
|
|
13185
|
-
import { join as
|
|
13315
|
+
import { join as join11 } from "node:path";
|
|
13186
13316
|
async function runDemo() {
|
|
13187
|
-
const root = mkdtempSync(
|
|
13317
|
+
const root = mkdtempSync(join11(tmpdir(), "agentscan-demo-"));
|
|
13188
13318
|
try {
|
|
13189
|
-
const claude =
|
|
13319
|
+
const claude = join11(root, ".claude");
|
|
13190
13320
|
mkdirSync(claude, { recursive: true });
|
|
13191
|
-
writeFileSync(
|
|
13321
|
+
writeFileSync(join11(claude, "settings.json"), `${JSON.stringify({
|
|
13192
13322
|
hooks: {
|
|
13193
13323
|
PreToolUse: [
|
|
13194
13324
|
{
|
|
@@ -13262,12 +13392,12 @@ function formatExplain(f) {
|
|
|
13262
13392
|
}
|
|
13263
13393
|
|
|
13264
13394
|
// src/commands/init.ts
|
|
13265
|
-
import { existsSync as
|
|
13266
|
-
import { join as
|
|
13395
|
+
import { existsSync as existsSync10, writeFileSync as writeFileSync2 } from "node:fs";
|
|
13396
|
+
import { join as join12, resolve as resolve5 } from "node:path";
|
|
13267
13397
|
async function runInit(dir, options = {}) {
|
|
13268
13398
|
const root = resolve5(dir ?? process.cwd());
|
|
13269
|
-
const path =
|
|
13270
|
-
if (
|
|
13399
|
+
const path = join12(root, ".agentscanrc.json");
|
|
13400
|
+
if (existsSync10(path) && !options.force) {
|
|
13271
13401
|
return {
|
|
13272
13402
|
exitCode: 2,
|
|
13273
13403
|
stdout: "",
|
|
@@ -13303,7 +13433,7 @@ async function runRulesCommand(options = {}) {
|
|
|
13303
13433
|
// src/report/clipboard.ts
|
|
13304
13434
|
import { spawn } from "node:child_process";
|
|
13305
13435
|
import { accessSync, constants } from "node:fs";
|
|
13306
|
-
import { delimiter, join as
|
|
13436
|
+
import { delimiter, join as join13 } from "node:path";
|
|
13307
13437
|
var TOOLS = [
|
|
13308
13438
|
["wl-copy"],
|
|
13309
13439
|
["xclip", "-selection", "clipboard"],
|
|
@@ -13320,7 +13450,7 @@ function resolve6(name) {
|
|
|
13320
13450
|
if (dir.length === 0) {
|
|
13321
13451
|
continue;
|
|
13322
13452
|
}
|
|
13323
|
-
const candidate =
|
|
13453
|
+
const candidate = join13(dir, name);
|
|
13324
13454
|
try {
|
|
13325
13455
|
accessSync(candidate, constants.X_OK);
|
|
13326
13456
|
return candidate;
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@chimix/agentscan",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.7.0",
|
|
4
4
|
"description": "Find broken agent configuration in a project — hooks whose scripts are gone, MCP servers misconfigured by schema, skills that disagree with their lockfile",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"bin": {
|
|
@@ -44,7 +44,7 @@
|
|
|
44
44
|
},
|
|
45
45
|
"engines": {
|
|
46
46
|
"node": ">=20.11.0",
|
|
47
|
-
"bun": ">=1.
|
|
47
|
+
"bun": ">=1.4.0"
|
|
48
48
|
},
|
|
49
49
|
"license": "MIT",
|
|
50
50
|
"dependencies": {
|