@chimix/agentscan 0.2.1 → 0.4.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 +19 -11
- package/dist/cli.js +177 -21
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -164,6 +164,7 @@ Flags for `check`:
|
|
|
164
164
|
| `--json` | JSON report (alias for `--output json`) |
|
|
165
165
|
| `--output <format>` | `human` (default) · `json` · `prompt` |
|
|
166
166
|
| `--copy` | Also copy the report to the system clipboard |
|
|
167
|
+
| `--no-color` | Never colour, even on a terminal (`NO_COLOR=1` does the same) |
|
|
167
168
|
| `--quiet` | Summary line only |
|
|
168
169
|
| `--verbose` | Show KEEP + info-severity findings |
|
|
169
170
|
| `--fail-on <level>` | `never` (default) · `warning` · `error` |
|
|
@@ -176,23 +177,30 @@ Flags for `check`:
|
|
|
176
177
|
## Sample output
|
|
177
178
|
|
|
178
179
|
```text
|
|
179
|
-
|
|
180
|
+
┌─────┐ 40/100 broken · touchagency
|
|
181
|
+
│ ✕ ✕ │ ██████████░░░░░░░░░░░░░░░
|
|
182
|
+
│ ⌒ │ 6 warn · 4 info hidden (--verbose)
|
|
183
|
+
└─────┘
|
|
180
184
|
|
|
181
|
-
|
|
185
|
+
agentscan v0.4.0 — touchagency
|
|
182
186
|
|
|
183
|
-
|
|
184
|
-
rule:hook.missing-script
|
|
185
|
-
PreToolUse hook points at a script that does not exist: .claude/hooks/guard-destructive-bash.js
|
|
186
|
-
evidence: hook PreToolUse @ …/.claude/settings.json · script .claude/hooks/guard-destructive-bash.js
|
|
187
|
+
Scanned: 46 deps · 54 skills · 1 mcp · 2 agents · packageManager=bun
|
|
187
188
|
|
|
188
|
-
WARN hook
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
189
|
+
WARN rule:hook.missing-script ×6
|
|
190
|
+
PreToolUse hook points at a script that does not exist
|
|
191
|
+
PostToolUse @ .claude/settings.json · .claude/hooks/notify-related-tests.js
|
|
192
|
+
PreToolUse @ .claude/settings.json · .claude/hooks/guard-destructive-bash.js
|
|
193
|
+
PreToolUse @ .claude/settings.json · .claude/hooks/protect-artists-json.js
|
|
194
|
+
PreToolUse @ .claude/settings.json · .claude/hooks/protect-env.js
|
|
192
195
|
|
|
193
|
-
Summary: 6 warn · 4 info hidden (--verbose) · score 40/100
|
|
196
|
+
Summary: 6 warn · 4 info hidden (--verbose) · score 40/100 broken
|
|
194
197
|
```
|
|
195
198
|
|
|
199
|
+
On a terminal the header is coloured green, yellow or red and the face tracks
|
|
200
|
+
the score. Redirect or pipe it and you get exactly the text above, with no
|
|
201
|
+
escape sequences — the header box is dropped too, so `--json`, `--output
|
|
202
|
+
prompt`, CI logs and `--copy` are unchanged by any of this.
|
|
203
|
+
|
|
196
204
|
The `Stack:` line is orientation only — which project, how big. The summary lists
|
|
197
205
|
only actions that actually occurred; a clean project prints `Summary: no findings`.
|
|
198
206
|
|
package/dist/cli.js
CHANGED
|
@@ -12534,6 +12534,45 @@ function score(findings) {
|
|
|
12534
12534
|
}
|
|
12535
12535
|
return Math.max(0, 100 - deduction);
|
|
12536
12536
|
}
|
|
12537
|
+
function scoreLabel(points) {
|
|
12538
|
+
if (points === 100) {
|
|
12539
|
+
return "clean";
|
|
12540
|
+
}
|
|
12541
|
+
if (points >= 90) {
|
|
12542
|
+
return "good";
|
|
12543
|
+
}
|
|
12544
|
+
if (points >= 70) {
|
|
12545
|
+
return "needs work";
|
|
12546
|
+
}
|
|
12547
|
+
if (points >= 50) {
|
|
12548
|
+
return "poor";
|
|
12549
|
+
}
|
|
12550
|
+
return "broken";
|
|
12551
|
+
}
|
|
12552
|
+
function scoreTone(points) {
|
|
12553
|
+
if (points >= 90) {
|
|
12554
|
+
return "green";
|
|
12555
|
+
}
|
|
12556
|
+
if (points >= 50) {
|
|
12557
|
+
return "yellow";
|
|
12558
|
+
}
|
|
12559
|
+
return "red";
|
|
12560
|
+
}
|
|
12561
|
+
function scoreFace(points) {
|
|
12562
|
+
if (points === 100) {
|
|
12563
|
+
return ["◠ ◠", " ▽ "];
|
|
12564
|
+
}
|
|
12565
|
+
if (points >= 90) {
|
|
12566
|
+
return ["• •", " ‿ "];
|
|
12567
|
+
}
|
|
12568
|
+
if (points >= 70) {
|
|
12569
|
+
return ["• •", " ─ "];
|
|
12570
|
+
}
|
|
12571
|
+
if (points >= 50) {
|
|
12572
|
+
return ["◡ ◡", " ⌒ "];
|
|
12573
|
+
}
|
|
12574
|
+
return ["✕ ✕", " ⌒ "];
|
|
12575
|
+
}
|
|
12537
12576
|
|
|
12538
12577
|
// src/report/exit-code.ts
|
|
12539
12578
|
function exitCode(findings, failOn, failUnder) {
|
|
@@ -12658,6 +12697,50 @@ function headingFor(severity) {
|
|
|
12658
12697
|
|
|
12659
12698
|
// src/report/text.ts
|
|
12660
12699
|
import { basename as basename4 } from "node:path";
|
|
12700
|
+
|
|
12701
|
+
// src/report/ansi.ts
|
|
12702
|
+
var CODES = {
|
|
12703
|
+
green: "\x1B[32m",
|
|
12704
|
+
yellow: "\x1B[33m",
|
|
12705
|
+
red: "\x1B[31m",
|
|
12706
|
+
dim: "\x1B[2m",
|
|
12707
|
+
bold: "\x1B[1m",
|
|
12708
|
+
reset: "\x1B[0m"
|
|
12709
|
+
};
|
|
12710
|
+
var PLAIN = {
|
|
12711
|
+
tone: (_tone, text) => text,
|
|
12712
|
+
strong: (_tone, text) => text,
|
|
12713
|
+
dim: (text) => text,
|
|
12714
|
+
bold: (text) => text,
|
|
12715
|
+
enabled: false
|
|
12716
|
+
};
|
|
12717
|
+
function paint(enabled) {
|
|
12718
|
+
if (!enabled) {
|
|
12719
|
+
return PLAIN;
|
|
12720
|
+
}
|
|
12721
|
+
return {
|
|
12722
|
+
tone: (tone, text) => `${CODES[tone]}${text}${CODES.reset}`,
|
|
12723
|
+
strong: (tone, text) => `${CODES.bold}${CODES[tone]}${text}${CODES.reset}`,
|
|
12724
|
+
dim: (text) => `${CODES.dim}${text}${CODES.reset}`,
|
|
12725
|
+
bold: (text) => `${CODES.bold}${text}${CODES.reset}`,
|
|
12726
|
+
enabled: true
|
|
12727
|
+
};
|
|
12728
|
+
}
|
|
12729
|
+
function shouldColour(args) {
|
|
12730
|
+
if (args.noColorFlag === true) {
|
|
12731
|
+
return false;
|
|
12732
|
+
}
|
|
12733
|
+
const { NO_COLOR, FORCE_COLOR } = args.env;
|
|
12734
|
+
if (NO_COLOR !== undefined && NO_COLOR !== "") {
|
|
12735
|
+
return false;
|
|
12736
|
+
}
|
|
12737
|
+
if (FORCE_COLOR !== undefined && FORCE_COLOR !== "" && FORCE_COLOR !== "0") {
|
|
12738
|
+
return true;
|
|
12739
|
+
}
|
|
12740
|
+
return args.isTTY;
|
|
12741
|
+
}
|
|
12742
|
+
|
|
12743
|
+
// src/report/text.ts
|
|
12661
12744
|
var ACTION_LABEL = {
|
|
12662
12745
|
keep: "KEEP",
|
|
12663
12746
|
delete: "DELETE",
|
|
@@ -12671,24 +12754,28 @@ function actionColumn(action) {
|
|
|
12671
12754
|
}
|
|
12672
12755
|
function renderText(args) {
|
|
12673
12756
|
const { version, facts, verbose, quiet } = args;
|
|
12757
|
+
const ink = paint(args.colour === true);
|
|
12674
12758
|
const sorted = sortFindings(args.findings);
|
|
12675
12759
|
const visible = verbose ? sorted : sorted.filter((f) => f.action !== "keep" && f.severity !== "info");
|
|
12676
|
-
const summary = formatSummary(sorted, verbose);
|
|
12760
|
+
const summary = formatSummary(sorted, verbose, ink);
|
|
12677
12761
|
if (quiet) {
|
|
12678
12762
|
return `${summary}
|
|
12679
12763
|
`;
|
|
12680
12764
|
}
|
|
12681
12765
|
const lines = [];
|
|
12682
12766
|
const name = projectLabel(facts.root);
|
|
12683
|
-
|
|
12767
|
+
if (ink.enabled) {
|
|
12768
|
+
lines.push(...formatHeaderBox(sorted, verbose, safe(name), ink));
|
|
12769
|
+
}
|
|
12770
|
+
lines.push(ink.dim(`agentscan v${version} — ${safe(name)}`));
|
|
12684
12771
|
if (args.resolvedFrom !== undefined) {
|
|
12685
12772
|
lines.push(` no package.json in ${safe(args.resolvedFrom)} — scanned ${safe(facts.root)}`);
|
|
12686
12773
|
}
|
|
12687
12774
|
lines.push("");
|
|
12688
|
-
lines.push(`
|
|
12775
|
+
lines.push(ink.dim(`Scanned: ${formatStack(facts)}`));
|
|
12689
12776
|
lines.push("");
|
|
12690
|
-
for (const
|
|
12691
|
-
lines.push(...
|
|
12777
|
+
for (const group of groupByRule(visible)) {
|
|
12778
|
+
lines.push(...formatGroup(group, facts.root, ink));
|
|
12692
12779
|
lines.push("");
|
|
12693
12780
|
}
|
|
12694
12781
|
lines.push(summary);
|
|
@@ -12696,6 +12783,22 @@ function renderText(args) {
|
|
|
12696
12783
|
return lines.join(`
|
|
12697
12784
|
`);
|
|
12698
12785
|
}
|
|
12786
|
+
var BAR_CELLS = 25;
|
|
12787
|
+
function formatHeaderBox(findings, verbose, name, ink) {
|
|
12788
|
+
const points = score(findings);
|
|
12789
|
+
const tone = scoreTone(points);
|
|
12790
|
+
const [eyes, mouth] = scoreFace(points);
|
|
12791
|
+
const filled = Math.round(points / 100 * BAR_CELLS);
|
|
12792
|
+
const bar = ink.tone(tone, "█".repeat(filled)) + ink.dim("░".repeat(BAR_CELLS - filled));
|
|
12793
|
+
const verdict = `${ink.strong(tone, `${points}/100`)} ${ink.tone(tone, scoreLabel(points))}${ink.dim(` · ${name}`)}`;
|
|
12794
|
+
return [
|
|
12795
|
+
` ${ink.tone(tone, "┌─────┐")} ${verdict}`,
|
|
12796
|
+
` ${ink.tone(tone, `│ ${eyes} │`)} ${bar}`,
|
|
12797
|
+
` ${ink.tone(tone, `│ ${mouth} │`)} ${ink.dim(countsLine(findings, verbose))}`,
|
|
12798
|
+
` ${ink.tone(tone, "└─────┘")}`,
|
|
12799
|
+
""
|
|
12800
|
+
];
|
|
12801
|
+
}
|
|
12699
12802
|
function projectLabel(root) {
|
|
12700
12803
|
const base = basename4(root);
|
|
12701
12804
|
return base.length > 0 ? base : root;
|
|
@@ -12712,18 +12815,52 @@ function formatStack(facts) {
|
|
|
12712
12815
|
`packageManager=${facts.packageManager}`
|
|
12713
12816
|
].join(" · ");
|
|
12714
12817
|
}
|
|
12715
|
-
function
|
|
12818
|
+
function groupByRule(findings) {
|
|
12819
|
+
const byRule = new Map;
|
|
12820
|
+
for (const f of findings) {
|
|
12821
|
+
const bucket = byRule.get(f.ruleId);
|
|
12822
|
+
if (bucket === undefined) {
|
|
12823
|
+
byRule.set(f.ruleId, [f]);
|
|
12824
|
+
} else {
|
|
12825
|
+
bucket.push(f);
|
|
12826
|
+
}
|
|
12827
|
+
}
|
|
12828
|
+
return [...byRule.values()];
|
|
12829
|
+
}
|
|
12830
|
+
function shortPath(value, root) {
|
|
12831
|
+
return value.split(`${root}/`).join("");
|
|
12832
|
+
}
|
|
12833
|
+
var ACTION_TONE = {
|
|
12834
|
+
delete: "red",
|
|
12835
|
+
warn: "yellow",
|
|
12836
|
+
drift: "yellow",
|
|
12837
|
+
add: "green",
|
|
12838
|
+
refresh: "green"
|
|
12839
|
+
};
|
|
12840
|
+
function formatGroup(group, root, ink) {
|
|
12841
|
+
const first = group[0];
|
|
12842
|
+
const count = group.length;
|
|
12716
12843
|
const out = [];
|
|
12717
|
-
|
|
12718
|
-
|
|
12719
|
-
out.push(
|
|
12720
|
-
|
|
12721
|
-
|
|
12722
|
-
|
|
12844
|
+
const tone = ACTION_TONE[first.action];
|
|
12845
|
+
const label = actionColumn(first.action);
|
|
12846
|
+
out.push(`${tone === undefined ? ink.dim(label) : ink.tone(tone, label)} ${ink.dim(`rule:${safe(first.ruleId)}`)}${count > 1 ? ink.bold(` ×${count}`) : ""}`);
|
|
12847
|
+
out.push(` ${safe(count === 1 ? first.message : ruleHeadline(first))}`);
|
|
12848
|
+
for (const f of group) {
|
|
12849
|
+
const where = f.evidence.filter((e) => e.kind !== "count" && e.kind !== "threshold").map((e) => shortPath(e.value, root));
|
|
12850
|
+
const location = where.length > 0 ? where.join(" · ") : f.subject;
|
|
12851
|
+
out.push(ink.dim(` ${safe(shortPath(location, root))}`));
|
|
12723
12852
|
}
|
|
12724
12853
|
return out;
|
|
12725
12854
|
}
|
|
12726
|
-
function
|
|
12855
|
+
function ruleHeadline(f) {
|
|
12856
|
+
const cut = f.message.indexOf(": ");
|
|
12857
|
+
return cut === -1 ? f.message : f.message.slice(0, cut);
|
|
12858
|
+
}
|
|
12859
|
+
function countsLine(findings, verbose) {
|
|
12860
|
+
const parts = actionParts(findings, verbose);
|
|
12861
|
+
return parts.length === 0 ? "no findings" : parts.join(" · ");
|
|
12862
|
+
}
|
|
12863
|
+
function actionParts(findings, verbose) {
|
|
12727
12864
|
const counts = {
|
|
12728
12865
|
keep: 0,
|
|
12729
12866
|
delete: 0,
|
|
@@ -12745,15 +12882,20 @@ function formatSummary(findings, verbose) {
|
|
|
12745
12882
|
if (!verbose && infoHidden > 0) {
|
|
12746
12883
|
parts.push(`${infoHidden} info hidden (--verbose)`);
|
|
12747
12884
|
}
|
|
12885
|
+
return parts;
|
|
12886
|
+
}
|
|
12887
|
+
function formatSummary(findings, verbose, ink) {
|
|
12888
|
+
const parts = actionParts(findings, verbose);
|
|
12748
12889
|
const points = score(findings);
|
|
12890
|
+
const scored = `score ${ink.tone(scoreTone(points), `${points}/100 ${scoreLabel(points)}`)}`;
|
|
12749
12891
|
if (parts.length === 0) {
|
|
12750
|
-
return `Summary: no findings ·
|
|
12892
|
+
return `Summary: no findings · ${scored}`;
|
|
12751
12893
|
}
|
|
12752
|
-
return `Summary: ${parts.join(" · ")} ·
|
|
12894
|
+
return `Summary: ${parts.join(" · ")} · ${scored}`;
|
|
12753
12895
|
}
|
|
12754
12896
|
|
|
12755
12897
|
// src/version.ts
|
|
12756
|
-
var VERSION = "0.
|
|
12898
|
+
var VERSION = "0.4.0";
|
|
12757
12899
|
|
|
12758
12900
|
// src/commands/check.ts
|
|
12759
12901
|
async function runCheck(options) {
|
|
@@ -12765,23 +12907,30 @@ async function runCheck(options) {
|
|
|
12765
12907
|
});
|
|
12766
12908
|
const format = options.output ?? (options.json === true ? "json" : "human");
|
|
12767
12909
|
let stdout;
|
|
12910
|
+
let plain;
|
|
12768
12911
|
if (format === "json") {
|
|
12769
12912
|
stdout = renderJson({ version: VERSION, root, facts, findings, resolvedFrom });
|
|
12770
12913
|
} else if (format === "prompt") {
|
|
12771
12914
|
stdout = renderPrompt({ version: VERSION, facts, findings });
|
|
12772
12915
|
} else {
|
|
12773
|
-
|
|
12916
|
+
const text = (colour) => renderText({
|
|
12774
12917
|
version: VERSION,
|
|
12775
12918
|
facts,
|
|
12776
12919
|
findings,
|
|
12777
12920
|
resolvedFrom,
|
|
12778
12921
|
verbose: options.verbose ?? false,
|
|
12779
|
-
quiet: options.quiet ?? false
|
|
12922
|
+
quiet: options.quiet ?? false,
|
|
12923
|
+
colour
|
|
12780
12924
|
});
|
|
12925
|
+
stdout = text(options.colour === true);
|
|
12926
|
+
if (options.colour === true) {
|
|
12927
|
+
plain = text(false);
|
|
12928
|
+
}
|
|
12781
12929
|
}
|
|
12782
12930
|
return {
|
|
12783
12931
|
exitCode: exitCode(findings, config.failOn, options.failUnder),
|
|
12784
|
-
stdout
|
|
12932
|
+
stdout,
|
|
12933
|
+
...plain === undefined ? {} : { plain }
|
|
12785
12934
|
};
|
|
12786
12935
|
}
|
|
12787
12936
|
|
|
@@ -12950,6 +13099,7 @@ check options:
|
|
|
12950
13099
|
--output <format> human (default) | json | prompt
|
|
12951
13100
|
prompt: a paste-ready handoff for a fixing agent
|
|
12952
13101
|
--copy Also copy the report to the system clipboard
|
|
13102
|
+
--no-color Never colour, even on a terminal (also: NO_COLOR=1)
|
|
12953
13103
|
--quiet Summary only
|
|
12954
13104
|
--verbose Show KEEP findings
|
|
12955
13105
|
--fail-on <level> never | warning | error (default: never)
|
|
@@ -12979,6 +13129,7 @@ async function main(argv) {
|
|
|
12979
13129
|
json: { type: "boolean", default: false },
|
|
12980
13130
|
output: { type: "string" },
|
|
12981
13131
|
copy: { type: "boolean", default: false },
|
|
13132
|
+
"no-color": { type: "boolean", default: false },
|
|
12982
13133
|
quiet: { type: "boolean", default: false },
|
|
12983
13134
|
verbose: { type: "boolean", default: false },
|
|
12984
13135
|
"fail-on": { type: "string" },
|
|
@@ -13045,11 +13196,16 @@ async function main(argv) {
|
|
|
13045
13196
|
failOn: failOnRaw,
|
|
13046
13197
|
...failUnder === undefined ? {} : { failUnder },
|
|
13047
13198
|
global: values.global,
|
|
13048
|
-
configPath: values.config
|
|
13199
|
+
configPath: values.config,
|
|
13200
|
+
colour: shouldColour({
|
|
13201
|
+
isTTY: process.stdout.isTTY === true,
|
|
13202
|
+
env: process.env,
|
|
13203
|
+
noColorFlag: values["no-color"]
|
|
13204
|
+
})
|
|
13049
13205
|
});
|
|
13050
13206
|
process.stdout.write(result.stdout);
|
|
13051
13207
|
if (values.copy === true) {
|
|
13052
|
-
const copied = await copyToClipboard(result.stdout);
|
|
13208
|
+
const copied = await copyToClipboard(result.plain ?? result.stdout);
|
|
13053
13209
|
process.stderr.write(copied.ok ? `Copied to clipboard via ${copied.tool}.
|
|
13054
13210
|
` : `Could not copy: ${safe(copied.reason)}. Redirect instead: agentscan check > report.md
|
|
13055
13211
|
`);
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@chimix/agentscan",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.4.0",
|
|
4
4
|
"description": "Find broken agent configuration in a project — hooks whose scripts are gone, MCP servers that cannot start, skills that disagree with their lockfile",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"bin": {
|