opscale-setup 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.
@@ -0,0 +1,33 @@
1
+ name: Publish to npm
2
+
3
+ on:
4
+ push:
5
+ tags:
6
+ - 'v*'
7
+
8
+ jobs:
9
+ publish:
10
+ runs-on: ubuntu-latest
11
+ permissions:
12
+ contents: read
13
+ id-token: write # required for npm provenance
14
+
15
+ steps:
16
+ - uses: actions/checkout@v4
17
+
18
+ - uses: actions/setup-node@v4
19
+ with:
20
+ node-version: '20'
21
+ registry-url: 'https://registry.npmjs.org'
22
+ cache: 'npm'
23
+
24
+ - name: Install dependencies
25
+ run: npm ci
26
+
27
+ - name: Run tests
28
+ run: npm test --if-present
29
+
30
+ - name: Publish with provenance
31
+ run: npm publish --provenance --access public
32
+ env:
33
+ NODE_AUTH_TOKEN: ${{ secrets.NPM_TOKEN }}
package/README.md ADDED
@@ -0,0 +1,111 @@
1
+ # opscale-setup
2
+
3
+ > Deploy a production-ready Claude Code environment in one command.
4
+
5
+ ```bash
6
+ npx opscale-setup
7
+ ```
8
+
9
+ No global install required. Answers 3 questions, then writes everything to your
10
+ project in under 10 seconds.
11
+
12
+ ---
13
+
14
+ ## What it installs
15
+
16
+ | File | Description |
17
+ |------|-------------|
18
+ | `CLAUDE.md` | Best-practice prompting guide, tailored to your stack |
19
+ | `.claude/settings.json` | Optimized Claude Code config with hooks wired up |
20
+ | `.claude/hooks/` (10 hooks) | Production-grade guardrails and automation |
21
+ | `.claude/skills/` (5 skills) | High-value slash commands for daily dev work |
22
+
23
+ ### Hooks
24
+
25
+ | Hook | Trigger | What it does |
26
+ |------|---------|--------------|
27
+ | `block-dangerous.js` | PreToolUse Bash | Blocks `rm -rf /`, `DROP DATABASE`, fork bombs, `curl \| bash` |
28
+ | `secret-guard.js` | PreToolUse Bash + Write | Detects AWS keys, GitHub tokens, Anthropic keys, private key blocks |
29
+ | `branch-guard.js` | PreToolUse Bash | Warns on direct push to main/master; blocks force-push |
30
+ | `dep-audit.js` | PreToolUse Bash | Intercepts npm/pip/cargo installs, flags typosquats + critical CVEs |
31
+ | `deploy-check.js` | PreToolUse Bash | Enforces clean working tree + passing tests before any deploy |
32
+ | `auto-commit-msg.js` | PostToolUse Bash | Validates Conventional Commits format after every commit |
33
+ | `cost-tracker.js` | Stop | Logs session token usage + estimated USD cost to `.claude/cost-log.json` |
34
+ | `test-runner.js` | PostToolUse Write | Auto-discovers and runs related tests when source files are written |
35
+ | `lint-check.js` | PostToolUse Write | Runs ESLint/Biome/Ruff/gofmt on written files |
36
+ | `build-validator.js` | PostToolUse Write | Runs `tsc --noEmit` / `cargo check` / `go build` on written files |
37
+
38
+ ### Skills
39
+
40
+ | Skill | Command | What it does |
41
+ |-------|---------|--------------|
42
+ | code-review | `/code-review` | Security + correctness review of the current diff |
43
+ | test-generation | `/test-generation <file>` | Generates a full, runnable test suite |
44
+ | pr-description | `/pr-description` | Writes PR title + body from git log + diff |
45
+ | debugging | `/debugging "<symptom>"` | Systematic 5-step debug protocol |
46
+ | deployment | `/deployment` | Pre-deploy checklist + rollback procedures |
47
+
48
+ ---
49
+
50
+ ## Setup wizard
51
+
52
+ The wizard asks exactly 3 questions:
53
+
54
+ 1. **Project type** — Node.js, Python, Go, Rust, Java, Ruby, Full-stack, Generic
55
+ 2. **Team size** — Solo, Small (2–10), Medium (11–50), Large (50+)
56
+ 3. **Deploy target** — AWS, GCP, Azure, Vercel, Kubernetes, Docker, VPS, Generic
57
+
58
+ Answers are embedded in `CLAUDE.md` and `settings.json` to tailor the generated
59
+ files to your environment.
60
+
61
+ ---
62
+
63
+ ## Requirements
64
+
65
+ - Node.js 18+
66
+ - Claude Code CLI (`npm install -g @anthropic-ai/claude-code`)
67
+
68
+ ---
69
+
70
+ ## Publishing your own fork
71
+
72
+ 1. Fork this repo and update `package.json` with your npm username.
73
+ 2. Add `NPM_TOKEN` to your GitHub repository secrets.
74
+ 3. Tag a release:
75
+
76
+ ```bash
77
+ git tag v1.0.0
78
+ git push origin v1.0.0
79
+ ```
80
+
81
+ GitHub Actions will run tests and publish to npm automatically.
82
+
83
+ ---
84
+
85
+ ## Customizing hooks
86
+
87
+ Hooks are plain Node.js scripts in `.claude/hooks/`. They receive tool input as
88
+ JSON on stdin and exit non-zero to abort the tool call.
89
+
90
+ ```js
91
+ // .claude/hooks/my-hook.js
92
+ import { readFileSync } from 'fs';
93
+
94
+ const input = JSON.parse(readFileSync('/dev/stdin', 'utf8'));
95
+ const command = input?.tool_input?.command ?? '';
96
+
97
+ if (command.includes('something-forbidden')) {
98
+ console.error('[my-hook] blocked');
99
+ process.exit(1);
100
+ }
101
+
102
+ process.exit(0);
103
+ ```
104
+
105
+ Wire it up in `.claude/settings.json` under `hooks.PreToolUse` or `hooks.PostToolUse`.
106
+
107
+ ---
108
+
109
+ ## License
110
+
111
+ MIT
package/bin/index.js ADDED
@@ -0,0 +1,8 @@
1
+ #!/usr/bin/env node
2
+
3
+ import { main } from '../src/index.js';
4
+
5
+ main().catch((err) => {
6
+ console.error(err.message);
7
+ process.exit(1);
8
+ });
package/package.json ADDED
@@ -0,0 +1,46 @@
1
+ {
2
+ "name": "opscale-setup",
3
+ "version": "1.0.0",
4
+ "description": "Deploy a production-ready Claude Code environment in one command",
5
+ "type": "module",
6
+ "main": "src/index.js",
7
+ "bin": {
8
+ "opscale-setup": "bin/index.js"
9
+ },
10
+ "scripts": {
11
+ "start": "node bin/index.js",
12
+ "test": "node --test src/__tests__/*.test.js"
13
+ },
14
+ "keywords": [
15
+ "claude",
16
+ "claude-code",
17
+ "ai",
18
+ "developer-tools",
19
+ "cli",
20
+ "setup",
21
+ "hooks",
22
+ "anthropic"
23
+ ],
24
+ "author": "opscale-setup contributors",
25
+ "license": "MIT",
26
+ "dependencies": {
27
+ "chalk": "^5.3.0",
28
+ "fs-extra": "^11.2.0",
29
+ "inquirer": "^9.2.12",
30
+ "ora": "^8.0.1"
31
+ },
32
+ "engines": {
33
+ "node": ">=18.0.0"
34
+ },
35
+ "repository": {
36
+ "type": "git",
37
+ "url": "git+https://github.com/jordan23wagner-ops/opscale-setup.git"
38
+ },
39
+ "bugs": {
40
+ "url": "https://github.com/YOUR_USERNAME/opscale-setup/issues"
41
+ },
42
+ "homepage": "https://github.com/YOUR_USERNAME/opscale-setup#readme",
43
+ "publishConfig": {
44
+ "access": "public"
45
+ }
46
+ }
package/src/index.js ADDED
@@ -0,0 +1,21 @@
1
+ import chalk from 'chalk';
2
+ import { runWizard } from './wizard.js';
3
+ import { install } from './installer.js';
4
+ import { log, banner } from './logger.js';
5
+
6
+ export async function main() {
7
+ banner();
8
+
9
+ const config = await runWizard();
10
+
11
+ log.info('Installing Claude Code environment...');
12
+ await install(config);
13
+
14
+ console.log('\n' + chalk.green.bold(' Setup complete!') + '\n');
15
+ console.log(chalk.dim(' Files created:'));
16
+ console.log(chalk.dim(' CLAUDE.md ') + chalk.white('Best-practice prompting guide'));
17
+ console.log(chalk.dim(' .claude/settings.json ') + chalk.white('Optimized Claude Code config'));
18
+ console.log(chalk.dim(' .claude/hooks/ ') + chalk.white('10 production-grade hooks'));
19
+ console.log(chalk.dim(' .claude/skills/ ') + chalk.white('5 high-value skill definitions'));
20
+ console.log('\n' + chalk.cyan(' Run') + ' claude ' + chalk.cyan('to start your session.\n'));
21
+ }
@@ -0,0 +1,75 @@
1
+ import path from 'path';
2
+ import { fileURLToPath } from 'url';
3
+ import fs from 'fs-extra';
4
+ import ora from 'ora';
5
+ import { log } from './logger.js';
6
+ import { renderClaudeMd } from './templates/CLAUDE.md.js';
7
+ import { renderSettings } from './templates/settings.js';
8
+
9
+ const __dirname = path.dirname(fileURLToPath(import.meta.url));
10
+ const TEMPLATES_DIR = path.join(__dirname, 'templates');
11
+ const HOOKS_DIR = path.join(TEMPLATES_DIR, 'hooks');
12
+ const SKILLS_DIR = path.join(TEMPLATES_DIR, 'skills');
13
+
14
+ export async function install(config) {
15
+ const cwd = process.cwd();
16
+ const claudeDir = path.join(cwd, '.claude');
17
+ const hooksDir = path.join(claudeDir, 'hooks');
18
+ const skillsDir = path.join(claudeDir, 'skills');
19
+
20
+ const spinner = ora({ text: 'Creating directories...', prefixText: ' ' }).start();
21
+
22
+ await fs.ensureDir(hooksDir);
23
+ await fs.ensureDir(skillsDir);
24
+ spinner.succeed('Directories ready');
25
+
26
+ // CLAUDE.md
27
+ spinner.start('Writing CLAUDE.md...');
28
+ await fs.writeFile(path.join(cwd, 'CLAUDE.md'), renderClaudeMd(config));
29
+ spinner.succeed('CLAUDE.md written');
30
+
31
+ // settings.json
32
+ spinner.start('Writing .claude/settings.json...');
33
+ await fs.writeJSON(path.join(claudeDir, 'settings.json'), renderSettings(config), { spaces: 2 });
34
+ spinner.succeed('.claude/settings.json written');
35
+
36
+ // Hooks
37
+ spinner.start('Installing hooks...');
38
+ const hookFiles = await fs.readdir(HOOKS_DIR);
39
+ for (const file of hookFiles) {
40
+ const src = path.join(HOOKS_DIR, file);
41
+ const dest = path.join(hooksDir, file);
42
+ await fs.copy(src, dest, { overwrite: false });
43
+ // Mark shell scripts executable on Unix
44
+ if (file.endsWith('.sh')) {
45
+ await fs.chmod(dest, 0o755).catch(() => {});
46
+ }
47
+ }
48
+ spinner.succeed(`${hookFiles.length} hooks installed`);
49
+
50
+ // Skills
51
+ spinner.start('Installing skills...');
52
+ const skillFiles = await fs.readdir(SKILLS_DIR);
53
+ for (const file of skillFiles) {
54
+ const src = path.join(SKILLS_DIR, file);
55
+ const dest = path.join(skillsDir, file);
56
+ await fs.copy(src, dest, { overwrite: false });
57
+ }
58
+ spinner.succeed(`${skillFiles.length} skills installed`);
59
+
60
+ // .gitignore safety — ensure .claude/hooks is gitignored if desired
61
+ await patchGitignore(cwd);
62
+ }
63
+
64
+ async function patchGitignore(cwd) {
65
+ const gitignorePath = path.join(cwd, '.gitignore');
66
+ const marker = '# opscale-setup';
67
+ const block = `\n${marker}\n.claude/settings.local.json\n`;
68
+
69
+ if (await fs.pathExists(gitignorePath)) {
70
+ const content = await fs.readFile(gitignorePath, 'utf8');
71
+ if (!content.includes(marker)) {
72
+ await fs.appendFile(gitignorePath, block);
73
+ }
74
+ }
75
+ }
package/src/logger.js ADDED
@@ -0,0 +1,20 @@
1
+ import chalk from 'chalk';
2
+
3
+ export function banner() {
4
+ console.log('\n' + chalk.bold.cyan(' ██████╗ ██████╗ ███████╗ ██████╗ █████╗ ██╗ ███████╗'));
5
+ console.log(chalk.bold.cyan(' ██╔═══██╗██╔══██╗██╔════╝██╔════╝██╔══██╗██║ ██╔════╝'));
6
+ console.log(chalk.bold.cyan(' ██║ ██║██████╔╝███████╗██║ ███████║██║ █████╗ '));
7
+ console.log(chalk.bold.cyan(' ██║ ██║██╔═══╝ ╚════██║██║ ██╔══██║██║ ██╔══╝ '));
8
+ console.log(chalk.bold.cyan(' ╚██████╔╝██║ ███████║╚██████╗██║ ██║███████╗███████╗'));
9
+ console.log(chalk.bold.cyan(' ╚═════╝ ╚═╝ ╚══════╝ ╚═════╝╚═╝ ╚═╝╚══════╝╚══════╝'));
10
+ console.log('\n' + chalk.bold(' opscale-setup') + chalk.dim(' — Production Claude Code in one command'));
11
+ console.log(chalk.dim(' ─────────────────────────────────────────────────────\n'));
12
+ }
13
+
14
+ export const log = {
15
+ info: (msg) => console.log(chalk.blue(' ℹ ') + msg),
16
+ success: (msg) => console.log(chalk.green(' ✓ ') + msg),
17
+ warn: (msg) => console.log(chalk.yellow(' ⚠ ') + msg),
18
+ error: (msg) => console.log(chalk.red(' ✗ ') + msg),
19
+ step: (msg) => console.log(chalk.cyan(' → ') + msg),
20
+ };
@@ -0,0 +1,82 @@
1
+ export function renderClaudeMd({ projectType, teamSize, deployTarget }) {
2
+ const projectLabel = {
3
+ node: 'Node.js / TypeScript', python: 'Python', go: 'Go', rust: 'Rust',
4
+ java: 'Java / Kotlin', ruby: 'Ruby on Rails', fullstack: 'Full-stack monorepo',
5
+ generic: 'Generic',
6
+ }[projectType] ?? projectType;
7
+
8
+ return `# CLAUDE.md
9
+
10
+ ## Project Context
11
+
12
+ - **Stack**: ${projectLabel}
13
+ - **Team size**: ${teamSize}
14
+ - **Deploy target**: ${deployTarget}
15
+
16
+ ---
17
+
18
+ ## How to Work with This Codebase
19
+
20
+ ### Before Starting Any Task
21
+
22
+ 1. Read existing tests to understand intended behavior before changing code.
23
+ 2. Check \`git log --oneline -20\` to understand recent changes and momentum.
24
+ 3. Grep for the symbol or pattern you're about to create — don't duplicate.
25
+ 4. If the task is ambiguous, ask one clarifying question before writing code.
26
+
27
+ ### Coding Standards
28
+
29
+ - Match the style of the surrounding code exactly (indentation, naming, quotes).
30
+ - No commented-out code. Delete dead code; git history preserves it.
31
+ - No TODO comments unless the task explicitly requires deferral.
32
+ - Prefer editing existing files over creating new abstractions.
33
+ - Three similar lines is better than a premature abstraction.
34
+
35
+ ### What NOT to Do
36
+
37
+ - Do not add error handling for scenarios that cannot happen in this codebase.
38
+ - Do not write multi-paragraph docstrings for self-explanatory functions.
39
+ - Do not add feature flags or backwards-compat shims unless asked.
40
+ - Do not create planning documents, analysis files, or READMEs unless asked.
41
+ - Do not run \`git add -A\` — stage specific files by name only.
42
+ - Do not amend published commits.
43
+
44
+ ### Security Rules
45
+
46
+ - Never log secrets, tokens, or PII.
47
+ - Never commit \`.env\` files or credential files.
48
+ - All user input must be validated at the boundary; trust internal types.
49
+ - SQL queries must use parameterized statements — no string interpolation.
50
+ - No \`eval()\`, \`exec()\`, or dynamic code execution on untrusted input.
51
+
52
+ ### Git Workflow
53
+
54
+ - Commit message format: \`<type>(<scope>): <what changed, present tense>\`
55
+ - Types: \`feat\`, \`fix\`, \`refactor\`, \`test\`, \`docs\`, \`chore\`, \`perf\`
56
+ - One logical change per commit. Keep PRs small and focused.
57
+ - Branch naming: \`<type>/<short-slug>\` e.g. \`feat/user-auth\`
58
+
59
+ ### Testing
60
+
61
+ - Write tests for every new public function.
62
+ - Tests should verify behavior, not implementation.
63
+ - Do not mock the database in integration tests.
64
+ - A failing test must be fixed before merging — never skip.
65
+
66
+ ### Deploy Checklist (${deployTarget})
67
+
68
+ - All tests pass locally before pushing.
69
+ - Environment variables documented in \`.env.example\`.
70
+ - Database migrations are backwards-compatible.
71
+ - Rollback plan documented for schema changes.
72
+ - Secrets rotation procedure known to team.
73
+
74
+ ---
75
+
76
+ ## Asking for Help
77
+
78
+ If you are unsure, say so. A wrong confident answer is worse than an honest
79
+ "I don't know — here's what I'd check." Prefer small, verifiable steps over
80
+ large speculative changes.
81
+ `;
82
+ }
@@ -0,0 +1,39 @@
1
+ #!/usr/bin/env node
2
+ /**
3
+ * PostToolUse / Bash — after a git commit, validates the commit message
4
+ * against Conventional Commits format and prints a warning if it doesn't
5
+ * match. Never blocks — this is advisory only.
6
+ */
7
+ import { readFileSync } from 'fs';
8
+ import { execSync } from 'child_process';
9
+
10
+ const CONVENTIONAL = /^(feat|fix|docs|style|refactor|perf|test|chore|ci|build|revert)(\(.+\))?: .{1,100}$/;
11
+
12
+ let input;
13
+ try {
14
+ input = JSON.parse(readFileSync('/dev/stdin', 'utf8'));
15
+ } catch {
16
+ process.exit(0);
17
+ }
18
+
19
+ const command = input?.tool_input?.command ?? '';
20
+ if (!/git\s+commit/.test(command)) process.exit(0);
21
+
22
+ // Read the last commit message
23
+ let msg = '';
24
+ try {
25
+ msg = execSync('git log -1 --pretty=%s', { stdio: ['pipe', 'pipe', 'pipe'] }).toString().trim();
26
+ } catch {
27
+ process.exit(0);
28
+ }
29
+
30
+ if (!CONVENTIONAL.test(msg)) {
31
+ console.error(
32
+ `[auto-commit-msg] WARNING: commit message doesn't follow Conventional Commits:\n` +
33
+ ` "${msg}"\n` +
34
+ ` Expected: <type>(<scope>): <description>\n` +
35
+ ` Types: feat|fix|docs|style|refactor|perf|test|chore|ci|build|revert`
36
+ );
37
+ }
38
+
39
+ process.exit(0);
@@ -0,0 +1,40 @@
1
+ #!/usr/bin/env node
2
+ /**
3
+ * PreToolUse / Bash — blocks shell commands that are almost never safe to run
4
+ * autonomously. Reads the tool input JSON from stdin and exits non-zero to abort.
5
+ */
6
+ import { readFileSync } from 'fs';
7
+
8
+ const BLOCKED = [
9
+ /rm\s+-rf\s+[/~]/, // rm -rf / or ~/
10
+ /rm\s+-rf\s+\*/, // rm -rf *
11
+ /DROP\s+DATABASE/i,
12
+ /DROP\s+TABLE/i,
13
+ /TRUNCATE\s+TABLE/i,
14
+ /chmod\s+777/,
15
+ /curl[^|]+\|\s*bash/, // curl ... | bash
16
+ /wget[^|]+\|\s*bash/, // wget ... | bash
17
+ /git\s+push\s+--force(?!\s*--no)/, // force push (not --force-with-lease)
18
+ /git\s+reset\s+--hard\s+HEAD~[2-9]/, // hard reset >1 commit
19
+ /:(){:|:&};:/, // fork bomb
20
+ ];
21
+
22
+ let input;
23
+ try {
24
+ input = JSON.parse(readFileSync('/dev/stdin', 'utf8'));
25
+ } catch {
26
+ process.exit(0); // no stdin — allow
27
+ }
28
+
29
+ const command = input?.tool_input?.command ?? '';
30
+
31
+ for (const pattern of BLOCKED) {
32
+ if (pattern.test(command)) {
33
+ console.error(
34
+ `[block-dangerous] BLOCKED: command matched safety pattern: ${pattern}\n Command: ${command}`
35
+ );
36
+ process.exit(1);
37
+ }
38
+ }
39
+
40
+ process.exit(0);
@@ -0,0 +1,48 @@
1
+ #!/usr/bin/env node
2
+ /**
3
+ * PreToolUse / Bash — warns when Claude is about to push or commit directly
4
+ * to protected branches (main, master, production, release/*).
5
+ * Exits non-zero to block; exits 0 to allow.
6
+ */
7
+ import { readFileSync } from 'fs';
8
+ import { execSync } from 'child_process';
9
+
10
+ const PROTECTED = /^(main|master|production|release\/.+)$/;
11
+
12
+ let input;
13
+ try {
14
+ input = JSON.parse(readFileSync('/dev/stdin', 'utf8'));
15
+ } catch {
16
+ process.exit(0);
17
+ }
18
+
19
+ const command = input?.tool_input?.command ?? '';
20
+
21
+ // Only care about git push / git commit on protected branches
22
+ if (!/git\s+(push|commit)/.test(command)) process.exit(0);
23
+
24
+ let branch = '';
25
+ try {
26
+ branch = execSync('git rev-parse --abbrev-ref HEAD', { stdio: ['pipe', 'pipe', 'pipe'] })
27
+ .toString().trim();
28
+ } catch {
29
+ process.exit(0);
30
+ }
31
+
32
+ if (PROTECTED.test(branch)) {
33
+ // Force-push to protected branch is always blocked
34
+ if (/--force/.test(command) || /-f\b/.test(command)) {
35
+ console.error(
36
+ `[branch-guard] BLOCKED: force push to protected branch "${branch}" is not allowed.`
37
+ );
38
+ process.exit(1);
39
+ }
40
+
41
+ // Warn but allow regular pushes — print to stderr so Claude sees it
42
+ console.error(
43
+ `[branch-guard] WARNING: you are about to push directly to "${branch}".\n` +
44
+ ` Consider using a feature branch and opening a pull request instead.`
45
+ );
46
+ }
47
+
48
+ process.exit(0);
@@ -0,0 +1,49 @@
1
+ #!/usr/bin/env node
2
+ /**
3
+ * PostToolUse / Write — after Claude writes a file, runs a fast type-check or
4
+ * build to catch compile errors immediately. Advisory only — never blocks.
5
+ *
6
+ * Detects: TypeScript (tsc --noEmit), Python (py_compile), Go (go build), Rust (cargo check).
7
+ */
8
+ import { readFileSync, existsSync } from 'fs';
9
+ import { execSync } from 'child_process';
10
+ import path from 'path';
11
+
12
+ let input;
13
+ try {
14
+ input = JSON.parse(readFileSync('/dev/stdin', 'utf8'));
15
+ } catch {
16
+ process.exit(0);
17
+ }
18
+
19
+ const filePath = input?.tool_input?.file_path ?? '';
20
+ if (!filePath || !existsSync(filePath)) process.exit(0);
21
+
22
+ const ext = path.extname(filePath);
23
+
24
+ function run(cmd, label) {
25
+ try {
26
+ execSync(cmd, { stdio: 'pipe', timeout: 60_000 });
27
+ console.error(`[build-validator] ${label} passed`);
28
+ } catch (err) {
29
+ console.error(
30
+ `[build-validator] ${label} failed:\n` +
31
+ (err.stdout?.toString() ?? '') +
32
+ (err.stderr?.toString() ?? '')
33
+ );
34
+ }
35
+ }
36
+
37
+ if (['.ts', '.tsx'].includes(ext)) {
38
+ if (existsSync('tsconfig.json')) {
39
+ run('npx tsc --noEmit --skipLibCheck', 'TypeScript');
40
+ }
41
+ } else if (ext === '.py') {
42
+ run(`python -m py_compile ${filePath}`, 'Python syntax');
43
+ } else if (ext === '.go') {
44
+ run('go build ./...', 'Go build');
45
+ } else if (ext === '.rs') {
46
+ run('cargo check --quiet', 'Rust check');
47
+ }
48
+
49
+ process.exit(0);
@@ -0,0 +1,63 @@
1
+ #!/usr/bin/env node
2
+ /**
3
+ * Stop hook — reads session usage from Claude Code's stop event and appends
4
+ * a cost record to .claude/cost-log.json so teams can track AI spend over time.
5
+ */
6
+ import { readFileSync, writeFileSync, existsSync } from 'fs';
7
+ import path from 'path';
8
+
9
+ // Claude Code pricing (USD per million tokens) as of mid-2025
10
+ // Update these when Anthropic changes pricing.
11
+ const PRICING = {
12
+ 'claude-opus-4': { input: 15.00, output: 75.00, cacheRead: 1.50, cacheWrite: 18.75 },
13
+ 'claude-sonnet-4': { input: 3.00, output: 15.00, cacheRead: 0.30, cacheWrite: 3.75 },
14
+ 'claude-haiku-4': { input: 0.80, output: 4.00, cacheRead: 0.08, cacheWrite: 1.00 },
15
+ };
16
+
17
+ function cost(usage, model) {
18
+ const rates = PRICING[model] ?? PRICING['claude-sonnet-4'];
19
+ const M = 1_000_000;
20
+ return (
21
+ (usage.input_tokens ?? 0) / M * rates.input +
22
+ (usage.output_tokens ?? 0) / M * rates.output +
23
+ (usage.cache_read_tokens ?? 0) / M * rates.cacheRead +
24
+ (usage.cache_creation_tokens ?? 0) / M * rates.cacheWrite
25
+ );
26
+ }
27
+
28
+ let event;
29
+ try {
30
+ event = JSON.parse(readFileSync('/dev/stdin', 'utf8'));
31
+ } catch {
32
+ process.exit(0);
33
+ }
34
+
35
+ const usage = event?.usage ?? event?.session_stats ?? {};
36
+ const model = event?.model ?? 'claude-sonnet-4';
37
+ const usd = cost(usage, model);
38
+
39
+ const logPath = path.join(process.cwd(), '.claude', 'cost-log.json');
40
+ let log = [];
41
+ if (existsSync(logPath)) {
42
+ try { log = JSON.parse(readFileSync(logPath, 'utf8')); } catch { log = []; }
43
+ }
44
+
45
+ log.push({
46
+ timestamp: new Date().toISOString(),
47
+ model,
48
+ usage,
49
+ estimated_usd: +usd.toFixed(6),
50
+ });
51
+
52
+ // Keep last 500 sessions to cap file size
53
+ if (log.length > 500) log = log.slice(-500);
54
+
55
+ try {
56
+ writeFileSync(logPath, JSON.stringify(log, null, 2));
57
+ const total = log.reduce((sum, r) => sum + (r.estimated_usd ?? 0), 0);
58
+ console.error(
59
+ `[cost-tracker] Session cost: ~$${usd.toFixed(4)} | All-time logged: ~$${total.toFixed(2)}`
60
+ );
61
+ } catch { /* read-only fs — skip silently */ }
62
+
63
+ process.exit(0);