bashstats 0.1.0 → 0.2.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 ADDED
@@ -0,0 +1,186 @@
1
+ # bashstats
2
+ <img width="1727" height="916" alt="bashstats2" src="https://github.com/user-attachments/assets/4029e711-f559-4771-9490-dedd4aeec1ee" />
3
+
4
+ Track every prompt, tool call, and late-night coding session. Earn badges. Build streaks. Watch your rank climb from Bronze to Obsidian.
5
+ bashstats hooks into Claude Code and quietly records everything — sessions, prompts, tool usage, errors, and streaks. It then turns it all into stats,
6
+ achievements, and a dashboard you'll check way too often.
7
+
8
+ ## Install
9
+
10
+ ```bash
11
+ npm install -g bashstats
12
+ bashstats init
13
+ ```
14
+
15
+ `bashstats init` installs Claude Code hooks and creates the local database at `~/.bashstats/bashstats.db`. Stats begin recording immediately.
16
+
17
+ ## CLI Commands
18
+
19
+ | Command | Description |
20
+ |---|---|
21
+ | `bashstats init` | Install hooks and set up database |
22
+ | `bashstats stats` | Quick stat summary in your terminal |
23
+ | `bashstats achievements` | List all badges with progress bars |
24
+ | `bashstats streak` | Show current and longest daily streak |
25
+ | `bashstats web` | Launch the browser dashboard |
26
+ | `bashstats export` | Export all data as JSON |
27
+ | `bashstats reset` | Wipe all data |
28
+ | `bashstats uninstall` | Remove hooks and data |
29
+
30
+ ### Options
31
+
32
+ ```bash
33
+ bashstats web --port 8080 # Custom port (default: 17900)
34
+ bashstats web --no-open # Don't auto-open browser
35
+ ```
36
+
37
+ ## Dashboard
38
+
39
+ The browser dashboard at `http://localhost:17900` includes:
40
+
41
+ - **Overview** - Recent badges, rank progress, stat cards, activity heatmap, and recent sessions at a glance
42
+ - **Stats** - Lifetime totals, tool breakdowns, time analysis, session records, and project stats in a 2x2 grid
43
+ - **Achievements** - All 53 badges with tier progress, organized by category
44
+ - **Timeline** - Activity heatmap and session history with sparkline charts
45
+
46
+ ## What Gets Tracked
47
+
48
+ bashstats hooks into 12 Claude Code events:
49
+
50
+ | Event | What it records |
51
+ |---|---|
52
+ | SessionStart | Session creation, project, agent type |
53
+ | UserPromptSubmit | Prompt content, character/word counts |
54
+ | PreToolUse | Tool invocations (Bash, Read, Edit, etc.) |
55
+ | PostToolUse | Tool results and exit codes |
56
+ | PostToolUseFailure | Failed tool calls |
57
+ | Stop | Session end time and duration |
58
+ | Notification | Errors and rate limits |
59
+ | SubagentStart | Subagent spawns |
60
+ | SubagentStop | Subagent completions |
61
+ | PreCompact | Context compactions |
62
+ | PermissionRequest | Permission prompts |
63
+ | Setup | Initialization events |
64
+
65
+ ## Achievements
66
+ <img width="1732" height="917" alt="bashstats" src="https://github.com/user-attachments/assets/63591b76-54ff-4659-b81a-e6310810e364" />
67
+
68
+ 53 badges across 10 categories, each with 5 tiers: Bronze, Silver, Gold, Diamond, Obsidian.
69
+
70
+ ### Volume
71
+ - **First Prompt** - Submit prompts to Claude
72
+ - **Tool Time** - Make tool calls
73
+ - **Marathon** - Spend hours in sessions
74
+ - **Wordsmith** - Type characters in prompts
75
+ - **Session Vet** - Complete sessions
76
+
77
+ ### Tool Mastery
78
+ - **Shell Lord** - Execute Bash commands
79
+ - **Bookworm** - Read files
80
+ - **Editor-in-Chief** - Edit files
81
+ - **Architect** - Create files
82
+ - **Detective** - Search with Grep and Glob
83
+ - **Web Crawler** - Fetch web pages
84
+ - **Delegator** - Spawn subagents
85
+
86
+ ### Time & Streaks
87
+ - **Iron Streak** - Maintain a daily streak
88
+ - **Night Owl** - Prompts between midnight and 5am
89
+ - **Early Bird** - Prompts between 5am and 8am
90
+ - **Weekend Warrior** - Weekend sessions
91
+
92
+ ### Behavioral
93
+ - **Creature of Habit** - Repeat your most-used prompt
94
+ - **Explorer** - Use unique tool types
95
+ - **Planner** - Use plan mode
96
+ - **Novelist** - Write prompts over 1000 characters
97
+ - **Speed Demon** - Complete sessions in under 5 minutes
98
+
99
+ ### Resilience
100
+ - **Clean Hands** - Longest error-free tool streak
101
+ - **Resilient** - Survive errors
102
+ - **Rate Limited** - Hit rate limits
103
+
104
+ ### Shipping & Projects
105
+ - **Shipper** - Make commits via Claude
106
+ - **PR Machine** - Create pull requests
107
+ - **Empire** - Work on unique projects
108
+ - **Polyglot** - Use different programming languages
109
+
110
+ ### Multi-Agent
111
+ - **Buddy System** - Use concurrent agents
112
+ - **Hive Mind** - Spawn subagents total
113
+
114
+ ### Humor
115
+ - **Please and Thank You** - "You're polite to the AI. When they take over, you'll be spared."
116
+ - **Wall of Text** - "Claude read your entire novel and didn't even complain."
117
+ - **The Fixer** - "At this point just rewrite the whole thing."
118
+ - **What Day Is It?** - "Your chair is now a part of you."
119
+ - **Copy Pasta** - "Maybe if I ask again it'll work differently."
120
+ - **Error Magnet** - "At this point, the errors are a feature."
121
+
122
+ ### Aspirational (Obsidian-only)
123
+ - **The Machine** - "You are no longer using the tool. You are the tool."
124
+ - **Year of Code** - "365 days. No breaks. Absolute unit."
125
+ - **Million Words** - "You've written more to Claude than most people write in a lifetime."
126
+ - **Lifer** - "At this point, Claude is your cofounder."
127
+ - **Transcendent** - "You've reached the peak. The view is nice up here."
128
+ - **Omniscient** - "You've mastered every tool. There is nothing left to teach you."
129
+
130
+ ### Secret
131
+ 10 hidden badges unlocked by specific behaviors. Discover them yourself.
132
+
133
+ ## Rank System
134
+
135
+ XP is earned from badge tiers. Your rank progresses through:
136
+
137
+ | Rank | XP Required |
138
+ |---|---|
139
+ | Bronze | 0 |
140
+ | Silver | 1,000 |
141
+ | Gold | 5,000 |
142
+ | Diamond | 25,000 |
143
+ | Obsidian | 100,000 |
144
+
145
+ ## Agent Support
146
+
147
+ bashstats detects which CLI agent is running:
148
+
149
+ - Claude Code (default)
150
+ - Gemini CLI
151
+ - Copilot CLI
152
+ - OpenCode
153
+
154
+ ## Data Storage
155
+
156
+ All data is stored locally in `~/.bashstats/bashstats.db` (SQLite with WAL mode). Nothing is sent anywhere. Tables:
157
+
158
+ - `events` - Every hook event with full context
159
+ - `sessions` - Session lifecycle (start, end, duration, counts)
160
+ - `prompts` - Prompt content and word/char counts
161
+ - `daily_activity` - Aggregated daily stats
162
+ - `achievement_unlocks` - Badge tier unlock timestamps
163
+
164
+ ## Tech Stack
165
+
166
+ - TypeScript + Node.js 18+
167
+ - SQLite via `better-sqlite3`
168
+ - Express for the dashboard server
169
+ - Commander for the CLI
170
+ - tsup for bundling
171
+ - vitest for tests
172
+
173
+ ## Development
174
+
175
+ ```bash
176
+ git clone https://github.com/GhostPeony/bashstats.git
177
+ cd bashstats
178
+ npm install
179
+ npm run build
180
+ npm link
181
+ bashstats init
182
+ ```
183
+
184
+ ## License
185
+
186
+ MIT
@@ -111,7 +111,11 @@ CREATE TABLE IF NOT EXISTS sessions (
111
111
  tool_count INTEGER DEFAULT 0,
112
112
  error_count INTEGER DEFAULT 0,
113
113
  project TEXT,
114
- duration_seconds INTEGER
114
+ duration_seconds INTEGER,
115
+ input_tokens INTEGER DEFAULT 0,
116
+ output_tokens INTEGER DEFAULT 0,
117
+ cache_creation_input_tokens INTEGER DEFAULT 0,
118
+ cache_read_input_tokens INTEGER DEFAULT 0
115
119
  );
116
120
 
117
121
  CREATE TABLE IF NOT EXISTS prompts (
@@ -130,7 +134,11 @@ CREATE TABLE IF NOT EXISTS daily_activity (
130
134
  prompts INTEGER DEFAULT 0,
131
135
  tool_calls INTEGER DEFAULT 0,
132
136
  errors INTEGER DEFAULT 0,
133
- duration_seconds INTEGER DEFAULT 0
137
+ duration_seconds INTEGER DEFAULT 0,
138
+ input_tokens INTEGER DEFAULT 0,
139
+ output_tokens INTEGER DEFAULT 0,
140
+ cache_creation_input_tokens INTEGER DEFAULT 0,
141
+ cache_read_input_tokens INTEGER DEFAULT 0
134
142
  );
135
143
 
136
144
  CREATE TABLE IF NOT EXISTS achievement_unlocks (
@@ -159,16 +167,30 @@ var BashStatsDB = class {
159
167
  constructor(dbPath) {
160
168
  this.db = new Database(dbPath);
161
169
  this.db.pragma("journal_mode = WAL");
170
+ this.db.pragma("busy_timeout = 5000");
162
171
  this.db.pragma("foreign_keys = ON");
163
172
  this.db.exec(SCHEMA);
164
173
  this.migrate();
165
174
  }
166
175
  migrate() {
167
- const columns = this.db.pragma("table_info(sessions)");
168
- const hasAgent = columns.some((c) => c.name === "agent");
169
- if (!hasAgent) {
176
+ const sessionCols = this.db.pragma("table_info(sessions)");
177
+ const sessionColNames = new Set(sessionCols.map((c) => c.name));
178
+ if (!sessionColNames.has("agent")) {
170
179
  this.db.exec("ALTER TABLE sessions ADD COLUMN agent TEXT NOT NULL DEFAULT 'claude-code'");
171
180
  }
181
+ const tokenCols = ["input_tokens", "output_tokens", "cache_creation_input_tokens", "cache_read_input_tokens"];
182
+ for (const col of tokenCols) {
183
+ if (!sessionColNames.has(col)) {
184
+ this.db.exec(`ALTER TABLE sessions ADD COLUMN ${col} INTEGER DEFAULT 0`);
185
+ }
186
+ }
187
+ const dailyCols = this.db.pragma("table_info(daily_activity)");
188
+ const dailyColNames = new Set(dailyCols.map((c) => c.name));
189
+ for (const col of tokenCols) {
190
+ if (!dailyColNames.has(col)) {
191
+ this.db.exec(`ALTER TABLE daily_activity ADD COLUMN ${col} INTEGER DEFAULT 0`);
192
+ }
193
+ }
172
194
  }
173
195
  close() {
174
196
  this.db.close();
@@ -243,6 +265,11 @@ var BashStatsDB = class {
243
265
  params.push(id);
244
266
  this.db.prepare(`UPDATE sessions SET ${sets.join(", ")} WHERE id = ?`).run(...params);
245
267
  }
268
+ updateSessionTokens(id, tokens) {
269
+ this.db.prepare(`
270
+ UPDATE sessions SET input_tokens = ?, output_tokens = ?, cache_creation_input_tokens = ?, cache_read_input_tokens = ? WHERE id = ?
271
+ `).run(tokens.input_tokens, tokens.output_tokens, tokens.cache_creation_input_tokens, tokens.cache_read_input_tokens, id);
272
+ }
246
273
  incrementSessionCounters(id, counters) {
247
274
  const sets = [];
248
275
  const params = [];
@@ -275,21 +302,29 @@ var BashStatsDB = class {
275
302
  // === Daily Activity ===
276
303
  incrementDailyActivity(date, increments) {
277
304
  this.db.prepare(`
278
- INSERT INTO daily_activity (date, sessions, prompts, tool_calls, errors, duration_seconds)
279
- VALUES (?, ?, ?, ?, ?, ?)
305
+ INSERT INTO daily_activity (date, sessions, prompts, tool_calls, errors, duration_seconds, input_tokens, output_tokens, cache_creation_input_tokens, cache_read_input_tokens)
306
+ VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
280
307
  ON CONFLICT(date) DO UPDATE SET
281
308
  sessions = sessions + excluded.sessions,
282
309
  prompts = prompts + excluded.prompts,
283
310
  tool_calls = tool_calls + excluded.tool_calls,
284
311
  errors = errors + excluded.errors,
285
- duration_seconds = duration_seconds + excluded.duration_seconds
312
+ duration_seconds = duration_seconds + excluded.duration_seconds,
313
+ input_tokens = input_tokens + excluded.input_tokens,
314
+ output_tokens = output_tokens + excluded.output_tokens,
315
+ cache_creation_input_tokens = cache_creation_input_tokens + excluded.cache_creation_input_tokens,
316
+ cache_read_input_tokens = cache_read_input_tokens + excluded.cache_read_input_tokens
286
317
  `).run(
287
318
  date,
288
319
  increments.sessions ?? 0,
289
320
  increments.prompts ?? 0,
290
321
  increments.tool_calls ?? 0,
291
322
  increments.errors ?? 0,
292
- increments.duration_seconds ?? 0
323
+ increments.duration_seconds ?? 0,
324
+ increments.input_tokens ?? 0,
325
+ increments.output_tokens ?? 0,
326
+ increments.cache_creation_input_tokens ?? 0,
327
+ increments.cache_read_input_tokens ?? 0
293
328
  );
294
329
  }
295
330
  getDailyActivity(date) {
@@ -550,7 +585,7 @@ var BashStatsWriter = class {
550
585
  errors: success === 0 ? 1 : 0
551
586
  });
552
587
  }
553
- recordSessionEnd(sessionId, stopReason) {
588
+ recordSessionEnd(sessionId, stopReason, tokens) {
554
589
  const timestamp = this.now();
555
590
  const session = this.db.getSession(sessionId);
556
591
  let durationSeconds;
@@ -564,6 +599,9 @@ var BashStatsWriter = class {
564
599
  stop_reason: stopReason,
565
600
  duration_seconds: durationSeconds
566
601
  });
602
+ if (tokens) {
603
+ this.db.updateSessionTokens(sessionId, tokens);
604
+ }
567
605
  this.db.insertEvent({
568
606
  session_id: sessionId,
569
607
  hook_type: "Stop",
@@ -576,8 +614,18 @@ var BashStatsWriter = class {
576
614
  project: null,
577
615
  timestamp
578
616
  });
617
+ const dailyIncrements = {};
579
618
  if (durationSeconds !== void 0) {
580
- this.db.incrementDailyActivity(this.today(), { duration_seconds: durationSeconds });
619
+ dailyIncrements.duration_seconds = durationSeconds;
620
+ }
621
+ if (tokens) {
622
+ dailyIncrements.input_tokens = tokens.input_tokens;
623
+ dailyIncrements.output_tokens = tokens.output_tokens;
624
+ dailyIncrements.cache_creation_input_tokens = tokens.cache_creation_input_tokens;
625
+ dailyIncrements.cache_read_input_tokens = tokens.cache_read_input_tokens;
626
+ }
627
+ if (Object.keys(dailyIncrements).length > 0) {
628
+ this.db.incrementDailyActivity(this.today(), dailyIncrements);
581
629
  }
582
630
  }
583
631
  recordNotification(sessionId, message, notificationType) {
@@ -635,7 +683,49 @@ var BashStatsWriter = class {
635
683
  // src/hooks/handler.ts
636
684
  import path3 from "path";
637
685
  import os2 from "os";
686
+ import fs3 from "fs";
687
+
688
+ // src/hooks/transcript.ts
638
689
  import fs2 from "fs";
690
+ import readline from "readline";
691
+ async function extractTokenUsage(transcriptPath) {
692
+ try {
693
+ if (!fs2.existsSync(transcriptPath)) return null;
694
+ const stream = fs2.createReadStream(transcriptPath, { encoding: "utf-8" });
695
+ const rl = readline.createInterface({ input: stream, crlfDelay: Infinity });
696
+ let inputTokens = 0;
697
+ let outputTokens = 0;
698
+ let cacheCreation = 0;
699
+ let cacheRead = 0;
700
+ let found = false;
701
+ for await (const line of rl) {
702
+ if (!line.trim()) continue;
703
+ try {
704
+ const entry = JSON.parse(line);
705
+ const usage = entry.usage ?? entry.response?.usage ?? entry.message?.usage;
706
+ if (usage && typeof usage === "object") {
707
+ inputTokens += usage.input_tokens ?? 0;
708
+ outputTokens += usage.output_tokens ?? 0;
709
+ cacheCreation += usage.cache_creation_input_tokens ?? 0;
710
+ cacheRead += usage.cache_read_input_tokens ?? 0;
711
+ found = true;
712
+ }
713
+ } catch {
714
+ }
715
+ }
716
+ if (!found) return null;
717
+ return {
718
+ input_tokens: inputTokens,
719
+ output_tokens: outputTokens,
720
+ cache_creation_input_tokens: cacheCreation,
721
+ cache_read_input_tokens: cacheRead
722
+ };
723
+ } catch {
724
+ return null;
725
+ }
726
+ }
727
+
728
+ // src/hooks/handler.ts
639
729
  function detectAgent() {
640
730
  if (process.env.GEMINI_CLI || process.env.GEMINI_API_KEY) return "gemini-cli";
641
731
  if (process.env.GITHUB_COPILOT_CLI) return "copilot-cli";
@@ -676,7 +766,7 @@ async function handleHookEvent(hookType) {
676
766
  const event = parseHookEvent(raw);
677
767
  if (!event) return;
678
768
  const dataDir = getDataDir();
679
- fs2.mkdirSync(dataDir, { recursive: true });
769
+ fs3.mkdirSync(dataDir, { recursive: true });
680
770
  const dbPath = getDbPath();
681
771
  const db = new BashStatsDB(dbPath);
682
772
  const writer = new BashStatsWriter(db);
@@ -717,7 +807,10 @@ async function handleHookEvent(hookType) {
717
807
  break;
718
808
  }
719
809
  case "Stop": {
720
- writer.recordSessionEnd(sessionId, "stopped");
810
+ const rawPath = event.transcript_path ?? "";
811
+ const transcriptPath = rawPath && rawPath.endsWith(".jsonl") ? path3.resolve(rawPath) : "";
812
+ const tokens = transcriptPath ? await extractTokenUsage(transcriptPath) : null;
813
+ writer.recordSessionEnd(sessionId, "stopped", tokens);
721
814
  break;
722
815
  }
723
816
  case "Notification": {
@@ -809,6 +902,19 @@ var StatsEngine = class {
809
902
  const totalRateLimits = this.queryScalar(
810
903
  "SELECT COUNT(*) as c FROM events WHERE hook_type = 'Notification' AND tool_input LIKE '%rate_limit%'"
811
904
  );
905
+ const totalInputTokens = this.queryScalar(
906
+ "SELECT COALESCE(SUM(input_tokens), 0) as c FROM sessions"
907
+ );
908
+ const totalOutputTokens = this.queryScalar(
909
+ "SELECT COALESCE(SUM(output_tokens), 0) as c FROM sessions"
910
+ );
911
+ const totalCacheCreationTokens = this.queryScalar(
912
+ "SELECT COALESCE(SUM(cache_creation_input_tokens), 0) as c FROM sessions"
913
+ );
914
+ const totalCacheReadTokens = this.queryScalar(
915
+ "SELECT COALESCE(SUM(cache_read_input_tokens), 0) as c FROM sessions"
916
+ );
917
+ const totalTokens = totalInputTokens + totalOutputTokens;
812
918
  return {
813
919
  totalSessions,
814
920
  totalDurationSeconds,
@@ -825,7 +931,12 @@ var StatsEngine = class {
825
931
  totalSubagents,
826
932
  totalCompactions,
827
933
  totalErrors,
828
- totalRateLimits
934
+ totalRateLimits,
935
+ totalInputTokens,
936
+ totalOutputTokens,
937
+ totalCacheCreationTokens,
938
+ totalCacheReadTokens,
939
+ totalTokens
829
940
  };
830
941
  }
831
942
  getToolBreakdown() {
@@ -939,6 +1050,12 @@ var StatsEngine = class {
939
1050
  const avgToolsPerSession = this.queryScalar(
940
1051
  "SELECT COALESCE(AVG(tool_count), 0) as c FROM sessions"
941
1052
  );
1053
+ const mostTokensInSession = this.queryScalar(
1054
+ "SELECT COALESCE(MAX(COALESCE(input_tokens, 0) + COALESCE(output_tokens, 0)), 0) as c FROM sessions"
1055
+ );
1056
+ const avgTokensPerSession = this.queryScalar(
1057
+ "SELECT COALESCE(AVG(COALESCE(input_tokens, 0) + COALESCE(output_tokens, 0)), 0) as c FROM sessions"
1058
+ );
942
1059
  return {
943
1060
  longestSessionSeconds,
944
1061
  mostToolsInSession,
@@ -946,7 +1063,9 @@ var StatsEngine = class {
946
1063
  fastestSessionSeconds,
947
1064
  avgDurationSeconds: Math.round(avgDurationSeconds),
948
1065
  avgPromptsPerSession: Math.round(avgPromptsPerSession * 100) / 100,
949
- avgToolsPerSession: Math.round(avgToolsPerSession * 100) / 100
1066
+ avgToolsPerSession: Math.round(avgToolsPerSession * 100) / 100,
1067
+ mostTokensInSession,
1068
+ avgTokensPerSession: Math.round(avgTokensPerSession)
950
1069
  };
951
1070
  }
952
1071
  getProjectStats() {
@@ -1367,4 +1486,4 @@ export {
1367
1486
  TIER_NAMES,
1368
1487
  AchievementEngine
1369
1488
  };
1370
- //# sourceMappingURL=chunk-2KXMOTBO.js.map
1489
+ //# sourceMappingURL=chunk-OYLQHCOY.js.map