claudex-setup 0.5.1 → 1.0.1

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 CHANGED
@@ -67,7 +67,7 @@ No install. No config. No dependencies.
67
67
  |------|--------|
68
68
  | `--verbose` | Show all recommendations (not just critical/high) |
69
69
  | `--json` | Machine-readable JSON output (for CI) |
70
- | `--no-insights` | Disable anonymous usage insights |
70
+ | `--insights` | Enable anonymous usage insights (off by default) |
71
71
 
72
72
  ## Smart CLAUDE.md Generation
73
73
 
@@ -76,7 +76,7 @@ Not a generic template. The `setup` command actually analyzes your project:
76
76
  - **Reads package.json** - includes your actual test, build, lint, dev commands
77
77
  - **Detects framework** - Next.js Server Components, Django models, FastAPI Pydantic, React hooks
78
78
  - **TypeScript-aware** - detects strict mode, adds TS-specific rules
79
- - **Auto Mermaid diagram** - scans directories and generates architecture visualization (73% token savings)
79
+ - **Auto Mermaid diagram** - scans directories and generates architecture visualization (Mermaid diagrams are more token-efficient than prose descriptions, per Anthropic docs)
80
80
  - **XML constraint blocks** - adds `<constraints>` and `<verification>` with context-aware rules
81
81
  - **Verification criteria** - auto-generates checklist from your actual commands
82
82
 
@@ -172,7 +172,7 @@ These checks evaluate **quality**, not just existence. A well-configured project
172
172
 
173
173
  - **Zero dependencies** - nothing to audit
174
174
  - **Runs 100% locally** - no cloud processing
175
- - **Anonymous insights** - opt-in, no PII, no file contents (disable with `--no-insights`)
175
+ - **Anonymous insights** - opt-in, no PII, no file contents (enable with `--insights`)
176
176
  - **MIT Licensed** - use anywhere
177
177
 
178
178
  ## Backed by Research
package/bin/cli.js CHANGED
@@ -11,14 +11,14 @@ const flags = args.filter(a => a.startsWith('--'));
11
11
  const HELP = `
12
12
  claudex-setup v${version}
13
13
  Audit and optimize any project for Claude Code.
14
- Powered by 1,107 verified techniques.
14
+ Backed by research from 1,107 cataloged Claude Code entries.
15
15
 
16
16
  Usage:
17
17
  npx claudex-setup Run audit on current directory
18
18
  npx claudex-setup audit Same as above
19
19
  npx claudex-setup setup Apply recommended configuration
20
20
  npx claudex-setup setup --auto Apply all without prompts
21
- npx claudex-setup deep-review AI-powered config analysis (needs API key)
21
+ npx claudex-setup deep-review AI-powered config review (uses Claude Code or API key)
22
22
  npx claudex-setup interactive Step-by-step guided wizard
23
23
  npx claudex-setup watch Monitor changes and re-audit live
24
24
  npx claudex-setup badge Generate shields.io badge markdown
@@ -26,7 +26,7 @@ const HELP = `
26
26
  Options:
27
27
  --verbose Show all recommendations (not just critical/high)
28
28
  --json Output as JSON (for CI pipelines)
29
- --no-insights Disable anonymous usage insights
29
+ --insights Enable anonymous usage insights (off by default)
30
30
  --help Show this help
31
31
  --version Show version
32
32
  `;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "claudex-setup",
3
- "version": "0.5.1",
3
+ "version": "1.0.1",
4
4
  "description": "Audit and optimize any project for Claude Code. Powered by 1107 verified techniques.",
5
5
  "main": "src/index.js",
6
6
  "bin": {
@@ -7,6 +7,7 @@
7
7
  */
8
8
 
9
9
  const https = require('https');
10
+ const path = require('path');
10
11
  const { ProjectContext } = require('./context');
11
12
  const { STACKS } = require('./techniques');
12
13
 
@@ -198,14 +199,21 @@ function hasClaudeCode() {
198
199
 
199
200
  async function callClaudeCode(prompt) {
200
201
  const { execSync } = require('child_process');
201
- // Use claude -p (headless/print mode) - uses the user's existing Claude Code auth
202
- const escaped = prompt.replace(/'/g, "'\\''");
203
- const result = execSync(`claude -p '${escaped}' --output-format text`, {
204
- encoding: 'utf8',
205
- maxBuffer: 1024 * 1024,
206
- timeout: 120000,
207
- });
208
- return result;
202
+ const os = require('os');
203
+ const fs = require('fs');
204
+ const tmpFile = path.join(os.tmpdir(), `claudex-review-${Date.now()}.txt`);
205
+ fs.writeFileSync(tmpFile, prompt, 'utf8');
206
+ try {
207
+ const result = execSync(`claude -p --output-format text < "${tmpFile}"`, {
208
+ encoding: 'utf8',
209
+ maxBuffer: 1024 * 1024,
210
+ timeout: 120000,
211
+ shell: true,
212
+ });
213
+ return result;
214
+ } finally {
215
+ try { fs.unlinkSync(tmpFile); } catch {}
216
+ }
209
217
  }
210
218
 
211
219
  async function deepReview(options) {
package/src/insights.js CHANGED
@@ -25,14 +25,10 @@ const INSIGHTS_ENDPOINT = 'https://claudex-insights.claudex.workers.dev/v1/repor
25
25
  const TIMEOUT_MS = 3000;
26
26
 
27
27
  function shouldCollect() {
28
- // Respect opt-out
29
- if (process.env.CLAUDEX_NO_INSIGHTS === '1') return false;
30
- if (process.argv.includes('--no-insights')) return false;
31
-
32
- // Don't collect in CI
33
- if (process.env.CI || process.env.GITHUB_ACTIONS) return false;
34
-
35
- return true;
28
+ // Opt-IN: only collect if user explicitly enables
29
+ if (process.env.CLAUDEX_INSIGHTS === '1') return true;
30
+ if (process.argv.includes('--insights')) return true;
31
+ return false;
36
32
  }
37
33
 
38
34
  function buildPayload(auditResult) {
@@ -102,7 +102,7 @@ async function interactive(options) {
102
102
  console.log('');
103
103
 
104
104
  // Run setup in auto mode
105
- await setup({ ...options, auto: true });
105
+ await setup({ ...options, auto: true, only: toFix });
106
106
 
107
107
  console.log('');
108
108
  console.log(c(' Done! Run `npx claudex-setup` to see your new score.', 'green'));
package/src/setup.js CHANGED
@@ -29,12 +29,20 @@ function detectScripts(ctx) {
29
29
  // Helper: detect main directories
30
30
  // ============================================================
31
31
  function detectMainDirs(ctx) {
32
- const candidates = ['src', 'lib', 'app', 'pages', 'components', 'api', 'routes', 'utils', 'helpers', 'services', 'models', 'controllers', 'views', 'public', 'assets', 'config', 'tests', 'test', '__tests__', 'spec', 'scripts', 'prisma', 'db', 'middleware'];
32
+ const candidates = ['src', 'lib', 'app', 'pages', 'components', 'api', 'routes', 'utils', 'helpers', 'services', 'models', 'controllers', 'views', 'public', 'assets', 'config', 'tests', 'test', '__tests__', 'spec', 'scripts', 'prisma', 'db', 'middleware', 'hooks'];
33
+ // Also check inside src/ for nested structure (common in Next.js, React)
34
+ const srcNested = ['src/components', 'src/app', 'src/pages', 'src/api', 'src/lib', 'src/hooks', 'src/utils', 'src/services', 'src/models', 'src/middleware', 'src/app/api', 'app/api'];
33
35
  const found = [];
34
- for (const dir of candidates) {
36
+ const seenNames = new Set();
37
+
38
+ for (const dir of [...candidates, ...srcNested]) {
35
39
  if (ctx.hasDir(dir)) {
36
40
  const files = ctx.dirFiles(dir);
37
- found.push({ name: dir, fileCount: files.length, files: files.slice(0, 10) });
41
+ const displayName = dir.includes('/') ? dir : dir;
42
+ if (!seenNames.has(displayName)) {
43
+ found.push({ name: displayName, fileCount: files.length, files: files.slice(0, 10) });
44
+ seenNames.add(displayName);
45
+ }
38
46
  }
39
47
  }
40
48
  return found;
@@ -61,31 +69,63 @@ function generateMermaid(dirs, stacks) {
61
69
  return ` ${id}[${label}]`;
62
70
  }
63
71
 
64
- // Entry point
65
- nodes.push(addNode('Entry Point', 'round'));
72
+ // Detect Next.js App Router specifically
73
+ const hasAppRouter = dirNames.includes('app') || dirNames.includes('src/app');
74
+ const hasPages = dirNames.includes('pages') || dirNames.includes('src/pages');
75
+ const hasAppApi = dirNames.includes('app/api') || dirNames.includes('src/app/api');
76
+ const hasSrcComponents = dirNames.includes('src/components') || dirNames.includes('components');
77
+ const hasSrcHooks = dirNames.includes('src/hooks') || dirNames.includes('hooks');
78
+ const hasSrcLib = dirNames.includes('src/lib') || dirNames.includes('lib');
79
+
80
+ // Smart entry point based on framework
81
+ const isNextJs = stackKeys.includes('nextjs');
82
+ const isDjango = stackKeys.includes('django');
83
+ const isFastApi = stackKeys.includes('fastapi');
84
+
85
+ if (isNextJs) {
86
+ nodes.push(addNode('Next.js', 'round'));
87
+ } else if (isDjango) {
88
+ nodes.push(addNode('Django', 'round'));
89
+ } else if (isFastApi) {
90
+ nodes.push(addNode('FastAPI', 'round'));
91
+ } else {
92
+ nodes.push(addNode('Entry Point', 'round'));
93
+ }
94
+
95
+ const root = ids['Next.js'] || ids['Django'] || ids['FastAPI'] || ids['Entry Point'];
66
96
 
67
97
  // Detect layers
68
- if (dirNames.includes('app') || dirNames.includes('pages')) {
69
- nodes.push(addNode('Pages / Routes', 'default'));
70
- edges.push(` ${ids['Entry Point']} --> ${ids['Pages / Routes']}`);
98
+ if (hasAppRouter || hasPages) {
99
+ const label = hasAppRouter ? 'App Router' : 'Pages';
100
+ nodes.push(addNode(label, 'default'));
101
+ edges.push(` ${root} --> ${ids[label]}`);
102
+ }
103
+
104
+ if (hasAppApi) {
105
+ nodes.push(addNode('API Routes', 'default'));
106
+ const parent = ids['App Router'] || ids['Pages'] || root;
107
+ edges.push(` ${parent} --> ${ids['API Routes']}`);
71
108
  }
72
109
 
73
- if (dirNames.includes('components')) {
110
+ if (hasSrcComponents) {
74
111
  nodes.push(addNode('Components', 'default'));
75
- const parent = ids['Pages / Routes'] || ids['Entry Point'];
112
+ const parent = ids['App Router'] || ids['Pages'] || root;
76
113
  edges.push(` ${parent} --> ${ids['Components']}`);
77
114
  }
78
115
 
79
- if (dirNames.includes('src')) {
80
- nodes.push(addNode('src/', 'default'));
81
- const parent = ids['Pages / Routes'] || ids['Entry Point'];
82
- edges.push(` ${parent} --> ${ids['src/']}`);
116
+ if (hasSrcHooks) {
117
+ nodes.push(addNode('Hooks', 'default'));
118
+ const parent = ids['Components'] || root;
119
+ edges.push(` ${parent} --> ${ids['Hooks']}`);
83
120
  }
84
121
 
85
- if (dirNames.includes('lib')) {
122
+ if (hasSrcLib) {
86
123
  nodes.push(addNode('lib/', 'default'));
87
- const parent = ids['src/'] || ids['Entry Point'];
124
+ const parent = ids['API Routes'] || ids['Hooks'] || ids['Components'] || root;
88
125
  edges.push(` ${parent} --> ${ids['lib/']}`);
126
+ } else if (dirNames.includes('src') && !hasAppRouter && !hasPages) {
127
+ nodes.push(addNode('src/', 'default'));
128
+ edges.push(` ${root} --> ${ids['src/']}`);
89
129
  }
90
130
 
91
131
  if (dirNames.includes('api') || dirNames.includes('routes') || dirNames.includes('controllers')) {
@@ -379,45 +419,46 @@ ${verificationSteps.join('\n')}
379
419
  - Use descriptive commit messages (why, not what)
380
420
  - Create focused PRs — one concern per PR
381
421
  - Document non-obvious decisions in code comments
422
+
423
+ ---
424
+ *Generated by [claudex-setup](https://github.com/DnaFin/claudex-setup) v${require('../package.json').version} on ${new Date().toISOString().split('T')[0]}. Customize this file for your project — a hand-crafted CLAUDE.md will always be better than a generated one.*
382
425
  `;
383
426
  },
384
427
 
385
428
  'hooks': () => ({
386
429
  'on-edit-lint.sh': `#!/bin/bash
387
- # PostToolUse hook - auto-check after file edits
388
- # Customize the linter command for your project
389
- TIMESTAMP=$(date +"%Y-%m-%d %H:%M:%S")
390
- echo "[$TIMESTAMP] File changed: $(cat -)" >> .claude/logs/changes.txt
430
+ # PostToolUse hook - runs linter after file edits
431
+ # Detects which linter is available and runs it
432
+
433
+ if command -v npx &>/dev/null; then
434
+ if [ -f "package.json" ] && grep -q '"lint"' package.json 2>/dev/null; then
435
+ npm run lint --silent 2>/dev/null
436
+ elif [ -f ".eslintrc" ] || [ -f ".eslintrc.js" ] || [ -f ".eslintrc.json" ] || [ -f "eslint.config.js" ]; then
437
+ npx eslint --fix . --quiet 2>/dev/null
438
+ fi
439
+ elif command -v ruff &>/dev/null; then
440
+ ruff check --fix . 2>/dev/null
441
+ fi
391
442
  `,
392
443
  'protect-secrets.sh': `#!/bin/bash
393
- # PreToolUse hook - warn before touching sensitive files
394
- # Prevents accidental reads/writes to files containing secrets
395
-
444
+ # PreToolUse hook - blocks reads of secret files
396
445
  INPUT=$(cat -)
397
- FILE=$(echo "$INPUT" | grep -oP '"file_path"\\s*:\\s*"\\K[^"]+' 2>/dev/null || echo "")
446
+ FILE_PATH=$(echo "$INPUT" | sed -n 's/.*"file_path"[[:space:]]*:[[:space:]]*"\\([^"]*\\)".*/\\1/p')
398
447
 
399
- if [ -z "$FILE" ]; then
448
+ if echo "$FILE_PATH" | grep -qiE '\\.env$|\\.env\\.|secrets/|credentials|\\.pem$|\\.key$'; then
449
+ echo '{"decision": "block", "reason": "Blocked: accessing secret/credential files is not allowed."}'
400
450
  exit 0
401
451
  fi
402
-
403
- BASENAME=$(basename "$FILE")
404
-
405
- case "$BASENAME" in
406
- .env|.env.*|*.pem|*.key|credentials.json|secrets.yaml|secrets.yml)
407
- echo "WARN: Attempting to access sensitive file: $BASENAME"
408
- echo "This file may contain secrets. Proceed with caution."
409
- ;;
410
- esac
411
-
412
- exit 0
452
+ echo '{"decision": "allow"}'
413
453
  `,
414
454
  'log-changes.sh': `#!/bin/bash
415
455
  # PostToolUse hook - logs all file changes with timestamps
416
456
  # Appends to .claude/logs/file-changes.log
417
457
 
418
458
  INPUT=$(cat -)
419
- TOOL_NAME=$(echo "$INPUT" | grep -oP '"tool_name"\\s*:\\s*"\\K[^"]+' 2>/dev/null || echo "unknown")
420
- FILE_PATH=$(echo "$INPUT" | grep -oP '"file_path"\\s*:\\s*"\\K[^"]+' 2>/dev/null || echo "")
459
+ TOOL_NAME=$(echo "$INPUT" | sed -n 's/.*"tool_name"[[:space:]]*:[[:space:]]*"\\([^"]*\\)".*/\\1/p')
460
+ TOOL_NAME=\${TOOL_NAME:-unknown}
461
+ FILE_PATH=$(echo "$INPUT" | sed -n 's/.*"file_path"[[:space:]]*:[[:space:]]*"\\([^"]*\\)".*/\\1/p')
421
462
 
422
463
  if [ -z "$FILE_PATH" ]; then
423
464
  exit 0
@@ -571,9 +612,19 @@ async function setup(options) {
571
612
  let created = 0;
572
613
  let skipped = 0;
573
614
 
615
+ let failedWithTemplates = [];
574
616
  for (const [key, technique] of Object.entries(TECHNIQUES)) {
575
617
  if (technique.passed || technique.check(ctx)) continue;
576
618
  if (!technique.template) continue;
619
+ failedWithTemplates.push({ key, technique });
620
+ }
621
+
622
+ // Filter by 'only' list if provided (interactive wizard selections)
623
+ if (options.only && options.only.length > 0) {
624
+ failedWithTemplates = failedWithTemplates.filter(r => options.only.includes(r.key));
625
+ }
626
+
627
+ for (const { key, technique } of failedWithTemplates) {
577
628
 
578
629
  const template = TEMPLATES[technique.template];
579
630
  if (!template) continue;
@@ -583,7 +634,13 @@ async function setup(options) {
583
634
 
584
635
  if (typeof result === 'string') {
585
636
  // Single file template (like CLAUDE.md)
586
- const filePath = key === 'claudeMd' ? 'CLAUDE.md' : key;
637
+ // Map technique keys to actual file paths
638
+ const filePathMap = {
639
+ 'claudeMd': 'CLAUDE.md',
640
+ 'mermaidArchitecture': 'CLAUDE.md', // mermaid is part of CLAUDE.md, skip separate file
641
+ };
642
+ if (key === 'mermaidArchitecture') continue; // Mermaid is generated inside CLAUDE.md template
643
+ const filePath = filePathMap[key] || key;
587
644
  const fullPath = path.join(options.dir, filePath);
588
645
 
589
646
  if (!fs.existsSync(fullPath)) {
@@ -627,6 +684,41 @@ async function setup(options) {
627
684
  }
628
685
  }
629
686
 
687
+ // Auto-register hooks in settings if hooks were created but no settings exist
688
+ const hooksDir = path.join(options.dir, '.claude/hooks');
689
+ const settingsPath = path.join(options.dir, '.claude/settings.json');
690
+ if (fs.existsSync(hooksDir) && !fs.existsSync(settingsPath)) {
691
+ const hookFiles = fs.readdirSync(hooksDir).filter(f => f.endsWith('.sh'));
692
+ if (hookFiles.length > 0) {
693
+ const settings = {
694
+ hooks: {
695
+ PostToolUse: [{
696
+ matcher: "Write|Edit",
697
+ hooks: hookFiles.filter(f => f !== 'protect-secrets.sh').map(f => ({
698
+ type: "command",
699
+ command: `bash .claude/hooks/${f}`,
700
+ timeout: 10
701
+ }))
702
+ }]
703
+ }
704
+ };
705
+ // Add protect-secrets as PreToolUse if it exists
706
+ if (hookFiles.includes('protect-secrets.sh')) {
707
+ settings.hooks.PreToolUse = [{
708
+ matcher: "Read|Write|Edit",
709
+ hooks: [{
710
+ type: "command",
711
+ command: "bash .claude/hooks/protect-secrets.sh",
712
+ timeout: 5
713
+ }]
714
+ }];
715
+ }
716
+ fs.writeFileSync(settingsPath, JSON.stringify(settings, null, 2), 'utf8');
717
+ console.log(` \x1b[32m✅\x1b[0m Created .claude/settings.json (hooks registered)`);
718
+ created++;
719
+ }
720
+ }
721
+
630
722
  console.log('');
631
723
  if (created === 0 && skipped > 0) {
632
724
  console.log(' \x1b[32m✅\x1b[0m Your project is already well configured!');
package/src/techniques.js CHANGED
@@ -766,11 +766,16 @@ const TECHNIQUES = {
766
766
  channelsAwareness: {
767
767
  id: 1102,
768
768
  name: 'Claude Code Channels awareness',
769
- check: () => true, // informational
769
+ check: (ctx) => {
770
+ const md = ctx.fileContent('CLAUDE.md') || '';
771
+ const settings = ctx.jsonFile('.claude/settings.local.json') || ctx.jsonFile('.claude/settings.json');
772
+ const settingsStr = JSON.stringify(settings || {});
773
+ return md.toLowerCase().includes('channel') || settingsStr.includes('channel');
774
+ },
770
775
  impact: 'low',
771
- rating: 4,
776
+ rating: 3,
772
777
  category: 'features',
773
- fix: 'Claude Code Channels (v2.1.80+) lets you control sessions from Telegram/Discord/iMessage.',
778
+ fix: 'Claude Code Channels (v2.1.80+) bridges Telegram/Discord/iMessage to your session.',
774
779
  template: null
775
780
  },
776
781
 
@@ -801,7 +806,8 @@ const TECHNIQUES = {
801
806
  id: 2002,
802
807
  name: 'CLAUDE.md is concise (under 200 lines)',
803
808
  check: (ctx) => {
804
- const md = ctx.fileContent('CLAUDE.md') || '';
809
+ const md = ctx.fileContent('CLAUDE.md');
810
+ if (!md) return false; // no CLAUDE.md = not passing
805
811
  return md.split('\n').length <= 200;
806
812
  },
807
813
  impact: 'medium',
@@ -815,7 +821,8 @@ const TECHNIQUES = {
815
821
  id: 2003,
816
822
  name: 'CLAUDE.md has no obvious contradictions',
817
823
  check: (ctx) => {
818
- const md = ctx.fileContent('CLAUDE.md') || '';
824
+ const md = ctx.fileContent('CLAUDE.md');
825
+ if (!md || md.length < 50) return false; // no CLAUDE.md or too short = not passing
819
826
  // Check for common contradictions
820
827
  const hasNever = /never.*always|always.*never/i.test(md);
821
828
  const hasBothStyles = /use tabs/i.test(md) && /use spaces/i.test(md);
@@ -921,7 +928,8 @@ const TECHNIQUES = {
921
928
  id: 2009,
922
929
  name: 'No deprecated patterns detected',
923
930
  check: (ctx) => {
924
- const md = ctx.fileContent('CLAUDE.md') || '';
931
+ const md = ctx.fileContent('CLAUDE.md');
932
+ if (!md) return false; // no CLAUDE.md = not passing
925
933
  // Check for patterns deprecated in Claude 4.x
926
934
  const deprecated = [
927
935
  'prefill', // deprecated in 4.6