claudex-setup 1.15.0 → 1.16.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 +17 -15
- package/bin/cli.js +77 -4
- package/content/launch-posts.md +8 -9
- package/package.json +8 -2
- package/src/activity.js +19 -0
- package/src/analyze.js +12 -0
- package/src/audit.js +10 -0
- package/src/benchmark.js +24 -0
- package/src/context.js +13 -0
- package/src/governance.js +25 -6
- package/src/interactive.js +8 -1
- package/src/plans.js +3 -3
- package/src/setup.js +56 -58
- package/src/techniques.js +18 -0
package/README.md
CHANGED
|
@@ -51,6 +51,8 @@ Tested on 4 real projects — not demos:
|
|
|
51
51
|
|
|
52
52
|
Most common gaps found: missing secrets protection, no deny rules, no mermaid diagram, no hooks in settings.
|
|
53
53
|
|
|
54
|
+
> Scores measured with claudex-setup@1.10.3 on 2026-04-03. Full case studies: [VTCLE](https://github.com/DnaFin/claudex/blob/main/research/case-study-vtcle-2026-04-03.md) | [Social](https://github.com/DnaFin/claudex/blob/main/research/case-study-social-2026-04-03.md) | [Polymiro](https://github.com/DnaFin/claudex/blob/main/research/case-study-polymiro-2026-04-03.md)
|
|
55
|
+
|
|
54
56
|
## What You Get
|
|
55
57
|
|
|
56
58
|
```
|
|
@@ -259,7 +261,7 @@ If you are using `npx` only, copy the same file from the GitHub repo at `content
|
|
|
259
261
|
|
|
260
262
|
The skill runs `npx claudex-setup --json`, summarizes the score, shows the top next actions, and points to the right next command without applying changes.
|
|
261
263
|
|
|
262
|
-
##
|
|
264
|
+
## 84 Checks Across 14 Categories
|
|
263
265
|
|
|
264
266
|
The exact applicable count can be lower on a given repo because stack-specific checks are skipped when they do not apply.
|
|
265
267
|
|
|
@@ -268,30 +270,30 @@ The exact applicable count can be lower on a given repo because stack-specific c
|
|
|
268
270
|
| Memory | 8 | CLAUDE.md, architecture, conventions |
|
|
269
271
|
| Quality | 7 | verification loops, self-correction |
|
|
270
272
|
| Git Safety | 5 | hooks, force-push protection |
|
|
271
|
-
| Workflow |
|
|
272
|
-
| Security |
|
|
273
|
+
| Workflow | 8 | commands, skills, rules, agents |
|
|
274
|
+
| Security | 6 | permissions, secrets, deny rules |
|
|
273
275
|
| Automation | 5 | PreToolUse, PostToolUse, SessionStart |
|
|
274
276
|
| Design | 4 | Mermaid, XML tags, structured prompts |
|
|
275
|
-
| DevOps |
|
|
276
|
-
| Hygiene |
|
|
277
|
+
| DevOps | 6 | Docker, CI, Terraform, K8s, pipelines |
|
|
278
|
+
| Hygiene | 7 | .gitignore, cleanup, structure |
|
|
277
279
|
| Performance | 3 | context management, compaction |
|
|
278
280
|
| MCP | 3 | servers, Context7, integrations |
|
|
279
|
-
| Prompting |
|
|
280
|
-
| Features |
|
|
281
|
-
| **Quality Deep** | **
|
|
281
|
+
| Prompting | 5 | constraints, validation, patterns, style |
|
|
282
|
+
| Features | 3 | /security-review, Channels, modern features |
|
|
283
|
+
| **Quality Deep** | **14** | **freshness, contradictions, deprecated patterns, maxTurns, $ARGUMENTS, hook specificity** |
|
|
282
284
|
|
|
283
285
|
## Stack Detection
|
|
284
286
|
|
|
285
|
-
Auto-detects and tailors output for
|
|
287
|
+
Auto-detects and tailors output for 30 stacks:
|
|
286
288
|
|
|
287
289
|
| | |
|
|
288
290
|
|--|--|
|
|
289
|
-
| **Frontend** | React, Vue, Angular, Next.js, Svelte |
|
|
290
|
-
| **Backend** | Node.js, Python, Django, FastAPI |
|
|
291
|
-
| **Mobile** | Flutter, Swift, Kotlin |
|
|
292
|
-
| **Systems** | Rust, Go, Java, Ruby, C++, Bazel |
|
|
291
|
+
| **Frontend** | React, Vue, Angular, Next.js, Svelte, Astro |
|
|
292
|
+
| **Backend** | Node.js, Python, Django, FastAPI, Express, NestJS, Spring Boot |
|
|
293
|
+
| **Mobile** | React Native, Expo, Flutter, Swift, Kotlin |
|
|
294
|
+
| **Systems** | Rust, Go, Java, Ruby, C++, Bazel, Deno, Bun |
|
|
293
295
|
| **Language** | TypeScript |
|
|
294
|
-
| **Infra** | Docker, Terraform, Kubernetes |
|
|
296
|
+
| **Infra** | Docker, Terraform, Kubernetes, Wrangler |
|
|
295
297
|
|
|
296
298
|
## GitHub Action
|
|
297
299
|
|
|
@@ -305,7 +307,7 @@ jobs:
|
|
|
305
307
|
runs-on: ubuntu-latest
|
|
306
308
|
steps:
|
|
307
309
|
- uses: actions/checkout@v4
|
|
308
|
-
- uses: DnaFin/claudex-setup@v1.
|
|
310
|
+
- uses: DnaFin/claudex-setup@v1.15.1
|
|
309
311
|
with:
|
|
310
312
|
threshold: 50
|
|
311
313
|
```
|
package/bin/cli.js
CHANGED
|
@@ -19,7 +19,7 @@ const COMMAND_ALIASES = {
|
|
|
19
19
|
suggest: 'suggest-only',
|
|
20
20
|
gov: 'governance',
|
|
21
21
|
};
|
|
22
|
-
const KNOWN_COMMANDS = ['audit', 'setup', 'augment', 'suggest-only', 'plan', 'apply', 'governance', 'benchmark', 'deep-review', 'interactive', 'watch', 'badge', 'insights', 'history', 'compare', 'trend', 'help', 'version'];
|
|
22
|
+
const KNOWN_COMMANDS = ['audit', 'setup', 'augment', 'suggest-only', 'plan', 'apply', 'governance', 'benchmark', 'deep-review', 'interactive', 'watch', 'badge', 'insights', 'history', 'compare', 'trend', 'scan', 'help', 'version'];
|
|
23
23
|
|
|
24
24
|
function levenshtein(a, b) {
|
|
25
25
|
const matrix = Array.from({ length: a.length + 1 }, () => Array(b.length + 1).fill(0));
|
|
@@ -63,6 +63,7 @@ function parseArgs(rawArgs) {
|
|
|
63
63
|
let mcpPacks = [];
|
|
64
64
|
let requireChecks = [];
|
|
65
65
|
let commandSet = false;
|
|
66
|
+
let extraArgs = [];
|
|
66
67
|
|
|
67
68
|
for (let i = 0; i < rawArgs.length; i++) {
|
|
68
69
|
const arg = rawArgs[i];
|
|
@@ -126,12 +127,14 @@ function parseArgs(rawArgs) {
|
|
|
126
127
|
if (!commandSet) {
|
|
127
128
|
command = arg;
|
|
128
129
|
commandSet = true;
|
|
130
|
+
} else {
|
|
131
|
+
extraArgs.push(arg);
|
|
129
132
|
}
|
|
130
133
|
}
|
|
131
134
|
|
|
132
135
|
const normalizedCommand = COMMAND_ALIASES[command] || command;
|
|
133
136
|
|
|
134
|
-
return { flags, command, normalizedCommand, threshold, out, planFile, only, profile, mcpPacks, requireChecks };
|
|
137
|
+
return { flags, command, normalizedCommand, threshold, out, planFile, only, profile, mcpPacks, requireChecks, extraArgs };
|
|
135
138
|
}
|
|
136
139
|
|
|
137
140
|
const HELP = `
|
|
@@ -155,6 +158,9 @@ const HELP = `
|
|
|
155
158
|
npx claudex-setup compare Compare latest vs previous snapshot
|
|
156
159
|
npx claudex-setup trend --out r.md Export trend report as markdown
|
|
157
160
|
|
|
161
|
+
Multi-repo:
|
|
162
|
+
npx claudex-setup scan dir1 dir2 Compare multiple repos side-by-side
|
|
163
|
+
|
|
158
164
|
Advanced:
|
|
159
165
|
npx claudex-setup governance Permission profiles, hooks, policy packs
|
|
160
166
|
npx claudex-setup benchmark Before/after in isolated temp copy
|
|
@@ -249,7 +255,7 @@ async function main() {
|
|
|
249
255
|
process.exit(1);
|
|
250
256
|
}
|
|
251
257
|
|
|
252
|
-
if (options.require && normalizedCommand !== 'audit' && !['audit', 'discover'].includes(command)) {
|
|
258
|
+
if (options.require && options.require.length > 0 && normalizedCommand !== 'audit' && !['audit', 'discover'].includes(command)) {
|
|
253
259
|
console.error(`\n Warning: --require is only supported with the audit command. Ignoring for '${normalizedCommand}'.\n`);
|
|
254
260
|
}
|
|
255
261
|
|
|
@@ -279,7 +285,74 @@ async function main() {
|
|
|
279
285
|
}
|
|
280
286
|
|
|
281
287
|
try {
|
|
282
|
-
if (normalizedCommand === '
|
|
288
|
+
if (normalizedCommand === 'scan') {
|
|
289
|
+
const scanDirs = parsed.extraArgs;
|
|
290
|
+
if (scanDirs.length === 0) {
|
|
291
|
+
console.error('\n Error: scan requires at least one directory argument.');
|
|
292
|
+
console.error(' Usage: npx claudex-setup scan dir1 dir2 dir3\n');
|
|
293
|
+
process.exit(1);
|
|
294
|
+
}
|
|
295
|
+
const fs = require('fs');
|
|
296
|
+
const pathMod = require('path');
|
|
297
|
+
const rows = [];
|
|
298
|
+
for (const rawDir of scanDirs) {
|
|
299
|
+
const dir = pathMod.resolve(rawDir);
|
|
300
|
+
if (!fs.existsSync(dir)) {
|
|
301
|
+
rows.push({ name: pathMod.basename(rawDir), dir: rawDir, score: null, passed: '-', failed: '-', suggested: '-', error: 'directory not found' });
|
|
302
|
+
continue;
|
|
303
|
+
}
|
|
304
|
+
try {
|
|
305
|
+
const result = await audit({ dir, silent: true });
|
|
306
|
+
rows.push({
|
|
307
|
+
name: pathMod.basename(dir),
|
|
308
|
+
dir: rawDir,
|
|
309
|
+
score: result.score,
|
|
310
|
+
passed: result.passed,
|
|
311
|
+
failed: result.failed,
|
|
312
|
+
suggested: result.suggestedNextCommand || '-',
|
|
313
|
+
error: null,
|
|
314
|
+
});
|
|
315
|
+
} catch (err) {
|
|
316
|
+
rows.push({ name: pathMod.basename(dir), dir: rawDir, score: null, passed: '-', failed: '-', suggested: '-', error: err.message });
|
|
317
|
+
}
|
|
318
|
+
}
|
|
319
|
+
|
|
320
|
+
if (options.json) {
|
|
321
|
+
console.log(JSON.stringify(rows, null, 2));
|
|
322
|
+
} else {
|
|
323
|
+
// Find weakest
|
|
324
|
+
const validRows = rows.filter(r => r.score !== null);
|
|
325
|
+
const minScore = validRows.length > 0 ? Math.min(...validRows.map(r => r.score)) : null;
|
|
326
|
+
const weakest = validRows.length > 1 && validRows.filter(r => r.score > minScore).length > 0
|
|
327
|
+
? validRows.find(r => r.score === minScore)
|
|
328
|
+
: null;
|
|
329
|
+
|
|
330
|
+
console.log('');
|
|
331
|
+
console.log('\x1b[1m claudex-setup multi-repo scan\x1b[0m');
|
|
332
|
+
console.log('\x1b[2m ═══════════════════════════════════════\x1b[0m');
|
|
333
|
+
console.log('');
|
|
334
|
+
|
|
335
|
+
// Table header
|
|
336
|
+
const nameW = Math.max(8, ...rows.map(r => r.name.length)) + 2;
|
|
337
|
+
const header = ` ${'Project'.padEnd(nameW)} ${'Score'.padStart(5)} ${'Pass'.padStart(4)} ${'Fail'.padStart(4)} Suggested Command`;
|
|
338
|
+
console.log('\x1b[1m' + header + '\x1b[0m');
|
|
339
|
+
console.log(' ' + '─'.repeat(header.trim().length));
|
|
340
|
+
|
|
341
|
+
for (const row of rows) {
|
|
342
|
+
if (row.error) {
|
|
343
|
+
console.log(` ${row.name.padEnd(nameW)} \x1b[31m${('ERR').padStart(5)}\x1b[0m ${String(row.passed).padStart(4)} ${String(row.failed).padStart(4)} ${row.error}`);
|
|
344
|
+
continue;
|
|
345
|
+
}
|
|
346
|
+
const isWeak = weakest && row.name === weakest.name && row.dir === weakest.dir;
|
|
347
|
+
const scoreColor = row.score >= 70 ? '\x1b[32m' : row.score >= 40 ? '\x1b[33m' : '\x1b[31m';
|
|
348
|
+
const prefix = isWeak ? '\x1b[31m⚠ ' : ' ';
|
|
349
|
+
const suffix = isWeak ? ' ← weakest\x1b[0m' : '';
|
|
350
|
+
console.log(`${prefix}${row.name.padEnd(nameW)} ${scoreColor}${String(row.score).padStart(5)}\x1b[0m ${String(row.passed).padStart(4)} ${String(row.failed).padStart(4)} ${row.suggested}${suffix}`);
|
|
351
|
+
}
|
|
352
|
+
console.log('');
|
|
353
|
+
}
|
|
354
|
+
process.exit(0);
|
|
355
|
+
} else if (normalizedCommand === 'history') {
|
|
283
356
|
const { formatHistory } = require('../src/activity');
|
|
284
357
|
console.log('');
|
|
285
358
|
console.log(formatHistory(options.dir));
|
package/content/launch-posts.md
CHANGED
|
@@ -5,16 +5,14 @@
|
|
|
5
5
|
**Title:** I built a tool that audits your project for Claude Code optimization — scores you 0-100
|
|
6
6
|
|
|
7
7
|
**Body:**
|
|
8
|
-
After cataloging 1,107 Claude Code entries and verifying 948 of them with evidence, I built a CLI that checks if your project is actually set up to get the most out of Claude Code.
|
|
8
|
+
After cataloging 1,107 Claude Code entries and verifying 948 of them with evidence, I built a CLI that checks if your project is actually set up to get the most out of Claude Code. It runs 84 checks across CLAUDE.md, hooks, commands, agents, diagrams, and more.
|
|
9
9
|
|
|
10
|
-
|
|
10
|
+
I tested it on 4 real repos. A FastAPI marketing engine went from 46 to 64. A React Native social app went from 40 to 48. A Polymiro project jumped from 35 to 48. The CLAUDEX catalog repo itself: 62 to 90.
|
|
11
11
|
|
|
12
12
|
```
|
|
13
13
|
npx claudex-setup
|
|
14
14
|
```
|
|
15
15
|
|
|
16
|
-
It checks for: CLAUDE.md, hooks, custom commands, skills, agents, Mermaid diagrams, XML tags, path rules, MCP config, permissions, and more.
|
|
17
|
-
|
|
18
16
|
Then `npx claudex-setup setup` auto-creates everything that's missing, tailored to your stack (React, Python, TypeScript, etc).
|
|
19
17
|
|
|
20
18
|
Zero dependencies. No API keys. Runs entirely local.
|
|
@@ -38,7 +36,8 @@ Turns out most projects are missing basic stuff that makes a huge difference:
|
|
|
38
36
|
- No custom commands (repeating the same prompts manually)
|
|
39
37
|
- No Mermaid diagrams (wasting 73% more tokens on prose descriptions)
|
|
40
38
|
|
|
41
|
-
|
|
39
|
+
I tested 4 real repos with 84 checks. Before optimization: scores ranged from 35 to 62. After: 48 to 90. A VTCLE FastAPI project jumped from 46→64 just from adding missing hooks and commands.
|
|
40
|
+
|
|
42
41
|
```
|
|
43
42
|
npx claudex-setup
|
|
44
43
|
```
|
|
@@ -54,7 +53,7 @@ Free, open source, zero dependencies: https://github.com/DnaFin/claudex-setup
|
|
|
54
53
|
**Title:** 1,107 Claude Code Entries: What I Learned Building the Most Comprehensive Catalog
|
|
55
54
|
|
|
56
55
|
**Body (excerpt):**
|
|
57
|
-
I set out to catalog every single Claude Code capability, technique, and best practice. After repeated research cycles, I have 1,107 entries — 948 verified with real evidence.
|
|
56
|
+
I set out to catalog every single Claude Code capability, technique, and best practice. After repeated research cycles, I have 1,107 entries — 948 verified with real evidence. I packaged this into an 84-check CLI audit and tested it on 4 real projects — scores before optimization ranged from 35 to 62, and after: 48 to 90.
|
|
58
57
|
|
|
59
58
|
Here are the top 10 things most developers are missing:
|
|
60
59
|
|
|
@@ -94,7 +93,7 @@ I cataloged 1,107 Claude Code entries and verified 948 of them with evidence.
|
|
|
94
93
|
|
|
95
94
|
Most projects use less than 5% of what Claude Code can do.
|
|
96
95
|
|
|
97
|
-
|
|
96
|
+
Tested on 4 real repos — a React Native app scored 40, a FastAPI engine scored 46. After auto-setup: 48 and 64. Here's the free 84-check audit:
|
|
98
97
|
|
|
99
98
|
npx claudex-setup
|
|
100
99
|
|
|
@@ -148,9 +147,9 @@ https://github.com/DnaFin/claudex-setup
|
|
|
148
147
|
**Body:**
|
|
149
148
|
I built a CLI tool that scores your project against Claude Code best practices.
|
|
150
149
|
|
|
151
|
-
After researching 1,107 entries (948 verified with evidence),
|
|
150
|
+
After researching 1,107 entries (948 verified with evidence), I tested 4 real repos with 84 checks. Scores before optimization: 35–62. After auto-setup: 48–90. The biggest jump was a research catalog repo going from 62 to 90.
|
|
152
151
|
|
|
153
|
-
npx claudex-setup → audit (0-100 score)
|
|
152
|
+
npx claudex-setup → audit (84 checks, 0-100 score)
|
|
154
153
|
npx claudex-setup setup → auto-fix
|
|
155
154
|
|
|
156
155
|
Detects your stack (React, Python, TS, Rust, Go, etc) and tailors recommendations.
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "claudex-setup",
|
|
3
|
-
"version": "1.
|
|
3
|
+
"version": "1.16.0",
|
|
4
4
|
"description": "Score your repo's Claude Code setup against 84 checks. See gaps, apply fixes selectively with rollback, govern hooks and permissions, and benchmark impact — without breaking existing config.",
|
|
5
5
|
"main": "src/index.js",
|
|
6
6
|
"bin": {
|
|
@@ -16,7 +16,10 @@
|
|
|
16
16
|
"scripts": {
|
|
17
17
|
"start": "node bin/cli.js",
|
|
18
18
|
"build": "npm pack --dry-run",
|
|
19
|
-
"test": "node test/run.js"
|
|
19
|
+
"test": "node test/run.js",
|
|
20
|
+
"test:jest": "jest",
|
|
21
|
+
"test:coverage": "jest --coverage",
|
|
22
|
+
"test:all": "node test/run.js && node test/check-matrix.js && node test/golden-matrix.js && node test/security-tests.js && jest"
|
|
20
23
|
},
|
|
21
24
|
"keywords": [
|
|
22
25
|
"claude",
|
|
@@ -42,5 +45,8 @@
|
|
|
42
45
|
},
|
|
43
46
|
"engines": {
|
|
44
47
|
"node": ">=18.0.0"
|
|
48
|
+
},
|
|
49
|
+
"devDependencies": {
|
|
50
|
+
"jest": "^30.3.0"
|
|
45
51
|
}
|
|
46
52
|
}
|
package/src/activity.js
CHANGED
|
@@ -140,6 +140,14 @@ function updateSnapshotIndex(snapshotDir, record) {
|
|
|
140
140
|
fs.writeFileSync(indexPath, JSON.stringify(entries, null, 2), 'utf8');
|
|
141
141
|
}
|
|
142
142
|
|
|
143
|
+
/**
|
|
144
|
+
* Write a normalized snapshot artifact to .claude/claudex-setup/snapshots/ and update the index.
|
|
145
|
+
* @param {string} dir - Project root directory.
|
|
146
|
+
* @param {string} snapshotKind - Snapshot type ('audit', 'benchmark', 'governance', 'augment', 'suggest-only').
|
|
147
|
+
* @param {Object} payload - Full result payload to persist.
|
|
148
|
+
* @param {Object} [meta={}] - Optional metadata fields merged into the envelope.
|
|
149
|
+
* @returns {Object} Artifact record with id, filePath, relativePath, indexPath, and summary.
|
|
150
|
+
*/
|
|
143
151
|
function writeSnapshotArtifact(dir, snapshotKind, payload, meta = {}) {
|
|
144
152
|
const id = timestampId();
|
|
145
153
|
const { snapshotDir } = ensureArtifactDirs(dir);
|
|
@@ -189,6 +197,12 @@ function readSnapshotIndex(dir) {
|
|
|
189
197
|
}
|
|
190
198
|
}
|
|
191
199
|
|
|
200
|
+
/**
|
|
201
|
+
* Get the audit score history from saved snapshots, most recent first.
|
|
202
|
+
* @param {string} dir - Project root directory.
|
|
203
|
+
* @param {number} [limit=20] - Maximum number of entries to return.
|
|
204
|
+
* @returns {Object[]} Array of snapshot index entries for audit snapshots.
|
|
205
|
+
*/
|
|
192
206
|
function getHistory(dir, limit = 20) {
|
|
193
207
|
const entries = readSnapshotIndex(dir);
|
|
194
208
|
return entries
|
|
@@ -197,6 +211,11 @@ function getHistory(dir, limit = 20) {
|
|
|
197
211
|
.slice(0, limit);
|
|
198
212
|
}
|
|
199
213
|
|
|
214
|
+
/**
|
|
215
|
+
* Compare the two most recent audit snapshots and return the delta.
|
|
216
|
+
* @param {string} dir - Project root directory.
|
|
217
|
+
* @returns {Object|null} Comparison with current/previous scores, delta, regressions, improvements, and trend. Null if fewer than 2 snapshots.
|
|
218
|
+
*/
|
|
200
219
|
function compareLatest(dir) {
|
|
201
220
|
const audits = getHistory(dir, 2);
|
|
202
221
|
if (audits.length < 2) return null;
|
package/src/analyze.js
CHANGED
|
@@ -308,6 +308,13 @@ function buildRolloutOrder(report) {
|
|
|
308
308
|
return steps;
|
|
309
309
|
}
|
|
310
310
|
|
|
311
|
+
/**
|
|
312
|
+
* Analyze a project's Claude Code setup and produce a structured recommendation report.
|
|
313
|
+
* @param {Object} options - Analysis options.
|
|
314
|
+
* @param {string} options.dir - Project directory to analyze.
|
|
315
|
+
* @param {string} [options.mode='augment'] - Analysis mode ('augment' or 'suggest-only').
|
|
316
|
+
* @returns {Promise<Object>} Structured report with project summary, gaps, strengths, and recommendations.
|
|
317
|
+
*/
|
|
311
318
|
async function analyzeProject(options) {
|
|
312
319
|
const mode = options.mode || 'augment';
|
|
313
320
|
const ctx = new ProjectContext(options.dir);
|
|
@@ -472,6 +479,11 @@ function printAnalysis(report, options = {}) {
|
|
|
472
479
|
}
|
|
473
480
|
}
|
|
474
481
|
|
|
482
|
+
/**
|
|
483
|
+
* Export an analysis report as a formatted markdown string.
|
|
484
|
+
* @param {Object} report - The report object returned by analyzeProject().
|
|
485
|
+
* @returns {string} Markdown-formatted report content.
|
|
486
|
+
*/
|
|
475
487
|
function exportMarkdown(report) {
|
|
476
488
|
const lines = [];
|
|
477
489
|
lines.push(`# Claudex Setup Analysis Report`);
|
package/src/audit.js
CHANGED
|
@@ -179,6 +179,16 @@ function printLiteAudit(result, dir) {
|
|
|
179
179
|
console.log('');
|
|
180
180
|
}
|
|
181
181
|
|
|
182
|
+
/**
|
|
183
|
+
* Run a full audit of a project's Claude Code setup against the CLAUDEX technique database.
|
|
184
|
+
* @param {Object} options - Audit options.
|
|
185
|
+
* @param {string} options.dir - Project directory to audit.
|
|
186
|
+
* @param {boolean} [options.silent] - Skip all console output, return result only.
|
|
187
|
+
* @param {boolean} [options.json] - Output result as JSON.
|
|
188
|
+
* @param {boolean} [options.lite] - Show short top-3 quick scan.
|
|
189
|
+
* @param {boolean} [options.verbose] - Show all recommendations including medium-impact.
|
|
190
|
+
* @returns {Promise<Object>} Audit result with score, passed/failed counts, quickWins, and topNextActions.
|
|
191
|
+
*/
|
|
182
192
|
async function audit(options) {
|
|
183
193
|
const silent = options.silent || false;
|
|
184
194
|
const ctx = new ProjectContext(options.dir);
|
package/src/benchmark.js
CHANGED
|
@@ -114,6 +114,21 @@ function buildExecutiveSummary(before, after, workflowEvidence) {
|
|
|
114
114
|
};
|
|
115
115
|
}
|
|
116
116
|
|
|
117
|
+
function buildPracticalValue(before, after, applyResult) {
|
|
118
|
+
const written = applyResult.writtenFiles || [];
|
|
119
|
+
return {
|
|
120
|
+
denyRulesAdded: written.includes('.claude/settings.json') ? 'yes' : 'no',
|
|
121
|
+
hooksCreated: written.filter(f => f.includes('hooks/')).length,
|
|
122
|
+
commandsCreated: written.filter(f => f.includes('commands/')).length,
|
|
123
|
+
agentsCreated: written.filter(f => f.includes('agents/')).length,
|
|
124
|
+
skillsCreated: written.filter(f => f.includes('skills/')).length,
|
|
125
|
+
rulesCreated: written.filter(f => f.includes('rules/')).length,
|
|
126
|
+
claudeMdCreated: written.includes('CLAUDE.md') ? 'yes' : 'no',
|
|
127
|
+
totalFilesCreated: written.length,
|
|
128
|
+
totalFilesPreserved: (applyResult.preservedFiles || []).length,
|
|
129
|
+
};
|
|
130
|
+
}
|
|
131
|
+
|
|
117
132
|
function buildCaseStudy(before, after, applyResult) {
|
|
118
133
|
return {
|
|
119
134
|
initialState: `Baseline score ${before.score}/100, organic ${before.organicScore}/100.`,
|
|
@@ -125,6 +140,7 @@ function buildCaseStudy(before, after, applyResult) {
|
|
|
125
140
|
organicDelta: after.organicScore - before.organicScore,
|
|
126
141
|
passedDelta: after.passed - before.passed,
|
|
127
142
|
},
|
|
143
|
+
practicalValue: buildPracticalValue(before, after, applyResult),
|
|
128
144
|
};
|
|
129
145
|
}
|
|
130
146
|
|
|
@@ -172,6 +188,14 @@ function renderBenchmarkMarkdown(report) {
|
|
|
172
188
|
].join('\n');
|
|
173
189
|
}
|
|
174
190
|
|
|
191
|
+
/**
|
|
192
|
+
* Run a before/after benchmark on an isolated copy of the project.
|
|
193
|
+
* @param {Object} options - Benchmark options.
|
|
194
|
+
* @param {string} options.dir - Project directory to benchmark.
|
|
195
|
+
* @param {string} [options.profile] - Permission profile to use during setup.
|
|
196
|
+
* @param {string[]} [options.mcpPacks] - MCP pack keys to include in setup.
|
|
197
|
+
* @returns {Promise<Object>} Benchmark report with before/after scores, delta, and workflow evidence.
|
|
198
|
+
*/
|
|
175
199
|
async function runBenchmark(options) {
|
|
176
200
|
const before = await audit({ dir: options.dir, silent: true });
|
|
177
201
|
const tempRoot = fs.mkdtempSync(path.join(os.tmpdir(), 'claudex-benchmark-'));
|
package/src/context.js
CHANGED
|
@@ -5,6 +5,10 @@
|
|
|
5
5
|
const fs = require('fs');
|
|
6
6
|
const path = require('path');
|
|
7
7
|
|
|
8
|
+
/**
|
|
9
|
+
* Scans and caches project files to provide fast lookups for technique checks.
|
|
10
|
+
* Reads the project directory on construction and exposes helpers for file content, JSON, and stack detection.
|
|
11
|
+
*/
|
|
8
12
|
class ProjectContext {
|
|
9
13
|
constructor(dir) {
|
|
10
14
|
this.dir = dir;
|
|
@@ -67,10 +71,19 @@ class ProjectContext {
|
|
|
67
71
|
}
|
|
68
72
|
}
|
|
69
73
|
|
|
74
|
+
/**
|
|
75
|
+
* Return the contents of the project's CLAUDE.md (root or .claude/ location).
|
|
76
|
+
* @returns {string|null} File content or null if not found.
|
|
77
|
+
*/
|
|
70
78
|
claudeMdContent() {
|
|
71
79
|
return this.fileContent('CLAUDE.md') || this.fileContent('.claude/CLAUDE.md');
|
|
72
80
|
}
|
|
73
81
|
|
|
82
|
+
/**
|
|
83
|
+
* Read and cache the content of a file relative to the project root.
|
|
84
|
+
* @param {string} filePath - Relative path from the project root.
|
|
85
|
+
* @returns {string|null} File content or null if not readable.
|
|
86
|
+
*/
|
|
74
87
|
fileContent(filePath) {
|
|
75
88
|
if (this._cache[filePath] !== undefined) return this._cache[filePath];
|
|
76
89
|
const fullPath = path.join(this.dir, filePath);
|
package/src/governance.js
CHANGED
|
@@ -254,36 +254,46 @@ function buildHookConfig(hookFiles, profileKey) {
|
|
|
254
254
|
return {};
|
|
255
255
|
}
|
|
256
256
|
|
|
257
|
+
// Detect hook runtime: .js files use node, .sh files use bash
|
|
258
|
+
const hookCommand = (file) => {
|
|
259
|
+
if (file.endsWith('.js')) return `node .claude/hooks/${file}`;
|
|
260
|
+
return `bash .claude/hooks/${file}`;
|
|
261
|
+
};
|
|
262
|
+
const isSecrets = (f) => f === 'protect-secrets.sh' || f === 'protect-secrets.js';
|
|
263
|
+
const isSession = (f) => f === 'session-start.sh' || f === 'session-start.js';
|
|
264
|
+
|
|
257
265
|
const hookConfig = {
|
|
258
266
|
PostToolUse: [{
|
|
259
267
|
matcher: 'Write|Edit',
|
|
260
268
|
hooks: uniqueFiles
|
|
261
|
-
.filter(file => file
|
|
269
|
+
.filter(file => !isSecrets(file) && !isSession(file))
|
|
262
270
|
.map(file => ({
|
|
263
271
|
type: 'command',
|
|
264
|
-
command:
|
|
272
|
+
command: hookCommand(file),
|
|
265
273
|
timeout: 10,
|
|
266
274
|
})),
|
|
267
275
|
}],
|
|
268
276
|
};
|
|
269
277
|
|
|
270
|
-
|
|
278
|
+
const secretsFile = uniqueFiles.find(isSecrets);
|
|
279
|
+
if (secretsFile) {
|
|
271
280
|
hookConfig.PreToolUse = [{
|
|
272
281
|
matcher: 'Read|Write|Edit',
|
|
273
282
|
hooks: [{
|
|
274
283
|
type: 'command',
|
|
275
|
-
command:
|
|
284
|
+
command: hookCommand(secretsFile),
|
|
276
285
|
timeout: 5,
|
|
277
286
|
}],
|
|
278
287
|
}];
|
|
279
288
|
}
|
|
280
289
|
|
|
281
|
-
|
|
290
|
+
const sessionFile = uniqueFiles.find(isSession);
|
|
291
|
+
if (sessionFile) {
|
|
282
292
|
hookConfig.SessionStart = [{
|
|
283
293
|
matcher: '*',
|
|
284
294
|
hooks: [{
|
|
285
295
|
type: 'command',
|
|
286
|
-
command:
|
|
296
|
+
command: hookCommand(sessionFile),
|
|
287
297
|
timeout: 5,
|
|
288
298
|
}],
|
|
289
299
|
}];
|
|
@@ -322,6 +332,10 @@ function buildSettingsForProfile({ profileKey = 'safe-write', hookFiles = [], ex
|
|
|
322
332
|
return base;
|
|
323
333
|
}
|
|
324
334
|
|
|
335
|
+
/**
|
|
336
|
+
* Return the full governance surface: permission profiles, hooks, policy packs, and pilot kit.
|
|
337
|
+
* @returns {Object} Summary containing permissionProfiles, hookRegistry, policyPacks, domainPacks, mcpPacks, and pilotRolloutKit.
|
|
338
|
+
*/
|
|
325
339
|
function getGovernanceSummary() {
|
|
326
340
|
return {
|
|
327
341
|
permissionProfiles: PERMISSION_PROFILES,
|
|
@@ -386,6 +400,11 @@ function printGovernanceSummary(summary, options = {}) {
|
|
|
386
400
|
console.log('');
|
|
387
401
|
}
|
|
388
402
|
|
|
403
|
+
/**
|
|
404
|
+
* Render a governance summary as a formatted markdown string.
|
|
405
|
+
* @param {Object} summary - The summary object returned by getGovernanceSummary().
|
|
406
|
+
* @returns {string} Markdown-formatted governance report.
|
|
407
|
+
*/
|
|
389
408
|
function renderGovernanceMarkdown(summary) {
|
|
390
409
|
const lines = [
|
|
391
410
|
'# Claudex Setup Governance Report',
|
package/src/interactive.js
CHANGED
|
@@ -98,7 +98,14 @@ async function interactive(options) {
|
|
|
98
98
|
}
|
|
99
99
|
|
|
100
100
|
console.log('');
|
|
101
|
-
console.log(c(
|
|
101
|
+
console.log(c(' ── Summary ──', 'magenta'));
|
|
102
|
+
console.log(` Selected ${c(String(toFix.length), 'bold')} improvements to apply:`);
|
|
103
|
+
for (const key of toFix) {
|
|
104
|
+
const item = results.find(r => r.key === key);
|
|
105
|
+
console.log(c(` • ${item ? item.name : key}`, 'dim'));
|
|
106
|
+
}
|
|
107
|
+
console.log('');
|
|
108
|
+
console.log(c(` Applying...`, 'bold'));
|
|
102
109
|
console.log('');
|
|
103
110
|
|
|
104
111
|
// Run setup in auto mode
|
package/src/plans.js
CHANGED
|
@@ -260,7 +260,7 @@ function buildAgentPatchFiles(ctx) {
|
|
|
260
260
|
|
|
261
261
|
function buildHookSettings(ctx, plannedHookFiles, options = {}) {
|
|
262
262
|
const existing = ctx.hasDir('.claude/hooks')
|
|
263
|
-
? ctx.dirFiles('.claude/hooks').filter(file => file.endsWith('.sh'))
|
|
263
|
+
? ctx.dirFiles('.claude/hooks').filter(file => file.endsWith('.sh') || file.endsWith('.js'))
|
|
264
264
|
: [];
|
|
265
265
|
const hookFiles = [...new Set([...existing, ...plannedHookFiles])].sort();
|
|
266
266
|
if (hookFiles.length === 0) {
|
|
@@ -385,7 +385,7 @@ async function buildProposalBundle(options) {
|
|
|
385
385
|
if (templateKey === 'hooks') {
|
|
386
386
|
const plannedHookFiles = templateFiles
|
|
387
387
|
.map(file => path.basename(file.path))
|
|
388
|
-
.filter(file => file.endsWith('.sh'));
|
|
388
|
+
.filter(file => file.endsWith('.sh') || file.endsWith('.js'));
|
|
389
389
|
const settingsFile = buildHookSettings(ctx, plannedHookFiles, options);
|
|
390
390
|
if (settingsFile) {
|
|
391
391
|
templateFiles.push(settingsFile);
|
|
@@ -476,7 +476,7 @@ function applyRuntimeSettingsOverlays(bundle, options) {
|
|
|
476
476
|
|
|
477
477
|
const ctx = new ProjectContext(options.dir);
|
|
478
478
|
const existingHooks = ctx.hasDir('.claude/hooks')
|
|
479
|
-
? ctx.dirFiles('.claude/hooks').filter(file => file.endsWith('.sh'))
|
|
479
|
+
? ctx.dirFiles('.claude/hooks').filter(file => file.endsWith('.sh') || file.endsWith('.js'))
|
|
480
480
|
: [];
|
|
481
481
|
|
|
482
482
|
const proposals = bundle.proposals.map((proposal) => {
|
package/src/setup.js
CHANGED
|
@@ -782,66 +782,64 @@ ${verificationSteps.join('\n')}
|
|
|
782
782
|
},
|
|
783
783
|
|
|
784
784
|
'hooks': () => ({
|
|
785
|
-
'on-edit-lint.
|
|
786
|
-
|
|
787
|
-
|
|
788
|
-
|
|
789
|
-
|
|
790
|
-
if
|
|
791
|
-
|
|
792
|
-
|
|
793
|
-
|
|
794
|
-
|
|
795
|
-
|
|
796
|
-
|
|
797
|
-
fi
|
|
785
|
+
'on-edit-lint.js': `#!/usr/bin/env node
|
|
786
|
+
// PostToolUse hook - runs linter after file edits
|
|
787
|
+
const { execSync } = require('child_process');
|
|
788
|
+
const fs = require('fs');
|
|
789
|
+
try {
|
|
790
|
+
if (fs.existsSync('package.json')) {
|
|
791
|
+
const pkg = JSON.parse(fs.readFileSync('package.json', 'utf8'));
|
|
792
|
+
if (pkg.scripts && pkg.scripts.lint) {
|
|
793
|
+
execSync('npm run lint --silent', { stdio: 'ignore', timeout: 30000 });
|
|
794
|
+
}
|
|
795
|
+
}
|
|
796
|
+
} catch (e) { /* linter not available or failed - non-blocking */ }
|
|
798
797
|
`,
|
|
799
|
-
'protect-secrets.
|
|
800
|
-
|
|
801
|
-
|
|
802
|
-
|
|
803
|
-
|
|
804
|
-
|
|
805
|
-
|
|
806
|
-
|
|
807
|
-
|
|
808
|
-
|
|
798
|
+
'protect-secrets.js': `#!/usr/bin/env node
|
|
799
|
+
// PreToolUse hook - blocks reads of secret files
|
|
800
|
+
let input = '';
|
|
801
|
+
process.stdin.on('data', d => input += d);
|
|
802
|
+
process.stdin.on('end', () => {
|
|
803
|
+
try {
|
|
804
|
+
const data = JSON.parse(input);
|
|
805
|
+
const fp = (data.tool_input && data.tool_input.file_path) || '';
|
|
806
|
+
if (/\\.env$|\\.env\\.|secrets[\\/\\\\]|credentials|\\.pem$|\\.key$/i.test(fp)) {
|
|
807
|
+
console.log(JSON.stringify({ decision: 'block', reason: 'Blocked: accessing secret/credential files is not allowed.' }));
|
|
808
|
+
} else {
|
|
809
|
+
console.log(JSON.stringify({ decision: 'allow' }));
|
|
810
|
+
}
|
|
811
|
+
} catch (e) {
|
|
812
|
+
console.log(JSON.stringify({ decision: 'allow' }));
|
|
813
|
+
}
|
|
814
|
+
});
|
|
809
815
|
`,
|
|
810
|
-
'log-changes.
|
|
811
|
-
|
|
812
|
-
|
|
813
|
-
|
|
814
|
-
|
|
815
|
-
|
|
816
|
-
|
|
817
|
-
|
|
818
|
-
|
|
819
|
-
|
|
820
|
-
|
|
821
|
-
|
|
822
|
-
|
|
823
|
-
|
|
824
|
-
|
|
825
|
-
|
|
826
|
-
|
|
827
|
-
|
|
828
|
-
TIMESTAMP=$(date +"%Y-%m-%d %H:%M:%S")
|
|
829
|
-
echo "[$TIMESTAMP] $TOOL_NAME: $FILE_PATH" >> "$LOG_FILE"
|
|
830
|
-
|
|
831
|
-
exit 0
|
|
816
|
+
'log-changes.js': `#!/usr/bin/env node
|
|
817
|
+
// PostToolUse hook - logs all file changes with timestamps
|
|
818
|
+
const fs = require('fs');
|
|
819
|
+
const path = require('path');
|
|
820
|
+
let input = '';
|
|
821
|
+
process.stdin.on('data', d => input += d);
|
|
822
|
+
process.stdin.on('end', () => {
|
|
823
|
+
try {
|
|
824
|
+
const data = JSON.parse(input);
|
|
825
|
+
const fp = (data.tool_input && data.tool_input.file_path) || '';
|
|
826
|
+
if (!fp) process.exit(0);
|
|
827
|
+
const toolName = data.tool_name || 'unknown';
|
|
828
|
+
const logDir = path.join('.claude', 'logs');
|
|
829
|
+
fs.mkdirSync(logDir, { recursive: true });
|
|
830
|
+
const ts = new Date().toISOString().replace('T', ' ').split('.')[0];
|
|
831
|
+
fs.appendFileSync(path.join(logDir, 'file-changes.log'), \`[\${ts}] \${toolName}: \${fp}\\n\`);
|
|
832
|
+
} catch (e) { /* non-blocking */ }
|
|
833
|
+
});
|
|
832
834
|
`,
|
|
833
|
-
'session-start.
|
|
834
|
-
|
|
835
|
-
|
|
836
|
-
|
|
837
|
-
|
|
838
|
-
|
|
839
|
-
|
|
840
|
-
|
|
841
|
-
TIMESTAMP=$(date +"%Y-%m-%d %H:%M:%S")
|
|
842
|
-
echo "[$TIMESTAMP] session started" >> "$LOG_FILE"
|
|
843
|
-
|
|
844
|
-
exit 0
|
|
835
|
+
'session-start.js': `#!/usr/bin/env node
|
|
836
|
+
// SessionStart hook - prepares logs and records session entry
|
|
837
|
+
const fs = require('fs');
|
|
838
|
+
const path = require('path');
|
|
839
|
+
const logDir = path.join('.claude', 'logs');
|
|
840
|
+
fs.mkdirSync(logDir, { recursive: true });
|
|
841
|
+
const ts = new Date().toISOString().replace('T', ' ').split('.')[0];
|
|
842
|
+
fs.appendFileSync(path.join(logDir, 'sessions.log'), \`[\${ts}] session started\\n\`);
|
|
845
843
|
`,
|
|
846
844
|
}),
|
|
847
845
|
|
|
@@ -1219,7 +1217,7 @@ async function setup(options) {
|
|
|
1219
1217
|
const hooksDir = path.join(options.dir, '.claude/hooks');
|
|
1220
1218
|
const settingsPath = path.join(options.dir, '.claude/settings.json');
|
|
1221
1219
|
if (fs.existsSync(hooksDir) && !fs.existsSync(settingsPath)) {
|
|
1222
|
-
const hookFiles = fs.readdirSync(hooksDir).filter(f => f.endsWith('.sh'));
|
|
1220
|
+
const hookFiles = fs.readdirSync(hooksDir).filter(f => f.endsWith('.sh') || f.endsWith('.js'));
|
|
1223
1221
|
if (hookFiles.length > 0) {
|
|
1224
1222
|
const settings = buildSettingsForProfile({
|
|
1225
1223
|
profileKey: options.profile || 'safe-write',
|
package/src/techniques.js
CHANGED
|
@@ -1295,6 +1295,24 @@ const TECHNIQUES = {
|
|
|
1295
1295
|
fix: 'CLAUDE.md references deprecated patterns (old model names or API formats). Update to current Claude 4.x conventions.',
|
|
1296
1296
|
template: null
|
|
1297
1297
|
},
|
|
1298
|
+
|
|
1299
|
+
claudeMdQuality: {
|
|
1300
|
+
id: 102502,
|
|
1301
|
+
name: 'CLAUDE.md has substantive content',
|
|
1302
|
+
check: (ctx) => {
|
|
1303
|
+
const md = ctx.claudeMdContent();
|
|
1304
|
+
if (!md) return null;
|
|
1305
|
+
const lines = md.split('\n').filter(l => l.trim());
|
|
1306
|
+
const sections = (md.match(/^##\s/gm) || []).length;
|
|
1307
|
+
const hasCommand = /\b(npm|yarn|pnpm|pytest|go |make |ruff |cargo |dotnet )\b/i.test(md);
|
|
1308
|
+
return lines.length >= 15 && sections >= 2 && hasCommand;
|
|
1309
|
+
},
|
|
1310
|
+
impact: 'medium',
|
|
1311
|
+
rating: 4,
|
|
1312
|
+
category: 'quality-deep',
|
|
1313
|
+
fix: 'CLAUDE.md exists but lacks substance. Add at least 2 sections (## headings) and include your test/build/lint commands.',
|
|
1314
|
+
template: null
|
|
1315
|
+
},
|
|
1298
1316
|
};
|
|
1299
1317
|
|
|
1300
1318
|
// Stack detection
|