@uxcontinuum/ccaudit 1.0.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 (4) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +97 -0
  3. package/index.js +444 -0
  4. package/package.json +33 -0
package/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 Matt Turley
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
package/README.md ADDED
@@ -0,0 +1,97 @@
1
+ # ccaudit
2
+
3
+ A diagnostic for your Claude Code setup. Run it, get graded across five dimensions, find the specific fixes.
4
+
5
+ ```bash
6
+ npx @uxcontinuum/ccaudit
7
+ ```
8
+
9
+ Reads `~/.claude/` locally. Zero dependencies. Nothing leaves your machine.
10
+
11
+ ## What you get
12
+
13
+ ```
14
+ ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
15
+ CCAUDIT your Claude Code report card
16
+ ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
17
+
18
+ OVERALL GRADE D+ (67/100)
19
+
20
+ Hook coverage B- ████████████████░░░░
21
+ 1 PreToolUse, 1 PostToolUse, 0 UserPromptSubmit hook(s).
22
+
23
+ Project hygiene (human) F ████████░░░░░░░░░░░░
24
+ 0% of your human sessions are titled. Avg prompt: 2350 chars.
25
+ → Title your sessions. Untitled sessions are unsearchable history.
26
+
27
+ Tool balance (human) F ███████████░░░░░░░░░
28
+ Bash 73%, Edit+Write 10%, Read 10%, Grep+Glob 2%, Agent/Task 0%.
29
+ → You are running things, not editing things. Use Edit/Write more.
30
+
31
+ Prompt tells C ███████████████░░░░░
32
+ You said "just" 10243 times across 19199 prompts (53%).
33
+ → The word "just" telegraphs that you think the task is simple. It is not.
34
+
35
+ Pipeline ops (agent sessions) B █████████████████░░░
36
+ 3253 agent-spawned sessions, 26.93M output tokens.
37
+ ```
38
+
39
+ ## What it checks
40
+
41
+ | Dimension | Source |
42
+ |-----------|--------|
43
+ | Hook coverage | `~/.claude/settings.json` (PreToolUse, PostToolUse, UserPromptSubmit hook counts) |
44
+ | Project hygiene | session titles, average prompt length, untitled rate (human sessions only) |
45
+ | Tool balance | distribution across Bash, Edit/Write, Read, Grep/Glob, Agent/Task (human sessions only) |
46
+ | Prompt tells | "just" frequency, "please" frequency, total prompt count |
47
+ | Pipeline ops | agent-spawned session stats: count, token spend, hook coverage relative to volume |
48
+
49
+ It separates human-driven sessions from agent-spawned worktrees (UUID and ULID-suffix dirs in your projects folder). Your operator-grade and your pipeline-grade get scored independently against different rubrics.
50
+
51
+ ## Install
52
+
53
+ ```bash
54
+ # Run once without installing
55
+ npx @uxcontinuum/ccaudit
56
+
57
+ # Or globally
58
+ npm i -g @uxcontinuum/ccaudit
59
+ ccaudit
60
+ ```
61
+
62
+ Requires Node 14+. No other dependencies.
63
+
64
+ ## Options
65
+
66
+ ```bash
67
+ ccaudit # full report, last 30 days of activity
68
+ ccaudit --days 7 # just last week
69
+ ccaudit --days 365 # full year
70
+ ccaudit --no-color # plain text for copying
71
+ ```
72
+
73
+ ## How it grades
74
+
75
+ Each dimension produces a 0-100 score and a letter grade (A+ through F). The overall grade is the mean of the dimension scores. The rubric weights:
76
+
77
+ - Hook coverage is hard-floored at 35 if you have zero hooks. Anything could happen overnight.
78
+ - Project hygiene scales linearly with titled-session percentage and penalizes both ultra-terse (<80 chars) and wall-of-text (>1500 chars) average prompts.
79
+ - Tool balance penalizes Bash dominance above 65% and rewards healthy editing (10-55% Edit+Write).
80
+ - Prompt tells subtract for high "just" frequency. "Just" telegraphs that you think the task is simple. It usually is not.
81
+ - Pipeline ops rewards low tokens-per-session and penalizes running an agent pipeline without runtime hooks.
82
+
83
+ The grade is opinionated, not objective. Read it as a diagnostic, not a judgment.
84
+
85
+ ## Why this exists
86
+
87
+ There is no public benchmark for "is my Claude Code setup any good." People burn weeks reading other people's CLAUDE.md files trying to figure out what they're doing wrong. This tool answers that question in 30 seconds.
88
+
89
+ If the audit flags two or more dimensions, the fix is usually a few days of work, not a rebuild. [Continuum](https://continuum.build) runs structured 2-week sprints for setups that need them.
90
+
91
+ ## Privacy
92
+
93
+ Reads `~/.claude/` on your machine. Outputs to stdout. Makes no network calls. No telemetry, no analytics, no opt-in submission (yet).
94
+
95
+ ---
96
+
97
+ Built by [Matt Turley](https://uxcontinuum.com) / [Continuum](https://continuum.build).
package/index.js ADDED
@@ -0,0 +1,444 @@
1
+ #!/usr/bin/env node
2
+ 'use strict';
3
+
4
+ // ccaudit: diagnostic for your Claude Code setup.
5
+ // Reads ~/.claude/ locally. Nothing leaves your machine.
6
+ // One-line install: npx @uxcontinuum/ccaudit
7
+
8
+ const fs = require('fs');
9
+ const path = require('path');
10
+ const os = require('os');
11
+
12
+ // ── COLOR + LAYOUT ────────────────────────────────────────────────────────────
13
+ const C = {
14
+ bold: '\x1b[1m', dim: '\x1b[2m', reset: '\x1b[0m',
15
+ green: '\x1b[92m', yellow: '\x1b[93m', red: '\x1b[91m',
16
+ cyan: '\x1b[96m', white: '\x1b[97m', magenta: '\x1b[95m',
17
+ };
18
+ const hasFlag = (f) => process.argv.includes(f);
19
+ if (hasFlag('--no-color')) { for (const k in C) C[k] = ''; }
20
+ const getArg = (f, d) => {
21
+ const i = process.argv.indexOf(f);
22
+ return i > -1 && process.argv[i+1] ? process.argv[i+1] : d;
23
+ };
24
+ const pr = (s = '') => process.stdout.write(s + '\n');
25
+
26
+ // ── CLAUDE DIR DISCOVERY (root-aware) ─────────────────────────────────────────
27
+ function findClaudeDirs() {
28
+ const candidates = [path.join(os.homedir(), '.claude')];
29
+ if (os.homedir() === '/root') {
30
+ try {
31
+ for (const u of fs.readdirSync('/home')) {
32
+ candidates.push(`/home/${u}/.claude`);
33
+ }
34
+ } catch (_) {}
35
+ }
36
+ const dirs = [];
37
+ for (const c of candidates) {
38
+ try { if (fs.statSync(c).isDirectory()) dirs.push(c); } catch (_) {}
39
+ }
40
+ return dirs.length ? dirs : [candidates[0]];
41
+ }
42
+ const CLAUDE_DIRS = findClaudeDirs();
43
+
44
+ // ── SESSION TYPE CLASSIFIER ───────────────────────────────────────────────────
45
+ // Agent-spawned worktrees end with a 32-char hex hash (your orchestrator's
46
+ // pattern). Named project dirs are human.
47
+ // Agent worktrees use either UUIDv4 names (orchestrator-spawned) or ULID-style
48
+ // hex suffixes appended to a path. Human dirs are word-segmented.
49
+ const UUID_RE = /[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}/;
50
+ const HEX_TAIL_RE = /-[0-9a-f]{20,}$/;
51
+ function isAgentProjectDir(name) {
52
+ if (UUID_RE.test(name)) return true;
53
+ if (HEX_TAIL_RE.test(name)) return true;
54
+ return false;
55
+ }
56
+
57
+ // ── FILE SCAN ─────────────────────────────────────────────────────────────────
58
+ function findJsonl(dir, cutoffMs, out = []) {
59
+ let entries;
60
+ try { entries = fs.readdirSync(dir, { withFileTypes: true }); } catch (_) { return out; }
61
+ for (const e of entries) {
62
+ if (e.name === 'subagents') continue;
63
+ const full = path.join(dir, e.name);
64
+ if (e.isDirectory()) { findJsonl(full, cutoffMs, out); continue; }
65
+ if (!e.name.endsWith('.jsonl')) continue;
66
+ try { if (fs.statSync(full).mtimeMs >= cutoffMs) out.push(full); } catch (_) {}
67
+ }
68
+ return out;
69
+ }
70
+
71
+ // ── SESSION PARSE ─────────────────────────────────────────────────────────────
72
+ function projDirName(filePath) {
73
+ const parts = filePath.split(path.sep);
74
+ const idx = parts.lastIndexOf('projects');
75
+ return idx >= 0 && idx + 1 < parts.length ? parts[idx + 1] : '';
76
+ }
77
+
78
+ function parseSession(filePath, cutoffMs) {
79
+ let lines;
80
+ try { lines = fs.readFileSync(filePath, 'utf8').split('\n'); } catch (_) { return null; }
81
+
82
+ const userPrompts = [];
83
+ const toolCalls = [];
84
+ const timestamps = [];
85
+ let title = null;
86
+ let outputTokens = 0;
87
+ let inputTokens = 0;
88
+
89
+ for (const raw of lines) {
90
+ if (!raw) continue;
91
+ let msg;
92
+ try { msg = JSON.parse(raw); } catch (_) { continue; }
93
+
94
+ if (msg.type === 'custom-title') { title = msg.title || ''; continue; }
95
+ if (msg.type !== 'user' && msg.type !== 'assistant') continue;
96
+
97
+ if (msg.timestamp) {
98
+ const t = Date.parse(msg.timestamp);
99
+ if (!isNaN(t) && t >= cutoffMs) timestamps.push(t);
100
+ }
101
+
102
+ if (msg.type === 'user') {
103
+ const c = msg.message?.content;
104
+ if (Array.isArray(c)) {
105
+ for (const b of c) if (b?.type === 'text' && b.text?.trim()) userPrompts.push(b.text.trim());
106
+ } else if (typeof c === 'string' && c.trim()) {
107
+ userPrompts.push(c.trim());
108
+ }
109
+ }
110
+
111
+ if (msg.type === 'assistant') {
112
+ const c = msg.message?.content;
113
+ if (Array.isArray(c)) {
114
+ for (const b of c) if (b?.type === 'tool_use') toolCalls.push(b.name || 'unknown');
115
+ }
116
+ const u = msg.message?.usage;
117
+ if (u) {
118
+ outputTokens += u.output_tokens || 0;
119
+ inputTokens += u.input_tokens || 0;
120
+ }
121
+ }
122
+ }
123
+
124
+ if (!timestamps.length) return null;
125
+
126
+ const projDir = projDirName(filePath);
127
+ return {
128
+ projDir,
129
+ isAgent: isAgentProjectDir(projDir),
130
+ title: title || '',
131
+ userPrompts,
132
+ toolCalls,
133
+ timestamps,
134
+ outputTokens,
135
+ inputTokens,
136
+ };
137
+ }
138
+
139
+ // ── SETTINGS / HOOK AUDIT ─────────────────────────────────────────────────────
140
+ function inspectClaudeDir(claudeDir) {
141
+ const out = {
142
+ claudeDir,
143
+ hasSettings: false,
144
+ settingsValid: false,
145
+ preToolUseHooks: 0,
146
+ postToolUseHooks: 0,
147
+ userPromptSubmitHooks: 0,
148
+ hookFiles: [],
149
+ hasClaudeMd: false,
150
+ claudeMdBytes: 0,
151
+ skillCount: 0,
152
+ };
153
+
154
+ const settingsPath = path.join(claudeDir, 'settings.json');
155
+ try {
156
+ if (fs.statSync(settingsPath).isFile()) {
157
+ out.hasSettings = true;
158
+ try {
159
+ const s = JSON.parse(fs.readFileSync(settingsPath, 'utf8'));
160
+ out.settingsValid = true;
161
+ const hooks = s.hooks || {};
162
+ const count = (entries) => (entries || []).reduce((n, e) => n + (e.hooks || []).length, 0);
163
+ out.preToolUseHooks = count(hooks.PreToolUse);
164
+ out.postToolUseHooks = count(hooks.PostToolUse);
165
+ out.userPromptSubmitHooks = count(hooks.UserPromptSubmit);
166
+ } catch (_) {}
167
+ }
168
+ } catch (_) {}
169
+
170
+ try {
171
+ const hooksDir = path.join(claudeDir, 'hooks');
172
+ if (fs.statSync(hooksDir).isDirectory()) {
173
+ for (const f of fs.readdirSync(hooksDir)) {
174
+ if (!f.startsWith('.')) out.hookFiles.push(f);
175
+ }
176
+ }
177
+ } catch (_) {}
178
+
179
+ for (const md of ['CLAUDE.md', 'CLAUDE-personal.md']) {
180
+ try {
181
+ const p = path.join(claudeDir, md);
182
+ const st = fs.statSync(p);
183
+ if (st.isFile()) {
184
+ out.hasClaudeMd = true;
185
+ out.claudeMdBytes += st.size;
186
+ }
187
+ } catch (_) {}
188
+ }
189
+
190
+ try {
191
+ const skillsDir = path.join(claudeDir, 'skills');
192
+ if (fs.statSync(skillsDir).isDirectory()) {
193
+ out.skillCount = fs.readdirSync(skillsDir).filter(d => {
194
+ try { return fs.statSync(path.join(skillsDir, d)).isDirectory(); } catch (_) { return false; }
195
+ }).length;
196
+ }
197
+ } catch (_) {}
198
+
199
+ return out;
200
+ }
201
+
202
+ // ── AGGREGATE ─────────────────────────────────────────────────────────────────
203
+ function aggregate(sessions) {
204
+ const human = sessions.filter(s => !s.isAgent);
205
+ const agent = sessions.filter(s => s.isAgent);
206
+
207
+ const bucket = (subset) => {
208
+ if (!subset.length) return null;
209
+ const prompts = subset.flatMap(s => s.userPrompts);
210
+ const tools = subset.flatMap(s => s.toolCalls);
211
+ const titled = subset.filter(s => s.title).length;
212
+
213
+ const toolCounts = {};
214
+ for (const t of tools) toolCounts[t] = (toolCounts[t] || 0) + 1;
215
+ const totalTools = tools.length || 1;
216
+ const pct = (k) => Math.round(100 * (toolCounts[k] || 0) / totalTools);
217
+
218
+ const totalChars = prompts.reduce((n, p) => n + p.length, 0);
219
+ const avgPromptLen = prompts.length ? Math.round(totalChars / prompts.length) : 0;
220
+
221
+ const justCount = prompts.reduce((n, p) => n + (p.toLowerCase().match(/\bjust\b/g)?.length || 0), 0);
222
+ const pleaseCount = prompts.reduce((n, p) => n + (p.toLowerCase().match(/\bplease\b/g)?.length || 0), 0);
223
+
224
+ const outputTokens = subset.reduce((n, s) => n + s.outputTokens, 0);
225
+ const inputTokens = subset.reduce((n, s) => n + s.inputTokens, 0);
226
+
227
+ return {
228
+ sessions: subset.length,
229
+ prompts: prompts.length,
230
+ titledPct: Math.round(100 * titled / subset.length),
231
+ avgPromptLen,
232
+ justCount,
233
+ pleaseCount,
234
+ bashPct: pct('Bash'),
235
+ editPct: pct('Edit') + pct('Write'),
236
+ readPct: pct('Read'),
237
+ grepPct: pct('Grep') + pct('Glob'),
238
+ agentPct: pct('Agent') + pct('Task'),
239
+ outputTokens,
240
+ inputTokens,
241
+ totalTools: tools.length,
242
+ };
243
+ };
244
+
245
+ return { human: bucket(human), agent: bucket(agent), totalSessions: sessions.length };
246
+ }
247
+
248
+ // ── GRADING ───────────────────────────────────────────────────────────────────
249
+ const LETTERS = ['F','D-','D','D+','C-','C','C+','B-','B','B+','A-','A','A+'];
250
+ function letterFor(score /* 0..100 */) {
251
+ if (score >= 97) return 'A+';
252
+ if (score >= 93) return 'A';
253
+ if (score >= 90) return 'A-';
254
+ if (score >= 87) return 'B+';
255
+ if (score >= 83) return 'B';
256
+ if (score >= 80) return 'B-';
257
+ if (score >= 77) return 'C+';
258
+ if (score >= 73) return 'C';
259
+ if (score >= 70) return 'C-';
260
+ if (score >= 67) return 'D+';
261
+ if (score >= 63) return 'D';
262
+ if (score >= 60) return 'D-';
263
+ return 'F';
264
+ }
265
+ function colorForLetter(L) {
266
+ if (L.startsWith('A')) return C.green;
267
+ if (L.startsWith('B')) return C.cyan;
268
+ if (L.startsWith('C')) return C.yellow;
269
+ return C.red;
270
+ }
271
+
272
+ function grade(stats, setup) {
273
+ const dims = [];
274
+ const human = stats.human || {};
275
+ const agent = stats.agent || {};
276
+
277
+ // 1. Hook coverage. Lives at the setup layer, applies to everyone.
278
+ const hookSignals = setup.preToolUseHooks + setup.postToolUseHooks + setup.userPromptSubmitHooks;
279
+ let hookScore;
280
+ if (hookSignals === 0) hookScore = 35;
281
+ else if (hookSignals === 1) hookScore = 65;
282
+ else if (hookSignals === 2) hookScore = 82;
283
+ else hookScore = Math.min(100, 88 + hookSignals * 2);
284
+ dims.push({
285
+ name: 'Hook coverage',
286
+ score: hookScore,
287
+ detail: hookSignals === 0
288
+ ? 'No PreToolUse, PostToolUse, or UserPromptSubmit hooks installed. Anything could happen overnight.'
289
+ : `${setup.preToolUseHooks} PreToolUse, ${setup.postToolUseHooks} PostToolUse, ${setup.userPromptSubmitHooks} UserPromptSubmit hook(s) installed.`,
290
+ fix: hookSignals === 0
291
+ ? 'Install claude-loop-sentinel for runaway-loop protection: npm i @uxcontinuum/claude-loop-sentinel'
292
+ : null,
293
+ });
294
+
295
+ // 2. Project hygiene (human sessions only).
296
+ if (human.sessions) {
297
+ let hScore = 50;
298
+ hScore += Math.round(human.titledPct * 0.5); // titled sessions help up to +50
299
+ if (human.avgPromptLen > 0 && human.avgPromptLen < 80) hScore -= 10; // too terse
300
+ if (human.avgPromptLen > 1500) hScore -= 8; // walls of text
301
+ hScore = Math.max(0, Math.min(100, hScore));
302
+ dims.push({
303
+ name: 'Project hygiene (human)',
304
+ score: hScore,
305
+ detail: `${human.titledPct}% of your human sessions are titled. Avg prompt: ${human.avgPromptLen} chars.`,
306
+ fix: human.titledPct < 30
307
+ ? 'Title your sessions. Untitled sessions are unsearchable history.'
308
+ : null,
309
+ });
310
+ }
311
+
312
+ // 3. Tool balance (human sessions only).
313
+ if (human.sessions && human.totalTools > 0) {
314
+ let bScore = 75;
315
+ if (human.bashPct > 65) bScore -= 18; // bash hammer
316
+ if (human.readPct + human.grepPct + human.editPct < 15) bScore -= 12;
317
+ if (human.editPct > 10 && human.editPct < 55) bScore += 8; // healthy editing
318
+ if (human.agentPct > 2) bScore += 7; // delegates work
319
+ bScore = Math.max(0, Math.min(100, bScore));
320
+ dims.push({
321
+ name: 'Tool balance (human)',
322
+ score: bScore,
323
+ detail: `Bash ${human.bashPct}%, Edit+Write ${human.editPct}%, Read ${human.readPct}%, Grep+Glob ${human.grepPct}%, Agent/Task ${human.agentPct}%.`,
324
+ fix: human.bashPct > 65
325
+ ? 'You are running things, not editing things. Use Edit/Write more.'
326
+ : null,
327
+ });
328
+ }
329
+
330
+ // 4. Prompt tells (the "just"/"please" tax).
331
+ if (human.sessions && human.prompts > 0) {
332
+ const justRate = human.justCount / human.prompts;
333
+ let pScore = 85;
334
+ if (justRate > 0.5) pScore -= 12;
335
+ if (justRate > 1.0) pScore -= 15;
336
+ if (human.pleaseCount / human.prompts > 0.3) pScore -= 6;
337
+ pScore = Math.max(0, Math.min(100, pScore));
338
+ dims.push({
339
+ name: 'Prompt tells',
340
+ score: pScore,
341
+ detail: `You said "just" ${human.justCount} times across ${human.prompts} prompts (${(justRate * 100).toFixed(0)}%).`,
342
+ fix: justRate > 0.5
343
+ ? 'The word "just" telegraphs that you think the task is simple. It is not. Strip it.'
344
+ : null,
345
+ });
346
+ }
347
+
348
+ // 5. Agent pipeline grade (only if agent sessions exist).
349
+ if (agent.sessions) {
350
+ let aScore = 75;
351
+ if (agent.sessions > 50) aScore += 8;
352
+ if (agent.outputTokens > 0 && agent.sessions > 0) {
353
+ const tokensPerSession = agent.outputTokens / agent.sessions;
354
+ if (tokensPerSession < 5000) aScore += 5; // efficient
355
+ if (tokensPerSession > 50000) aScore -= 8; // bloated
356
+ }
357
+ if (hookSignals === 0) aScore -= 12; // running pipeline with no safety net is reckless
358
+ aScore = Math.max(0, Math.min(100, aScore));
359
+ dims.push({
360
+ name: 'Pipeline ops (agent sessions)',
361
+ score: aScore,
362
+ detail: `${agent.sessions} agent-spawned sessions, ${(agent.outputTokens / 1e6).toFixed(2)}M output tokens.`,
363
+ fix: hookSignals === 0 ? 'Your pipeline has no runtime hooks. Install claude-loop-sentinel before the next overnight run.' : null,
364
+ });
365
+ }
366
+
367
+ const overall = Math.round(dims.reduce((s, d) => s + d.score, 0) / dims.length);
368
+ return { overall, letter: letterFor(overall), dims };
369
+ }
370
+
371
+ // ── OUTPUT ────────────────────────────────────────────────────────────────────
372
+ function renderCard(stats, setup, graded) {
373
+ const bar = (n) => {
374
+ const filled = Math.round(n / 5);
375
+ return '█'.repeat(filled) + '░'.repeat(20 - filled);
376
+ };
377
+
378
+ pr();
379
+ pr(` ${C.bold}${C.white}━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━${C.reset}`);
380
+ pr(` ${C.bold}${C.white} CCAUDIT${C.reset}${C.dim} your Claude Code report card${C.reset}`);
381
+ pr(` ${C.bold}${C.white}━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━${C.reset}`);
382
+ pr();
383
+
384
+ const L = graded.letter;
385
+ const lc = colorForLetter(L);
386
+ pr(` ${C.bold}OVERALL GRADE ${lc}${C.bold}${L}${C.reset} ${C.dim}(${graded.overall}/100)${C.reset}`);
387
+ pr();
388
+
389
+ for (const d of graded.dims) {
390
+ const dL = letterFor(d.score);
391
+ const dc = colorForLetter(dL);
392
+ pr(` ${C.bold}${d.name.padEnd(34)}${C.reset}${dc}${C.bold}${dL.padStart(3)}${C.reset} ${C.dim}${bar(d.score)}${C.reset}`);
393
+ pr(` ${C.dim}${d.detail}${C.reset}`);
394
+ if (d.fix) pr(` ${C.yellow}→ ${d.fix}${C.reset}`);
395
+ pr();
396
+ }
397
+
398
+ pr(` ${C.dim}━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━${C.reset}`);
399
+ pr(` ${C.bold}YOUR DATA${C.reset}`);
400
+ if (stats.human) {
401
+ pr(` ${C.dim}Human sessions:${C.reset} ${stats.human.sessions} sessions, ${stats.human.prompts} prompts, ${(stats.human.outputTokens / 1e6).toFixed(2)}M output tokens`);
402
+ }
403
+ if (stats.agent) {
404
+ pr(` ${C.dim}Agent sessions:${C.reset} ${stats.agent.sessions} sessions, ${(stats.agent.outputTokens / 1e6).toFixed(2)}M output tokens`);
405
+ }
406
+ pr(` ${C.dim}Hooks installed:${C.reset} ${setup.preToolUseHooks + setup.postToolUseHooks + setup.userPromptSubmitHooks} (${setup.hookFiles.length} hook file(s) in ~/.claude/hooks/)`);
407
+ pr(` ${C.dim}CLAUDE.md:${C.reset} ${setup.hasClaudeMd ? `${setup.claudeMdBytes} bytes` : 'not found'}`);
408
+ pr(` ${C.dim}Skills installed:${C.reset} ${setup.skillCount}`);
409
+
410
+ pr();
411
+ pr(` ${C.dim}━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━${C.reset}`);
412
+ pr(` ${C.dim}Run it on your own machine: ${C.reset}${C.bold}npx @uxcontinuum/ccaudit${C.reset}`);
413
+ pr(` ${C.dim}Source + fixes:${C.reset} github.com/turleydesigns/claude-audit`);
414
+
415
+ const failingDims = graded.dims.filter(d => d.score < 75).length;
416
+ if (failingDims >= 2) {
417
+ pr();
418
+ pr(` ${C.bold}${C.yellow}${failingDims} dimensions flagged. The Continuum Sprint fixes setups like this in 2 weeks.${C.reset}`);
419
+ pr(` ${C.dim}continuum.build${C.reset}`);
420
+ }
421
+ pr();
422
+ }
423
+
424
+ // ── MAIN ──────────────────────────────────────────────────────────────────────
425
+ const days = parseInt(getArg('--days', '30'), 10);
426
+ const cutoffMs = Date.now() - days * 86400000;
427
+
428
+ const projectsDirs = CLAUDE_DIRS.map(d => path.join(d, 'projects'))
429
+ .filter(d => { try { return fs.statSync(d).isDirectory(); } catch (_) { return false; } });
430
+
431
+ const files = projectsDirs.flatMap(d => findJsonl(d, cutoffMs));
432
+ const sessions = files.map(f => parseSession(f, cutoffMs)).filter(Boolean);
433
+
434
+ if (!sessions.length) {
435
+ pr(`No Claude Code sessions found in the last ${days} days.`);
436
+ pr(`Looked in: ${projectsDirs.join(', ') || CLAUDE_DIRS.join(', ')}`);
437
+ process.exit(0);
438
+ }
439
+
440
+ const stats = aggregate(sessions);
441
+ const setup = inspectClaudeDir(CLAUDE_DIRS[0]);
442
+ const graded = grade(stats, setup);
443
+
444
+ renderCard(stats, setup, graded);
package/package.json ADDED
@@ -0,0 +1,33 @@
1
+ {
2
+ "name": "@uxcontinuum/ccaudit",
3
+ "version": "1.0.0",
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
+ "main": "index.js",
6
+ "bin": {
7
+ "ccaudit": "./index.js"
8
+ },
9
+ "scripts": {
10
+ "start": "node index.js"
11
+ },
12
+ "keywords": [
13
+ "claude",
14
+ "claude-code",
15
+ "anthropic",
16
+ "ai",
17
+ "developer-tools",
18
+ "audit",
19
+ "diagnostic",
20
+ "report-card",
21
+ "cli"
22
+ ],
23
+ "author": "Matt Turley <mt@turleydesigns.com>",
24
+ "license": "MIT",
25
+ "repository": {
26
+ "type": "git",
27
+ "url": "https://github.com/turleydesigns/claude-audit"
28
+ },
29
+ "homepage": "https://github.com/turleydesigns/claude-audit#readme",
30
+ "engines": {
31
+ "node": ">=14.0.0"
32
+ }
33
+ }