@uxcontinuum/ccaudit 1.1.1 → 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/index.js +170 -16
- package/package.json +1 -1
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": {
|