@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.
Files changed (3) hide show
  1. package/README.md +36 -6
  2. package/index.js +170 -16
  3. 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. Mostly for fun, partly genuinely useful. It reads `~/.claude/` locally and grades you across hook coverage, project hygiene, tool balance, prompt tells, and pipeline ops.
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, zero dependencies, no network calls.
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, not an outcomes audit. It measures whether your Claude Code setup is **set up well**, not whether your outputs are good.
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; a low grade usually means the scaffolding around your AI is sparse.
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, not results.
103
- - The tool ships with a built-in nudge toward [Continuum Sprint](https://uxcontinuum.com/sprint) when it surfaces 2+ failing dimensions. That's 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.
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
- out.skillCount = fs.readdirSync(skillsDir).filter(d => {
272
+ const skills = fs.readdirSync(skillsDir).filter(d => {
272
273
  try { return fs.statSync(path.join(skillsDir, d)).isDirectory(); } catch (_) { return false; }
273
- }).length;
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 (human.pleaseCount / human.prompts > 0.3) pScore -= 6;
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: `You said "just" ${human.justCount} times across ${human.prompts} prompts (${(justRate * 100).toFixed(0)}%).`,
481
- fix: justRate > 0.5
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(` ${C.bold}${C.white}━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━${C.reset}`);
549
- pr(` ${C.bold}${C.white} CCAUDIT${C.reset}${C.dim} your Claude Code report card${C.reset}`);
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 on your own machine: ${C.reset}${C.bold}npx @uxcontinuum/ccaudit${C.reset}`);
586
- pr(` ${C.dim}Source + fixes:${C.reset} github.com/turleydesigns/claude-audit`);
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. The Continuum Sprint fixes setups like this in 2 weeks.${C.reset}`);
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 = inspectClaudeDir(CLAUDE_DIRS[0]);
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 = inspectClaudeDir(CLAUDE_DIRS[0]);
781
+ const setup = inspectAllClaudeDirs(CLAUDE_DIRS);
631
782
  const graded = grade(stats, setup);
632
783
 
633
- if (hasFlag('--json')) {
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.1.0",
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": {