bashstats 0.1.0 → 0.2.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.
@@ -3,7 +3,7 @@
3
3
  // src/hooks/handler.ts
4
4
  import path2 from "path";
5
5
  import os from "os";
6
- import fs from "fs";
6
+ import fs2 from "fs";
7
7
 
8
8
  // src/constants.ts
9
9
  var DATA_DIR = ".bashstats";
@@ -42,7 +42,11 @@ CREATE TABLE IF NOT EXISTS sessions (
42
42
  tool_count INTEGER DEFAULT 0,
43
43
  error_count INTEGER DEFAULT 0,
44
44
  project TEXT,
45
- duration_seconds INTEGER
45
+ duration_seconds INTEGER,
46
+ input_tokens INTEGER DEFAULT 0,
47
+ output_tokens INTEGER DEFAULT 0,
48
+ cache_creation_input_tokens INTEGER DEFAULT 0,
49
+ cache_read_input_tokens INTEGER DEFAULT 0
46
50
  );
47
51
 
48
52
  CREATE TABLE IF NOT EXISTS prompts (
@@ -61,7 +65,11 @@ CREATE TABLE IF NOT EXISTS daily_activity (
61
65
  prompts INTEGER DEFAULT 0,
62
66
  tool_calls INTEGER DEFAULT 0,
63
67
  errors INTEGER DEFAULT 0,
64
- duration_seconds INTEGER DEFAULT 0
68
+ duration_seconds INTEGER DEFAULT 0,
69
+ input_tokens INTEGER DEFAULT 0,
70
+ output_tokens INTEGER DEFAULT 0,
71
+ cache_creation_input_tokens INTEGER DEFAULT 0,
72
+ cache_read_input_tokens INTEGER DEFAULT 0
65
73
  );
66
74
 
67
75
  CREATE TABLE IF NOT EXISTS achievement_unlocks (
@@ -90,16 +98,30 @@ var BashStatsDB = class {
90
98
  constructor(dbPath) {
91
99
  this.db = new Database(dbPath);
92
100
  this.db.pragma("journal_mode = WAL");
101
+ this.db.pragma("busy_timeout = 5000");
93
102
  this.db.pragma("foreign_keys = ON");
94
103
  this.db.exec(SCHEMA);
95
104
  this.migrate();
96
105
  }
97
106
  migrate() {
98
- const columns = this.db.pragma("table_info(sessions)");
99
- const hasAgent = columns.some((c) => c.name === "agent");
100
- if (!hasAgent) {
107
+ const sessionCols = this.db.pragma("table_info(sessions)");
108
+ const sessionColNames = new Set(sessionCols.map((c) => c.name));
109
+ if (!sessionColNames.has("agent")) {
101
110
  this.db.exec("ALTER TABLE sessions ADD COLUMN agent TEXT NOT NULL DEFAULT 'claude-code'");
102
111
  }
112
+ const tokenCols = ["input_tokens", "output_tokens", "cache_creation_input_tokens", "cache_read_input_tokens"];
113
+ for (const col of tokenCols) {
114
+ if (!sessionColNames.has(col)) {
115
+ this.db.exec(`ALTER TABLE sessions ADD COLUMN ${col} INTEGER DEFAULT 0`);
116
+ }
117
+ }
118
+ const dailyCols = this.db.pragma("table_info(daily_activity)");
119
+ const dailyColNames = new Set(dailyCols.map((c) => c.name));
120
+ for (const col of tokenCols) {
121
+ if (!dailyColNames.has(col)) {
122
+ this.db.exec(`ALTER TABLE daily_activity ADD COLUMN ${col} INTEGER DEFAULT 0`);
123
+ }
124
+ }
103
125
  }
104
126
  close() {
105
127
  this.db.close();
@@ -174,6 +196,11 @@ var BashStatsDB = class {
174
196
  params.push(id);
175
197
  this.db.prepare(`UPDATE sessions SET ${sets.join(", ")} WHERE id = ?`).run(...params);
176
198
  }
199
+ updateSessionTokens(id, tokens) {
200
+ this.db.prepare(`
201
+ UPDATE sessions SET input_tokens = ?, output_tokens = ?, cache_creation_input_tokens = ?, cache_read_input_tokens = ? WHERE id = ?
202
+ `).run(tokens.input_tokens, tokens.output_tokens, tokens.cache_creation_input_tokens, tokens.cache_read_input_tokens, id);
203
+ }
177
204
  incrementSessionCounters(id, counters) {
178
205
  const sets = [];
179
206
  const params = [];
@@ -206,21 +233,29 @@ var BashStatsDB = class {
206
233
  // === Daily Activity ===
207
234
  incrementDailyActivity(date, increments) {
208
235
  this.db.prepare(`
209
- INSERT INTO daily_activity (date, sessions, prompts, tool_calls, errors, duration_seconds)
210
- VALUES (?, ?, ?, ?, ?, ?)
236
+ INSERT INTO daily_activity (date, sessions, prompts, tool_calls, errors, duration_seconds, input_tokens, output_tokens, cache_creation_input_tokens, cache_read_input_tokens)
237
+ VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
211
238
  ON CONFLICT(date) DO UPDATE SET
212
239
  sessions = sessions + excluded.sessions,
213
240
  prompts = prompts + excluded.prompts,
214
241
  tool_calls = tool_calls + excluded.tool_calls,
215
242
  errors = errors + excluded.errors,
216
- duration_seconds = duration_seconds + excluded.duration_seconds
243
+ duration_seconds = duration_seconds + excluded.duration_seconds,
244
+ input_tokens = input_tokens + excluded.input_tokens,
245
+ output_tokens = output_tokens + excluded.output_tokens,
246
+ cache_creation_input_tokens = cache_creation_input_tokens + excluded.cache_creation_input_tokens,
247
+ cache_read_input_tokens = cache_read_input_tokens + excluded.cache_read_input_tokens
217
248
  `).run(
218
249
  date,
219
250
  increments.sessions ?? 0,
220
251
  increments.prompts ?? 0,
221
252
  increments.tool_calls ?? 0,
222
253
  increments.errors ?? 0,
223
- increments.duration_seconds ?? 0
254
+ increments.duration_seconds ?? 0,
255
+ increments.input_tokens ?? 0,
256
+ increments.output_tokens ?? 0,
257
+ increments.cache_creation_input_tokens ?? 0,
258
+ increments.cache_read_input_tokens ?? 0
224
259
  );
225
260
  }
226
261
  getDailyActivity(date) {
@@ -356,7 +391,7 @@ var BashStatsWriter = class {
356
391
  errors: success === 0 ? 1 : 0
357
392
  });
358
393
  }
359
- recordSessionEnd(sessionId, stopReason) {
394
+ recordSessionEnd(sessionId, stopReason, tokens) {
360
395
  const timestamp = this.now();
361
396
  const session = this.db.getSession(sessionId);
362
397
  let durationSeconds;
@@ -370,6 +405,9 @@ var BashStatsWriter = class {
370
405
  stop_reason: stopReason,
371
406
  duration_seconds: durationSeconds
372
407
  });
408
+ if (tokens) {
409
+ this.db.updateSessionTokens(sessionId, tokens);
410
+ }
373
411
  this.db.insertEvent({
374
412
  session_id: sessionId,
375
413
  hook_type: "Stop",
@@ -382,8 +420,18 @@ var BashStatsWriter = class {
382
420
  project: null,
383
421
  timestamp
384
422
  });
423
+ const dailyIncrements = {};
385
424
  if (durationSeconds !== void 0) {
386
- this.db.incrementDailyActivity(this.today(), { duration_seconds: durationSeconds });
425
+ dailyIncrements.duration_seconds = durationSeconds;
426
+ }
427
+ if (tokens) {
428
+ dailyIncrements.input_tokens = tokens.input_tokens;
429
+ dailyIncrements.output_tokens = tokens.output_tokens;
430
+ dailyIncrements.cache_creation_input_tokens = tokens.cache_creation_input_tokens;
431
+ dailyIncrements.cache_read_input_tokens = tokens.cache_read_input_tokens;
432
+ }
433
+ if (Object.keys(dailyIncrements).length > 0) {
434
+ this.db.incrementDailyActivity(this.today(), dailyIncrements);
387
435
  }
388
436
  }
389
437
  recordNotification(sessionId, message, notificationType) {
@@ -438,6 +486,54 @@ var BashStatsWriter = class {
438
486
  }
439
487
  };
440
488
 
489
+ // src/hooks/transcript.ts
490
+ import fs from "fs";
491
+ import readline from "readline";
492
+ async function extractTokenUsage(transcriptPath) {
493
+ try {
494
+ if (!fs.existsSync(transcriptPath)) return null;
495
+ const stream = fs.createReadStream(transcriptPath, { encoding: "utf-8" });
496
+ const rl = readline.createInterface({ input: stream, crlfDelay: Infinity });
497
+ const seenMessages = /* @__PURE__ */ new Map();
498
+ for await (const line of rl) {
499
+ if (!line.trim()) continue;
500
+ try {
501
+ const entry = JSON.parse(line);
502
+ const usage = entry.usage ?? entry.response?.usage ?? entry.message?.usage;
503
+ if (usage && typeof usage === "object" && "input_tokens" in usage) {
504
+ const msgId = entry.message?.id ?? entry.id ?? `_line_${seenMessages.size}`;
505
+ seenMessages.set(msgId, {
506
+ input_tokens: usage.input_tokens ?? 0,
507
+ output_tokens: usage.output_tokens ?? 0,
508
+ cache_creation_input_tokens: usage.cache_creation_input_tokens ?? 0,
509
+ cache_read_input_tokens: usage.cache_read_input_tokens ?? 0
510
+ });
511
+ }
512
+ } catch {
513
+ }
514
+ }
515
+ if (seenMessages.size === 0) return null;
516
+ let inputTokens = 0;
517
+ let outputTokens = 0;
518
+ let cacheCreation = 0;
519
+ let cacheRead = 0;
520
+ for (const u of seenMessages.values()) {
521
+ inputTokens += u.input_tokens;
522
+ outputTokens += u.output_tokens;
523
+ cacheCreation += u.cache_creation_input_tokens;
524
+ cacheRead += u.cache_read_input_tokens;
525
+ }
526
+ return {
527
+ input_tokens: inputTokens,
528
+ output_tokens: outputTokens,
529
+ cache_creation_input_tokens: cacheCreation,
530
+ cache_read_input_tokens: cacheRead
531
+ };
532
+ } catch {
533
+ return null;
534
+ }
535
+ }
536
+
441
537
  // src/hooks/handler.ts
442
538
  function detectAgent() {
443
539
  if (process.env.GEMINI_CLI || process.env.GEMINI_API_KEY) return "gemini-cli";
@@ -479,7 +575,7 @@ async function handleHookEvent(hookType) {
479
575
  const event = parseHookEvent(raw);
480
576
  if (!event) return;
481
577
  const dataDir = getDataDir();
482
- fs.mkdirSync(dataDir, { recursive: true });
578
+ fs2.mkdirSync(dataDir, { recursive: true });
483
579
  const dbPath = getDbPath();
484
580
  const db = new BashStatsDB(dbPath);
485
581
  const writer = new BashStatsWriter(db);
@@ -520,7 +616,10 @@ async function handleHookEvent(hookType) {
520
616
  break;
521
617
  }
522
618
  case "Stop": {
523
- writer.recordSessionEnd(sessionId, "stopped");
619
+ const rawPath = event.transcript_path ?? "";
620
+ const transcriptPath = rawPath && rawPath.endsWith(".jsonl") ? path2.resolve(rawPath) : "";
621
+ const tokens = transcriptPath ? await extractTokenUsage(transcriptPath) : null;
622
+ writer.recordSessionEnd(sessionId, "stopped", tokens);
524
623
  break;
525
624
  }
526
625
  case "Notification": {
@@ -563,4 +662,4 @@ async function handleHookEvent(hookType) {
563
662
  export {
564
663
  handleHookEvent
565
664
  };
566
- //# sourceMappingURL=chunk-EFVDQUHM.js.map
665
+ //# sourceMappingURL=chunk-37VUNTM4.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../../src/hooks/handler.ts","../../src/constants.ts","../../src/db/database.ts","../../src/db/writer.ts","../../src/hooks/transcript.ts"],"sourcesContent":["import path from 'path'\nimport os from 'os'\nimport fs from 'fs'\nimport { DATA_DIR, DB_FILENAME } from '../constants.js'\nimport { BashStatsDB } from '../db/database.js'\nimport { BashStatsWriter } from '../db/writer.js'\nimport type { AgentType } from '../types.js'\nimport { extractTokenUsage } from './transcript.js'\n\n/**\n * Detect which CLI agent is running based on environment variables and process context.\n * Currently supports Claude Code (default), Gemini CLI, Copilot CLI, and OpenCode.\n */\nexport function detectAgent(): AgentType {\n if (process.env.GEMINI_CLI || process.env.GEMINI_API_KEY) return 'gemini-cli'\n if (process.env.GITHUB_COPILOT_CLI) return 'copilot-cli'\n if (process.env.OPENCODE) return 'opencode'\n return 'claude-code'\n}\n\nexport function parseHookEvent(input: string): Record<string, unknown> | null {\n try {\n if (!input) return null\n return JSON.parse(input) as Record<string, unknown>\n } catch {\n return null\n }\n}\n\nexport function getProjectFromCwd(cwd: string): string {\n return path.basename(cwd)\n}\n\nexport function getDataDir(): string {\n return path.join(os.homedir(), DATA_DIR)\n}\n\nexport function getDbPath(): string {\n return path.join(os.homedir(), DATA_DIR, DB_FILENAME)\n}\n\nexport async function readStdin(): Promise<string> {\n if (process.env.CLAUDE_HOOK_EVENT) {\n return process.env.CLAUDE_HOOK_EVENT\n }\n\n return new Promise<string>((resolve) => {\n let data = ''\n process.stdin.setEncoding('utf-8')\n process.stdin.on('data', (chunk: string) => {\n data += chunk\n })\n process.stdin.on('end', () => {\n resolve(data)\n })\n })\n}\n\nexport async function handleHookEvent(hookType: string): Promise<void> {\n const raw = await readStdin()\n const event = parseHookEvent(raw)\n if (!event) return\n\n const dataDir = getDataDir()\n fs.mkdirSync(dataDir, { recursive: true })\n\n const dbPath = getDbPath()\n const db = new BashStatsDB(dbPath)\n const writer = new BashStatsWriter(db)\n\n try {\n const sessionId = (event.session_id as string) ?? ''\n const cwd = (event.cwd as string) ?? ''\n\n switch (hookType) {\n case 'SessionStart': {\n const source = (event.source as string) ?? 'startup'\n const agent = detectAgent()\n writer.recordSessionStart(sessionId, cwd, source, agent)\n break\n }\n\n case 'UserPromptSubmit': {\n const prompt = (event.prompt as string) ?? ''\n writer.recordPrompt(sessionId, prompt)\n break\n }\n\n case 'PreToolUse': {\n const toolName = (event.tool_name as string) ?? ''\n const toolInput = (event.tool_input as Record<string, unknown>) ?? {}\n writer.recordToolUse(sessionId, 'PreToolUse', toolName, toolInput, {}, 0, cwd)\n break\n }\n\n case 'PostToolUse': {\n const toolName = (event.tool_name as string) ?? ''\n const toolInput = (event.tool_input as Record<string, unknown>) ?? {}\n const toolResponse = (event.tool_response as Record<string, unknown>) ?? {}\n const exitCode = (event.exit_code as number) ?? 0\n writer.recordToolUse(sessionId, 'PostToolUse', toolName, toolInput, toolResponse, exitCode, cwd)\n break\n }\n\n case 'PostToolUseFailure': {\n const toolName = (event.tool_name as string) ?? ''\n const toolInput = (event.tool_input as Record<string, unknown>) ?? {}\n const toolResponse = (event.tool_response as Record<string, unknown>) ?? {}\n writer.recordToolUse(sessionId, 'PostToolUseFailure', toolName, toolInput, toolResponse, 1, cwd)\n break\n }\n\n case 'Stop': {\n const rawPath = (event.transcript_path as string) ?? ''\n const transcriptPath = rawPath && rawPath.endsWith('.jsonl') ? path.resolve(rawPath) : ''\n const tokens = transcriptPath ? await extractTokenUsage(transcriptPath) : null\n writer.recordSessionEnd(sessionId, 'stopped', tokens)\n break\n }\n\n case 'Notification': {\n const message = (event.message as string) ?? ''\n const notificationType = (event.notification_type as string) ?? ''\n writer.recordNotification(sessionId, message, notificationType)\n break\n }\n\n case 'SubagentStart': {\n const agentId = (event.agent_id as string) ?? ''\n const agentType = (event.agent_type as string) ?? ''\n writer.recordSubagent(sessionId, 'SubagentStart', agentId, agentType)\n break\n }\n\n case 'SubagentStop': {\n const agentId = (event.agent_id as string) ?? ''\n writer.recordSubagent(sessionId, 'SubagentStop', agentId)\n break\n }\n\n case 'PreCompact': {\n const trigger = (event.trigger as string) ?? 'manual'\n writer.recordCompaction(sessionId, trigger)\n break\n }\n\n case 'PermissionRequest': {\n const toolName = (event.tool_name as string) ?? ''\n const toolInput = (event.tool_input as Record<string, unknown>) ?? {}\n writer.recordToolUse(sessionId, 'PermissionRequest', toolName, toolInput, {}, 0, cwd)\n break\n }\n\n case 'Setup': {\n // no-op\n return\n }\n }\n } finally {\n db.close()\n }\n}\n","import type { BadgeDefinition } from './types.js'\n\nexport const BADGE_DEFINITIONS: BadgeDefinition[] = [\n // ===================================================================\n // VOLUME (5)\n // ===================================================================\n { id: 'first_prompt', name: 'First Prompt', icon: '\\u{1F4AC}', description: 'Submit prompts to Claude', category: 'volume', stat: 'totalPrompts', tiers: [1, 250, 2500, 10000, 50000], trigger: 'Total prompts submitted across all sessions' },\n { id: 'tool_time', name: 'Tool Time', icon: '\\u{1F527}', description: 'Make tool calls', category: 'volume', stat: 'totalToolCalls', tiers: [50, 2500, 25000, 100000, 500000], trigger: 'Total tool calls made across all sessions' },\n { id: 'marathon', name: 'Marathon', icon: '\\u{1F3C3}', description: 'Spend hours in sessions', category: 'volume', stat: 'totalSessionHours', tiers: [1, 25, 250, 1000, 5000], trigger: 'Total hours spent in sessions (rounded down)' },\n { id: 'wordsmith', name: 'Wordsmith', icon: '\\u{270D}', description: 'Type characters in prompts', category: 'volume', stat: 'totalCharsTyped', tiers: [1000, 100000, 1000000, 5000000, 25000000], trigger: 'Total characters typed in prompts' },\n { id: 'session_vet', name: 'Session Vet', icon: '\\u{1F3C5}', description: 'Complete sessions', category: 'volume', stat: 'totalSessions', tiers: [1, 100, 1000, 5000, 25000], trigger: 'Total sessions completed' },\n\n // ===================================================================\n // TOOL MASTERY (7)\n // ===================================================================\n { id: 'shell_lord', name: 'Shell Lord', icon: '\\u{1F4BB}', description: 'Execute Bash commands', category: 'tool_mastery', stat: 'totalBashCommands', tiers: [25, 250, 2500, 10000, 50000], trigger: 'Bash commands executed via PostToolUse' },\n { id: 'bookworm', name: 'Bookworm', icon: '\\u{1F4D6}', description: 'Read files', category: 'tool_mastery', stat: 'totalFilesRead', tiers: [50, 500, 5000, 25000, 100000], trigger: 'Files read with the Read tool' },\n { id: 'editor_in_chief', name: 'Editor-in-Chief', icon: '\\u{1F4DD}', description: 'Edit files', category: 'tool_mastery', stat: 'totalFilesEdited', tiers: [25, 250, 2500, 10000, 50000], trigger: 'Files edited with the Edit tool' },\n { id: 'architect', name: 'Architect', icon: '\\u{1F3D7}', description: 'Create files', category: 'tool_mastery', stat: 'totalFilesCreated', tiers: [10, 100, 500, 2500, 10000], trigger: 'Files created with the Write tool' },\n { id: 'detective', name: 'Detective', icon: '\\u{1F50D}', description: 'Search with Grep and Glob', category: 'tool_mastery', stat: 'totalSearches', tiers: [50, 500, 5000, 25000, 100000], trigger: 'Grep and Glob searches performed' },\n { id: 'web_crawler', name: 'Web Crawler', icon: '\\u{1F310}', description: 'Fetch web pages', category: 'tool_mastery', stat: 'totalWebFetches', tiers: [5, 50, 200, 1000, 5000], trigger: 'WebFetch calls made' },\n { id: 'delegator', name: 'Delegator', icon: '\\u{1F916}', description: 'Spawn subagents', category: 'tool_mastery', stat: 'totalSubagents', tiers: [10, 100, 500, 2500, 10000], trigger: 'Subagents spawned via SubagentStart events' },\n\n // ===================================================================\n // TIME & PATTERNS (4 existing + 6 new = 10)\n // ===================================================================\n { id: 'iron_streak', name: 'Iron Streak', icon: '\\u{1F525}', description: 'Maintain a daily streak', category: 'time', stat: 'longestStreak', tiers: [3, 7, 30, 100, 365], trigger: 'Longest streak of consecutive days with activity' },\n { id: 'night_owl', name: 'Night Owl', icon: '\\u{1F989}', description: 'Prompts between midnight and 5am', category: 'time', stat: 'nightOwlCount', tiers: [10, 50, 200, 1000, 5000], trigger: 'Prompts submitted between midnight and 5 AM' },\n { id: 'early_bird', name: 'Early Bird', icon: '\\u{1F426}', description: 'Prompts between 5am and 8am', category: 'time', stat: 'earlyBirdCount', tiers: [10, 50, 200, 1000, 5000], trigger: 'Prompts submitted between 5 AM and 8 AM' },\n { id: 'weekend_warrior', name: 'Weekend Warrior', icon: '\\u{2694}', description: 'Weekend sessions', category: 'time', stat: 'weekendSessions', tiers: [5, 25, 100, 500, 2000], trigger: 'Sessions started on Saturday or Sunday' },\n // New Time & Patterns\n { id: 'witching_hour', name: 'Witching Hour', icon: '\\u{1F9D9}', description: '3 AM hits different when you\\'re debugging.', category: 'time', stat: 'witchingHourPrompts', tiers: [1, 10, 50, 200, 1000], trigger: 'Prompts submitted between 2 AM and 4 AM' },\n { id: 'lunch_break_coder', name: 'Lunch Break Coder', icon: '\\u{1F354}', description: 'Who needs food when you have Claude?', category: 'time', stat: 'lunchBreakDays', tiers: [5, 15, 30, 60, 120], trigger: 'Distinct days with sessions during 12-1 PM' },\n { id: 'monday_motivation', name: 'Monday Motivation', icon: '\\u{1F4AA}', description: 'Starting the week strong (or desperate).', category: 'time', stat: 'mondaySessions', tiers: [10, 50, 100, 250, 500], trigger: 'Sessions started on Monday' },\n { id: 'friday_shipper', name: 'Friday Shipper', icon: '\\u{1F6A2}', description: 'Deploy on Friday? You absolute madlad.', category: 'time', stat: 'fridayCommits', tiers: [1, 10, 50, 200, 1000], trigger: 'Git commits made on Friday (via Bash tool)' },\n { id: 'timezone_traveler', name: 'Timezone Traveler', icon: '\\u{2708}', description: 'Your sleep schedule is a suggestion.', category: 'time', stat: 'maxUniqueHoursInDay', tiers: [6, 8, 12, 16, 20], trigger: 'Most unique hours with prompts in a single day' },\n { id: 'seasonal_coder', name: 'Seasonal Coder', icon: '\\u{1F343}', description: \"You've coded through all four seasons.\", category: 'time', stat: 'uniqueQuarters', tiers: [1, 2, 3, 4, 8], trigger: 'Unique quarter-year combos (e.g. Q1-2025, Q2-2025) with activity' },\n\n // ===================================================================\n // SESSION BEHAVIOR (6 new)\n // ===================================================================\n { id: 'one_more_thing', name: 'One More Thing', icon: '\\u{261D}', description: 'You said you were done 5 prompts ago.', category: 'session_behavior', stat: 'extendedSessionCount', tiers: [1, 5, 25, 100, 500], trigger: 'Sessions over 1 hour with 15+ prompts' },\n { id: 'quick_draw', name: 'Quick Draw', icon: '\\u{1F52B}', description: 'In and out. 20 second adventure.', category: 'session_behavior', stat: 'quickDrawSessions', tiers: [5, 25, 100, 500, 2500], trigger: 'Sessions under 2 minutes with successful tool use' },\n { id: 'the_pivot', name: 'The Pivot', icon: '\\u{1F504}', description: 'Started with CSS, ended with Kubernetes.', category: 'session_behavior', stat: 'diverseToolSessions', tiers: [5, 25, 100, 500, 2500], trigger: 'Sessions using 5+ distinct tool types' },\n { id: 'context_crunch', name: 'Context Crunch', icon: '\\u{1F4A6}', description: 'Your context window is sweating.', category: 'session_behavior', stat: 'totalCompactions', tiers: [1, 5, 25, 100, 500], trigger: 'PreCompact events triggered (manual or auto)' },\n { id: 'permission_slip', name: 'Permission Slip', icon: '\\u{1F4DD}', description: 'Always asking for permission. So polite.', category: 'session_behavior', stat: 'permissionRequests', tiers: [10, 100, 500, 2500, 10000], trigger: 'PermissionRequest events recorded' },\n { id: 'the_returner', name: 'The Returner', icon: '\\u{1F519}', description: 'Back so soon? Missed me?', category: 'session_behavior', stat: 'returnerDays', tiers: [1, 5, 25, 100, 500], trigger: 'Days with 5+ sessions on the same project' },\n\n // ===================================================================\n // BEHAVIORAL (5)\n // ===================================================================\n { id: 'creature_of_habit', name: 'Creature of Habit', icon: '\\u{1F501}', description: 'Repeat your most-used prompt', category: 'behavioral', stat: 'mostRepeatedPromptCount', tiers: [25, 100, 500, 2000, 10000], trigger: 'Count of your single most repeated prompt' },\n { id: 'explorer', name: 'Explorer', icon: '\\u{1F9ED}', description: 'Use unique tool types', category: 'behavioral', stat: 'uniqueToolsUsed', tiers: [3, 5, 8, 12, 18], trigger: 'Distinct tool types used across all sessions' },\n { id: 'planner', name: 'Planner', icon: '\\u{1F4CB}', description: 'Use plan mode', category: 'behavioral', stat: 'planModeUses', tiers: [5, 25, 100, 500, 2000], trigger: 'Task tool invocations (plan mode)' },\n { id: 'novelist', name: 'Novelist', icon: '\\u{1F4D6}', description: 'Write prompts over 1000 characters', category: 'behavioral', stat: 'longPromptCount', tiers: [5, 25, 100, 500, 2000], trigger: 'Prompts with over 1,000 characters' },\n { id: 'speed_demon', name: 'Speed Demon', icon: '\\u{26A1}', description: 'Complete sessions in under 5 minutes', category: 'behavioral', stat: 'quickSessionCount', tiers: [5, 25, 100, 500, 2000], trigger: 'Sessions under 5 minutes with tool use' },\n\n // ===================================================================\n // PROMPT PATTERNS (6 new)\n // ===================================================================\n { id: 'minimalist', name: 'Minimalist', icon: '\\u{1F90F}', description: 'A person of few words.', category: 'prompt_patterns', stat: 'shortPromptCount', tiers: [5, 25, 100, 500, 2000], trigger: 'Prompts with fewer than 10 words' },\n { id: 'question_master', name: 'Question Master', icon: '\\u{2753}', description: 'So many questions, so little time.', category: 'prompt_patterns', stat: 'questionPromptCount', tiers: [10, 50, 200, 1000, 5000], trigger: 'Prompts ending with a question mark' },\n { id: 'the_apologizer', name: 'The Apologizer', icon: '\\u{1F625}', description: 'Sorry for asking, but...', category: 'prompt_patterns', stat: 'sorryPromptCount', tiers: [1, 10, 50, 200, 1000], trigger: \"Prompts containing the word 'sorry'\" },\n { id: 'caps_lock_energy', name: 'CAPS LOCK ENERGY', icon: '\\u{1F4E2}', description: 'WHY ARE WE YELLING?', category: 'prompt_patterns', stat: 'capsLockPromptCount', tiers: [1, 5, 25, 100, 500], trigger: 'Prompts that are fully uppercase (10+ characters)' },\n { id: 'emoji_whisperer', name: 'Emoji Whisperer', icon: '\\u{1F680}', description: 'Deploying vibes', category: 'prompt_patterns', stat: 'emojiPromptCount', tiers: [5, 25, 100, 500, 2000], trigger: 'Prompts containing emoji characters' },\n { id: 'code_dump', name: 'Code Dump', icon: '\\u{1F4E6}', description: \"Here's 500 lines, figure it out.\", category: 'prompt_patterns', stat: 'codeDumpPromptCount', tiers: [1, 10, 50, 200, 1000], trigger: 'Prompts with 50+ lines of text' },\n\n // ===================================================================\n // RESILIENCE (3 existing)\n // ===================================================================\n { id: 'clean_hands', name: 'Clean Hands', icon: '\\u{2728}', description: 'Longest error-free tool streak', category: 'resilience', stat: 'longestErrorFreeStreak', tiers: [50, 200, 500, 2000, 10000], trigger: 'Consecutive successful tool calls without any error' },\n { id: 'resilient', name: 'Resilient', icon: '\\u{1F6E1}', description: 'Survive errors', category: 'resilience', stat: 'totalErrors', tiers: [10, 50, 200, 1000, 5000], trigger: 'Total errors survived across all sessions' },\n { id: 'rate_limited', name: 'Rate Limited', icon: '\\u{1F6A7}', description: 'Hit rate limits', category: 'resilience', stat: 'totalRateLimits', tiers: [3, 10, 25, 50, 100], trigger: 'Rate limit notification events received' },\n\n // ===================================================================\n // ERROR & RECOVERY (5 new)\n // ===================================================================\n { id: 'rubber_duck', name: 'Rubber Duck', icon: '\\u{1F986}', description: 'Explaining the problem IS the solution.', category: 'error_recovery', stat: 'rubberDuckCount', tiers: [1, 5, 25, 100, 500], trigger: 'Tool failure followed by same tool success without Edit in between' },\n { id: 'third_times_charm', name: \"Third Time's the Charm\", icon: '\\u{1F340}', description: 'Persistence is a virtue.', category: 'error_recovery', stat: 'thirdTimeCharmCount', tiers: [1, 5, 25, 100, 500], trigger: 'Tool success after 2+ consecutive failures of same tool' },\n { id: 'the_undoer', name: 'The Undoer', icon: '\\u{21A9}', description: 'Ctrl+Z energy.', category: 'error_recovery', stat: 'undoEditCount', tiers: [1, 10, 50, 200, 1000], trigger: 'Back-to-back Edit calls on the same file in a session' },\n { id: 'crash_test_dummy', name: 'Crash Test Dummy', icon: '\\u{1F4A5}', description: 'Testing in production, I see.', category: 'error_recovery', stat: 'crashySessions', tiers: [1, 5, 25, 100, 500], trigger: 'Sessions with 10+ errors' },\n { id: 'phoenix', name: 'Phoenix', icon: '\\u{1F985}', description: 'From the ashes of 100 errors, you rise.', category: 'error_recovery', stat: 'totalLifetimeErrors', tiers: [100, 500, 1000, 5000, 10000], trigger: 'Total lifetime errors survived across all sessions' },\n\n // ===================================================================\n // TOOL COMBOS (5 new)\n // ===================================================================\n { id: 'read_edit_run', name: 'Read-Edit-Run', icon: '\\u{1F3AF}', description: 'The holy trinity.', category: 'tool_combos', stat: 'readEditRunCount', tiers: [25, 100, 500, 2000, 10000], trigger: 'Read \\u2192 Edit \\u2192 Bash sequences detected in events' },\n { id: 'grep_ninja', name: 'Grep Ninja', icon: '\\u{1F977}', description: 'Finding needles in haystacks since day one.', category: 'tool_combos', stat: 'totalSearches', tiers: [250, 1000, 5000, 25000, 100000], trigger: 'Total Grep and Glob searches performed' },\n { id: 'file_factory', name: 'File Factory', icon: '\\u{1F3ED}', description: \"You're not creating files, you're creating art.\", category: 'tool_combos', stat: 'maxFilesCreatedInSession', tiers: [10, 20, 50, 100, 200], trigger: 'Max Write tool calls in a single session' },\n { id: 'the_refactorer', name: 'The Refactorer', icon: '\\u{267B}', description: 'Same file, different day.', category: 'tool_combos', stat: 'maxSameFileEditsLifetime', tiers: [50, 100, 250, 500, 2000], trigger: 'Max Edit calls to any single file path across all sessions' },\n { id: 'search_and_destroy', name: 'Search and Destroy', icon: '\\u{1F4A2}', description: 'Grep it, then wreck it.', category: 'tool_combos', stat: 'searchThenEditCount', tiers: [25, 100, 500, 2500, 10000], trigger: 'Grep/Glob followed by Edit within same session' },\n\n // ===================================================================\n // SHIPPING & PROJECTS (4 existing)\n // ===================================================================\n { id: 'shipper', name: 'Shipper', icon: '\\u{1F4E6}', description: 'Make commits via Claude', category: 'shipping', stat: 'totalCommits', tiers: [5, 50, 200, 1000, 5000], trigger: \"Bash tool calls containing 'git commit'\" },\n { id: 'pr_machine', name: 'PR Machine', icon: '\\u{1F500}', description: 'Create pull requests', category: 'shipping', stat: 'totalPRs', tiers: [3, 25, 100, 500, 2000], trigger: \"Bash tool calls containing 'gh pr create'\" },\n { id: 'empire', name: 'Empire', icon: '\\u{1F3F0}', description: 'Work on unique projects', category: 'shipping', stat: 'uniqueProjects', tiers: [2, 5, 10, 25, 50], trigger: 'Unique project directories worked in' },\n { id: 'polyglot', name: 'Polyglot', icon: '\\u{1F30D}', description: 'Use different programming languages', category: 'shipping', stat: 'uniqueLanguages', tiers: [3, 5, 8, 15, 25], trigger: 'Distinct file extensions in Edit/Write/Read events' },\n\n // ===================================================================\n // PROJECT DEDICATION (5 new)\n // ===================================================================\n { id: 'monogamous', name: 'Monogamous', icon: '\\u{1F48D}', description: 'One project. True love.', category: 'project_dedication', stat: 'maxProjectSessions', tiers: [50, 100, 250, 500, 1000], trigger: 'Max sessions on any single project' },\n { id: 'project_hopper', name: 'Project Hopper', icon: '\\u{1F407}', description: \"Commitment issues? Never heard of her.\", category: 'project_dedication', stat: 'maxProjectsInDay', tiers: [3, 5, 8, 10, 15], trigger: 'Max unique projects worked on in a single day' },\n { id: 'the_finisher', name: 'The Finisher', icon: '\\u{1F3C1}', description: 'You actually completed something.', category: 'project_dedication', stat: 'finishedProjects', tiers: [1, 3, 5, 10, 25], trigger: 'Projects with git commits followed by 7+ days of inactivity' },\n { id: 'legacy_code', name: 'Legacy Code', icon: '\\u{1F9D3}', description: 'Revisiting your past mistakes.', category: 'project_dedication', stat: 'legacyReturns', tiers: [1, 3, 5, 10, 25], trigger: 'Returns to a project after 30+ days of inactivity' },\n { id: 'greenfield', name: 'Greenfield', icon: '\\u{1F331}', description: 'That new project smell.', category: 'project_dedication', stat: 'totalUniqueProjects', tiers: [10, 25, 50, 100, 200], trigger: 'Total unique projects initialized' },\n\n // ===================================================================\n // MULTI-AGENT (2 existing + 4 new = 6)\n // ===================================================================\n { id: 'buddy_system', name: 'Buddy System', icon: '\\u{1F91D}', description: 'Use concurrent agents', category: 'multi_agent', stat: 'concurrentAgentUses', tiers: [1, 5, 25, 100, 500], trigger: 'Sessions with SubagentStart events' },\n { id: 'hive_mind', name: 'Hive Mind', icon: '\\u{1F41D}', description: 'Spawn subagents total', category: 'multi_agent', stat: 'totalSubagents', tiers: [25, 250, 1000, 5000, 25000], trigger: 'Total SubagentStart events across all sessions' },\n // New Multi-Agent\n { id: 'swarm_intelligence', name: 'Swarm Intelligence', icon: '\\u{1F41C}', description: \"You've built an army.\", category: 'multi_agent', stat: 'maxConcurrentSubagents', tiers: [5, 8, 10, 15, 20], trigger: 'Max concurrent subagents active at any point' },\n { id: 'micromanager', name: 'Micromanager', icon: '\\u{1F440}', description: 'Let them cook? Never heard of it.', category: 'multi_agent', stat: 'quickSubagentStops', tiers: [1, 5, 25, 100, 500], trigger: 'Subagents stopped within 30 seconds of starting' },\n { id: 'the_orchestrator', name: 'The Orchestrator', icon: '\\u{1F3BC}', description: \"You don't code. You conduct.\", category: 'multi_agent', stat: 'totalSubagentSpawns', tiers: [100, 500, 2500, 10000, 50000], trigger: 'Total subagent spawns across all sessions' },\n { id: 'agent_smith', name: 'Agent Smith', icon: '\\u{1F576}', description: \"They're multiplying.\", category: 'multi_agent', stat: 'maxSubagentsInSession', tiers: [25, 50, 100, 250, 500], trigger: 'Max SubagentStart events in a single session' },\n\n // ===================================================================\n // HUMOR & META (7 existing + 8 new = 15)\n // ===================================================================\n { id: 'please_thank_you', name: 'Please and Thank You', icon: '\\u{1F64F}', description: \"You're polite to the AI. When they take over, you'll be spared.\", category: 'humor', stat: 'politePromptCount', tiers: [10, 50, 200, 1000, 5000], humor: true, trigger: \"Prompts containing 'please' or 'thank'\" },\n { id: 'wall_of_text', name: 'Wall of Text', icon: '\\u{1F4DC}', description: \"Claude read your entire novel and didn't even complain.\", category: 'humor', stat: 'hugePromptCount', tiers: [1, 10, 50, 200, 1000], humor: true, trigger: 'Prompts over 5,000 characters' },\n { id: 'the_fixer', name: 'The Fixer', icon: '\\u{1F6E0}', description: 'At this point just rewrite the whole thing.', category: 'humor', stat: 'maxSameFileEdits', tiers: [10, 20, 50, 100, 200], humor: true, trigger: 'Max Edit calls to a single file' },\n { id: 'what_day_is_it', name: 'What Day Is It?', icon: '\\u{1F62B}', description: 'Your chair is now a part of you.', category: 'humor', stat: 'longSessionCount', tiers: [1, 5, 25, 100, 500], humor: true, trigger: 'Sessions exceeding 8 hours' },\n { id: 'copy_pasta', name: 'Copy Pasta', icon: '\\u{1F35D}', description: \"Maybe if I ask again it'll work differently.\", category: 'humor', stat: 'repeatedPromptCount', tiers: [3, 10, 50, 200, 1000], humor: true, trigger: 'Total duplicate prompts submitted' },\n { id: 'error_magnet', name: 'Error Magnet', icon: '\\u{1F9F2}', description: 'At this point, the errors are a feature.', category: 'humor', stat: 'maxErrorsInSession', tiers: [10, 25, 50, 100, 200], humor: true, trigger: 'Max errors in a single session' },\n { id: 'creature_humor', name: 'Creature of Habit', icon: '\\u{1F503}', description: \"You have a type. And it's the same prompt.\", category: 'humor', stat: 'mostRepeatedPromptCount', tiers: [25, 100, 500, 2000, 10000], humor: true, trigger: 'Count of your single most repeated prompt' },\n // New Humor & Meta\n { id: 'deja_vu', name: 'D\\u00E9j\\u00E0 Vu', icon: '\\u{1F408}', description: \"Didn't we just do this?\", category: 'humor', stat: 'dejaVuCount', tiers: [1, 5, 25, 100, 500], humor: true, trigger: 'Same prompt submitted twice within 5 minutes' },\n { id: 'trust_issues', name: 'Trust Issues', icon: '\\u{1F50E}', description: 'You read the file Claude just wrote.', category: 'humor', stat: 'trustIssueCount', tiers: [1, 10, 50, 200, 1000], humor: true, trigger: 'Read immediately after Write on the same file' },\n { id: 'backseat_driver', name: 'Backseat Driver', icon: '\\u{1F697}', description: 'Let me tell you exactly how to do your job.', category: 'humor', stat: 'backseatDriverCount', tiers: [1, 10, 50, 200, 1000], humor: true, trigger: \"Prompts with numbered step-by-step instructions (e.g. '1.' '2.')\" },\n { id: 'the_negotiator', name: 'The Negotiator', icon: '\\u{1F91C}', description: 'Can you try again but better?', category: 'humor', stat: 'negotiatorCount', tiers: [1, 10, 50, 200, 1000], humor: true, trigger: \"Prompts containing 'try again' or 'one more time'\" },\n { id: 'rubber_stamp', name: 'Rubber Stamp', icon: '\\u{2705}', description: 'Yes. Yes. Yes. Approved.', category: 'humor', stat: 'maxConsecutivePermissions', tiers: [25, 50, 100, 250, 500], humor: true, trigger: 'Max consecutive PermissionRequest events' },\n { id: 'touch_grass_humor', name: 'Touch Grass', icon: '\\u{1F3DE}', description: \"You've been here 8 hours. Go outside.\", category: 'humor', stat: 'longSessionCount', tiers: [1, 5, 25, 100, 500], humor: true, trigger: 'Sessions exceeding 8 hours (28,800 seconds)' },\n { id: 'inbox_zero', name: 'Inbox Zero', icon: '\\u{2728}', description: 'No errors. No warnings. Just vibes.', category: 'humor', stat: 'longestErrorFreeStreak', tiers: [50, 100, 200, 500, 1000], humor: true, trigger: 'Longest streak of consecutive successful tool calls' },\n { id: 'it_works_on_my_machine', name: 'It Works On My Machine', icon: '\\u{1F937}', description: 'The classic excuse.', category: 'humor', stat: 'bashRetrySuccessCount', tiers: [1, 10, 50, 200, 1000], humor: true, trigger: 'Bash success (exit code 0) after a previous Bash failure' },\n\n // ===================================================================\n // TOKEN USAGE (10 new)\n // ===================================================================\n { id: 'token_burner', name: 'Token Burner', icon: '\\u{1F525}', description: 'Consume tokens across all sessions', category: 'token_usage', stat: 'totalTokens', tiers: [100000000, 1000000000, 5000000000, 20000000000, 100000000000], trigger: 'Total tokens consumed (input + output + cache read + cache creation)' },\n { id: 'output_machine', name: 'Output Machine', icon: '\\u{1F5A8}', description: 'Generate output tokens from Claude', category: 'token_usage', stat: 'totalOutputTokens', tiers: [500000, 5000000, 25000000, 100000000, 500000000], trigger: 'Total output tokens generated by Claude across all sessions' },\n { id: 'cache_royalty', name: 'Cache Royalty', icon: '\\u{1F451}', description: 'Read tokens from prompt cache', category: 'token_usage', stat: 'totalCacheReadTokens', tiers: [100000000, 1000000000, 5000000000, 20000000000, 100000000000], trigger: 'Total cache read tokens (cached context reused across turns)' },\n { id: 'context_crafter', name: 'Context Crafter', icon: '\\u{1F9F1}', description: 'Create new cache entries', category: 'token_usage', stat: 'totalCacheCreationTokens', tiers: [10000000, 100000000, 1000000000, 5000000000, 25000000000], trigger: 'Total cache creation tokens (new context written to cache)' },\n { id: 'token_whale', name: 'Token Whale', icon: '\\u{1F40B}', description: 'Massive token consumption in a single session', category: 'token_usage', stat: 'mostTokensInSession', tiers: [5000000, 25000000, 100000000, 500000000, 2000000000], trigger: 'Most total tokens consumed in any single session' },\n { id: 'heavy_hitter', name: 'Heavy Hitter', icon: '\\u{1F4AA}', description: 'Sessions exceeding 1M total tokens', category: 'token_usage', stat: 'heavyTokenSessions', tiers: [25, 100, 500, 2500, 10000], trigger: 'Sessions with 1,000,000+ total tokens' },\n { id: 'featherweight', name: 'Featherweight', icon: '\\u{1FAB6}', description: 'Lean sessions that still get work done', category: 'token_usage', stat: 'lightTokenSessions', tiers: [1, 10, 50, 200, 1000], trigger: 'Sessions under 50,000 total tokens with at least 1 tool call' },\n { id: 'token_velocity', name: 'Token Velocity', icon: '\\u{26A1}', description: 'High average tokens per session', category: 'token_usage', stat: 'avgTokensPerSession', tiers: [2500000, 5000000, 10000000, 30000000, 100000000], trigger: 'Average total tokens per session across all sessions' },\n { id: 'prolific_session', name: 'Prolific', icon: '\\u{270D}', description: 'Most output generated in one session', category: 'token_usage', stat: 'maxOutputInSession', tiers: [50000, 250000, 1000000, 5000000, 20000000], trigger: 'Most output tokens generated by Claude in a single session' },\n { id: 'input_flood', name: 'Input Flood', icon: '\\u{1F30A}', description: 'Total raw input tokens sent to the API', category: 'token_usage', stat: 'totalInputTokens', tiers: [500000, 2500000, 10000000, 50000000, 250000000], trigger: 'Total non-cached input tokens (the small uncached portion of each request)' },\n\n // ===================================================================\n // ASPIRATIONAL (6 existing + 5 new + 1 token = 12) - Obsidian-only\n // ===================================================================\n { id: 'the_machine', name: 'The Machine', icon: '\\u{2699}', description: 'You are no longer using the tool. You are the tool.', category: 'aspirational', stat: 'totalToolCalls', tiers: [100000, 100000, 100000, 100000, 100000], aspirational: true, trigger: 'Reach 100,000 total tool calls' },\n { id: 'year_of_code', name: 'Year of Code', icon: '\\u{1F4C5}', description: '365 days. No breaks. Absolute unit.', category: 'aspirational', stat: 'longestStreak', tiers: [365, 365, 365, 365, 365], aspirational: true, trigger: 'Achieve a 365-day consecutive streak' },\n { id: 'million_words', name: 'Million Words', icon: '\\u{1F4DA}', description: \"You've written more to Claude than most people write in a lifetime.\", category: 'aspirational', stat: 'totalCharsTyped', tiers: [10000000, 10000000, 10000000, 10000000, 10000000], aspirational: true, trigger: 'Type 10 million characters in prompts' },\n { id: 'lifer', name: 'Lifer', icon: '\\u{1F451}', description: 'At this point, Claude is your cofounder.', category: 'aspirational', stat: 'totalSessions', tiers: [10000, 10000, 10000, 10000, 10000], aspirational: true, trigger: 'Complete 10,000 sessions' },\n { id: 'transcendent', name: 'Transcendent', icon: '\\u{2B50}', description: \"You've reached the peak. The view is nice up here.\", category: 'aspirational', stat: 'totalXP', tiers: [100000, 100000, 100000, 100000, 100000], aspirational: true, trigger: 'Earn 100,000 total XP' },\n { id: 'omniscient', name: 'Omniscient', icon: '\\u{1F441}', description: \"You've mastered every tool. There is nothing left to teach you.\", category: 'aspirational', stat: 'allToolsObsidian', tiers: [1, 1, 1, 1, 1], aspirational: true, trigger: 'All tool mastery badges at Obsidian tier' },\n // New Aspirational\n { id: 'ten_thousand_hours', name: '10,000 Hours', icon: '\\u{23F0}', description: 'Malcolm Gladwell would be proud.', category: 'aspirational', stat: 'totalSessionHours', tiers: [10000, 10000, 10000, 10000, 10000], aspirational: true, trigger: 'Spend 10,000 hours in sessions' },\n { id: 'master_architect', name: 'The Architect', icon: '\\u{1F3DB}', description: \"You've built more than most companies ship.\", category: 'aspirational', stat: 'totalFilesCreated', tiers: [1000, 1000, 1000, 1000, 1000], aspirational: true, trigger: 'Create 1,000+ files with the Write tool' },\n { id: 'eternal_flame', name: 'Eternal Flame', icon: '\\u{1F56F}', description: 'Your streak outlasted relationships.', category: 'aspirational', stat: 'longestStreak', tiers: [180, 180, 180, 180, 180], aspirational: true, trigger: 'Maintain a 180-day consecutive streak' },\n { id: 'the_collector', name: 'The Collector', icon: '\\u{1F4BF}', description: \"Gotta catch 'em all.\", category: 'aspirational', stat: 'allNonSecretBadgesUnlocked', tiers: [1, 1, 1, 1, 1], aspirational: true, trigger: 'Unlock every non-secret, non-aspirational badge' },\n { id: 'centimillionaire', name: 'Centimillionaire', icon: '\\u{2328}', description: '100 million characters. Your keyboard weeps.', category: 'aspirational', stat: 'totalCharsTyped', tiers: [100000000, 100000000, 100000000, 100000000, 100000000], aspirational: true, trigger: 'Type 100 million characters in prompts' },\n { id: 'token_billionaire', name: 'Token Billionaire', icon: '\\u{1F4B0}', description: 'A billion tokens. You single-handedly funded a GPU cluster.', category: 'aspirational', stat: 'totalTokens', tiers: [1000000000, 1000000000, 1000000000, 1000000000, 1000000000], aspirational: true, trigger: 'Consume 1 billion total tokens' },\n\n // ===================================================================\n // SECRET (10 existing + 6 new + 1 token = 17)\n // ===================================================================\n { id: 'rm_rf_survivor', name: 'rm -rf Survivor', icon: '\\u{1F4A3}', description: \"You almost mass deleted that folder. But you didn't. And honestly, we're all better for it.\", category: 'secret', stat: 'dangerousCommandBlocked', tiers: [1, 1, 1, 1, 1], secret: true, trigger: \"rm -rf or rm -r / detected in PreToolUse event\" },\n { id: 'touch_grass', name: 'Touch Grass', icon: '\\u{1F33F}', description: \"Welcome back. The codebase missed you. (It didn't change, but still.)\", category: 'secret', stat: 'returnAfterBreak', tiers: [1, 1, 1, 1, 1], secret: true, trigger: 'Return after 7+ day gap between sessions' },\n { id: 'three_am_coder', name: '3am Coder', icon: '\\u{1F319}', description: 'Nothing good happens at 3am. Except shipping code, apparently.', category: 'secret', stat: 'threeAmPrompt', tiers: [1, 1, 1, 1, 1], secret: true, trigger: 'Submit a prompt at exactly 3 AM' },\n { id: 'night_shift', name: 'Night Shift', icon: '\\u{1F303}', description: 'Started yesterday, finishing today. Time is a construct.', category: 'secret', stat: 'midnightSpanSession', tiers: [1, 1, 1, 1, 1], secret: true, trigger: 'Session that started before midnight and ended after' },\n { id: 'inception', name: 'Inception', icon: '\\u{1F300}', description: 'We need to go deeper.', category: 'secret', stat: 'nestedSubagent', tiers: [1, 1, 1, 1, 1], secret: true, trigger: 'Any SubagentStart event detected' },\n { id: 'holiday_hacker', name: 'Holiday Hacker', icon: '\\u{1F384}', description: \"Your family is wondering where you are. You're deploying.\", category: 'secret', stat: 'holidayActivity', tiers: [1, 1, 1, 1, 1], secret: true, trigger: 'Session on Dec 25, Jan 1, or Jul 4' },\n { id: 'speed_run', name: 'Speed Run Any%', icon: '\\u{23F1}', description: 'In and out. Twenty-second adventure.', category: 'secret', stat: 'speedRunSession', tiers: [1, 1, 1, 1, 1], secret: true, trigger: 'Session under 20 seconds with tool use' },\n { id: 'full_send', name: 'Full Send', icon: '\\u{1F680}', description: 'Bash, Read, Write, Edit, Grep, Glob, WebFetch -- the whole buffet.', category: 'secret', stat: 'allToolsInSession', tiers: [1, 1, 1, 1, 1], secret: true, trigger: 'Use all 7 core tools in one session' },\n { id: 'launch_day', name: 'Launch Day', icon: '\\u{1F389}', description: 'Welcome to bashstats. Your stats are now being watched. Forever.', category: 'secret', stat: 'firstEverSession', tiers: [1, 1, 1, 1, 1], secret: true, trigger: 'Complete your first ever session' },\n { id: 'the_completionist', name: 'The Completionist', icon: '\\u{1F3C6}', description: 'You absolute legend.', category: 'secret', stat: 'allBadgesGold', tiers: [1, 1, 1, 1, 1], secret: true, trigger: 'All non-secret, non-aspirational badges at Gold+ tier' },\n // New Secret\n { id: 'easter_egg_hunter', name: 'Easter Egg Hunter', icon: '\\u{1F95A}', description: 'You found me!', category: 'secret', stat: 'easterEggActivity', tiers: [1, 1, 1, 1, 1], secret: true, trigger: \"Session on Easter, Valentine's Day, or Thanksgiving\" },\n { id: 'full_moon_coder', name: 'Full Moon Coder', icon: '\\u{1F315}', description: 'Lycanthropic debugging.', category: 'secret', stat: 'fullMoonSession', tiers: [1, 1, 1, 1, 1], secret: true, trigger: 'Session during a full moon (calculated from lunar cycle)' },\n { id: 'birthday_bash', name: 'Birthday Bash', icon: '\\u{1F382}', description: 'Celebrating with Claude.', category: 'secret', stat: 'birthdaySession', tiers: [1, 1, 1, 1, 1], secret: true, trigger: 'Session on your bashstats install anniversary' },\n { id: 'lucky_number', name: 'Lucky Number', icon: '\\u{1F340}', description: '7-7-7', category: 'secret', stat: 'luckyNumber', tiers: [1, 1, 1, 1, 1], secret: true, trigger: 'Reach 777 prompts or 7,777 tool calls' },\n { id: 'ghost_session', name: 'Ghost Session', icon: '\\u{1F47B}', description: 'Boo!', category: 'secret', stat: 'ghostSessions', tiers: [1, 1, 1, 1, 1], secret: true, trigger: 'Complete a session with 0 tool calls' },\n { id: 'bullseye', name: 'Bullseye', icon: '\\u{1F3AF}', description: 'First try, no errors.', category: 'secret', stat: 'bullseyeSessions', tiers: [1, 1, 1, 1, 1], secret: true, trigger: 'Session with 1 prompt, 0 errors, and 1+ tool calls' },\n { id: 'token_singularity', name: 'Token Singularity', icon: '\\u{1F573}', description: 'The context window stared into the abyss, and the abyss stared back.', category: 'secret', stat: 'hasTenMillionSession', tiers: [1, 1, 1, 1, 1], secret: true, trigger: 'Complete a session exceeding 10 million total tokens' },\n]\n\nexport const RANK_THRESHOLDS = [\n { rank: 'Obsidian', xp: 100000 },\n { rank: 'Diamond', xp: 25000 },\n { rank: 'Gold', xp: 5000 },\n { rank: 'Silver', xp: 1000 },\n { rank: 'Bronze', xp: 0 },\n]\n\nexport const TIER_XP = [0, 50, 100, 200, 500, 1000]\n\nexport const DATA_DIR = '.bashstats'\nexport const DB_FILENAME = 'bashstats.db'\nexport const DEFAULT_PORT = 17900\n","import Database from 'better-sqlite3'\nimport type { EventRow, SessionRow, PromptRow, DailyActivityRow, AchievementUnlockRow, TokenUsage } from '../types.js'\n\nfunction localNow(): string {\n const d = new Date()\n const pad = (n: number) => String(n).padStart(2, '0')\n const ms = String(d.getMilliseconds()).padStart(3, '0')\n return `${d.getFullYear()}-${pad(d.getMonth() + 1)}-${pad(d.getDate())}T${pad(d.getHours())}:${pad(d.getMinutes())}:${pad(d.getSeconds())}.${ms}`\n}\n\nconst SCHEMA = `\nCREATE TABLE IF NOT EXISTS events (\n id INTEGER PRIMARY KEY AUTOINCREMENT,\n session_id TEXT NOT NULL,\n hook_type TEXT NOT NULL,\n tool_name TEXT,\n tool_input TEXT,\n tool_output TEXT,\n exit_code INTEGER,\n success INTEGER,\n cwd TEXT,\n project TEXT,\n timestamp TEXT NOT NULL\n);\n\nCREATE TABLE IF NOT EXISTS sessions (\n id TEXT PRIMARY KEY,\n agent TEXT NOT NULL DEFAULT 'claude-code',\n started_at TEXT NOT NULL,\n ended_at TEXT,\n stop_reason TEXT,\n prompt_count INTEGER DEFAULT 0,\n tool_count INTEGER DEFAULT 0,\n error_count INTEGER DEFAULT 0,\n project TEXT,\n duration_seconds INTEGER,\n input_tokens INTEGER DEFAULT 0,\n output_tokens INTEGER DEFAULT 0,\n cache_creation_input_tokens INTEGER DEFAULT 0,\n cache_read_input_tokens INTEGER DEFAULT 0\n);\n\nCREATE TABLE IF NOT EXISTS prompts (\n id INTEGER PRIMARY KEY AUTOINCREMENT,\n session_id TEXT NOT NULL,\n content TEXT NOT NULL,\n char_count INTEGER NOT NULL,\n word_count INTEGER NOT NULL,\n timestamp TEXT NOT NULL,\n FOREIGN KEY (session_id) REFERENCES sessions(id)\n);\n\nCREATE TABLE IF NOT EXISTS daily_activity (\n date TEXT PRIMARY KEY,\n sessions INTEGER DEFAULT 0,\n prompts INTEGER DEFAULT 0,\n tool_calls INTEGER DEFAULT 0,\n errors INTEGER DEFAULT 0,\n duration_seconds INTEGER DEFAULT 0,\n input_tokens INTEGER DEFAULT 0,\n output_tokens INTEGER DEFAULT 0,\n cache_creation_input_tokens INTEGER DEFAULT 0,\n cache_read_input_tokens INTEGER DEFAULT 0\n);\n\nCREATE TABLE IF NOT EXISTS achievement_unlocks (\n badge_id TEXT NOT NULL,\n tier INTEGER NOT NULL,\n unlocked_at TEXT NOT NULL,\n notified INTEGER DEFAULT 0,\n PRIMARY KEY (badge_id, tier)\n);\n\nCREATE TABLE IF NOT EXISTS metadata (\n key TEXT PRIMARY KEY,\n value TEXT\n);\n\nCREATE INDEX IF NOT EXISTS idx_events_session ON events(session_id);\nCREATE INDEX IF NOT EXISTS idx_events_hook_type ON events(hook_type);\nCREATE INDEX IF NOT EXISTS idx_events_tool_name ON events(tool_name);\nCREATE INDEX IF NOT EXISTS idx_events_timestamp ON events(timestamp);\nCREATE INDEX IF NOT EXISTS idx_events_project ON events(project);\nCREATE INDEX IF NOT EXISTS idx_prompts_session ON prompts(session_id);\nCREATE INDEX IF NOT EXISTS idx_prompts_timestamp ON prompts(timestamp);\n`\n\nexport class BashStatsDB {\n private db: Database.Database\n\n constructor(dbPath: string) {\n this.db = new Database(dbPath)\n this.db.pragma('journal_mode = WAL')\n this.db.pragma('busy_timeout = 5000')\n this.db.pragma('foreign_keys = ON')\n this.db.exec(SCHEMA)\n this.migrate()\n }\n\n private migrate(): void {\n const sessionCols = this.db.pragma('table_info(sessions)') as { name: string }[]\n const sessionColNames = new Set(sessionCols.map(c => c.name))\n\n if (!sessionColNames.has('agent')) {\n this.db.exec(\"ALTER TABLE sessions ADD COLUMN agent TEXT NOT NULL DEFAULT 'claude-code'\")\n }\n\n const tokenCols = ['input_tokens', 'output_tokens', 'cache_creation_input_tokens', 'cache_read_input_tokens']\n for (const col of tokenCols) {\n if (!sessionColNames.has(col)) {\n this.db.exec(`ALTER TABLE sessions ADD COLUMN ${col} INTEGER DEFAULT 0`)\n }\n }\n\n const dailyCols = this.db.pragma('table_info(daily_activity)') as { name: string }[]\n const dailyColNames = new Set(dailyCols.map(c => c.name))\n for (const col of tokenCols) {\n if (!dailyColNames.has(col)) {\n this.db.exec(`ALTER TABLE daily_activity ADD COLUMN ${col} INTEGER DEFAULT 0`)\n }\n }\n }\n\n close(): void {\n this.db.close()\n }\n\n getTableNames(): string[] {\n const rows = this.db.prepare(\"SELECT name FROM sqlite_master WHERE type='table'\").all() as { name: string }[]\n return rows.map(r => r.name)\n }\n\n // === Events ===\n\n insertEvent(event: Omit<EventRow, 'id'>): number {\n const stmt = this.db.prepare(`\n INSERT INTO events (session_id, hook_type, tool_name, tool_input, tool_output, exit_code, success, cwd, project, timestamp)\n VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?)\n `)\n const result = stmt.run(\n event.session_id, event.hook_type, event.tool_name, event.tool_input,\n event.tool_output, event.exit_code, event.success, event.cwd, event.project, event.timestamp\n )\n return result.lastInsertRowid as number\n }\n\n getEvents(filter: { session_id?: string; hook_type?: string; tool_name?: string }): EventRow[] {\n let sql = 'SELECT * FROM events WHERE 1=1'\n const params: unknown[] = []\n if (filter.session_id) { sql += ' AND session_id = ?'; params.push(filter.session_id) }\n if (filter.hook_type) { sql += ' AND hook_type = ?'; params.push(filter.hook_type) }\n if (filter.tool_name) { sql += ' AND tool_name = ?'; params.push(filter.tool_name) }\n sql += ' ORDER BY timestamp ASC'\n return this.db.prepare(sql).all(...params) as EventRow[]\n }\n\n // === Sessions ===\n\n insertSession(session: { id: string; agent?: string; started_at: string; project?: string | null }): void {\n this.db.prepare(`\n INSERT OR IGNORE INTO sessions (id, agent, started_at, project) VALUES (?, ?, ?, ?)\n `).run(session.id, session.agent ?? 'claude-code', session.started_at, session.project ?? null)\n }\n\n getSession(id: string): SessionRow | null {\n return this.db.prepare('SELECT * FROM sessions WHERE id = ?').get(id) as SessionRow | null\n }\n\n updateSession(id: string, updates: Partial<Pick<SessionRow, 'ended_at' | 'stop_reason' | 'duration_seconds'>>): void {\n const sets: string[] = []\n const params: unknown[] = []\n if (updates.ended_at !== undefined) { sets.push('ended_at = ?'); params.push(updates.ended_at) }\n if (updates.stop_reason !== undefined) { sets.push('stop_reason = ?'); params.push(updates.stop_reason) }\n if (updates.duration_seconds !== undefined) { sets.push('duration_seconds = ?'); params.push(updates.duration_seconds) }\n if (sets.length === 0) return\n params.push(id)\n this.db.prepare(`UPDATE sessions SET ${sets.join(', ')} WHERE id = ?`).run(...params)\n }\n\n updateSessionTokens(id: string, tokens: TokenUsage): void {\n this.db.prepare(`\n UPDATE sessions SET input_tokens = ?, output_tokens = ?, cache_creation_input_tokens = ?, cache_read_input_tokens = ? WHERE id = ?\n `).run(tokens.input_tokens, tokens.output_tokens, tokens.cache_creation_input_tokens, tokens.cache_read_input_tokens, id)\n }\n\n incrementSessionCounters(id: string, counters: { prompts?: number; tools?: number; errors?: number }): void {\n const sets: string[] = []\n const params: unknown[] = []\n if (counters.prompts) { sets.push('prompt_count = prompt_count + ?'); params.push(counters.prompts) }\n if (counters.tools) { sets.push('tool_count = tool_count + ?'); params.push(counters.tools) }\n if (counters.errors) { sets.push('error_count = error_count + ?'); params.push(counters.errors) }\n if (sets.length === 0) return\n params.push(id)\n this.db.prepare(`UPDATE sessions SET ${sets.join(', ')} WHERE id = ?`).run(...params)\n }\n\n // === Prompts ===\n\n insertPrompt(prompt: Omit<PromptRow, 'id'>): number {\n const result = this.db.prepare(`\n INSERT INTO prompts (session_id, content, char_count, word_count, timestamp) VALUES (?, ?, ?, ?, ?)\n `).run(prompt.session_id, prompt.content, prompt.char_count, prompt.word_count, prompt.timestamp)\n return result.lastInsertRowid as number\n }\n\n getPrompts(sessionId: string): PromptRow[] {\n return this.db.prepare('SELECT * FROM prompts WHERE session_id = ? ORDER BY timestamp ASC').all(sessionId) as PromptRow[]\n }\n\n // === Daily Activity ===\n\n incrementDailyActivity(date: string, increments: { sessions?: number; prompts?: number; tool_calls?: number; errors?: number; duration_seconds?: number; input_tokens?: number; output_tokens?: number; cache_creation_input_tokens?: number; cache_read_input_tokens?: number }): void {\n this.db.prepare(`\n INSERT INTO daily_activity (date, sessions, prompts, tool_calls, errors, duration_seconds, input_tokens, output_tokens, cache_creation_input_tokens, cache_read_input_tokens)\n VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?)\n ON CONFLICT(date) DO UPDATE SET\n sessions = sessions + excluded.sessions,\n prompts = prompts + excluded.prompts,\n tool_calls = tool_calls + excluded.tool_calls,\n errors = errors + excluded.errors,\n duration_seconds = duration_seconds + excluded.duration_seconds,\n input_tokens = input_tokens + excluded.input_tokens,\n output_tokens = output_tokens + excluded.output_tokens,\n cache_creation_input_tokens = cache_creation_input_tokens + excluded.cache_creation_input_tokens,\n cache_read_input_tokens = cache_read_input_tokens + excluded.cache_read_input_tokens\n `).run(\n date,\n increments.sessions ?? 0,\n increments.prompts ?? 0,\n increments.tool_calls ?? 0,\n increments.errors ?? 0,\n increments.duration_seconds ?? 0,\n increments.input_tokens ?? 0,\n increments.output_tokens ?? 0,\n increments.cache_creation_input_tokens ?? 0,\n increments.cache_read_input_tokens ?? 0,\n )\n }\n\n getDailyActivity(date: string): DailyActivityRow | null {\n return this.db.prepare('SELECT * FROM daily_activity WHERE date = ?').get(date) as DailyActivityRow | null\n }\n\n getAllDailyActivity(days?: number): DailyActivityRow[] {\n if (days) {\n return this.db.prepare('SELECT * FROM daily_activity ORDER BY date DESC LIMIT ?').all(days) as DailyActivityRow[]\n }\n return this.db.prepare('SELECT * FROM daily_activity ORDER BY date DESC').all() as DailyActivityRow[]\n }\n\n // === Achievement Unlocks ===\n\n insertUnlock(badgeId: string, tier: number): void {\n this.db.prepare(`\n INSERT OR IGNORE INTO achievement_unlocks (badge_id, tier, unlocked_at) VALUES (?, ?, ?)\n `).run(badgeId, tier, localNow())\n }\n\n getUnlocks(): AchievementUnlockRow[] {\n return this.db.prepare('SELECT * FROM achievement_unlocks ORDER BY unlocked_at DESC').all() as AchievementUnlockRow[]\n }\n\n getUnnotifiedUnlocks(): AchievementUnlockRow[] {\n return this.db.prepare('SELECT * FROM achievement_unlocks WHERE notified = 0').all() as AchievementUnlockRow[]\n }\n\n markNotified(badgeId: string, tier: number): void {\n this.db.prepare('UPDATE achievement_unlocks SET notified = 1 WHERE badge_id = ? AND tier = ?').run(badgeId, tier)\n }\n\n // === Metadata ===\n\n setMetadata(key: string, value: string): void {\n this.db.prepare('INSERT OR REPLACE INTO metadata (key, value) VALUES (?, ?)').run(key, value)\n }\n\n getMetadata(key: string): string | null {\n const row = this.db.prepare('SELECT value FROM metadata WHERE key = ?').get(key) as { value: string } | undefined\n return row?.value ?? null\n }\n\n // === Raw DB access for stats engine ===\n\n prepare(sql: string): Database.Statement {\n return this.db.prepare(sql)\n }\n}\n","import { BashStatsDB } from './database.js'\nimport type { TokenUsage } from '../types.js'\nimport path from 'path'\n\nexport class BashStatsWriter {\n private db: BashStatsDB\n\n constructor(db: BashStatsDB) {\n this.db = db\n }\n\n private extractProject(cwd: string): string {\n return path.basename(cwd)\n }\n\n private today(): string {\n const d = new Date()\n const pad = (n: number) => String(n).padStart(2, '0')\n return `${d.getFullYear()}-${pad(d.getMonth() + 1)}-${pad(d.getDate())}`\n }\n\n private now(): string {\n const d = new Date()\n const pad = (n: number) => String(n).padStart(2, '0')\n const ms = String(d.getMilliseconds()).padStart(3, '0')\n return `${d.getFullYear()}-${pad(d.getMonth() + 1)}-${pad(d.getDate())}T${pad(d.getHours())}:${pad(d.getMinutes())}:${pad(d.getSeconds())}.${ms}`\n }\n\n recordSessionStart(sessionId: string, cwd: string, source: string, agent?: string): void {\n const project = this.extractProject(cwd)\n const timestamp = this.now()\n\n this.db.insertSession({\n id: sessionId,\n agent,\n started_at: timestamp,\n project,\n })\n\n this.db.insertEvent({\n session_id: sessionId,\n hook_type: 'SessionStart',\n tool_name: null,\n tool_input: JSON.stringify({ source }),\n tool_output: null,\n exit_code: null,\n success: null,\n cwd,\n project,\n timestamp,\n })\n\n this.db.incrementDailyActivity(this.today(), { sessions: 1 })\n }\n\n recordPrompt(sessionId: string, content: string): void {\n const timestamp = this.now()\n const wordCount = content.split(/\\s+/).filter(w => w.length > 0).length\n const charCount = content.length\n\n this.db.insertPrompt({\n session_id: sessionId,\n content,\n char_count: charCount,\n word_count: wordCount,\n timestamp,\n })\n\n this.db.insertEvent({\n session_id: sessionId,\n hook_type: 'UserPromptSubmit',\n tool_name: null,\n tool_input: null,\n tool_output: null,\n exit_code: null,\n success: null,\n cwd: null,\n project: null,\n timestamp,\n })\n\n this.db.incrementSessionCounters(sessionId, { prompts: 1 })\n this.db.incrementDailyActivity(this.today(), { prompts: 1 })\n }\n\n recordToolUse(\n sessionId: string,\n hookType: string,\n toolName: string,\n toolInput: Record<string, unknown>,\n toolOutput: Record<string, unknown>,\n exitCode: number | null,\n cwd: string,\n ): void {\n const timestamp = this.now()\n const project = this.extractProject(cwd)\n const success = exitCode === 0 ? 1 : 0\n\n this.db.insertEvent({\n session_id: sessionId,\n hook_type: hookType,\n tool_name: toolName,\n tool_input: JSON.stringify(toolInput),\n tool_output: JSON.stringify(toolOutput),\n exit_code: exitCode,\n success,\n cwd,\n project,\n timestamp,\n })\n\n this.db.incrementSessionCounters(sessionId, {\n tools: 1,\n errors: success === 0 ? 1 : 0,\n })\n\n this.db.incrementDailyActivity(this.today(), {\n tool_calls: 1,\n errors: success === 0 ? 1 : 0,\n })\n }\n\n recordSessionEnd(sessionId: string, stopReason: string, tokens?: TokenUsage | null): void {\n const timestamp = this.now()\n const session = this.db.getSession(sessionId)\n\n let durationSeconds: number | undefined\n if (session) {\n const startTime = new Date(session.started_at).getTime()\n const endTime = new Date(timestamp).getTime()\n durationSeconds = Math.round((endTime - startTime) / 1000)\n }\n\n this.db.updateSession(sessionId, {\n ended_at: timestamp,\n stop_reason: stopReason,\n duration_seconds: durationSeconds,\n })\n\n if (tokens) {\n this.db.updateSessionTokens(sessionId, tokens)\n }\n\n this.db.insertEvent({\n session_id: sessionId,\n hook_type: 'Stop',\n tool_name: null,\n tool_input: JSON.stringify({ stop_reason: stopReason }),\n tool_output: null,\n exit_code: null,\n success: null,\n cwd: null,\n project: null,\n timestamp,\n })\n\n const dailyIncrements: Record<string, number> = {}\n if (durationSeconds !== undefined) {\n dailyIncrements.duration_seconds = durationSeconds\n }\n if (tokens) {\n dailyIncrements.input_tokens = tokens.input_tokens\n dailyIncrements.output_tokens = tokens.output_tokens\n dailyIncrements.cache_creation_input_tokens = tokens.cache_creation_input_tokens\n dailyIncrements.cache_read_input_tokens = tokens.cache_read_input_tokens\n }\n if (Object.keys(dailyIncrements).length > 0) {\n this.db.incrementDailyActivity(this.today(), dailyIncrements)\n }\n }\n\n recordNotification(sessionId: string, message: string, notificationType: string): void {\n const timestamp = this.now()\n const isError = notificationType === 'error' || notificationType === 'rate_limit'\n\n this.db.insertEvent({\n session_id: sessionId,\n hook_type: 'Notification',\n tool_name: null,\n tool_input: JSON.stringify({ message, notification_type: notificationType }),\n tool_output: null,\n exit_code: null,\n success: null,\n cwd: null,\n project: null,\n timestamp,\n })\n\n if (isError) {\n this.db.incrementSessionCounters(sessionId, { errors: 1 })\n this.db.incrementDailyActivity(this.today(), { errors: 1 })\n }\n }\n\n recordSubagent(sessionId: string, hookType: string, agentId: string, agentType?: string): void {\n const timestamp = this.now()\n\n this.db.insertEvent({\n session_id: sessionId,\n hook_type: hookType,\n tool_name: null,\n tool_input: JSON.stringify({ agent_id: agentId, agent_type: agentType ?? null }),\n tool_output: null,\n exit_code: null,\n success: null,\n cwd: null,\n project: null,\n timestamp,\n })\n }\n\n recordCompaction(sessionId: string, trigger: string): void {\n const timestamp = this.now()\n\n this.db.insertEvent({\n session_id: sessionId,\n hook_type: 'PreCompact',\n tool_name: null,\n tool_input: JSON.stringify({ trigger }),\n tool_output: null,\n exit_code: null,\n success: null,\n cwd: null,\n project: null,\n timestamp,\n })\n }\n}\n","import fs from 'fs'\nimport readline from 'readline'\nimport type { TokenUsage } from '../types.js'\n\n/**\n * Read a Claude Code transcript JSONL file and sum all token usage.\n *\n * Claude Code writes multiple transcript entries per API call (one per streaming\n * content block), each carrying the same usage object. We deduplicate by message\n * ID so each API call's usage is counted exactly once.\n *\n * Returns null on any failure (missing file, no usage data, parse errors).\n */\nexport async function extractTokenUsage(transcriptPath: string): Promise<TokenUsage | null> {\n try {\n if (!fs.existsSync(transcriptPath)) return null\n\n const stream = fs.createReadStream(transcriptPath, { encoding: 'utf-8' })\n const rl = readline.createInterface({ input: stream, crlfDelay: Infinity })\n\n // Track usage per unique message ID to avoid counting streaming duplicates\n const seenMessages = new Map<string, {\n input_tokens: number\n output_tokens: number\n cache_creation_input_tokens: number\n cache_read_input_tokens: number\n }>()\n\n for await (const line of rl) {\n if (!line.trim()) continue\n try {\n const entry = JSON.parse(line)\n const usage = entry.usage ?? entry.response?.usage ?? entry.message?.usage\n if (usage && typeof usage === 'object' && 'input_tokens' in usage) {\n // Use message ID to deduplicate; fall back to a unique key if absent\n const msgId = entry.message?.id ?? entry.id ?? `_line_${seenMessages.size}`\n // Keep the last occurrence (most complete usage data for this message)\n seenMessages.set(msgId, {\n input_tokens: usage.input_tokens ?? 0,\n output_tokens: usage.output_tokens ?? 0,\n cache_creation_input_tokens: usage.cache_creation_input_tokens ?? 0,\n cache_read_input_tokens: usage.cache_read_input_tokens ?? 0,\n })\n }\n } catch {\n // skip unparseable lines\n }\n }\n\n if (seenMessages.size === 0) return null\n\n let inputTokens = 0\n let outputTokens = 0\n let cacheCreation = 0\n let cacheRead = 0\n for (const u of seenMessages.values()) {\n inputTokens += u.input_tokens\n outputTokens += u.output_tokens\n cacheCreation += u.cache_creation_input_tokens\n cacheRead += u.cache_read_input_tokens\n }\n\n return {\n input_tokens: inputTokens,\n output_tokens: outputTokens,\n cache_creation_input_tokens: cacheCreation,\n cache_read_input_tokens: cacheRead,\n }\n } catch {\n return null\n }\n}\n"],"mappings":"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","names":["path","fs","path","fs"]}
@@ -1,7 +1,7 @@
1
1
  #!/usr/bin/env node
2
2
  import {
3
3
  handleHookEvent
4
- } from "./chunk-EFVDQUHM.js";
4
+ } from "./chunk-37VUNTM4.js";
5
5
 
6
6
  // src/hooks/scripts/notification.ts
7
7
  handleHookEvent("Notification").catch(() => process.exit(0));
@@ -1,7 +1,7 @@
1
1
  #!/usr/bin/env node
2
2
  import {
3
3
  handleHookEvent
4
- } from "./chunk-EFVDQUHM.js";
4
+ } from "./chunk-37VUNTM4.js";
5
5
 
6
6
  // src/hooks/scripts/permission-request.ts
7
7
  handleHookEvent("PermissionRequest").catch(() => process.exit(0));
@@ -1,7 +1,7 @@
1
1
  #!/usr/bin/env node
2
2
  import {
3
3
  handleHookEvent
4
- } from "./chunk-EFVDQUHM.js";
4
+ } from "./chunk-37VUNTM4.js";
5
5
 
6
6
  // src/hooks/scripts/post-tool-failure.ts
7
7
  handleHookEvent("PostToolUseFailure").catch(() => process.exit(0));
@@ -1,7 +1,7 @@
1
1
  #!/usr/bin/env node
2
2
  import {
3
3
  handleHookEvent
4
- } from "./chunk-EFVDQUHM.js";
4
+ } from "./chunk-37VUNTM4.js";
5
5
 
6
6
  // src/hooks/scripts/post-tool-use.ts
7
7
  handleHookEvent("PostToolUse").catch(() => process.exit(0));
@@ -1,7 +1,7 @@
1
1
  #!/usr/bin/env node
2
2
  import {
3
3
  handleHookEvent
4
- } from "./chunk-EFVDQUHM.js";
4
+ } from "./chunk-37VUNTM4.js";
5
5
 
6
6
  // src/hooks/scripts/pre-compact.ts
7
7
  handleHookEvent("PreCompact").catch(() => process.exit(0));
@@ -1,7 +1,7 @@
1
1
  #!/usr/bin/env node
2
2
  import {
3
3
  handleHookEvent
4
- } from "./chunk-EFVDQUHM.js";
4
+ } from "./chunk-37VUNTM4.js";
5
5
 
6
6
  // src/hooks/scripts/pre-tool-use.ts
7
7
  handleHookEvent("PreToolUse").catch(() => process.exit(0));
@@ -1,7 +1,7 @@
1
1
  #!/usr/bin/env node
2
2
  import {
3
3
  handleHookEvent
4
- } from "./chunk-EFVDQUHM.js";
4
+ } from "./chunk-37VUNTM4.js";
5
5
 
6
6
  // src/hooks/scripts/session-start.ts
7
7
  handleHookEvent("SessionStart").catch(() => process.exit(0));
@@ -1,7 +1,7 @@
1
1
  #!/usr/bin/env node
2
2
  import {
3
3
  handleHookEvent
4
- } from "./chunk-EFVDQUHM.js";
4
+ } from "./chunk-37VUNTM4.js";
5
5
 
6
6
  // src/hooks/scripts/setup.ts
7
7
  handleHookEvent("Setup").catch(() => process.exit(0));
@@ -1,7 +1,7 @@
1
1
  #!/usr/bin/env node
2
2
  import {
3
3
  handleHookEvent
4
- } from "./chunk-EFVDQUHM.js";
4
+ } from "./chunk-37VUNTM4.js";
5
5
 
6
6
  // src/hooks/scripts/stop.ts
7
7
  handleHookEvent("Stop").catch(() => process.exit(0));
@@ -1,7 +1,7 @@
1
1
  #!/usr/bin/env node
2
2
  import {
3
3
  handleHookEvent
4
- } from "./chunk-EFVDQUHM.js";
4
+ } from "./chunk-37VUNTM4.js";
5
5
 
6
6
  // src/hooks/scripts/subagent-start.ts
7
7
  handleHookEvent("SubagentStart").catch(() => process.exit(0));
@@ -1,7 +1,7 @@
1
1
  #!/usr/bin/env node
2
2
  import {
3
3
  handleHookEvent
4
- } from "./chunk-EFVDQUHM.js";
4
+ } from "./chunk-37VUNTM4.js";
5
5
 
6
6
  // src/hooks/scripts/subagent-stop.ts
7
7
  handleHookEvent("SubagentStop").catch(() => process.exit(0));
@@ -1,7 +1,7 @@
1
1
  #!/usr/bin/env node
2
2
  import {
3
3
  handleHookEvent
4
- } from "./chunk-EFVDQUHM.js";
4
+ } from "./chunk-37VUNTM4.js";
5
5
 
6
6
  // src/hooks/scripts/user-prompt-submit.ts
7
7
  handleHookEvent("UserPromptSubmit").catch(() => process.exit(0));