@uxcontinuum/ccaudit 1.1.0 → 1.2.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 +36 -6
- package/index.js +170 -16
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -1,18 +1,30 @@
|
|
|
1
1
|
# ccaudit
|
|
2
2
|
|
|
3
|
-
A diagnostic for your Claude Code setup.
|
|
3
|
+
A diagnostic for your Claude Code setup. Three things at once:
|
|
4
|
+
|
|
5
|
+
1. **A fun report card** you can screenshot and share.
|
|
6
|
+
2. **A hygiene linter** that surfaces what's missing.
|
|
7
|
+
3. **A discovery tool** that shows you which parts of Claude Code you are not using yet.
|
|
4
8
|
|
|
5
9
|
```bash
|
|
6
10
|
npx @uxcontinuum/ccaudit
|
|
7
11
|
```
|
|
8
12
|
|
|
9
|
-
Zero install
|
|
13
|
+
Zero install. Zero dependencies. No network calls. Reads `~/.claude/` on your machine and outputs a grade card.
|
|
14
|
+
|
|
15
|
+
## Why this exists
|
|
16
|
+
|
|
17
|
+
Most Claude Code users are running on a fraction of the surface area. No hooks installed. No skills configured. No MCP servers. No idea what their token cost per shipped feature is. No concept of how often their agent fails on first try.
|
|
18
|
+
|
|
19
|
+
The hype is on the model. The actual constraint is everything around the model. The scaffolding.
|
|
20
|
+
|
|
21
|
+
ccaudit grades the scaffolding.
|
|
10
22
|
|
|
11
23
|
## What the grade is and isn't
|
|
12
24
|
|
|
13
|
-
This is a hygiene audit
|
|
25
|
+
This is a **hygiene and discovery audit**, not an outcomes audit. It measures whether your Claude Code setup is **set up well** and **uses what's available**, not whether your specific outputs are good.
|
|
14
26
|
|
|
15
|
-
Think of it as a linter for your AI workflow. Passing lint doesn't guarantee your code is good. Failing lint usually means something is missing. Same here: a high grade doesn't mean Claude is shipping perfect work for you
|
|
27
|
+
Think of it as a linter for your AI workflow. Passing lint doesn't guarantee your code is good. Failing lint usually means something is missing. Same here: a high grade doesn't mean Claude is shipping perfect work for you. A low grade usually means there's surface area of Claude Code you haven't unlocked yet.
|
|
16
28
|
|
|
17
29
|
The grade can be gamed (install five no-op hooks, auto-title every session, scrub "just" from your prompts). Don't bother. The findings under the grade are the value, not the letter.
|
|
18
30
|
|
|
@@ -99,8 +111,26 @@ Reads `~/.claude/` on your machine. Outputs to stdout. No network calls, no tele
|
|
|
99
111
|
|
|
100
112
|
- The grade is opinionated, not objective.
|
|
101
113
|
- The rubric will change as the tool matures.
|
|
102
|
-
- High grade ≠ good outputs. Low grade ≠ bad outputs. The grade is about scaffolding
|
|
103
|
-
- The tool ships with a built-in nudge toward [Continuum Sprint](https://uxcontinuum.com/sprint) when it surfaces 2+ failing dimensions. That
|
|
114
|
+
- High grade ≠ good outputs. Low grade ≠ bad outputs. The grade is about **scaffolding and feature coverage**, not results.
|
|
115
|
+
- The tool ships with a built-in nudge toward [Continuum Sprint](https://uxcontinuum.com/sprint) when it surfaces 2+ failing dimensions. That is intentional. If your setup is genuinely broken in two places, a 2-week sprint is often what fixes it. Ignore the nudge if you don't want it.
|
|
116
|
+
|
|
117
|
+
## The story behind this
|
|
118
|
+
|
|
119
|
+
Karpathy keeps saying we're entering vibe coding. Software you write in English while AI generates the code. He is not wrong about where this is going.
|
|
120
|
+
|
|
121
|
+
I bought in six months ago. Built a multi-agent pipeline. Started shipping production code through it. Six weeks of recent data: 333 PRs, $1,132 in tokens, $3.40 per shipped PR.
|
|
122
|
+
|
|
123
|
+
Then I ran ccaudit on myself, expecting an A.
|
|
124
|
+
|
|
125
|
+
I got a B-.
|
|
126
|
+
|
|
127
|
+
The findings were valid. The reason I assumed A was that I had been optimizing the agents and ignoring the room they live in. Almost everyone running Claude Code is doing the same thing. The hype is on the model. The constraint is the scaffolding.
|
|
128
|
+
|
|
129
|
+
If you want to know what your scaffolding looks like graded:
|
|
130
|
+
|
|
131
|
+
```bash
|
|
132
|
+
npx @uxcontinuum/ccaudit
|
|
133
|
+
```
|
|
104
134
|
|
|
105
135
|
---
|
|
106
136
|
|
package/index.js
CHANGED
|
@@ -204,6 +204,7 @@ function inspectClaudeDir(claudeDir) {
|
|
|
204
204
|
hasClaudeMd: false,
|
|
205
205
|
claudeMdBytes: 0,
|
|
206
206
|
skillCount: 0,
|
|
207
|
+
skillNames: [],
|
|
207
208
|
};
|
|
208
209
|
|
|
209
210
|
const settingsPath = path.join(claudeDir, 'settings.json');
|
|
@@ -268,15 +269,55 @@ function inspectClaudeDir(claudeDir) {
|
|
|
268
269
|
try {
|
|
269
270
|
const skillsDir = path.join(claudeDir, 'skills');
|
|
270
271
|
if (fs.statSync(skillsDir).isDirectory()) {
|
|
271
|
-
|
|
272
|
+
const skills = fs.readdirSync(skillsDir).filter(d => {
|
|
272
273
|
try { return fs.statSync(path.join(skillsDir, d)).isDirectory(); } catch (_) { return false; }
|
|
273
|
-
})
|
|
274
|
+
});
|
|
275
|
+
out.skillCount = skills.length;
|
|
276
|
+
out.skillNames = skills;
|
|
274
277
|
}
|
|
275
278
|
} catch (_) {}
|
|
276
279
|
|
|
277
280
|
return out;
|
|
278
281
|
}
|
|
279
282
|
|
|
283
|
+
// Aggregate setup data across multiple ~/.claude/ candidates so root + user
|
|
284
|
+
// installs (or any multi-home setup) all get counted. Hooks, skills, MCP, and
|
|
285
|
+
// CLAUDE.md bytes sum across dirs; skill names dedupe.
|
|
286
|
+
function inspectAllClaudeDirs(claudeDirs) {
|
|
287
|
+
const merged = {
|
|
288
|
+
primary: claudeDirs[0],
|
|
289
|
+
hooksByEvent: {},
|
|
290
|
+
totalHooks: 0,
|
|
291
|
+
autoMemoryEnabled: false,
|
|
292
|
+
mcpServers: 0,
|
|
293
|
+
hookFiles: [],
|
|
294
|
+
hasClaudeMd: false,
|
|
295
|
+
claudeMdBytes: 0,
|
|
296
|
+
skillCount: 0,
|
|
297
|
+
skillNames: [],
|
|
298
|
+
settingsParseError: null,
|
|
299
|
+
perDir: [],
|
|
300
|
+
};
|
|
301
|
+
const seenSkills = new Set();
|
|
302
|
+
for (const d of claudeDirs) {
|
|
303
|
+
const r = inspectClaudeDir(d);
|
|
304
|
+
merged.perDir.push(r);
|
|
305
|
+
for (const [evt, n] of Object.entries(r.hooksByEvent)) {
|
|
306
|
+
merged.hooksByEvent[evt] = (merged.hooksByEvent[evt] || 0) + n;
|
|
307
|
+
merged.totalHooks += n;
|
|
308
|
+
}
|
|
309
|
+
merged.autoMemoryEnabled = merged.autoMemoryEnabled || r.autoMemoryEnabled;
|
|
310
|
+
merged.mcpServers = Math.max(merged.mcpServers, r.mcpServers);
|
|
311
|
+
merged.hookFiles = merged.hookFiles.concat(r.hookFiles);
|
|
312
|
+
merged.hasClaudeMd = merged.hasClaudeMd || r.hasClaudeMd;
|
|
313
|
+
merged.claudeMdBytes += r.claudeMdBytes;
|
|
314
|
+
for (const s of r.skillNames) if (!seenSkills.has(s)) { seenSkills.add(s); merged.skillNames.push(s); }
|
|
315
|
+
if (r.settingsParseError && !merged.settingsParseError) merged.settingsParseError = r.settingsParseError;
|
|
316
|
+
}
|
|
317
|
+
merged.skillCount = merged.skillNames.length;
|
|
318
|
+
return merged;
|
|
319
|
+
}
|
|
320
|
+
|
|
280
321
|
// ── AGGREGATE ─────────────────────────────────────────────────────────────────
|
|
281
322
|
function aggregate(sessions) {
|
|
282
323
|
const human = sessions.filter(s => !s.isAgent);
|
|
@@ -469,18 +510,28 @@ function grade(stats, setup) {
|
|
|
469
510
|
// 4. Prompt tells (the "just"/"please" tax).
|
|
470
511
|
if (human.sessions && human.prompts > 0) {
|
|
471
512
|
const justRate = human.justCount / human.prompts;
|
|
513
|
+
const pleaseRate = human.pleaseCount / human.prompts;
|
|
472
514
|
let pScore = 85;
|
|
473
515
|
if (justRate > 0.5) pScore -= 12;
|
|
474
516
|
if (justRate > 1.0) pScore -= 15;
|
|
475
|
-
if (
|
|
517
|
+
if (pleaseRate > 0.3) pScore -= 6;
|
|
476
518
|
pScore = Math.max(0, Math.min(100, pScore));
|
|
519
|
+
|
|
520
|
+
// Build a data-driven fix message that names the actual ratios.
|
|
521
|
+
let pFix = null;
|
|
522
|
+
if (justRate > 1.0) {
|
|
523
|
+
pFix = `${(justRate*100).toFixed(0)}% of your prompts contain "just" (often multiple times). You are telegraphing that you think every task is trivial. They are not. Strip it from the next 10 prompts and watch what changes.`;
|
|
524
|
+
} else if (justRate > 0.5) {
|
|
525
|
+
pFix = `Roughly one in two of your prompts contains "just" (${human.justCount} out of ${human.prompts}). It is the most reliable signal that you think the task is simpler than it is. Worth dropping.`;
|
|
526
|
+
} else if (pleaseRate > 0.3) {
|
|
527
|
+
pFix = `You said "please" in ${(pleaseRate*100).toFixed(0)}% of your prompts. Politeness costs tokens and confuses scope. Direct prompts work better.`;
|
|
528
|
+
}
|
|
529
|
+
|
|
477
530
|
dims.push({
|
|
478
531
|
name: 'Prompt tells',
|
|
479
532
|
score: pScore,
|
|
480
|
-
detail: `
|
|
481
|
-
fix:
|
|
482
|
-
? 'The word "just" telegraphs that you think the task is simple. It is not. Strip it.'
|
|
483
|
-
: null,
|
|
533
|
+
detail: `"just" appears in ${(justRate*100).toFixed(0)}% of prompts (${human.justCount}x). "please" in ${(pleaseRate*100).toFixed(0)}% (${human.pleaseCount}x).`,
|
|
534
|
+
fix: pFix,
|
|
484
535
|
});
|
|
485
536
|
}
|
|
486
537
|
|
|
@@ -538,6 +589,14 @@ function grade(stats, setup) {
|
|
|
538
589
|
}
|
|
539
590
|
|
|
540
591
|
// ── OUTPUT ────────────────────────────────────────────────────────────────────
|
|
592
|
+
const LOGO = [
|
|
593
|
+
' ██████ ██████ ██████ ██ ██ ██████ ██ ██████████',
|
|
594
|
+
' ██ ██ ██ ██ ██ ██ ██ ██ ██ ██ ',
|
|
595
|
+
' ██ ██ ██████ ██ ██ ██ ██ ██ ██ ',
|
|
596
|
+
' ██ ██ ██ ██ ██ ██ ██ ██ ██ ██ ',
|
|
597
|
+
' ██████ ██████ ██ ██ ██████ ██████ ██ ██ ',
|
|
598
|
+
];
|
|
599
|
+
|
|
541
600
|
function renderCard(stats, setup, graded) {
|
|
542
601
|
const bar = (n) => {
|
|
543
602
|
const filled = Math.round(n / 5);
|
|
@@ -545,8 +604,9 @@ function renderCard(stats, setup, graded) {
|
|
|
545
604
|
};
|
|
546
605
|
|
|
547
606
|
pr();
|
|
548
|
-
pr(
|
|
549
|
-
pr(
|
|
607
|
+
for (const line of LOGO) pr(`${C.bold}${C.cyan}${line}${C.reset}`);
|
|
608
|
+
pr();
|
|
609
|
+
pr(` ${C.dim}your claude code report card${C.reset} ${C.dim}·${C.reset} ${C.dim}npx @uxcontinuum/ccaudit${C.reset}`);
|
|
550
610
|
pr(` ${C.bold}${C.white}━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━${C.reset}`);
|
|
551
611
|
pr();
|
|
552
612
|
|
|
@@ -582,18 +642,109 @@ function renderCard(stats, setup, graded) {
|
|
|
582
642
|
|
|
583
643
|
pr();
|
|
584
644
|
pr(` ${C.dim}━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━${C.reset}`);
|
|
585
|
-
pr(` ${C.dim}Run it
|
|
586
|
-
pr(` ${C.dim}
|
|
645
|
+
pr(` ${C.dim}Run it yourself:${C.reset} ${C.bold}npx @uxcontinuum/ccaudit${C.reset}`);
|
|
646
|
+
pr(` ${C.dim}See next steps:${C.reset} ${C.bold}npx @uxcontinuum/ccaudit --next-steps${C.reset}`);
|
|
647
|
+
pr(` ${C.dim}Source + fixes:${C.reset} github.com/turleydesigns/claude-audit`);
|
|
587
648
|
|
|
588
649
|
const failingDims = graded.dims.filter(d => d.score < 75).length;
|
|
589
650
|
if (failingDims >= 2) {
|
|
590
651
|
pr();
|
|
591
|
-
pr(` ${C.bold}${C.yellow}${failingDims} dimensions flagged.
|
|
592
|
-
pr(` ${C.dim}continuum.build${C.reset}`);
|
|
652
|
+
pr(` ${C.bold}${C.yellow}${failingDims} dimensions flagged.${C.reset} ${C.dim}Need help with your setup?${C.reset} ${C.bold}uxcontinuum.com${C.reset}`);
|
|
593
653
|
}
|
|
594
654
|
pr();
|
|
595
655
|
}
|
|
596
656
|
|
|
657
|
+
|
|
658
|
+
// ── NEXT STEPS RENDERER ───────────────────────────────────────────────────────
|
|
659
|
+
function renderNextSteps(stats, setup, graded) {
|
|
660
|
+
pr();
|
|
661
|
+
pr(` ${C.bold}${C.cyan}NEXT STEPS${C.reset} ${C.dim}prioritized actions based on your audit${C.reset}`);
|
|
662
|
+
pr(` ${C.dim}━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━${C.reset}`);
|
|
663
|
+
pr();
|
|
664
|
+
|
|
665
|
+
const steps = [];
|
|
666
|
+
|
|
667
|
+
if (setup.totalHooks === 0) {
|
|
668
|
+
steps.push({ p: 'HIGH', a: 'Install at least one PreToolUse hook',
|
|
669
|
+
d: 'You have zero hooks. Runaway agent loops will burn money overnight if you let them.',
|
|
670
|
+
link: 'github.com/turleydesigns/claude-loop-sentinel' });
|
|
671
|
+
} else if (setup.totalHooks < 3) {
|
|
672
|
+
steps.push({ p: 'MED', a: 'Expand hook coverage',
|
|
673
|
+
d: `${setup.totalHooks} hook(s) configured. Most healthy setups have at least PreToolUse, PostToolUse, and Stop.`,
|
|
674
|
+
link: 'docs.anthropic.com/en/docs/claude-code/hooks' });
|
|
675
|
+
}
|
|
676
|
+
|
|
677
|
+
const pipeDim = graded.dims.find(d => d.name === 'Pipeline ops (agent sessions)');
|
|
678
|
+
if (pipeDim && setup.totalHooks === 0) {
|
|
679
|
+
steps.push({ p: 'HIGH', a: 'Add hooks before next overnight run',
|
|
680
|
+
d: 'You are running an agent pipeline with no runtime guards. This is how bills explode.',
|
|
681
|
+
link: 'github.com/turleydesigns/claude-loop-sentinel' });
|
|
682
|
+
}
|
|
683
|
+
|
|
684
|
+
if (stats.human && stats.human.titledPct < 20 && stats.human.uniqueCwds < 5) {
|
|
685
|
+
steps.push({ p: 'MED', a: 'Title your important sessions or scope by CWD',
|
|
686
|
+
d: `${stats.human.titledPct}% titled, ${stats.human.uniqueCwds} distinct working dirs. You will not find this work later.`,
|
|
687
|
+
link: null });
|
|
688
|
+
}
|
|
689
|
+
|
|
690
|
+
if (stats.human && stats.human.bashPct > 65) {
|
|
691
|
+
const editAbs = Math.round(stats.human.editPct * stats.human.totalTools / 100);
|
|
692
|
+
if (editAbs < 200) {
|
|
693
|
+
steps.push({ p: 'MED', a: 'Edit/Write instead of running commands',
|
|
694
|
+
d: `Bash is ${stats.human.bashPct}% of your tool calls (${editAbs} Edit calls total). You are running things, not editing them.`,
|
|
695
|
+
link: null });
|
|
696
|
+
}
|
|
697
|
+
}
|
|
698
|
+
|
|
699
|
+
if (stats.human && stats.human.justCount / stats.human.prompts > 0.5) {
|
|
700
|
+
const rate = Math.round(100 * stats.human.justCount / stats.human.prompts);
|
|
701
|
+
steps.push({ p: 'LOW', a: 'Drop "just" from your prompts',
|
|
702
|
+
d: `"just" in ${rate}% of your prompts. Try the next 10 without it and watch the responses change.`,
|
|
703
|
+
link: null });
|
|
704
|
+
}
|
|
705
|
+
|
|
706
|
+
if (stats.human && stats.human.toolErrorRate > 8) {
|
|
707
|
+
steps.push({ p: 'MED', a: 'Investigate tool error rate',
|
|
708
|
+
d: `${stats.human.toolErrorRate}% of your tool calls error. Sessions are fighting the environment.`,
|
|
709
|
+
link: null });
|
|
710
|
+
}
|
|
711
|
+
|
|
712
|
+
if (setup.mcpServers === 0) {
|
|
713
|
+
steps.push({ p: 'LOW', a: 'Try an MCP server',
|
|
714
|
+
d: 'Zero MCP servers configured. They unlock work Claude Code cannot do alone (filesystem, browser, API access).',
|
|
715
|
+
link: 'docs.anthropic.com/en/docs/claude-code/mcp' });
|
|
716
|
+
}
|
|
717
|
+
|
|
718
|
+
if (setup.skillCount < 3) {
|
|
719
|
+
steps.push({ p: 'LOW', a: 'Install a few Claude Code skills',
|
|
720
|
+
d: `${setup.skillCount} skill(s) installed. Skills let you invoke domain expertise with /name.`,
|
|
721
|
+
link: 'docs.anthropic.com/en/docs/claude-code/skills' });
|
|
722
|
+
}
|
|
723
|
+
|
|
724
|
+
if (!steps.length) {
|
|
725
|
+
pr(` ${C.green}Your setup looks healthy. No critical next steps surfaced.${C.reset}`);
|
|
726
|
+
pr();
|
|
727
|
+
return;
|
|
728
|
+
}
|
|
729
|
+
|
|
730
|
+
const order = { HIGH: 0, MED: 1, LOW: 2 };
|
|
731
|
+
steps.sort((a, b) => order[a.p] - order[b.p]);
|
|
732
|
+
|
|
733
|
+
let i = 1;
|
|
734
|
+
for (const s of steps) {
|
|
735
|
+
const pColor = s.p === 'HIGH' ? C.red : (s.p === 'MED' ? C.yellow : C.cyan);
|
|
736
|
+
pr(` ${C.bold}${String(i).padStart(2)}.${C.reset} ${pColor}${C.bold}[${s.p}]${C.reset} ${C.bold}${s.a}${C.reset}`);
|
|
737
|
+
pr(` ${C.dim}${s.d}${C.reset}`);
|
|
738
|
+
if (s.link) pr(` ${C.dim}→ ${s.link}${C.reset}`);
|
|
739
|
+
pr();
|
|
740
|
+
i++;
|
|
741
|
+
}
|
|
742
|
+
|
|
743
|
+
pr(` ${C.dim}━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━${C.reset}`);
|
|
744
|
+
pr(` ${C.dim}Need help working through these?${C.reset} ${C.bold}uxcontinuum.com${C.reset}`);
|
|
745
|
+
pr();
|
|
746
|
+
}
|
|
747
|
+
|
|
597
748
|
// ── MAIN ──────────────────────────────────────────────────────────────────────
|
|
598
749
|
const days = parseInt(getArg('--days', '30'), 10);
|
|
599
750
|
const cutoffMs = Date.now() - days * 86400000;
|
|
@@ -615,7 +766,7 @@ if (!sessions.length) {
|
|
|
615
766
|
pr(` ${C.dim}Try --days 90 or --days 365 to widen the window.${C.reset}`);
|
|
616
767
|
}
|
|
617
768
|
// Still surface the setup audit even with no sessions, so brand-new users get value.
|
|
618
|
-
const setupOnly =
|
|
769
|
+
const setupOnly = inspectAllClaudeDirs(CLAUDE_DIRS);
|
|
619
770
|
pr();
|
|
620
771
|
pr(` ${C.bold}Setup snapshot:${C.reset}`);
|
|
621
772
|
pr(` Hooks: ${setupOnly.totalHooks} (${Object.keys(setupOnly.hooksByEvent).join(', ') || 'none'})`);
|
|
@@ -627,10 +778,13 @@ if (!sessions.length) {
|
|
|
627
778
|
}
|
|
628
779
|
|
|
629
780
|
const stats = aggregate(sessions);
|
|
630
|
-
const setup =
|
|
781
|
+
const setup = inspectAllClaudeDirs(CLAUDE_DIRS);
|
|
631
782
|
const graded = grade(stats, setup);
|
|
632
783
|
|
|
633
|
-
if (hasFlag('--
|
|
784
|
+
if (hasFlag('--next-steps')) {
|
|
785
|
+
renderCard(stats, setup, graded);
|
|
786
|
+
renderNextSteps(stats, setup, graded);
|
|
787
|
+
} else if (hasFlag('--json')) {
|
|
634
788
|
// Programmatic output. Stable shape for downstream tools and the future
|
|
635
789
|
// public-benchmark backend. No personal content (prompts, slugs, CWDs).
|
|
636
790
|
const payload = {
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@uxcontinuum/ccaudit",
|
|
3
|
-
"version": "1.
|
|
3
|
+
"version": "1.2.0",
|
|
4
4
|
"description": "A diagnostic for your Claude Code setup. Reads ~/.claude/ locally, grades you across hook coverage, project hygiene, tool balance, prompt tells, and pipeline ops. Zero install: npx @uxcontinuum/ccaudit",
|
|
5
5
|
"main": "index.js",
|
|
6
6
|
"bin": {
|