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,1370 @@
1
+ #!/usr/bin/env node
2
+
3
+ // src/constants.ts
4
+ var BADGE_DEFINITIONS = [
5
+ // === VOLUME (5) ===
6
+ { id: "first_prompt", name: "First Prompt", icon: "\u{1F4AC}", description: "Submit prompts to Claude", category: "volume", stat: "totalPrompts", tiers: [1, 100, 1e3, 5e3, 25e3] },
7
+ { id: "tool_time", name: "Tool Time", icon: "\u{1F527}", description: "Make tool calls", category: "volume", stat: "totalToolCalls", tiers: [10, 500, 5e3, 25e3, 1e5] },
8
+ { id: "marathon", name: "Marathon", icon: "\u{1F3C3}", description: "Spend hours in sessions", category: "volume", stat: "totalSessionHours", tiers: [1, 10, 100, 500, 2e3] },
9
+ { id: "wordsmith", name: "Wordsmith", icon: "\u270D", description: "Type characters in prompts", category: "volume", stat: "totalCharsTyped", tiers: [1e3, 5e4, 5e5, 2e6, 1e7] },
10
+ { id: "session_vet", name: "Session Vet", icon: "\u{1F3C5}", description: "Complete sessions", category: "volume", stat: "totalSessions", tiers: [1, 50, 500, 2e3, 1e4] },
11
+ // === TOOL MASTERY (7) ===
12
+ { id: "shell_lord", name: "Shell Lord", icon: "\u{1F4BB}", description: "Execute Bash commands", category: "tool_mastery", stat: "totalBashCommands", tiers: [10, 100, 500, 2e3, 1e4] },
13
+ { id: "bookworm", name: "Bookworm", icon: "\u{1F4D6}", description: "Read files", category: "tool_mastery", stat: "totalFilesRead", tiers: [25, 250, 1e3, 5e3, 25e3] },
14
+ { id: "editor_in_chief", name: "Editor-in-Chief", icon: "\u{1F4DD}", description: "Edit files", category: "tool_mastery", stat: "totalFilesEdited", tiers: [10, 100, 500, 2e3, 1e4] },
15
+ { id: "architect", name: "Architect", icon: "\u{1F3D7}", description: "Create files", category: "tool_mastery", stat: "totalFilesCreated", tiers: [10, 50, 200, 1e3, 5e3] },
16
+ { id: "detective", name: "Detective", icon: "\u{1F50D}", description: "Search with Grep and Glob", category: "tool_mastery", stat: "totalSearches", tiers: [25, 250, 1e3, 5e3, 25e3] },
17
+ { id: "web_crawler", name: "Web Crawler", icon: "\u{1F310}", description: "Fetch web pages", category: "tool_mastery", stat: "totalWebFetches", tiers: [5, 50, 200, 1e3, 5e3] },
18
+ { id: "delegator", name: "Delegator", icon: "\u{1F916}", description: "Spawn subagents", category: "tool_mastery", stat: "totalSubagents", tiers: [5, 50, 200, 1e3, 5e3] },
19
+ // === TIME & STREAKS (4) ===
20
+ { id: "iron_streak", name: "Iron Streak", icon: "\u{1F525}", description: "Maintain a daily streak", category: "time", stat: "longestStreak", tiers: [3, 7, 30, 100, 365] },
21
+ { id: "night_owl", name: "Night Owl", icon: "\u{1F989}", description: "Prompts between midnight and 5am", category: "time", stat: "nightOwlCount", tiers: [10, 50, 200, 1e3, 5e3] },
22
+ { id: "early_bird", name: "Early Bird", icon: "\u{1F426}", description: "Prompts between 5am and 8am", category: "time", stat: "earlyBirdCount", tiers: [10, 50, 200, 1e3, 5e3] },
23
+ { id: "weekend_warrior", name: "Weekend Warrior", icon: "\u2694", description: "Weekend sessions", category: "time", stat: "weekendSessions", tiers: [5, 25, 100, 500, 2e3] },
24
+ // === BEHAVIORAL (5) ===
25
+ { 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, 2e3, 1e4] },
26
+ { id: "explorer", name: "Explorer", icon: "\u{1F9ED}", description: "Use unique tool types", category: "behavioral", stat: "uniqueToolsUsed", tiers: [3, 5, 8, 11, 14] },
27
+ { id: "planner", name: "Planner", icon: "\u{1F4CB}", description: "Use plan mode", category: "behavioral", stat: "planModeUses", tiers: [5, 25, 100, 500, 2e3] },
28
+ { id: "novelist", name: "Novelist", icon: "\u{1F4D6}", description: "Write prompts over 1000 characters", category: "behavioral", stat: "longPromptCount", tiers: [5, 25, 100, 500, 2e3] },
29
+ { id: "speed_demon", name: "Speed Demon", icon: "\u26A1", description: "Complete sessions in under 5 minutes", category: "behavioral", stat: "quickSessionCount", tiers: [5, 25, 100, 500, 2e3] },
30
+ // === RESILIENCE (3) ===
31
+ { id: "clean_hands", name: "Clean Hands", icon: "\u2728", description: "Longest error-free tool streak", category: "resilience", stat: "longestErrorFreeStreak", tiers: [50, 200, 500, 2e3, 1e4] },
32
+ { id: "resilient", name: "Resilient", icon: "\u{1F6E1}", description: "Survive errors", category: "resilience", stat: "totalErrors", tiers: [10, 50, 200, 1e3, 5e3] },
33
+ { id: "rate_limited", name: "Rate Limited", icon: "\u{1F6A7}", description: "Hit rate limits", category: "resilience", stat: "totalRateLimits", tiers: [3, 10, 25, 50, 100] },
34
+ // === SHIPPING & PROJECTS (4) ===
35
+ { id: "shipper", name: "Shipper", icon: "\u{1F4E6}", description: "Make commits via Claude", category: "shipping", stat: "totalCommits", tiers: [5, 50, 200, 1e3, 5e3] },
36
+ { id: "pr_machine", name: "PR Machine", icon: "\u{1F500}", description: "Create pull requests", category: "shipping", stat: "totalPRs", tiers: [3, 25, 100, 500, 2e3] },
37
+ { id: "empire", name: "Empire", icon: "\u{1F3F0}", description: "Work on unique projects", category: "shipping", stat: "uniqueProjects", tiers: [2, 5, 10, 25, 50] },
38
+ { id: "polyglot", name: "Polyglot", icon: "\u{1F30D}", description: "Use different programming languages", category: "shipping", stat: "uniqueLanguages", tiers: [2, 3, 5, 8, 12] },
39
+ // === MULTI-AGENT (2) ===
40
+ { id: "buddy_system", name: "Buddy System", icon: "\u{1F91D}", description: "Use concurrent agents", category: "multi_agent", stat: "concurrentAgentUses", tiers: [1, 5, 25, 100, 500] },
41
+ { id: "hive_mind", name: "Hive Mind", icon: "\u{1F41D}", description: "Spawn subagents total", category: "multi_agent", stat: "totalSubagents", tiers: [10, 100, 500, 2e3, 1e4] },
42
+ // === PUBLIC HUMOR (7) ===
43
+ { 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, 1e3, 5e3], humor: true },
44
+ { 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, 1e3], humor: true },
45
+ { 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 },
46
+ { 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 },
47
+ { 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, 1e3], humor: true },
48
+ { 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 },
49
+ { 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, 2e3, 1e4], humor: true },
50
+ // === ASPIRATIONAL (6) - Obsidian-only ===
51
+ { id: "the_machine", name: "The Machine", icon: "\u2699", description: "You are no longer using the tool. You are the tool.", category: "aspirational", stat: "totalToolCalls", tiers: [1e5, 1e5, 1e5, 1e5, 1e5], aspirational: true },
52
+ { 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 },
53
+ { 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: [1e7, 1e7, 1e7, 1e7, 1e7], aspirational: true },
54
+ { id: "lifer", name: "Lifer", icon: "\u{1F451}", description: "At this point, Claude is your cofounder.", category: "aspirational", stat: "totalSessions", tiers: [1e4, 1e4, 1e4, 1e4, 1e4], aspirational: true },
55
+ { id: "transcendent", name: "Transcendent", icon: "\u2B50", description: "You've reached the peak. The view is nice up here.", category: "aspirational", stat: "totalXP", tiers: [1e5, 1e5, 1e5, 1e5, 1e5], aspirational: true },
56
+ { 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 },
57
+ // === SECRET (10) ===
58
+ { 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 },
59
+ { 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 },
60
+ { 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 },
61
+ { 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 },
62
+ { 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 },
63
+ { 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 },
64
+ { id: "speed_run", name: "Speed Run Any%", icon: "\u23F1", description: "In and out. Twenty-second adventure.", category: "secret", stat: "speedRunSession", tiers: [1, 1, 1, 1, 1], secret: true },
65
+ { 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 },
66
+ { 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 },
67
+ { 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 }
68
+ ];
69
+ var RANK_THRESHOLDS = [
70
+ { rank: "Obsidian", xp: 1e5 },
71
+ { rank: "Diamond", xp: 25e3 },
72
+ { rank: "Gold", xp: 5e3 },
73
+ { rank: "Silver", xp: 1e3 },
74
+ { rank: "Bronze", xp: 0 }
75
+ ];
76
+ var TIER_XP = [0, 50, 100, 200, 500, 1e3];
77
+ var DATA_DIR = ".bashstats";
78
+ var DB_FILENAME = "bashstats.db";
79
+ var DEFAULT_PORT = 17900;
80
+
81
+ // src/db/database.ts
82
+ import Database from "better-sqlite3";
83
+ function localNow() {
84
+ const d = /* @__PURE__ */ new Date();
85
+ const pad = (n) => String(n).padStart(2, "0");
86
+ const ms = String(d.getMilliseconds()).padStart(3, "0");
87
+ return `${d.getFullYear()}-${pad(d.getMonth() + 1)}-${pad(d.getDate())}T${pad(d.getHours())}:${pad(d.getMinutes())}:${pad(d.getSeconds())}.${ms}`;
88
+ }
89
+ var SCHEMA = `
90
+ CREATE TABLE IF NOT EXISTS events (
91
+ id INTEGER PRIMARY KEY AUTOINCREMENT,
92
+ session_id TEXT NOT NULL,
93
+ hook_type TEXT NOT NULL,
94
+ tool_name TEXT,
95
+ tool_input TEXT,
96
+ tool_output TEXT,
97
+ exit_code INTEGER,
98
+ success INTEGER,
99
+ cwd TEXT,
100
+ project TEXT,
101
+ timestamp TEXT NOT NULL
102
+ );
103
+
104
+ CREATE TABLE IF NOT EXISTS sessions (
105
+ id TEXT PRIMARY KEY,
106
+ agent TEXT NOT NULL DEFAULT 'claude-code',
107
+ started_at TEXT NOT NULL,
108
+ ended_at TEXT,
109
+ stop_reason TEXT,
110
+ prompt_count INTEGER DEFAULT 0,
111
+ tool_count INTEGER DEFAULT 0,
112
+ error_count INTEGER DEFAULT 0,
113
+ project TEXT,
114
+ duration_seconds INTEGER
115
+ );
116
+
117
+ CREATE TABLE IF NOT EXISTS prompts (
118
+ id INTEGER PRIMARY KEY AUTOINCREMENT,
119
+ session_id TEXT NOT NULL,
120
+ content TEXT NOT NULL,
121
+ char_count INTEGER NOT NULL,
122
+ word_count INTEGER NOT NULL,
123
+ timestamp TEXT NOT NULL,
124
+ FOREIGN KEY (session_id) REFERENCES sessions(id)
125
+ );
126
+
127
+ CREATE TABLE IF NOT EXISTS daily_activity (
128
+ date TEXT PRIMARY KEY,
129
+ sessions INTEGER DEFAULT 0,
130
+ prompts INTEGER DEFAULT 0,
131
+ tool_calls INTEGER DEFAULT 0,
132
+ errors INTEGER DEFAULT 0,
133
+ duration_seconds INTEGER DEFAULT 0
134
+ );
135
+
136
+ CREATE TABLE IF NOT EXISTS achievement_unlocks (
137
+ badge_id TEXT NOT NULL,
138
+ tier INTEGER NOT NULL,
139
+ unlocked_at TEXT NOT NULL,
140
+ notified INTEGER DEFAULT 0,
141
+ PRIMARY KEY (badge_id, tier)
142
+ );
143
+
144
+ CREATE TABLE IF NOT EXISTS metadata (
145
+ key TEXT PRIMARY KEY,
146
+ value TEXT
147
+ );
148
+
149
+ CREATE INDEX IF NOT EXISTS idx_events_session ON events(session_id);
150
+ CREATE INDEX IF NOT EXISTS idx_events_hook_type ON events(hook_type);
151
+ CREATE INDEX IF NOT EXISTS idx_events_tool_name ON events(tool_name);
152
+ CREATE INDEX IF NOT EXISTS idx_events_timestamp ON events(timestamp);
153
+ CREATE INDEX IF NOT EXISTS idx_events_project ON events(project);
154
+ CREATE INDEX IF NOT EXISTS idx_prompts_session ON prompts(session_id);
155
+ CREATE INDEX IF NOT EXISTS idx_prompts_timestamp ON prompts(timestamp);
156
+ `;
157
+ var BashStatsDB = class {
158
+ db;
159
+ constructor(dbPath) {
160
+ this.db = new Database(dbPath);
161
+ this.db.pragma("journal_mode = WAL");
162
+ this.db.pragma("foreign_keys = ON");
163
+ this.db.exec(SCHEMA);
164
+ this.migrate();
165
+ }
166
+ migrate() {
167
+ const columns = this.db.pragma("table_info(sessions)");
168
+ const hasAgent = columns.some((c) => c.name === "agent");
169
+ if (!hasAgent) {
170
+ this.db.exec("ALTER TABLE sessions ADD COLUMN agent TEXT NOT NULL DEFAULT 'claude-code'");
171
+ }
172
+ }
173
+ close() {
174
+ this.db.close();
175
+ }
176
+ getTableNames() {
177
+ const rows = this.db.prepare("SELECT name FROM sqlite_master WHERE type='table'").all();
178
+ return rows.map((r) => r.name);
179
+ }
180
+ // === Events ===
181
+ insertEvent(event) {
182
+ const stmt = this.db.prepare(`
183
+ INSERT INTO events (session_id, hook_type, tool_name, tool_input, tool_output, exit_code, success, cwd, project, timestamp)
184
+ VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
185
+ `);
186
+ const result = stmt.run(
187
+ event.session_id,
188
+ event.hook_type,
189
+ event.tool_name,
190
+ event.tool_input,
191
+ event.tool_output,
192
+ event.exit_code,
193
+ event.success,
194
+ event.cwd,
195
+ event.project,
196
+ event.timestamp
197
+ );
198
+ return result.lastInsertRowid;
199
+ }
200
+ getEvents(filter) {
201
+ let sql = "SELECT * FROM events WHERE 1=1";
202
+ const params = [];
203
+ if (filter.session_id) {
204
+ sql += " AND session_id = ?";
205
+ params.push(filter.session_id);
206
+ }
207
+ if (filter.hook_type) {
208
+ sql += " AND hook_type = ?";
209
+ params.push(filter.hook_type);
210
+ }
211
+ if (filter.tool_name) {
212
+ sql += " AND tool_name = ?";
213
+ params.push(filter.tool_name);
214
+ }
215
+ sql += " ORDER BY timestamp ASC";
216
+ return this.db.prepare(sql).all(...params);
217
+ }
218
+ // === Sessions ===
219
+ insertSession(session) {
220
+ this.db.prepare(`
221
+ INSERT OR IGNORE INTO sessions (id, agent, started_at, project) VALUES (?, ?, ?, ?)
222
+ `).run(session.id, session.agent ?? "claude-code", session.started_at, session.project ?? null);
223
+ }
224
+ getSession(id) {
225
+ return this.db.prepare("SELECT * FROM sessions WHERE id = ?").get(id);
226
+ }
227
+ updateSession(id, updates) {
228
+ const sets = [];
229
+ const params = [];
230
+ if (updates.ended_at !== void 0) {
231
+ sets.push("ended_at = ?");
232
+ params.push(updates.ended_at);
233
+ }
234
+ if (updates.stop_reason !== void 0) {
235
+ sets.push("stop_reason = ?");
236
+ params.push(updates.stop_reason);
237
+ }
238
+ if (updates.duration_seconds !== void 0) {
239
+ sets.push("duration_seconds = ?");
240
+ params.push(updates.duration_seconds);
241
+ }
242
+ if (sets.length === 0) return;
243
+ params.push(id);
244
+ this.db.prepare(`UPDATE sessions SET ${sets.join(", ")} WHERE id = ?`).run(...params);
245
+ }
246
+ incrementSessionCounters(id, counters) {
247
+ const sets = [];
248
+ const params = [];
249
+ if (counters.prompts) {
250
+ sets.push("prompt_count = prompt_count + ?");
251
+ params.push(counters.prompts);
252
+ }
253
+ if (counters.tools) {
254
+ sets.push("tool_count = tool_count + ?");
255
+ params.push(counters.tools);
256
+ }
257
+ if (counters.errors) {
258
+ sets.push("error_count = error_count + ?");
259
+ params.push(counters.errors);
260
+ }
261
+ if (sets.length === 0) return;
262
+ params.push(id);
263
+ this.db.prepare(`UPDATE sessions SET ${sets.join(", ")} WHERE id = ?`).run(...params);
264
+ }
265
+ // === Prompts ===
266
+ insertPrompt(prompt) {
267
+ const result = this.db.prepare(`
268
+ INSERT INTO prompts (session_id, content, char_count, word_count, timestamp) VALUES (?, ?, ?, ?, ?)
269
+ `).run(prompt.session_id, prompt.content, prompt.char_count, prompt.word_count, prompt.timestamp);
270
+ return result.lastInsertRowid;
271
+ }
272
+ getPrompts(sessionId) {
273
+ return this.db.prepare("SELECT * FROM prompts WHERE session_id = ? ORDER BY timestamp ASC").all(sessionId);
274
+ }
275
+ // === Daily Activity ===
276
+ incrementDailyActivity(date, increments) {
277
+ this.db.prepare(`
278
+ INSERT INTO daily_activity (date, sessions, prompts, tool_calls, errors, duration_seconds)
279
+ VALUES (?, ?, ?, ?, ?, ?)
280
+ ON CONFLICT(date) DO UPDATE SET
281
+ sessions = sessions + excluded.sessions,
282
+ prompts = prompts + excluded.prompts,
283
+ tool_calls = tool_calls + excluded.tool_calls,
284
+ errors = errors + excluded.errors,
285
+ duration_seconds = duration_seconds + excluded.duration_seconds
286
+ `).run(
287
+ date,
288
+ increments.sessions ?? 0,
289
+ increments.prompts ?? 0,
290
+ increments.tool_calls ?? 0,
291
+ increments.errors ?? 0,
292
+ increments.duration_seconds ?? 0
293
+ );
294
+ }
295
+ getDailyActivity(date) {
296
+ return this.db.prepare("SELECT * FROM daily_activity WHERE date = ?").get(date);
297
+ }
298
+ getAllDailyActivity(days) {
299
+ if (days) {
300
+ return this.db.prepare("SELECT * FROM daily_activity ORDER BY date DESC LIMIT ?").all(days);
301
+ }
302
+ return this.db.prepare("SELECT * FROM daily_activity ORDER BY date DESC").all();
303
+ }
304
+ // === Achievement Unlocks ===
305
+ insertUnlock(badgeId, tier) {
306
+ this.db.prepare(`
307
+ INSERT OR IGNORE INTO achievement_unlocks (badge_id, tier, unlocked_at) VALUES (?, ?, ?)
308
+ `).run(badgeId, tier, localNow());
309
+ }
310
+ getUnlocks() {
311
+ return this.db.prepare("SELECT * FROM achievement_unlocks ORDER BY unlocked_at DESC").all();
312
+ }
313
+ getUnnotifiedUnlocks() {
314
+ return this.db.prepare("SELECT * FROM achievement_unlocks WHERE notified = 0").all();
315
+ }
316
+ markNotified(badgeId, tier) {
317
+ this.db.prepare("UPDATE achievement_unlocks SET notified = 1 WHERE badge_id = ? AND tier = ?").run(badgeId, tier);
318
+ }
319
+ // === Metadata ===
320
+ setMetadata(key, value) {
321
+ this.db.prepare("INSERT OR REPLACE INTO metadata (key, value) VALUES (?, ?)").run(key, value);
322
+ }
323
+ getMetadata(key) {
324
+ const row = this.db.prepare("SELECT value FROM metadata WHERE key = ?").get(key);
325
+ return row?.value ?? null;
326
+ }
327
+ // === Raw DB access for stats engine ===
328
+ prepare(sql) {
329
+ return this.db.prepare(sql);
330
+ }
331
+ };
332
+
333
+ // src/installer/installer.ts
334
+ import fs from "fs";
335
+ import path from "path";
336
+ import os from "os";
337
+ import { fileURLToPath } from "url";
338
+ var HOOK_SCRIPTS = {
339
+ SessionStart: "session-start.js",
340
+ UserPromptSubmit: "user-prompt-submit.js",
341
+ PreToolUse: "pre-tool-use.js",
342
+ PostToolUse: "post-tool-use.js",
343
+ PostToolUseFailure: "post-tool-failure.js",
344
+ Stop: "stop.js",
345
+ Notification: "notification.js",
346
+ SubagentStart: "subagent-start.js",
347
+ SubagentStop: "subagent-stop.js",
348
+ PreCompact: "pre-compact.js",
349
+ PermissionRequest: "permission-request.js",
350
+ Setup: "setup.js"
351
+ };
352
+ var MARKER = "# bashstats-managed";
353
+ function mergeHooks(settings, hooksDir) {
354
+ const result = { ...settings };
355
+ if (!result.hooks) {
356
+ result.hooks = {};
357
+ }
358
+ for (const [event, scriptFile] of Object.entries(HOOK_SCRIPTS)) {
359
+ const command = `node "${path.join(hooksDir, scriptFile)}" ${MARKER}`;
360
+ const existing = result.hooks[event] ?? [];
361
+ const nonBashstats = existing.filter((entry) => {
362
+ return !entry.hooks?.some((h) => h.command?.includes(MARKER));
363
+ });
364
+ const bashstatsEntry = {
365
+ matcher: "",
366
+ hooks: [{ type: "command", command }]
367
+ };
368
+ result.hooks[event] = [...nonBashstats, bashstatsEntry];
369
+ }
370
+ return result;
371
+ }
372
+ function getClaudeSettingsPath() {
373
+ return path.join(os.homedir(), ".claude", "settings.json");
374
+ }
375
+ function getHooksDir() {
376
+ const __filename = fileURLToPath(import.meta.url);
377
+ const __dirname = path.dirname(__filename);
378
+ return path.resolve(__dirname, "hooks");
379
+ }
380
+ function install() {
381
+ try {
382
+ const dataDir = path.join(os.homedir(), DATA_DIR);
383
+ fs.mkdirSync(dataDir, { recursive: true });
384
+ const dbPath = path.join(dataDir, DB_FILENAME);
385
+ const db = new BashStatsDB(dbPath);
386
+ const now = (/* @__PURE__ */ new Date()).toISOString();
387
+ db.setMetadata("installed_at", now);
388
+ if (!db.getMetadata("first_run")) {
389
+ db.setMetadata("first_run", now);
390
+ }
391
+ db.close();
392
+ const claudeDir = path.join(os.homedir(), ".claude");
393
+ fs.mkdirSync(claudeDir, { recursive: true });
394
+ const settingsPath = getClaudeSettingsPath();
395
+ let settings = {};
396
+ if (fs.existsSync(settingsPath)) {
397
+ const raw = fs.readFileSync(settingsPath, "utf-8");
398
+ settings = JSON.parse(raw);
399
+ }
400
+ const hooksDir = getHooksDir();
401
+ settings = mergeHooks(settings, hooksDir);
402
+ fs.writeFileSync(settingsPath, JSON.stringify(settings, null, 2), "utf-8");
403
+ return { success: true, message: "bashstats hooks installed successfully." };
404
+ } catch (err) {
405
+ const message = err instanceof Error ? err.message : String(err);
406
+ return { success: false, message: `Installation failed: ${message}` };
407
+ }
408
+ }
409
+ function uninstall() {
410
+ try {
411
+ const settingsPath = getClaudeSettingsPath();
412
+ if (!fs.existsSync(settingsPath)) {
413
+ return { success: true, message: "No settings.json found; nothing to uninstall." };
414
+ }
415
+ const raw = fs.readFileSync(settingsPath, "utf-8");
416
+ const settings = JSON.parse(raw);
417
+ if (settings.hooks) {
418
+ for (const event of Object.keys(settings.hooks)) {
419
+ settings.hooks[event] = settings.hooks[event].filter((entry) => {
420
+ return !entry.hooks?.some((h) => h.command?.includes(MARKER));
421
+ });
422
+ if (settings.hooks[event].length === 0) {
423
+ delete settings.hooks[event];
424
+ }
425
+ }
426
+ if (Object.keys(settings.hooks).length === 0) {
427
+ delete settings.hooks;
428
+ }
429
+ }
430
+ fs.writeFileSync(settingsPath, JSON.stringify(settings, null, 2), "utf-8");
431
+ return { success: true, message: "bashstats hooks removed successfully." };
432
+ } catch (err) {
433
+ const message = err instanceof Error ? err.message : String(err);
434
+ return { success: false, message: `Uninstall failed: ${message}` };
435
+ }
436
+ }
437
+ function isInstalled() {
438
+ try {
439
+ const settingsPath = getClaudeSettingsPath();
440
+ if (!fs.existsSync(settingsPath)) return false;
441
+ const raw = fs.readFileSync(settingsPath, "utf-8");
442
+ const settings = JSON.parse(raw);
443
+ if (!settings.hooks) return false;
444
+ for (const event of Object.keys(settings.hooks)) {
445
+ const entries = settings.hooks[event];
446
+ for (const entry of entries) {
447
+ if (entry.hooks?.some((h) => h.command?.includes(MARKER))) {
448
+ return true;
449
+ }
450
+ }
451
+ }
452
+ return false;
453
+ } catch {
454
+ return false;
455
+ }
456
+ }
457
+
458
+ // src/db/writer.ts
459
+ import path2 from "path";
460
+ var BashStatsWriter = class {
461
+ db;
462
+ constructor(db) {
463
+ this.db = db;
464
+ }
465
+ extractProject(cwd) {
466
+ return path2.basename(cwd);
467
+ }
468
+ today() {
469
+ const d = /* @__PURE__ */ new Date();
470
+ const pad = (n) => String(n).padStart(2, "0");
471
+ return `${d.getFullYear()}-${pad(d.getMonth() + 1)}-${pad(d.getDate())}`;
472
+ }
473
+ now() {
474
+ const d = /* @__PURE__ */ new Date();
475
+ const pad = (n) => String(n).padStart(2, "0");
476
+ const ms = String(d.getMilliseconds()).padStart(3, "0");
477
+ return `${d.getFullYear()}-${pad(d.getMonth() + 1)}-${pad(d.getDate())}T${pad(d.getHours())}:${pad(d.getMinutes())}:${pad(d.getSeconds())}.${ms}`;
478
+ }
479
+ recordSessionStart(sessionId, cwd, source, agent) {
480
+ const project = this.extractProject(cwd);
481
+ const timestamp = this.now();
482
+ this.db.insertSession({
483
+ id: sessionId,
484
+ agent,
485
+ started_at: timestamp,
486
+ project
487
+ });
488
+ this.db.insertEvent({
489
+ session_id: sessionId,
490
+ hook_type: "SessionStart",
491
+ tool_name: null,
492
+ tool_input: JSON.stringify({ source }),
493
+ tool_output: null,
494
+ exit_code: null,
495
+ success: null,
496
+ cwd,
497
+ project,
498
+ timestamp
499
+ });
500
+ this.db.incrementDailyActivity(this.today(), { sessions: 1 });
501
+ }
502
+ recordPrompt(sessionId, content) {
503
+ const timestamp = this.now();
504
+ const wordCount = content.split(/\s+/).filter((w) => w.length > 0).length;
505
+ const charCount = content.length;
506
+ this.db.insertPrompt({
507
+ session_id: sessionId,
508
+ content,
509
+ char_count: charCount,
510
+ word_count: wordCount,
511
+ timestamp
512
+ });
513
+ this.db.insertEvent({
514
+ session_id: sessionId,
515
+ hook_type: "UserPromptSubmit",
516
+ tool_name: null,
517
+ tool_input: null,
518
+ tool_output: null,
519
+ exit_code: null,
520
+ success: null,
521
+ cwd: null,
522
+ project: null,
523
+ timestamp
524
+ });
525
+ this.db.incrementSessionCounters(sessionId, { prompts: 1 });
526
+ this.db.incrementDailyActivity(this.today(), { prompts: 1 });
527
+ }
528
+ recordToolUse(sessionId, hookType, toolName, toolInput, toolOutput, exitCode, cwd) {
529
+ const timestamp = this.now();
530
+ const project = this.extractProject(cwd);
531
+ const success = exitCode === 0 ? 1 : 0;
532
+ this.db.insertEvent({
533
+ session_id: sessionId,
534
+ hook_type: hookType,
535
+ tool_name: toolName,
536
+ tool_input: JSON.stringify(toolInput),
537
+ tool_output: JSON.stringify(toolOutput),
538
+ exit_code: exitCode,
539
+ success,
540
+ cwd,
541
+ project,
542
+ timestamp
543
+ });
544
+ this.db.incrementSessionCounters(sessionId, {
545
+ tools: 1,
546
+ errors: success === 0 ? 1 : 0
547
+ });
548
+ this.db.incrementDailyActivity(this.today(), {
549
+ tool_calls: 1,
550
+ errors: success === 0 ? 1 : 0
551
+ });
552
+ }
553
+ recordSessionEnd(sessionId, stopReason) {
554
+ const timestamp = this.now();
555
+ const session = this.db.getSession(sessionId);
556
+ let durationSeconds;
557
+ if (session) {
558
+ const startTime = new Date(session.started_at).getTime();
559
+ const endTime = new Date(timestamp).getTime();
560
+ durationSeconds = Math.round((endTime - startTime) / 1e3);
561
+ }
562
+ this.db.updateSession(sessionId, {
563
+ ended_at: timestamp,
564
+ stop_reason: stopReason,
565
+ duration_seconds: durationSeconds
566
+ });
567
+ this.db.insertEvent({
568
+ session_id: sessionId,
569
+ hook_type: "Stop",
570
+ tool_name: null,
571
+ tool_input: JSON.stringify({ stop_reason: stopReason }),
572
+ tool_output: null,
573
+ exit_code: null,
574
+ success: null,
575
+ cwd: null,
576
+ project: null,
577
+ timestamp
578
+ });
579
+ if (durationSeconds !== void 0) {
580
+ this.db.incrementDailyActivity(this.today(), { duration_seconds: durationSeconds });
581
+ }
582
+ }
583
+ recordNotification(sessionId, message, notificationType) {
584
+ const timestamp = this.now();
585
+ const isError = notificationType === "error" || notificationType === "rate_limit";
586
+ this.db.insertEvent({
587
+ session_id: sessionId,
588
+ hook_type: "Notification",
589
+ tool_name: null,
590
+ tool_input: JSON.stringify({ message, notification_type: notificationType }),
591
+ tool_output: null,
592
+ exit_code: null,
593
+ success: null,
594
+ cwd: null,
595
+ project: null,
596
+ timestamp
597
+ });
598
+ if (isError) {
599
+ this.db.incrementSessionCounters(sessionId, { errors: 1 });
600
+ this.db.incrementDailyActivity(this.today(), { errors: 1 });
601
+ }
602
+ }
603
+ recordSubagent(sessionId, hookType, agentId, agentType) {
604
+ const timestamp = this.now();
605
+ this.db.insertEvent({
606
+ session_id: sessionId,
607
+ hook_type: hookType,
608
+ tool_name: null,
609
+ tool_input: JSON.stringify({ agent_id: agentId, agent_type: agentType ?? null }),
610
+ tool_output: null,
611
+ exit_code: null,
612
+ success: null,
613
+ cwd: null,
614
+ project: null,
615
+ timestamp
616
+ });
617
+ }
618
+ recordCompaction(sessionId, trigger) {
619
+ const timestamp = this.now();
620
+ this.db.insertEvent({
621
+ session_id: sessionId,
622
+ hook_type: "PreCompact",
623
+ tool_name: null,
624
+ tool_input: JSON.stringify({ trigger }),
625
+ tool_output: null,
626
+ exit_code: null,
627
+ success: null,
628
+ cwd: null,
629
+ project: null,
630
+ timestamp
631
+ });
632
+ }
633
+ };
634
+
635
+ // src/hooks/handler.ts
636
+ import path3 from "path";
637
+ import os2 from "os";
638
+ import fs2 from "fs";
639
+ function detectAgent() {
640
+ if (process.env.GEMINI_CLI || process.env.GEMINI_API_KEY) return "gemini-cli";
641
+ if (process.env.GITHUB_COPILOT_CLI) return "copilot-cli";
642
+ if (process.env.OPENCODE) return "opencode";
643
+ return "claude-code";
644
+ }
645
+ function parseHookEvent(input) {
646
+ try {
647
+ if (!input) return null;
648
+ return JSON.parse(input);
649
+ } catch {
650
+ return null;
651
+ }
652
+ }
653
+ function getDataDir() {
654
+ return path3.join(os2.homedir(), DATA_DIR);
655
+ }
656
+ function getDbPath() {
657
+ return path3.join(os2.homedir(), DATA_DIR, DB_FILENAME);
658
+ }
659
+ async function readStdin() {
660
+ if (process.env.CLAUDE_HOOK_EVENT) {
661
+ return process.env.CLAUDE_HOOK_EVENT;
662
+ }
663
+ return new Promise((resolve) => {
664
+ let data = "";
665
+ process.stdin.setEncoding("utf-8");
666
+ process.stdin.on("data", (chunk) => {
667
+ data += chunk;
668
+ });
669
+ process.stdin.on("end", () => {
670
+ resolve(data);
671
+ });
672
+ });
673
+ }
674
+ async function handleHookEvent(hookType) {
675
+ const raw = await readStdin();
676
+ const event = parseHookEvent(raw);
677
+ if (!event) return;
678
+ const dataDir = getDataDir();
679
+ fs2.mkdirSync(dataDir, { recursive: true });
680
+ const dbPath = getDbPath();
681
+ const db = new BashStatsDB(dbPath);
682
+ const writer = new BashStatsWriter(db);
683
+ try {
684
+ const sessionId = event.session_id ?? "";
685
+ const cwd = event.cwd ?? "";
686
+ switch (hookType) {
687
+ case "SessionStart": {
688
+ const source = event.source ?? "startup";
689
+ const agent = detectAgent();
690
+ writer.recordSessionStart(sessionId, cwd, source, agent);
691
+ break;
692
+ }
693
+ case "UserPromptSubmit": {
694
+ const prompt = event.prompt ?? "";
695
+ writer.recordPrompt(sessionId, prompt);
696
+ break;
697
+ }
698
+ case "PreToolUse": {
699
+ const toolName = event.tool_name ?? "";
700
+ const toolInput = event.tool_input ?? {};
701
+ writer.recordToolUse(sessionId, "PreToolUse", toolName, toolInput, {}, 0, cwd);
702
+ break;
703
+ }
704
+ case "PostToolUse": {
705
+ const toolName = event.tool_name ?? "";
706
+ const toolInput = event.tool_input ?? {};
707
+ const toolResponse = event.tool_response ?? {};
708
+ const exitCode = event.exit_code ?? 0;
709
+ writer.recordToolUse(sessionId, "PostToolUse", toolName, toolInput, toolResponse, exitCode, cwd);
710
+ break;
711
+ }
712
+ case "PostToolUseFailure": {
713
+ const toolName = event.tool_name ?? "";
714
+ const toolInput = event.tool_input ?? {};
715
+ const toolResponse = event.tool_response ?? {};
716
+ writer.recordToolUse(sessionId, "PostToolUseFailure", toolName, toolInput, toolResponse, 1, cwd);
717
+ break;
718
+ }
719
+ case "Stop": {
720
+ writer.recordSessionEnd(sessionId, "stopped");
721
+ break;
722
+ }
723
+ case "Notification": {
724
+ const message = event.message ?? "";
725
+ const notificationType = event.notification_type ?? "";
726
+ writer.recordNotification(sessionId, message, notificationType);
727
+ break;
728
+ }
729
+ case "SubagentStart": {
730
+ const agentId = event.agent_id ?? "";
731
+ const agentType = event.agent_type ?? "";
732
+ writer.recordSubagent(sessionId, "SubagentStart", agentId, agentType);
733
+ break;
734
+ }
735
+ case "SubagentStop": {
736
+ const agentId = event.agent_id ?? "";
737
+ writer.recordSubagent(sessionId, "SubagentStop", agentId);
738
+ break;
739
+ }
740
+ case "PreCompact": {
741
+ const trigger = event.trigger ?? "manual";
742
+ writer.recordCompaction(sessionId, trigger);
743
+ break;
744
+ }
745
+ case "PermissionRequest": {
746
+ const toolName = event.tool_name ?? "";
747
+ const toolInput = event.tool_input ?? {};
748
+ writer.recordToolUse(sessionId, "PermissionRequest", toolName, toolInput, {}, 0, cwd);
749
+ break;
750
+ }
751
+ case "Setup": {
752
+ return;
753
+ }
754
+ }
755
+ } finally {
756
+ db.close();
757
+ }
758
+ }
759
+
760
+ // src/stats/engine.ts
761
+ var StatsEngine = class {
762
+ db;
763
+ constructor(db) {
764
+ this.db = db;
765
+ }
766
+ queryScalar(sql, ...params) {
767
+ const row = this.db.prepare(sql).get(...params);
768
+ if (!row) return 0;
769
+ return Object.values(row)[0] ?? 0;
770
+ }
771
+ getLifetimeStats() {
772
+ const totalSessions = this.queryScalar("SELECT COUNT(*) as c FROM sessions");
773
+ const totalPrompts = this.queryScalar("SELECT COUNT(*) as c FROM prompts");
774
+ const totalCharsTyped = this.queryScalar("SELECT COALESCE(SUM(char_count), 0) as c FROM prompts");
775
+ const totalToolCalls = this.queryScalar(
776
+ "SELECT COUNT(*) as c FROM events WHERE hook_type IN ('PostToolUse', 'PostToolUseFailure')"
777
+ );
778
+ const totalDurationSeconds = this.queryScalar(
779
+ "SELECT COALESCE(SUM(duration_seconds), 0) as c FROM sessions"
780
+ );
781
+ const totalFilesRead = this.queryScalar(
782
+ "SELECT COUNT(*) as c FROM events WHERE tool_name = 'Read' AND hook_type = 'PostToolUse'"
783
+ );
784
+ const totalFilesWritten = this.queryScalar(
785
+ "SELECT COUNT(*) as c FROM events WHERE tool_name = 'Write' AND hook_type IN ('PostToolUse', 'PostToolUseFailure')"
786
+ );
787
+ const totalFilesEdited = this.queryScalar(
788
+ "SELECT COUNT(*) as c FROM events WHERE tool_name = 'Edit' AND hook_type IN ('PostToolUse', 'PostToolUseFailure')"
789
+ );
790
+ const totalFilesCreated = totalFilesWritten;
791
+ const totalBashCommands = this.queryScalar(
792
+ "SELECT COUNT(*) as c FROM events WHERE tool_name = 'Bash' AND hook_type IN ('PostToolUse', 'PostToolUseFailure')"
793
+ );
794
+ const totalWebSearches = this.queryScalar(
795
+ "SELECT COUNT(*) as c FROM events WHERE tool_name = 'WebSearch' AND hook_type IN ('PostToolUse', 'PostToolUseFailure')"
796
+ );
797
+ const totalWebFetches = this.queryScalar(
798
+ "SELECT COUNT(*) as c FROM events WHERE tool_name = 'WebFetch' AND hook_type IN ('PostToolUse', 'PostToolUseFailure')"
799
+ );
800
+ const totalSubagents = this.queryScalar(
801
+ "SELECT COUNT(*) as c FROM events WHERE hook_type = 'SubagentStart'"
802
+ );
803
+ const totalCompactions = this.queryScalar(
804
+ "SELECT COUNT(*) as c FROM events WHERE hook_type = 'PreCompact'"
805
+ );
806
+ const totalErrors = this.queryScalar(
807
+ `SELECT COUNT(*) as c FROM events WHERE hook_type = 'PostToolUseFailure' OR (hook_type = 'Notification' AND (tool_input LIKE '%"notification_type":"error"%' OR tool_input LIKE '%"notification_type":"rate_limit"%'))`
808
+ );
809
+ const totalRateLimits = this.queryScalar(
810
+ "SELECT COUNT(*) as c FROM events WHERE hook_type = 'Notification' AND tool_input LIKE '%rate_limit%'"
811
+ );
812
+ return {
813
+ totalSessions,
814
+ totalDurationSeconds,
815
+ totalPrompts,
816
+ totalCharsTyped,
817
+ totalToolCalls,
818
+ totalFilesRead,
819
+ totalFilesWritten,
820
+ totalFilesEdited,
821
+ totalFilesCreated,
822
+ totalBashCommands,
823
+ totalWebSearches,
824
+ totalWebFetches,
825
+ totalSubagents,
826
+ totalCompactions,
827
+ totalErrors,
828
+ totalRateLimits
829
+ };
830
+ }
831
+ getToolBreakdown() {
832
+ const rows = this.db.prepare(
833
+ "SELECT tool_name, COUNT(*) as cnt FROM events WHERE hook_type = 'PostToolUse' AND tool_name IS NOT NULL GROUP BY tool_name"
834
+ ).all();
835
+ const breakdown = {};
836
+ for (const row of rows) {
837
+ breakdown[row.tool_name] = row.cnt;
838
+ }
839
+ return breakdown;
840
+ }
841
+ getTimeStats() {
842
+ const dailyRows = this.db.prepare("SELECT date FROM daily_activity WHERE sessions > 0 OR prompts > 0 OR tool_calls > 0 ORDER BY date ASC").all();
843
+ let longestStreak = 0;
844
+ let currentStreak = 0;
845
+ if (dailyRows.length > 0) {
846
+ let streak = 1;
847
+ for (let i = 1; i < dailyRows.length; i++) {
848
+ const prevDate = /* @__PURE__ */ new Date(dailyRows[i - 1].date + "T00:00:00Z");
849
+ const currDate = /* @__PURE__ */ new Date(dailyRows[i].date + "T00:00:00Z");
850
+ const diffDays = (currDate.getTime() - prevDate.getTime()) / (1e3 * 60 * 60 * 24);
851
+ if (diffDays === 1) {
852
+ streak++;
853
+ } else {
854
+ longestStreak = Math.max(longestStreak, streak);
855
+ streak = 1;
856
+ }
857
+ }
858
+ longestStreak = Math.max(longestStreak, streak);
859
+ const today = /* @__PURE__ */ new Date();
860
+ const todayStr = today.toISOString().slice(0, 10);
861
+ const activeDates = new Set(dailyRows.map((r) => r.date));
862
+ let checkDate = /* @__PURE__ */ new Date(todayStr + "T00:00:00Z");
863
+ currentStreak = 0;
864
+ if (activeDates.has(todayStr)) {
865
+ currentStreak = 1;
866
+ checkDate.setUTCDate(checkDate.getUTCDate() - 1);
867
+ while (activeDates.has(checkDate.toISOString().slice(0, 10))) {
868
+ currentStreak++;
869
+ checkDate.setUTCDate(checkDate.getUTCDate() - 1);
870
+ }
871
+ } else {
872
+ checkDate.setUTCDate(checkDate.getUTCDate() - 1);
873
+ const yesterdayStr = checkDate.toISOString().slice(0, 10);
874
+ if (activeDates.has(yesterdayStr)) {
875
+ currentStreak = 1;
876
+ checkDate.setUTCDate(checkDate.getUTCDate() - 1);
877
+ while (activeDates.has(checkDate.toISOString().slice(0, 10))) {
878
+ currentStreak++;
879
+ checkDate.setUTCDate(checkDate.getUTCDate() - 1);
880
+ }
881
+ }
882
+ }
883
+ }
884
+ const peakHourRow = this.db.prepare(
885
+ "SELECT CAST(strftime('%H', timestamp) AS INTEGER) as hour, COUNT(*) as cnt FROM prompts GROUP BY hour ORDER BY cnt DESC LIMIT 1"
886
+ ).get();
887
+ const peakHour = peakHourRow?.hour ?? 0;
888
+ const peakHourCount = peakHourRow?.cnt ?? 0;
889
+ const nightOwlCount = this.queryScalar(
890
+ "SELECT COUNT(*) as c FROM prompts WHERE CAST(strftime('%H', timestamp) AS INTEGER) < 5"
891
+ );
892
+ const earlyBirdCount = this.queryScalar(
893
+ "SELECT COUNT(*) as c FROM prompts WHERE CAST(strftime('%H', timestamp) AS INTEGER) BETWEEN 5 AND 7"
894
+ );
895
+ const weekendSessions = this.queryScalar(
896
+ "SELECT COUNT(*) as c FROM sessions WHERE CAST(strftime('%w', started_at) AS INTEGER) IN (0, 6)"
897
+ );
898
+ const mostActiveDayRow = this.db.prepare(
899
+ "SELECT CAST(strftime('%w', started_at) AS INTEGER) as dow, COUNT(*) as cnt FROM sessions GROUP BY dow ORDER BY cnt DESC LIMIT 1"
900
+ ).get();
901
+ const mostActiveDay = mostActiveDayRow?.dow ?? 0;
902
+ const busiestDateRow = this.db.prepare(
903
+ "SELECT date, (sessions + prompts + tool_calls) as total FROM daily_activity ORDER BY total DESC LIMIT 1"
904
+ ).get();
905
+ const busiestDate = busiestDateRow?.date ?? "";
906
+ const busiestDateCount = busiestDateRow?.total ?? 0;
907
+ return {
908
+ currentStreak,
909
+ longestStreak,
910
+ peakHour,
911
+ peakHourCount,
912
+ nightOwlCount,
913
+ earlyBirdCount,
914
+ weekendSessions,
915
+ mostActiveDay,
916
+ busiestDate,
917
+ busiestDateCount
918
+ };
919
+ }
920
+ getSessionRecords() {
921
+ const longestSessionSeconds = this.queryScalar(
922
+ "SELECT COALESCE(MAX(duration_seconds), 0) as c FROM sessions"
923
+ );
924
+ const mostToolsInSession = this.queryScalar(
925
+ "SELECT COALESCE(MAX(tool_count), 0) as c FROM sessions"
926
+ );
927
+ const mostPromptsInSession = this.queryScalar(
928
+ "SELECT COALESCE(MAX(prompt_count), 0) as c FROM sessions"
929
+ );
930
+ const fastestSessionSeconds = this.queryScalar(
931
+ "SELECT COALESCE(MIN(duration_seconds), 0) as c FROM sessions WHERE duration_seconds IS NOT NULL AND duration_seconds > 0"
932
+ );
933
+ const avgDurationSeconds = this.queryScalar(
934
+ "SELECT COALESCE(AVG(duration_seconds), 0) as c FROM sessions WHERE duration_seconds IS NOT NULL"
935
+ );
936
+ const avgPromptsPerSession = this.queryScalar(
937
+ "SELECT COALESCE(AVG(prompt_count), 0) as c FROM sessions"
938
+ );
939
+ const avgToolsPerSession = this.queryScalar(
940
+ "SELECT COALESCE(AVG(tool_count), 0) as c FROM sessions"
941
+ );
942
+ return {
943
+ longestSessionSeconds,
944
+ mostToolsInSession,
945
+ mostPromptsInSession,
946
+ fastestSessionSeconds,
947
+ avgDurationSeconds: Math.round(avgDurationSeconds),
948
+ avgPromptsPerSession: Math.round(avgPromptsPerSession * 100) / 100,
949
+ avgToolsPerSession: Math.round(avgToolsPerSession * 100) / 100
950
+ };
951
+ }
952
+ getProjectStats() {
953
+ const uniqueProjects = this.queryScalar(
954
+ "SELECT COUNT(DISTINCT project) as c FROM sessions WHERE project IS NOT NULL"
955
+ );
956
+ const projectRows = this.db.prepare(
957
+ "SELECT project, COUNT(*) as cnt FROM sessions WHERE project IS NOT NULL GROUP BY project ORDER BY cnt DESC"
958
+ ).all();
959
+ const mostVisitedProject = projectRows.length > 0 ? projectRows[0].project : "";
960
+ const mostVisitedProjectCount = projectRows.length > 0 ? projectRows[0].cnt : 0;
961
+ const projectBreakdown = {};
962
+ for (const row of projectRows) {
963
+ projectBreakdown[row.project] = row.cnt;
964
+ }
965
+ return {
966
+ uniqueProjects,
967
+ mostVisitedProject,
968
+ mostVisitedProjectCount,
969
+ projectBreakdown
970
+ };
971
+ }
972
+ getAllStats() {
973
+ return {
974
+ lifetime: this.getLifetimeStats(),
975
+ tools: this.getToolBreakdown(),
976
+ time: this.getTimeStats(),
977
+ sessions: this.getSessionRecords(),
978
+ projects: this.getProjectStats()
979
+ };
980
+ }
981
+ };
982
+
983
+ // src/types.ts
984
+ var AGENT_DISPLAY_NAMES = {
985
+ "claude-code": "Claude Code",
986
+ "gemini-cli": "Gemini CLI",
987
+ "copilot-cli": "Copilot CLI",
988
+ "opencode": "OpenCode",
989
+ "unknown": "Unknown"
990
+ };
991
+ var TIER_NAMES = {
992
+ 0: "Locked",
993
+ 1: "Bronze",
994
+ 2: "Silver",
995
+ 3: "Gold",
996
+ 4: "Diamond",
997
+ 5: "Obsidian"
998
+ };
999
+
1000
+ // src/achievements/compute.ts
1001
+ var AchievementEngine = class {
1002
+ db;
1003
+ stats;
1004
+ constructor(db, stats) {
1005
+ this.db = db;
1006
+ this.stats = stats;
1007
+ }
1008
+ computeBadges() {
1009
+ const allStats = this.stats.getAllStats();
1010
+ const flat = this.flattenStats(allStats);
1011
+ return BADGE_DEFINITIONS.map((badge) => {
1012
+ const value = flat[badge.stat] ?? 0;
1013
+ let tier = 0;
1014
+ if (badge.aspirational) {
1015
+ tier = value >= badge.tiers[4] ? 5 : 0;
1016
+ } else if (badge.secret) {
1017
+ tier = value >= badge.tiers[0] ? 1 : 0;
1018
+ } else {
1019
+ for (let i = 0; i < badge.tiers.length; i++) {
1020
+ if (value >= badge.tiers[i]) {
1021
+ tier = i + 1;
1022
+ } else {
1023
+ break;
1024
+ }
1025
+ }
1026
+ }
1027
+ let nextThreshold = 0;
1028
+ let progress = 0;
1029
+ let maxed = false;
1030
+ if (badge.aspirational) {
1031
+ nextThreshold = badge.tiers[4];
1032
+ progress = tier === 5 ? 1 : Math.min(value / nextThreshold, 0.99);
1033
+ maxed = tier === 5;
1034
+ } else if (badge.secret) {
1035
+ nextThreshold = badge.tiers[0];
1036
+ progress = tier >= 1 ? 1 : 0;
1037
+ maxed = tier >= 1;
1038
+ } else if (tier >= 5) {
1039
+ nextThreshold = badge.tiers[4];
1040
+ progress = 1;
1041
+ maxed = true;
1042
+ } else {
1043
+ const tierIdx = tier;
1044
+ nextThreshold = badge.tiers[tierIdx];
1045
+ const prevThreshold = tierIdx > 0 ? badge.tiers[tierIdx - 1] : 0;
1046
+ const range = nextThreshold - prevThreshold;
1047
+ progress = range > 0 ? Math.min((value - prevThreshold) / range, 0.99) : 0;
1048
+ }
1049
+ if (tier > 0) {
1050
+ for (let t = 1; t <= tier; t++) {
1051
+ this.db.insertUnlock(badge.id, t);
1052
+ }
1053
+ }
1054
+ return {
1055
+ id: badge.id,
1056
+ name: badge.name,
1057
+ icon: badge.icon,
1058
+ description: badge.description,
1059
+ category: badge.category,
1060
+ stat: badge.stat,
1061
+ tiers: badge.tiers,
1062
+ tier,
1063
+ tierName: TIER_NAMES[tier],
1064
+ value,
1065
+ nextThreshold,
1066
+ progress,
1067
+ maxed,
1068
+ secret: badge.secret ?? false,
1069
+ unlocked: tier > 0
1070
+ };
1071
+ });
1072
+ }
1073
+ computeXP() {
1074
+ const allStats = this.stats.getAllStats();
1075
+ const badges = this.computeBadges();
1076
+ let totalXP = 0;
1077
+ totalXP += allStats.lifetime.totalPrompts * 1;
1078
+ totalXP += allStats.lifetime.totalToolCalls * 1;
1079
+ totalXP += allStats.lifetime.totalSessions * 10;
1080
+ totalXP += allStats.time.nightOwlCount * 2;
1081
+ totalXP += Math.floor(allStats.time.longestStreak / 100) * 25;
1082
+ for (const badge of badges) {
1083
+ if (badge.tier > 0) {
1084
+ totalXP += TIER_XP[badge.tier] ?? 0;
1085
+ }
1086
+ }
1087
+ let rank = "Bronze";
1088
+ let nextRankXP = RANK_THRESHOLDS[RANK_THRESHOLDS.length - 2]?.xp ?? 1e3;
1089
+ for (const threshold of RANK_THRESHOLDS) {
1090
+ if (totalXP >= threshold.xp) {
1091
+ rank = threshold.rank;
1092
+ break;
1093
+ }
1094
+ }
1095
+ let progress = 0;
1096
+ const rankIndex = RANK_THRESHOLDS.findIndex((t) => t.rank === rank);
1097
+ if (rankIndex <= 0) {
1098
+ nextRankXP = RANK_THRESHOLDS[0].xp;
1099
+ progress = 1;
1100
+ } else {
1101
+ const currentThreshold = RANK_THRESHOLDS[rankIndex].xp;
1102
+ nextRankXP = RANK_THRESHOLDS[rankIndex - 1].xp;
1103
+ const range = nextRankXP - currentThreshold;
1104
+ progress = range > 0 ? Math.min((totalXP - currentThreshold) / range, 0.99) : 0;
1105
+ }
1106
+ return {
1107
+ totalXP,
1108
+ rank,
1109
+ nextRankXP,
1110
+ progress
1111
+ };
1112
+ }
1113
+ getAchievementsPayload() {
1114
+ const allStats = this.stats.getAllStats();
1115
+ return {
1116
+ stats: allStats,
1117
+ badges: this.computeBadges(),
1118
+ xp: this.computeXP()
1119
+ };
1120
+ }
1121
+ flattenStats(allStats) {
1122
+ const flat = {};
1123
+ flat.totalPrompts = allStats.lifetime.totalPrompts;
1124
+ flat.totalToolCalls = allStats.lifetime.totalToolCalls;
1125
+ flat.totalSessions = allStats.lifetime.totalSessions;
1126
+ flat.totalCharsTyped = allStats.lifetime.totalCharsTyped;
1127
+ flat.totalBashCommands = allStats.lifetime.totalBashCommands;
1128
+ flat.totalFilesRead = allStats.lifetime.totalFilesRead;
1129
+ flat.totalFilesEdited = allStats.lifetime.totalFilesEdited;
1130
+ flat.totalFilesCreated = allStats.lifetime.totalFilesCreated;
1131
+ flat.totalSubagents = allStats.lifetime.totalSubagents;
1132
+ flat.totalErrors = allStats.lifetime.totalErrors;
1133
+ flat.totalRateLimits = allStats.lifetime.totalRateLimits;
1134
+ flat.totalWebFetches = allStats.lifetime.totalWebFetches;
1135
+ flat.totalWebSearches = allStats.lifetime.totalWebSearches;
1136
+ flat.totalCompactions = allStats.lifetime.totalCompactions;
1137
+ flat.totalSessionHours = Math.floor(allStats.lifetime.totalDurationSeconds / 3600);
1138
+ flat.longestStreak = allStats.time.longestStreak;
1139
+ flat.nightOwlCount = allStats.time.nightOwlCount;
1140
+ flat.earlyBirdCount = allStats.time.earlyBirdCount;
1141
+ flat.weekendSessions = allStats.time.weekendSessions;
1142
+ flat.totalSearches = this.queryTotalSearches();
1143
+ flat.mostRepeatedPromptCount = this.queryMostRepeatedPromptCount();
1144
+ flat.uniqueToolsUsed = this.queryUniqueToolsUsed();
1145
+ flat.planModeUses = this.queryPlanModeUses();
1146
+ flat.longPromptCount = this.queryLongPromptCount();
1147
+ flat.quickSessionCount = this.queryQuickSessionCount();
1148
+ flat.longestErrorFreeStreak = this.queryLongestErrorFreeStreak();
1149
+ flat.politePromptCount = this.queryPolitePromptCount();
1150
+ flat.hugePromptCount = this.queryHugePromptCount();
1151
+ flat.maxSameFileEdits = this.queryMaxSameFileEdits();
1152
+ flat.longSessionCount = this.queryLongSessionCount();
1153
+ flat.repeatedPromptCount = this.queryRepeatedPromptCount();
1154
+ flat.maxErrorsInSession = this.queryMaxErrorsInSession();
1155
+ flat.totalCommits = this.queryTotalCommits();
1156
+ flat.totalPRs = this.queryTotalPRs();
1157
+ flat.uniqueProjects = allStats.projects.uniqueProjects;
1158
+ flat.uniqueLanguages = this.queryUniqueLanguages();
1159
+ flat.concurrentAgentUses = this.queryConcurrentAgentUses();
1160
+ flat.dangerousCommandBlocked = this.queryDangerousCommandBlocked();
1161
+ flat.returnAfterBreak = this.queryReturnAfterBreak();
1162
+ flat.threeAmPrompt = this.queryThreeAmPrompt();
1163
+ flat.midnightSpanSession = this.queryMidnightSpanSession();
1164
+ flat.nestedSubagent = this.queryNestedSubagent();
1165
+ flat.holidayActivity = this.queryHolidayActivity();
1166
+ flat.speedRunSession = this.querySpeedRunSession();
1167
+ flat.allToolsInSession = this.queryAllToolsInSession();
1168
+ flat.firstEverSession = allStats.lifetime.totalSessions > 0 ? 1 : 0;
1169
+ flat.allBadgesGold = 0;
1170
+ flat.totalXP = 0;
1171
+ flat.allToolsObsidian = 0;
1172
+ return flat;
1173
+ }
1174
+ // === Computed stat query helpers ===
1175
+ queryScalar(sql, ...params) {
1176
+ const row = this.db.prepare(sql).get(...params);
1177
+ if (!row) return 0;
1178
+ return Object.values(row)[0] ?? 0;
1179
+ }
1180
+ queryTotalSearches() {
1181
+ return this.queryScalar(
1182
+ "SELECT COUNT(*) as c FROM events WHERE tool_name IN ('Grep', 'Glob') AND hook_type = 'PostToolUse'"
1183
+ );
1184
+ }
1185
+ queryPolitePromptCount() {
1186
+ return this.queryScalar(
1187
+ "SELECT COUNT(*) as c FROM prompts WHERE LOWER(content) LIKE '%please%' OR LOWER(content) LIKE '%thank%'"
1188
+ );
1189
+ }
1190
+ queryHugePromptCount() {
1191
+ return this.queryScalar(
1192
+ "SELECT COUNT(*) as c FROM prompts WHERE char_count > 5000"
1193
+ );
1194
+ }
1195
+ queryLongPromptCount() {
1196
+ return this.queryScalar(
1197
+ "SELECT COUNT(*) as c FROM prompts WHERE char_count > 1000"
1198
+ );
1199
+ }
1200
+ queryMostRepeatedPromptCount() {
1201
+ return this.queryScalar(
1202
+ "SELECT COUNT(*) as c FROM prompts GROUP BY LOWER(TRIM(content)) ORDER BY c DESC LIMIT 1"
1203
+ );
1204
+ }
1205
+ queryUniqueToolsUsed() {
1206
+ return this.queryScalar(
1207
+ "SELECT COUNT(DISTINCT tool_name) as c FROM events WHERE hook_type = 'PostToolUse' AND tool_name IS NOT NULL"
1208
+ );
1209
+ }
1210
+ queryPlanModeUses() {
1211
+ return this.queryScalar(
1212
+ "SELECT COUNT(*) as c FROM events WHERE hook_type = 'PostToolUse' AND tool_name = 'Task'"
1213
+ );
1214
+ }
1215
+ queryQuickSessionCount() {
1216
+ return this.queryScalar(
1217
+ "SELECT COUNT(*) as c FROM sessions WHERE duration_seconds IS NOT NULL AND duration_seconds < 300 AND tool_count > 0"
1218
+ );
1219
+ }
1220
+ queryLongSessionCount() {
1221
+ return this.queryScalar(
1222
+ "SELECT COUNT(*) as c FROM sessions WHERE duration_seconds IS NOT NULL AND duration_seconds > 28800"
1223
+ );
1224
+ }
1225
+ queryMaxErrorsInSession() {
1226
+ return this.queryScalar(
1227
+ "SELECT COALESCE(MAX(error_count), 0) as c FROM sessions"
1228
+ );
1229
+ }
1230
+ queryMaxSameFileEdits() {
1231
+ return this.queryScalar(
1232
+ "SELECT COUNT(*) as c FROM events WHERE tool_name = 'Edit' AND hook_type = 'PostToolUse' GROUP BY json_extract(tool_input, '$.file_path') ORDER BY c DESC LIMIT 1"
1233
+ );
1234
+ }
1235
+ queryRepeatedPromptCount() {
1236
+ return this.queryScalar(
1237
+ "SELECT COALESCE(SUM(cnt), 0) as c FROM (SELECT COUNT(*) as cnt FROM prompts GROUP BY LOWER(TRIM(content)) HAVING cnt > 1)"
1238
+ );
1239
+ }
1240
+ queryLongestErrorFreeStreak() {
1241
+ const rows = this.db.prepare(
1242
+ "SELECT hook_type FROM events WHERE hook_type IN ('PostToolUse', 'PostToolUseFailure') ORDER BY timestamp ASC"
1243
+ ).all();
1244
+ let longest = 0;
1245
+ let current = 0;
1246
+ for (const row of rows) {
1247
+ if (row.hook_type === "PostToolUse") {
1248
+ current++;
1249
+ longest = Math.max(longest, current);
1250
+ } else {
1251
+ current = 0;
1252
+ }
1253
+ }
1254
+ return longest;
1255
+ }
1256
+ queryTotalCommits() {
1257
+ return this.queryScalar(
1258
+ "SELECT COUNT(*) as c FROM events WHERE tool_name = 'Bash' AND hook_type = 'PostToolUse' AND tool_input LIKE '%git commit%'"
1259
+ );
1260
+ }
1261
+ queryTotalPRs() {
1262
+ return this.queryScalar(
1263
+ "SELECT COUNT(*) as c FROM events WHERE tool_name = 'Bash' AND hook_type = 'PostToolUse' AND tool_input LIKE '%gh pr create%'"
1264
+ );
1265
+ }
1266
+ queryUniqueLanguages() {
1267
+ const rows = this.db.prepare(
1268
+ "SELECT DISTINCT json_extract(tool_input, '$.file_path') as fp FROM events WHERE tool_name IN ('Edit', 'Write', 'Read') AND hook_type = 'PostToolUse' AND tool_input IS NOT NULL"
1269
+ ).all();
1270
+ const extensions = /* @__PURE__ */ new Set();
1271
+ for (const row of rows) {
1272
+ if (row.fp) {
1273
+ const match = row.fp.match(/\.([a-zA-Z0-9]+)$/);
1274
+ if (match) {
1275
+ extensions.add(match[1].toLowerCase());
1276
+ }
1277
+ }
1278
+ }
1279
+ return extensions.size;
1280
+ }
1281
+ queryConcurrentAgentUses() {
1282
+ return this.queryScalar(
1283
+ "SELECT COUNT(DISTINCT session_id) as c FROM events WHERE hook_type = 'SubagentStart'"
1284
+ );
1285
+ }
1286
+ // === Secret stat helpers ===
1287
+ queryDangerousCommandBlocked() {
1288
+ return this.queryScalar(
1289
+ "SELECT COUNT(*) as c FROM events WHERE hook_type = 'PreToolUse' AND tool_name = 'Bash' AND (tool_input LIKE '%rm -rf%' OR tool_input LIKE '%rm -r /%')"
1290
+ );
1291
+ }
1292
+ queryReturnAfterBreak() {
1293
+ const rows = this.db.prepare(
1294
+ "SELECT started_at FROM sessions ORDER BY started_at ASC"
1295
+ ).all();
1296
+ if (rows.length < 2) return 0;
1297
+ for (let i = 1; i < rows.length; i++) {
1298
+ const prev = new Date(rows[i - 1].started_at).getTime();
1299
+ const curr = new Date(rows[i].started_at).getTime();
1300
+ const diffDays = (curr - prev) / (1e3 * 60 * 60 * 24);
1301
+ if (diffDays >= 7) return 1;
1302
+ }
1303
+ return 0;
1304
+ }
1305
+ queryThreeAmPrompt() {
1306
+ return this.queryScalar(
1307
+ "SELECT COUNT(*) as c FROM prompts WHERE CAST(strftime('%H', timestamp) AS INTEGER) = 3"
1308
+ ) > 0 ? 1 : 0;
1309
+ }
1310
+ queryMidnightSpanSession() {
1311
+ const rows = this.db.prepare(
1312
+ "SELECT started_at, ended_at FROM sessions WHERE ended_at IS NOT NULL"
1313
+ ).all();
1314
+ for (const row of rows) {
1315
+ const startDate = row.started_at.slice(0, 10);
1316
+ const endDate = row.ended_at.slice(0, 10);
1317
+ if (startDate !== endDate) return 1;
1318
+ }
1319
+ return 0;
1320
+ }
1321
+ queryNestedSubagent() {
1322
+ return this.queryScalar(
1323
+ "SELECT COUNT(*) as c FROM events WHERE hook_type = 'SubagentStart'"
1324
+ ) > 0 ? 1 : 0;
1325
+ }
1326
+ queryHolidayActivity() {
1327
+ const holidays = this.queryScalar(
1328
+ "SELECT COUNT(*) as c FROM sessions WHERE strftime('%m-%d', started_at) IN ('12-25', '01-01', '07-04')"
1329
+ );
1330
+ return holidays > 0 ? 1 : 0;
1331
+ }
1332
+ querySpeedRunSession() {
1333
+ return this.queryScalar(
1334
+ "SELECT COUNT(*) as c FROM sessions WHERE duration_seconds IS NOT NULL AND duration_seconds <= 20 AND tool_count > 0"
1335
+ ) > 0 ? 1 : 0;
1336
+ }
1337
+ queryAllToolsInSession() {
1338
+ const requiredTools = ["Bash", "Read", "Write", "Edit", "Grep", "Glob", "WebFetch"];
1339
+ const rows = this.db.prepare(
1340
+ "SELECT session_id, COUNT(DISTINCT tool_name) as cnt FROM events WHERE hook_type = 'PostToolUse' AND tool_name IN ('Bash', 'Read', 'Write', 'Edit', 'Grep', 'Glob', 'WebFetch') GROUP BY session_id"
1341
+ ).all();
1342
+ for (const row of rows) {
1343
+ if (row.cnt >= requiredTools.length) return 1;
1344
+ }
1345
+ return 0;
1346
+ }
1347
+ };
1348
+
1349
+ export {
1350
+ BADGE_DEFINITIONS,
1351
+ RANK_THRESHOLDS,
1352
+ TIER_XP,
1353
+ DATA_DIR,
1354
+ DB_FILENAME,
1355
+ DEFAULT_PORT,
1356
+ BashStatsDB,
1357
+ install,
1358
+ uninstall,
1359
+ isInstalled,
1360
+ BashStatsWriter,
1361
+ detectAgent,
1362
+ parseHookEvent,
1363
+ getDbPath,
1364
+ handleHookEvent,
1365
+ StatsEngine,
1366
+ AGENT_DISPLAY_NAMES,
1367
+ TIER_NAMES,
1368
+ AchievementEngine
1369
+ };
1370
+ //# sourceMappingURL=chunk-2KXMOTBO.js.map