bashstats 0.1.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 (38) hide show
  1. package/debug-hook.cjs +38 -0
  2. package/dist/chunk-2KXMOTBO.js +1370 -0
  3. package/dist/chunk-2KXMOTBO.js.map +1 -0
  4. package/dist/cli.d.ts +2 -0
  5. package/dist/cli.js +396 -0
  6. package/dist/cli.js.map +1 -0
  7. package/dist/hooks/chunk-EFVDQUHM.js +566 -0
  8. package/dist/hooks/chunk-EFVDQUHM.js.map +1 -0
  9. package/dist/hooks/notification.js +8 -0
  10. package/dist/hooks/notification.js.map +1 -0
  11. package/dist/hooks/permission-request.js +8 -0
  12. package/dist/hooks/permission-request.js.map +1 -0
  13. package/dist/hooks/post-tool-failure.js +8 -0
  14. package/dist/hooks/post-tool-failure.js.map +1 -0
  15. package/dist/hooks/post-tool-use.js +8 -0
  16. package/dist/hooks/post-tool-use.js.map +1 -0
  17. package/dist/hooks/pre-compact.js +8 -0
  18. package/dist/hooks/pre-compact.js.map +1 -0
  19. package/dist/hooks/pre-tool-use.js +8 -0
  20. package/dist/hooks/pre-tool-use.js.map +1 -0
  21. package/dist/hooks/session-start.js +8 -0
  22. package/dist/hooks/session-start.js.map +1 -0
  23. package/dist/hooks/setup.js +8 -0
  24. package/dist/hooks/setup.js.map +1 -0
  25. package/dist/hooks/stop.js +8 -0
  26. package/dist/hooks/stop.js.map +1 -0
  27. package/dist/hooks/subagent-start.js +8 -0
  28. package/dist/hooks/subagent-start.js.map +1 -0
  29. package/dist/hooks/subagent-stop.js +8 -0
  30. package/dist/hooks/subagent-stop.js.map +1 -0
  31. package/dist/hooks/user-prompt-submit.js +8 -0
  32. package/dist/hooks/user-prompt-submit.js.map +1 -0
  33. package/dist/index.d.ts +355 -0
  34. package/dist/index.js +42 -0
  35. package/dist/index.js.map +1 -0
  36. package/dist/static/index.html +1884 -0
  37. package/nul +1 -0
  38. package/package.json +45 -0
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../src/cli.ts","../src/commands/init.ts","../src/commands/web.ts","../src/dashboard/server.ts","../src/commands/stats.ts","../src/commands/achievements.ts","../src/commands/streak.ts","../src/commands/export-data.ts","../src/commands/reset.ts","../src/commands/uninstall.ts"],"sourcesContent":["import { Command } from 'commander'\nimport { runInit } from './commands/init.js'\nimport { runWeb } from './commands/web.js'\nimport { runStats } from './commands/stats.js'\nimport { runAchievements } from './commands/achievements.js'\nimport { runStreak } from './commands/streak.js'\nimport { runExport } from './commands/export-data.js'\nimport { runReset } from './commands/reset.js'\nimport { runUninstall } from './commands/uninstall.js'\nimport { DEFAULT_PORT } from './constants.js'\n\nconst program = new Command()\n\nprogram\n .name('bashstats')\n .description('Obsessive stat tracking, achievements, and badges for Claude Code')\n .version('0.1.0')\n\nprogram.command('init').description('Install hooks and set up database').action(runInit)\nprogram.command('web')\n .description('Open browser dashboard')\n .option('-p, --port <number>', 'Port to run on', String(DEFAULT_PORT))\n .option('--no-open', 'Do not open browser automatically')\n .action(runWeb)\nprogram.command('stats').description('Quick stat summary').action(runStats)\nprogram.command('achievements').description('List all badges with progress').action(runAchievements)\nprogram.command('streak').description('Show current and longest streak').action(runStreak)\nprogram.command('export').description('Export all data as JSON').action(runExport)\nprogram.command('reset').description('Wipe all data').action(runReset)\nprogram.command('uninstall').description('Remove hooks and data').action(runUninstall)\n\nprogram.action(() => {\n console.log('bashstats - Obsessive stat tracking for Claude Code')\n console.log('')\n console.log('Use \"bashstats init\" to install hooks.')\n console.log('Use \"bashstats stats\" for a quick summary.')\n console.log('Use \"bashstats --help\" for all commands.')\n})\n\nprogram.parse()\n","import { install, isInstalled } from '../installer/installer.js'\n\nexport function runInit(): void {\n if (isInstalled()) {\n console.log('bashstats hooks are already installed. Reinstalling...')\n }\n\n const result = install()\n if (result.success) {\n console.log('')\n console.log(' bashstats installed successfully!')\n console.log('')\n console.log(' ' + result.message)\n console.log('')\n console.log(' Run \"bashstats\" to open the dashboard.')\n console.log(' Your Claude Code sessions are now being tracked.')\n console.log('')\n console.log(' [SECRET ACHIEVEMENT UNLOCKED] Launch Day')\n console.log(' \"Welcome to bashstats. Your stats are now being watched. Forever.\"')\n console.log('')\n } else {\n console.error('Installation failed: ' + result.message)\n process.exit(1)\n }\n}\n","import fs from 'fs'\nimport { exec } from 'child_process'\nimport { getDbPath } from '../hooks/handler.js'\nimport { BashStatsDB } from '../db/database.js'\nimport { startServer } from '../dashboard/server.js'\nimport { DEFAULT_PORT } from '../constants.js'\n\nfunction openBrowser(url: string): void {\n const platform = process.platform\n let command: string\n\n if (platform === 'win32') {\n command = `start \"\" \"${url}\"`\n } else if (platform === 'darwin') {\n command = `open \"${url}\"`\n } else {\n command = `xdg-open \"${url}\"`\n }\n\n exec(command, (err) => {\n if (err) {\n console.log(`Could not open browser automatically. Visit: ${url}`)\n }\n })\n}\n\nexport function runWeb(options: { port?: string; open?: boolean }): void {\n const dbPath = getDbPath()\n\n if (!fs.existsSync(dbPath)) {\n console.log('No bashstats database found.')\n console.log('Run \"bashstats init\" first to set up tracking.')\n return\n }\n\n const port = options.port ? parseInt(options.port, 10) : DEFAULT_PORT\n\n if (isNaN(port) || port < 1 || port > 65535) {\n console.log(`Invalid port: ${options.port}`)\n return\n }\n\n const db = new BashStatsDB(dbPath)\n const url = `http://127.0.0.1:${port}`\n\n startServer(db, port)\n\n const shouldOpen = options.open !== false\n if (shouldOpen) {\n // Small delay to let the server start before opening\n setTimeout(() => openBrowser(url), 500)\n }\n}\n","import express from 'express'\nimport path from 'path'\nimport { fileURLToPath } from 'url'\nimport { BashStatsDB } from '../db/database.js'\nimport { StatsEngine } from '../stats/engine.js'\nimport { AchievementEngine } from '../achievements/compute.js'\nimport { DEFAULT_PORT } from '../constants.js'\n\nconst __dirname = path.dirname(fileURLToPath(import.meta.url))\n\nexport function createApp(db: BashStatsDB): express.Express {\n const app = express()\n const stats = new StatsEngine(db)\n const achievements = new AchievementEngine(db, stats)\n\n // Serve static files\n app.use(express.static(path.join(__dirname, 'static')))\n\n // API endpoints\n app.get('/api/health', (_req, res) => {\n res.json({ status: 'ok', version: '0.1.0' })\n })\n\n app.get('/api/stats', (_req, res) => {\n try {\n res.json(stats.getAllStats())\n } catch (error) {\n res.status(500).json({ error: 'Failed to fetch stats' })\n }\n })\n\n app.get('/api/achievements', (_req, res) => {\n try {\n res.json(achievements.getAchievementsPayload())\n } catch (error) {\n res.status(500).json({ error: 'Failed to fetch achievements' })\n }\n })\n\n app.get('/api/activity', (_req, res) => {\n try {\n const days = parseInt((_req.query.days as string) || '365')\n res.json(db.getAllDailyActivity(days))\n } catch (error) {\n res.status(500).json({ error: 'Failed to fetch activity' })\n }\n })\n\n app.get('/api/sessions', (_req, res) => {\n try {\n const sessions = db.prepare('SELECT * FROM sessions ORDER BY started_at DESC LIMIT 100').all()\n res.json(sessions)\n } catch (error) {\n res.status(500).json({ error: 'Failed to fetch sessions' })\n }\n })\n\n // Fallback to index.html for SPA\n app.get('/{*splat}', (_req, res) => {\n res.sendFile(path.join(__dirname, 'static', 'index.html'))\n })\n\n return app\n}\n\nexport function startServer(db: BashStatsDB, port: number = DEFAULT_PORT): void {\n const app = createApp(db)\n app.listen(port, '127.0.0.1', () => {\n console.log(`bashstats dashboard running at http://127.0.0.1:${port}`)\n })\n}\n","import os from 'os'\nimport path from 'path'\nimport { BashStatsDB } from '../db/database.js'\nimport { StatsEngine } from '../stats/engine.js'\nimport { DATA_DIR, DB_FILENAME } from '../constants.js'\n\nfunction formatDuration(seconds: number): string {\n const hours = Math.floor(seconds / 3600)\n const minutes = Math.floor((seconds % 3600) / 60)\n if (hours > 0) return `${hours}h ${minutes}m`\n return `${minutes}m`\n}\n\nfunction formatNumber(n: number): string {\n return n.toLocaleString()\n}\n\nexport function runStats(): void {\n const dbPath = path.join(os.homedir(), DATA_DIR, DB_FILENAME)\n let db: BashStatsDB\n try {\n db = new BashStatsDB(dbPath)\n } catch {\n console.log('No data yet. Run \"bashstats init\" to start tracking.')\n return\n }\n\n const engine = new StatsEngine(db)\n\n try {\n const lifetime = engine.getLifetimeStats()\n const tools = engine.getToolBreakdown()\n const time = engine.getTimeStats()\n const records = engine.getSessionRecords()\n const projects = engine.getProjectStats()\n\n console.log('')\n console.log(' bashstats - Your Claude Code Stats')\n console.log(' ===================================')\n\n // Lifetime Totals\n console.log('')\n console.log(' LIFETIME TOTALS')\n console.log(` Sessions: ${formatNumber(lifetime.totalSessions)}`)\n console.log(` Time: ${formatDuration(lifetime.totalDurationSeconds)}`)\n console.log(` Prompts: ${formatNumber(lifetime.totalPrompts)}`)\n console.log(` Chars typed: ${formatNumber(lifetime.totalCharsTyped)}`)\n console.log(` Tool calls: ${formatNumber(lifetime.totalToolCalls)}`)\n console.log(` Errors: ${formatNumber(lifetime.totalErrors)}`)\n\n // Tool Breakdown\n if (Object.keys(tools).length > 0) {\n console.log('')\n console.log(' TOOL BREAKDOWN')\n const sorted = Object.entries(tools).sort((a, b) => b[1] - a[1])\n for (const [name, count] of sorted.slice(0, 10)) {\n console.log(` ${name.padEnd(15)} ${formatNumber(count)}`)\n }\n }\n\n // Time & Streaks\n console.log('')\n console.log(' TIME & STREAKS')\n console.log(` Current streak: ${time.currentStreak} days`)\n console.log(` Longest streak: ${time.longestStreak} days`)\n console.log(` Peak hour: ${time.peakHour}:00 (${formatNumber(time.peakHourCount)} prompts)`)\n console.log(` Night owl: ${formatNumber(time.nightOwlCount)} late-night prompts`)\n console.log(` Weekend: ${formatNumber(time.weekendSessions)} weekend sessions`)\n\n // Session Records\n console.log('')\n console.log(' SESSION RECORDS')\n console.log(` Longest: ${formatDuration(records.longestSessionSeconds)}`)\n console.log(` Most tools: ${formatNumber(records.mostToolsInSession)} in one session`)\n console.log(` Most prompts: ${formatNumber(records.mostPromptsInSession)} in one session`)\n console.log(` Avg duration: ${formatDuration(records.avgDurationSeconds)}`)\n\n // Projects\n if (projects.uniqueProjects > 0) {\n console.log('')\n console.log(' PROJECTS')\n console.log(` Unique: ${projects.uniqueProjects}`)\n console.log(` Most visited: ${projects.mostVisitedProject} (${formatNumber(projects.mostVisitedProjectCount)} sessions)`)\n }\n\n console.log('')\n } finally {\n db.close()\n }\n}\n","import os from 'os'\nimport path from 'path'\nimport { BashStatsDB } from '../db/database.js'\nimport { StatsEngine } from '../stats/engine.js'\nimport { AchievementEngine } from '../achievements/compute.js'\nimport { DATA_DIR, DB_FILENAME } from '../constants.js'\nimport type { BadgeResult } from '../types.js'\n\nfunction progressBar(progress: number, width: number = 20): string {\n const filled = Math.round(progress * width)\n const empty = width - filled\n return '#'.repeat(filled) + '.'.repeat(empty)\n}\n\nfunction tierColor(tierName: string): string {\n return `[${tierName}]`\n}\n\nexport function runAchievements(): void {\n const dbPath = path.join(os.homedir(), DATA_DIR, DB_FILENAME)\n let db: BashStatsDB\n try {\n db = new BashStatsDB(dbPath)\n } catch {\n console.log('No data yet. Run \"bashstats init\" to start tracking.')\n return\n }\n\n const stats = new StatsEngine(db)\n const engine = new AchievementEngine(db, stats)\n\n try {\n const payload = engine.getAchievementsPayload()\n\n // Header\n console.log('')\n console.log(` bashstats - Achievements`)\n console.log(` Rank: ${payload.xp.rank} XP: ${payload.xp.totalXP.toLocaleString()} / ${payload.xp.nextRankXP.toLocaleString()}`)\n console.log(` ${progressBar(payload.xp.progress, 40)} ${Math.round(payload.xp.progress * 100)}%`)\n console.log('')\n\n // Group badges by category\n const categories = new Map<string, BadgeResult[]>()\n for (const badge of payload.badges) {\n // Skip secrets that aren't unlocked\n if (badge.secret && !badge.unlocked) continue\n const cat = badge.category\n if (!categories.has(cat)) categories.set(cat, [])\n categories.get(cat)!.push(badge)\n }\n\n const categoryNames: Record<string, string> = {\n volume: 'VOLUME',\n tool_mastery: 'TOOL MASTERY',\n time: 'TIME & STREAKS',\n behavioral: 'BEHAVIORAL',\n resilience: 'RESILIENCE',\n shipping: 'SHIPPING & PROJECTS',\n multi_agent: 'MULTI-AGENT',\n humor: 'HUMOR',\n aspirational: 'ASPIRATIONAL',\n secret: 'SECRET',\n }\n\n for (const [cat, badges] of categories) {\n console.log(` ${categoryNames[cat] || cat.toUpperCase()}`)\n for (const badge of badges) {\n const tier = badge.tier > 0 ? tierColor(badge.tierName) : '[Locked]'\n const bar = badge.maxed ? ' MAXED' : ` ${progressBar(badge.progress, 15)}`\n const value = badge.value.toLocaleString()\n const next = badge.maxed ? '' : ` / ${badge.nextThreshold.toLocaleString()}`\n console.log(` ${tier.padEnd(12)} ${badge.name.padEnd(22)} ${value}${next}${bar}`)\n if (badge.description && (badge.category === 'humor' || badge.category === 'secret')) {\n console.log(` \"${badge.description}\"`)\n }\n }\n console.log('')\n }\n } finally {\n db.close()\n }\n}\n","import os from 'os'\nimport path from 'path'\nimport { BashStatsDB } from '../db/database.js'\nimport { StatsEngine } from '../stats/engine.js'\nimport { DATA_DIR, DB_FILENAME } from '../constants.js'\n\nexport function runStreak(): void {\n const dbPath = path.join(os.homedir(), DATA_DIR, DB_FILENAME)\n let db: BashStatsDB\n try {\n db = new BashStatsDB(dbPath)\n } catch {\n console.log('No data yet. Run \"bashstats init\" to start tracking.')\n return\n }\n\n const engine = new StatsEngine(db)\n\n try {\n const time = engine.getTimeStats()\n const daily = db.getAllDailyActivity(30)\n\n console.log('')\n console.log(' bashstats - Streak')\n console.log(` Current streak: ${time.currentStreak} days`)\n console.log(` Longest streak: ${time.longestStreak} days`)\n console.log('')\n\n // Last 30 days calendar\n if (daily.length > 0) {\n console.log(' Last 30 days:')\n const dates = new Set(daily.map(d => d.date))\n const today = new Date()\n let line = ' '\n for (let i = 29; i >= 0; i--) {\n const d = new Date(today)\n d.setDate(d.getDate() - i)\n const dateStr = d.toISOString().slice(0, 10)\n line += dates.has(dateStr) ? '#' : '.'\n }\n console.log(line)\n }\n console.log('')\n } finally {\n db.close()\n }\n}\n","import os from 'os'\nimport path from 'path'\nimport { BashStatsDB } from '../db/database.js'\nimport { StatsEngine } from '../stats/engine.js'\nimport { AchievementEngine } from '../achievements/compute.js'\nimport { DATA_DIR, DB_FILENAME } from '../constants.js'\n\nexport function runExport(): void {\n const dbPath = path.join(os.homedir(), DATA_DIR, DB_FILENAME)\n let db: BashStatsDB\n try {\n db = new BashStatsDB(dbPath)\n } catch {\n console.error('No data found. Run \"bashstats init\" first.')\n process.exit(1)\n }\n\n const stats = new StatsEngine(db)\n const achievements = new AchievementEngine(db, stats)\n\n try {\n const payload = {\n exported_at: new Date().toISOString(),\n stats: stats.getAllStats(),\n achievements: achievements.getAchievementsPayload(),\n daily_activity: db.getAllDailyActivity(),\n }\n console.log(JSON.stringify(payload, null, 2))\n } finally {\n db.close()\n }\n}\n","import os from 'os'\nimport path from 'path'\nimport fs from 'fs'\nimport { BashStatsDB } from '../db/database.js'\nimport { DATA_DIR, DB_FILENAME } from '../constants.js'\n\nexport function runReset(): void {\n const dbPath = path.join(os.homedir(), DATA_DIR, DB_FILENAME)\n\n if (!fs.existsSync(dbPath)) {\n console.log('No data to reset.')\n return\n }\n\n // Delete and recreate\n fs.unlinkSync(dbPath)\n const db = new BashStatsDB(dbPath)\n db.setMetadata('first_run', new Date().toISOString())\n db.close()\n\n console.log('All data has been reset. Starting fresh.')\n}\n","import os from 'os'\nimport path from 'path'\nimport fs from 'fs'\nimport { uninstall as removeHooks } from '../installer/installer.js'\nimport { DATA_DIR } from '../constants.js'\n\nexport function runUninstall(): void {\n // Remove hooks\n const hookResult = removeHooks()\n console.log(hookResult.message)\n\n // Remove data directory\n const dataDir = path.join(os.homedir(), DATA_DIR)\n if (fs.existsSync(dataDir)) {\n fs.rmSync(dataDir, { recursive: true })\n console.log(`Removed data directory: ${dataDir}`)\n }\n\n console.log('bashstats has been uninstalled.')\n}\n"],"mappings":"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","names":["path","path","os","path","path","os","os","path","path","os","os","path","path","os","os","path","fs","path","os","fs","os","path","fs","path","os","fs"]}
@@ -0,0 +1,566 @@
1
+ #!/usr/bin/env node
2
+
3
+ // src/hooks/handler.ts
4
+ import path2 from "path";
5
+ import os from "os";
6
+ import fs from "fs";
7
+
8
+ // src/constants.ts
9
+ var DATA_DIR = ".bashstats";
10
+ var DB_FILENAME = "bashstats.db";
11
+
12
+ // src/db/database.ts
13
+ import Database from "better-sqlite3";
14
+ function localNow() {
15
+ const d = /* @__PURE__ */ new Date();
16
+ const pad = (n) => String(n).padStart(2, "0");
17
+ const ms = String(d.getMilliseconds()).padStart(3, "0");
18
+ return `${d.getFullYear()}-${pad(d.getMonth() + 1)}-${pad(d.getDate())}T${pad(d.getHours())}:${pad(d.getMinutes())}:${pad(d.getSeconds())}.${ms}`;
19
+ }
20
+ var SCHEMA = `
21
+ CREATE TABLE IF NOT EXISTS events (
22
+ id INTEGER PRIMARY KEY AUTOINCREMENT,
23
+ session_id TEXT NOT NULL,
24
+ hook_type TEXT NOT NULL,
25
+ tool_name TEXT,
26
+ tool_input TEXT,
27
+ tool_output TEXT,
28
+ exit_code INTEGER,
29
+ success INTEGER,
30
+ cwd TEXT,
31
+ project TEXT,
32
+ timestamp TEXT NOT NULL
33
+ );
34
+
35
+ CREATE TABLE IF NOT EXISTS sessions (
36
+ id TEXT PRIMARY KEY,
37
+ agent TEXT NOT NULL DEFAULT 'claude-code',
38
+ started_at TEXT NOT NULL,
39
+ ended_at TEXT,
40
+ stop_reason TEXT,
41
+ prompt_count INTEGER DEFAULT 0,
42
+ tool_count INTEGER DEFAULT 0,
43
+ error_count INTEGER DEFAULT 0,
44
+ project TEXT,
45
+ duration_seconds INTEGER
46
+ );
47
+
48
+ CREATE TABLE IF NOT EXISTS prompts (
49
+ id INTEGER PRIMARY KEY AUTOINCREMENT,
50
+ session_id TEXT NOT NULL,
51
+ content TEXT NOT NULL,
52
+ char_count INTEGER NOT NULL,
53
+ word_count INTEGER NOT NULL,
54
+ timestamp TEXT NOT NULL,
55
+ FOREIGN KEY (session_id) REFERENCES sessions(id)
56
+ );
57
+
58
+ CREATE TABLE IF NOT EXISTS daily_activity (
59
+ date TEXT PRIMARY KEY,
60
+ sessions INTEGER DEFAULT 0,
61
+ prompts INTEGER DEFAULT 0,
62
+ tool_calls INTEGER DEFAULT 0,
63
+ errors INTEGER DEFAULT 0,
64
+ duration_seconds INTEGER DEFAULT 0
65
+ );
66
+
67
+ CREATE TABLE IF NOT EXISTS achievement_unlocks (
68
+ badge_id TEXT NOT NULL,
69
+ tier INTEGER NOT NULL,
70
+ unlocked_at TEXT NOT NULL,
71
+ notified INTEGER DEFAULT 0,
72
+ PRIMARY KEY (badge_id, tier)
73
+ );
74
+
75
+ CREATE TABLE IF NOT EXISTS metadata (
76
+ key TEXT PRIMARY KEY,
77
+ value TEXT
78
+ );
79
+
80
+ CREATE INDEX IF NOT EXISTS idx_events_session ON events(session_id);
81
+ CREATE INDEX IF NOT EXISTS idx_events_hook_type ON events(hook_type);
82
+ CREATE INDEX IF NOT EXISTS idx_events_tool_name ON events(tool_name);
83
+ CREATE INDEX IF NOT EXISTS idx_events_timestamp ON events(timestamp);
84
+ CREATE INDEX IF NOT EXISTS idx_events_project ON events(project);
85
+ CREATE INDEX IF NOT EXISTS idx_prompts_session ON prompts(session_id);
86
+ CREATE INDEX IF NOT EXISTS idx_prompts_timestamp ON prompts(timestamp);
87
+ `;
88
+ var BashStatsDB = class {
89
+ db;
90
+ constructor(dbPath) {
91
+ this.db = new Database(dbPath);
92
+ this.db.pragma("journal_mode = WAL");
93
+ this.db.pragma("foreign_keys = ON");
94
+ this.db.exec(SCHEMA);
95
+ this.migrate();
96
+ }
97
+ migrate() {
98
+ const columns = this.db.pragma("table_info(sessions)");
99
+ const hasAgent = columns.some((c) => c.name === "agent");
100
+ if (!hasAgent) {
101
+ this.db.exec("ALTER TABLE sessions ADD COLUMN agent TEXT NOT NULL DEFAULT 'claude-code'");
102
+ }
103
+ }
104
+ close() {
105
+ this.db.close();
106
+ }
107
+ getTableNames() {
108
+ const rows = this.db.prepare("SELECT name FROM sqlite_master WHERE type='table'").all();
109
+ return rows.map((r) => r.name);
110
+ }
111
+ // === Events ===
112
+ insertEvent(event) {
113
+ const stmt = this.db.prepare(`
114
+ INSERT INTO events (session_id, hook_type, tool_name, tool_input, tool_output, exit_code, success, cwd, project, timestamp)
115
+ VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
116
+ `);
117
+ const result = stmt.run(
118
+ event.session_id,
119
+ event.hook_type,
120
+ event.tool_name,
121
+ event.tool_input,
122
+ event.tool_output,
123
+ event.exit_code,
124
+ event.success,
125
+ event.cwd,
126
+ event.project,
127
+ event.timestamp
128
+ );
129
+ return result.lastInsertRowid;
130
+ }
131
+ getEvents(filter) {
132
+ let sql = "SELECT * FROM events WHERE 1=1";
133
+ const params = [];
134
+ if (filter.session_id) {
135
+ sql += " AND session_id = ?";
136
+ params.push(filter.session_id);
137
+ }
138
+ if (filter.hook_type) {
139
+ sql += " AND hook_type = ?";
140
+ params.push(filter.hook_type);
141
+ }
142
+ if (filter.tool_name) {
143
+ sql += " AND tool_name = ?";
144
+ params.push(filter.tool_name);
145
+ }
146
+ sql += " ORDER BY timestamp ASC";
147
+ return this.db.prepare(sql).all(...params);
148
+ }
149
+ // === Sessions ===
150
+ insertSession(session) {
151
+ this.db.prepare(`
152
+ INSERT OR IGNORE INTO sessions (id, agent, started_at, project) VALUES (?, ?, ?, ?)
153
+ `).run(session.id, session.agent ?? "claude-code", session.started_at, session.project ?? null);
154
+ }
155
+ getSession(id) {
156
+ return this.db.prepare("SELECT * FROM sessions WHERE id = ?").get(id);
157
+ }
158
+ updateSession(id, updates) {
159
+ const sets = [];
160
+ const params = [];
161
+ if (updates.ended_at !== void 0) {
162
+ sets.push("ended_at = ?");
163
+ params.push(updates.ended_at);
164
+ }
165
+ if (updates.stop_reason !== void 0) {
166
+ sets.push("stop_reason = ?");
167
+ params.push(updates.stop_reason);
168
+ }
169
+ if (updates.duration_seconds !== void 0) {
170
+ sets.push("duration_seconds = ?");
171
+ params.push(updates.duration_seconds);
172
+ }
173
+ if (sets.length === 0) return;
174
+ params.push(id);
175
+ this.db.prepare(`UPDATE sessions SET ${sets.join(", ")} WHERE id = ?`).run(...params);
176
+ }
177
+ incrementSessionCounters(id, counters) {
178
+ const sets = [];
179
+ const params = [];
180
+ if (counters.prompts) {
181
+ sets.push("prompt_count = prompt_count + ?");
182
+ params.push(counters.prompts);
183
+ }
184
+ if (counters.tools) {
185
+ sets.push("tool_count = tool_count + ?");
186
+ params.push(counters.tools);
187
+ }
188
+ if (counters.errors) {
189
+ sets.push("error_count = error_count + ?");
190
+ params.push(counters.errors);
191
+ }
192
+ if (sets.length === 0) return;
193
+ params.push(id);
194
+ this.db.prepare(`UPDATE sessions SET ${sets.join(", ")} WHERE id = ?`).run(...params);
195
+ }
196
+ // === Prompts ===
197
+ insertPrompt(prompt) {
198
+ const result = this.db.prepare(`
199
+ INSERT INTO prompts (session_id, content, char_count, word_count, timestamp) VALUES (?, ?, ?, ?, ?)
200
+ `).run(prompt.session_id, prompt.content, prompt.char_count, prompt.word_count, prompt.timestamp);
201
+ return result.lastInsertRowid;
202
+ }
203
+ getPrompts(sessionId) {
204
+ return this.db.prepare("SELECT * FROM prompts WHERE session_id = ? ORDER BY timestamp ASC").all(sessionId);
205
+ }
206
+ // === Daily Activity ===
207
+ incrementDailyActivity(date, increments) {
208
+ this.db.prepare(`
209
+ INSERT INTO daily_activity (date, sessions, prompts, tool_calls, errors, duration_seconds)
210
+ VALUES (?, ?, ?, ?, ?, ?)
211
+ ON CONFLICT(date) DO UPDATE SET
212
+ sessions = sessions + excluded.sessions,
213
+ prompts = prompts + excluded.prompts,
214
+ tool_calls = tool_calls + excluded.tool_calls,
215
+ errors = errors + excluded.errors,
216
+ duration_seconds = duration_seconds + excluded.duration_seconds
217
+ `).run(
218
+ date,
219
+ increments.sessions ?? 0,
220
+ increments.prompts ?? 0,
221
+ increments.tool_calls ?? 0,
222
+ increments.errors ?? 0,
223
+ increments.duration_seconds ?? 0
224
+ );
225
+ }
226
+ getDailyActivity(date) {
227
+ return this.db.prepare("SELECT * FROM daily_activity WHERE date = ?").get(date);
228
+ }
229
+ getAllDailyActivity(days) {
230
+ if (days) {
231
+ return this.db.prepare("SELECT * FROM daily_activity ORDER BY date DESC LIMIT ?").all(days);
232
+ }
233
+ return this.db.prepare("SELECT * FROM daily_activity ORDER BY date DESC").all();
234
+ }
235
+ // === Achievement Unlocks ===
236
+ insertUnlock(badgeId, tier) {
237
+ this.db.prepare(`
238
+ INSERT OR IGNORE INTO achievement_unlocks (badge_id, tier, unlocked_at) VALUES (?, ?, ?)
239
+ `).run(badgeId, tier, localNow());
240
+ }
241
+ getUnlocks() {
242
+ return this.db.prepare("SELECT * FROM achievement_unlocks ORDER BY unlocked_at DESC").all();
243
+ }
244
+ getUnnotifiedUnlocks() {
245
+ return this.db.prepare("SELECT * FROM achievement_unlocks WHERE notified = 0").all();
246
+ }
247
+ markNotified(badgeId, tier) {
248
+ this.db.prepare("UPDATE achievement_unlocks SET notified = 1 WHERE badge_id = ? AND tier = ?").run(badgeId, tier);
249
+ }
250
+ // === Metadata ===
251
+ setMetadata(key, value) {
252
+ this.db.prepare("INSERT OR REPLACE INTO metadata (key, value) VALUES (?, ?)").run(key, value);
253
+ }
254
+ getMetadata(key) {
255
+ const row = this.db.prepare("SELECT value FROM metadata WHERE key = ?").get(key);
256
+ return row?.value ?? null;
257
+ }
258
+ // === Raw DB access for stats engine ===
259
+ prepare(sql) {
260
+ return this.db.prepare(sql);
261
+ }
262
+ };
263
+
264
+ // src/db/writer.ts
265
+ import path from "path";
266
+ var BashStatsWriter = class {
267
+ db;
268
+ constructor(db) {
269
+ this.db = db;
270
+ }
271
+ extractProject(cwd) {
272
+ return path.basename(cwd);
273
+ }
274
+ today() {
275
+ const d = /* @__PURE__ */ new Date();
276
+ const pad = (n) => String(n).padStart(2, "0");
277
+ return `${d.getFullYear()}-${pad(d.getMonth() + 1)}-${pad(d.getDate())}`;
278
+ }
279
+ now() {
280
+ const d = /* @__PURE__ */ new Date();
281
+ const pad = (n) => String(n).padStart(2, "0");
282
+ const ms = String(d.getMilliseconds()).padStart(3, "0");
283
+ return `${d.getFullYear()}-${pad(d.getMonth() + 1)}-${pad(d.getDate())}T${pad(d.getHours())}:${pad(d.getMinutes())}:${pad(d.getSeconds())}.${ms}`;
284
+ }
285
+ recordSessionStart(sessionId, cwd, source, agent) {
286
+ const project = this.extractProject(cwd);
287
+ const timestamp = this.now();
288
+ this.db.insertSession({
289
+ id: sessionId,
290
+ agent,
291
+ started_at: timestamp,
292
+ project
293
+ });
294
+ this.db.insertEvent({
295
+ session_id: sessionId,
296
+ hook_type: "SessionStart",
297
+ tool_name: null,
298
+ tool_input: JSON.stringify({ source }),
299
+ tool_output: null,
300
+ exit_code: null,
301
+ success: null,
302
+ cwd,
303
+ project,
304
+ timestamp
305
+ });
306
+ this.db.incrementDailyActivity(this.today(), { sessions: 1 });
307
+ }
308
+ recordPrompt(sessionId, content) {
309
+ const timestamp = this.now();
310
+ const wordCount = content.split(/\s+/).filter((w) => w.length > 0).length;
311
+ const charCount = content.length;
312
+ this.db.insertPrompt({
313
+ session_id: sessionId,
314
+ content,
315
+ char_count: charCount,
316
+ word_count: wordCount,
317
+ timestamp
318
+ });
319
+ this.db.insertEvent({
320
+ session_id: sessionId,
321
+ hook_type: "UserPromptSubmit",
322
+ tool_name: null,
323
+ tool_input: null,
324
+ tool_output: null,
325
+ exit_code: null,
326
+ success: null,
327
+ cwd: null,
328
+ project: null,
329
+ timestamp
330
+ });
331
+ this.db.incrementSessionCounters(sessionId, { prompts: 1 });
332
+ this.db.incrementDailyActivity(this.today(), { prompts: 1 });
333
+ }
334
+ recordToolUse(sessionId, hookType, toolName, toolInput, toolOutput, exitCode, cwd) {
335
+ const timestamp = this.now();
336
+ const project = this.extractProject(cwd);
337
+ const success = exitCode === 0 ? 1 : 0;
338
+ this.db.insertEvent({
339
+ session_id: sessionId,
340
+ hook_type: hookType,
341
+ tool_name: toolName,
342
+ tool_input: JSON.stringify(toolInput),
343
+ tool_output: JSON.stringify(toolOutput),
344
+ exit_code: exitCode,
345
+ success,
346
+ cwd,
347
+ project,
348
+ timestamp
349
+ });
350
+ this.db.incrementSessionCounters(sessionId, {
351
+ tools: 1,
352
+ errors: success === 0 ? 1 : 0
353
+ });
354
+ this.db.incrementDailyActivity(this.today(), {
355
+ tool_calls: 1,
356
+ errors: success === 0 ? 1 : 0
357
+ });
358
+ }
359
+ recordSessionEnd(sessionId, stopReason) {
360
+ const timestamp = this.now();
361
+ const session = this.db.getSession(sessionId);
362
+ let durationSeconds;
363
+ if (session) {
364
+ const startTime = new Date(session.started_at).getTime();
365
+ const endTime = new Date(timestamp).getTime();
366
+ durationSeconds = Math.round((endTime - startTime) / 1e3);
367
+ }
368
+ this.db.updateSession(sessionId, {
369
+ ended_at: timestamp,
370
+ stop_reason: stopReason,
371
+ duration_seconds: durationSeconds
372
+ });
373
+ this.db.insertEvent({
374
+ session_id: sessionId,
375
+ hook_type: "Stop",
376
+ tool_name: null,
377
+ tool_input: JSON.stringify({ stop_reason: stopReason }),
378
+ tool_output: null,
379
+ exit_code: null,
380
+ success: null,
381
+ cwd: null,
382
+ project: null,
383
+ timestamp
384
+ });
385
+ if (durationSeconds !== void 0) {
386
+ this.db.incrementDailyActivity(this.today(), { duration_seconds: durationSeconds });
387
+ }
388
+ }
389
+ recordNotification(sessionId, message, notificationType) {
390
+ const timestamp = this.now();
391
+ const isError = notificationType === "error" || notificationType === "rate_limit";
392
+ this.db.insertEvent({
393
+ session_id: sessionId,
394
+ hook_type: "Notification",
395
+ tool_name: null,
396
+ tool_input: JSON.stringify({ message, notification_type: notificationType }),
397
+ tool_output: null,
398
+ exit_code: null,
399
+ success: null,
400
+ cwd: null,
401
+ project: null,
402
+ timestamp
403
+ });
404
+ if (isError) {
405
+ this.db.incrementSessionCounters(sessionId, { errors: 1 });
406
+ this.db.incrementDailyActivity(this.today(), { errors: 1 });
407
+ }
408
+ }
409
+ recordSubagent(sessionId, hookType, agentId, agentType) {
410
+ const timestamp = this.now();
411
+ this.db.insertEvent({
412
+ session_id: sessionId,
413
+ hook_type: hookType,
414
+ tool_name: null,
415
+ tool_input: JSON.stringify({ agent_id: agentId, agent_type: agentType ?? null }),
416
+ tool_output: null,
417
+ exit_code: null,
418
+ success: null,
419
+ cwd: null,
420
+ project: null,
421
+ timestamp
422
+ });
423
+ }
424
+ recordCompaction(sessionId, trigger) {
425
+ const timestamp = this.now();
426
+ this.db.insertEvent({
427
+ session_id: sessionId,
428
+ hook_type: "PreCompact",
429
+ tool_name: null,
430
+ tool_input: JSON.stringify({ trigger }),
431
+ tool_output: null,
432
+ exit_code: null,
433
+ success: null,
434
+ cwd: null,
435
+ project: null,
436
+ timestamp
437
+ });
438
+ }
439
+ };
440
+
441
+ // src/hooks/handler.ts
442
+ function detectAgent() {
443
+ if (process.env.GEMINI_CLI || process.env.GEMINI_API_KEY) return "gemini-cli";
444
+ if (process.env.GITHUB_COPILOT_CLI) return "copilot-cli";
445
+ if (process.env.OPENCODE) return "opencode";
446
+ return "claude-code";
447
+ }
448
+ function parseHookEvent(input) {
449
+ try {
450
+ if (!input) return null;
451
+ return JSON.parse(input);
452
+ } catch {
453
+ return null;
454
+ }
455
+ }
456
+ function getDataDir() {
457
+ return path2.join(os.homedir(), DATA_DIR);
458
+ }
459
+ function getDbPath() {
460
+ return path2.join(os.homedir(), DATA_DIR, DB_FILENAME);
461
+ }
462
+ async function readStdin() {
463
+ if (process.env.CLAUDE_HOOK_EVENT) {
464
+ return process.env.CLAUDE_HOOK_EVENT;
465
+ }
466
+ return new Promise((resolve) => {
467
+ let data = "";
468
+ process.stdin.setEncoding("utf-8");
469
+ process.stdin.on("data", (chunk) => {
470
+ data += chunk;
471
+ });
472
+ process.stdin.on("end", () => {
473
+ resolve(data);
474
+ });
475
+ });
476
+ }
477
+ async function handleHookEvent(hookType) {
478
+ const raw = await readStdin();
479
+ const event = parseHookEvent(raw);
480
+ if (!event) return;
481
+ const dataDir = getDataDir();
482
+ fs.mkdirSync(dataDir, { recursive: true });
483
+ const dbPath = getDbPath();
484
+ const db = new BashStatsDB(dbPath);
485
+ const writer = new BashStatsWriter(db);
486
+ try {
487
+ const sessionId = event.session_id ?? "";
488
+ const cwd = event.cwd ?? "";
489
+ switch (hookType) {
490
+ case "SessionStart": {
491
+ const source = event.source ?? "startup";
492
+ const agent = detectAgent();
493
+ writer.recordSessionStart(sessionId, cwd, source, agent);
494
+ break;
495
+ }
496
+ case "UserPromptSubmit": {
497
+ const prompt = event.prompt ?? "";
498
+ writer.recordPrompt(sessionId, prompt);
499
+ break;
500
+ }
501
+ case "PreToolUse": {
502
+ const toolName = event.tool_name ?? "";
503
+ const toolInput = event.tool_input ?? {};
504
+ writer.recordToolUse(sessionId, "PreToolUse", toolName, toolInput, {}, 0, cwd);
505
+ break;
506
+ }
507
+ case "PostToolUse": {
508
+ const toolName = event.tool_name ?? "";
509
+ const toolInput = event.tool_input ?? {};
510
+ const toolResponse = event.tool_response ?? {};
511
+ const exitCode = event.exit_code ?? 0;
512
+ writer.recordToolUse(sessionId, "PostToolUse", toolName, toolInput, toolResponse, exitCode, cwd);
513
+ break;
514
+ }
515
+ case "PostToolUseFailure": {
516
+ const toolName = event.tool_name ?? "";
517
+ const toolInput = event.tool_input ?? {};
518
+ const toolResponse = event.tool_response ?? {};
519
+ writer.recordToolUse(sessionId, "PostToolUseFailure", toolName, toolInput, toolResponse, 1, cwd);
520
+ break;
521
+ }
522
+ case "Stop": {
523
+ writer.recordSessionEnd(sessionId, "stopped");
524
+ break;
525
+ }
526
+ case "Notification": {
527
+ const message = event.message ?? "";
528
+ const notificationType = event.notification_type ?? "";
529
+ writer.recordNotification(sessionId, message, notificationType);
530
+ break;
531
+ }
532
+ case "SubagentStart": {
533
+ const agentId = event.agent_id ?? "";
534
+ const agentType = event.agent_type ?? "";
535
+ writer.recordSubagent(sessionId, "SubagentStart", agentId, agentType);
536
+ break;
537
+ }
538
+ case "SubagentStop": {
539
+ const agentId = event.agent_id ?? "";
540
+ writer.recordSubagent(sessionId, "SubagentStop", agentId);
541
+ break;
542
+ }
543
+ case "PreCompact": {
544
+ const trigger = event.trigger ?? "manual";
545
+ writer.recordCompaction(sessionId, trigger);
546
+ break;
547
+ }
548
+ case "PermissionRequest": {
549
+ const toolName = event.tool_name ?? "";
550
+ const toolInput = event.tool_input ?? {};
551
+ writer.recordToolUse(sessionId, "PermissionRequest", toolName, toolInput, {}, 0, cwd);
552
+ break;
553
+ }
554
+ case "Setup": {
555
+ return;
556
+ }
557
+ }
558
+ } finally {
559
+ db.close();
560
+ }
561
+ }
562
+
563
+ export {
564
+ handleHookEvent
565
+ };
566
+ //# sourceMappingURL=chunk-EFVDQUHM.js.map