bashstats 0.2.1 → 0.2.2

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.
@@ -9,7 +9,7 @@ var BADGE_DEFINITIONS = [
9
9
  { id: "tool_time", name: "Tool Time", icon: "\u{1F527}", description: "Make tool calls", category: "volume", stat: "totalToolCalls", tiers: [50, 2500, 25e3, 1e5, 5e5], trigger: "Total tool calls made across all sessions" },
10
10
  { id: "marathon", name: "Marathon", icon: "\u{1F3C3}", description: "Spend hours in sessions", category: "volume", stat: "totalSessionHours", tiers: [1, 25, 250, 1e3, 5e3], trigger: "Total hours spent in sessions (rounded down)" },
11
11
  { id: "wordsmith", name: "Wordsmith", icon: "\u270D", description: "Type characters in prompts", category: "volume", stat: "totalCharsTyped", tiers: [1e3, 1e5, 1e6, 5e6, 25e6], trigger: "Total characters typed in prompts" },
12
- { id: "session_vet", name: "Session Vet", icon: "\u{1F3C5}", description: "Complete sessions", category: "volume", stat: "totalSessions", tiers: [1, 100, 1e3, 5e3, 25e3], trigger: "Total sessions completed" },
12
+ { id: "session_vet", name: "Session Vet", icon: "\u{1F3C5}", description: "Complete sessions", category: "volume", stat: "totalSessions", tiers: [1, 50, 500, 2500, 1e4], trigger: "Total sessions completed" },
13
13
  // ===================================================================
14
14
  // TOOL MASTERY (7)
15
15
  // ===================================================================
@@ -21,21 +21,20 @@ var BADGE_DEFINITIONS = [
21
21
  { id: "web_crawler", name: "Web Crawler", icon: "\u{1F310}", description: "Fetch web pages", category: "tool_mastery", stat: "totalWebFetches", tiers: [5, 50, 200, 1e3, 5e3], trigger: "WebFetch calls made" },
22
22
  { id: "delegator", name: "Delegator", icon: "\u{1F916}", description: "Spawn subagents", category: "tool_mastery", stat: "totalSubagents", tiers: [10, 100, 500, 2500, 1e4], trigger: "Subagents spawned via SubagentStart events" },
23
23
  // ===================================================================
24
- // TIME & PATTERNS (4 existing + 6 new = 10)
24
+ // TIME & PATTERNS (10)
25
25
  // ===================================================================
26
26
  { 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" },
27
27
  { 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], trigger: "Prompts submitted between midnight and 5 AM" },
28
28
  { 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], trigger: "Prompts submitted between 5 AM and 8 AM" },
29
29
  { id: "weekend_warrior", name: "Weekend Warrior", icon: "\u2694", description: "Weekend sessions", category: "time", stat: "weekendSessions", tiers: [5, 25, 100, 500, 2e3], trigger: "Sessions started on Saturday or Sunday" },
30
- // New Time & Patterns
31
30
  { 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, 1e3], trigger: "Prompts submitted between 2 AM and 4 AM" },
32
31
  { 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" },
33
- { 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" },
32
+ { id: "monday_motivation", name: "Monday Motivation", icon: "\u{1F4AA}", description: "Starting the week strong (or desperate).", category: "time", stat: "mondaySessions", tiers: [5, 25, 75, 200, 500], trigger: "Sessions started on Monday" },
34
33
  { 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, 1e3], trigger: "Git commits made on Friday (via Bash tool)" },
35
34
  { id: "timezone_traveler", name: "Timezone Traveler", icon: "\u2708", 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" },
36
- { 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" },
35
+ { id: "seasonal_coder", name: "Seasonal Coder", icon: "\u{1F343}", description: "You've coded through all four seasons.", category: "time", stat: "uniqueQuarters", tiers: [1, 2, 4, 6, 8], trigger: "Unique quarter-year combos (e.g. Q1-2025, Q2-2025) with activity" },
37
36
  // ===================================================================
38
- // SESSION BEHAVIOR (6 new)
37
+ // SESSION BEHAVIOR (6)
39
38
  // ===================================================================
40
39
  { id: "one_more_thing", name: "One More Thing", icon: "\u261D", 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" },
41
40
  { 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" },
@@ -52,7 +51,7 @@ var BADGE_DEFINITIONS = [
52
51
  { id: "novelist", name: "Novelist", icon: "\u{1F4D6}", description: "Write prompts over 1000 characters", category: "behavioral", stat: "longPromptCount", tiers: [5, 25, 100, 500, 2e3], trigger: "Prompts with over 1,000 characters" },
53
52
  { 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], trigger: "Sessions under 5 minutes with tool use" },
54
53
  // ===================================================================
55
- // PROMPT PATTERNS (6 new)
54
+ // PROMPT PATTERNS (6)
56
55
  // ===================================================================
57
56
  { id: "minimalist", name: "Minimalist", icon: "\u{1F90F}", description: "A person of few words.", category: "prompt_patterns", stat: "shortPromptCount", tiers: [5, 25, 100, 500, 2e3], trigger: "Prompts with fewer than 10 words" },
58
57
  { id: "question_master", name: "Question Master", icon: "\u2753", description: "So many questions, so little time.", category: "prompt_patterns", stat: "questionPromptCount", tiers: [10, 50, 200, 1e3, 5e3], trigger: "Prompts ending with a question mark" },
@@ -61,13 +60,13 @@ var BADGE_DEFINITIONS = [
61
60
  { id: "emoji_whisperer", name: "Emoji Whisperer", icon: "\u{1F680}", description: "Deploying vibes", category: "prompt_patterns", stat: "emojiPromptCount", tiers: [5, 25, 100, 500, 2e3], trigger: "Prompts containing emoji characters" },
62
61
  { 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, 1e3], trigger: "Prompts with 50+ lines of text" },
63
62
  // ===================================================================
64
- // RESILIENCE (3 existing)
63
+ // RESILIENCE (3)
65
64
  // ===================================================================
66
65
  { id: "clean_hands", name: "Clean Hands", icon: "\u2728", description: "Longest error-free tool streak", category: "resilience", stat: "longestErrorFreeStreak", tiers: [50, 200, 500, 2e3, 1e4], trigger: "Consecutive successful tool calls without any error" },
67
66
  { id: "resilient", name: "Resilient", icon: "\u{1F6E1}", description: "Survive errors", category: "resilience", stat: "totalErrors", tiers: [10, 50, 200, 1e3, 5e3], trigger: "Total errors survived across all sessions" },
68
67
  { 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" },
69
68
  // ===================================================================
70
- // ERROR & RECOVERY (5 new)
69
+ // ERROR & RECOVERY (5)
71
70
  // ===================================================================
72
71
  { 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" },
73
72
  { 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" },
@@ -75,7 +74,7 @@ var BADGE_DEFINITIONS = [
75
74
  { 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" },
76
75
  { id: "phoenix", name: "Phoenix", icon: "\u{1F985}", description: "From the ashes of 100 errors, you rise.", category: "error_recovery", stat: "totalLifetimeErrors", tiers: [100, 500, 1e3, 5e3, 1e4], trigger: "Total lifetime errors survived across all sessions" },
77
76
  // ===================================================================
78
- // TOOL COMBOS (5 new)
77
+ // TOOL COMBOS (5)
79
78
  // ===================================================================
80
79
  { id: "read_edit_run", name: "Read-Edit-Run", icon: "\u{1F3AF}", description: "The holy trinity.", category: "tool_combos", stat: "readEditRunCount", tiers: [25, 100, 500, 2e3, 1e4], trigger: "Read \u2192 Edit \u2192 Bash sequences detected in events" },
81
80
  { id: "grep_ninja", name: "Grep Ninja", icon: "\u{1F977}", description: "Finding needles in haystacks since day one.", category: "tool_combos", stat: "totalSearches", tiers: [250, 1e3, 5e3, 25e3, 1e5], trigger: "Total Grep and Glob searches performed" },
@@ -83,14 +82,14 @@ var BADGE_DEFINITIONS = [
83
82
  { id: "the_refactorer", name: "The Refactorer", icon: "\u267B", description: "Same file, different day.", category: "tool_combos", stat: "maxSameFileEditsLifetime", tiers: [50, 100, 250, 500, 2e3], trigger: "Max Edit calls to any single file path across all sessions" },
84
83
  { 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, 1e4], trigger: "Grep/Glob followed by Edit within same session" },
85
84
  // ===================================================================
86
- // SHIPPING & PROJECTS (4 existing)
85
+ // SHIPPING & PROJECTS (4)
87
86
  // ===================================================================
88
87
  { id: "shipper", name: "Shipper", icon: "\u{1F4E6}", description: "Make commits via Claude", category: "shipping", stat: "totalCommits", tiers: [5, 50, 200, 1e3, 5e3], trigger: "Bash tool calls containing 'git commit'" },
89
88
  { id: "pr_machine", name: "PR Machine", icon: "\u{1F500}", description: "Create pull requests", category: "shipping", stat: "totalPRs", tiers: [3, 25, 100, 500, 2e3], trigger: "Bash tool calls containing 'gh pr create'" },
90
89
  { 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" },
91
90
  { 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" },
92
91
  // ===================================================================
93
- // PROJECT DEDICATION (5 new)
92
+ // PROJECT DEDICATION (5)
94
93
  // ===================================================================
95
94
  { id: "monogamous", name: "Monogamous", icon: "\u{1F48D}", description: "One project. True love.", category: "project_dedication", stat: "maxProjectSessions", tiers: [50, 100, 250, 500, 1e3], trigger: "Max sessions on any single project" },
96
95
  { 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" },
@@ -98,57 +97,58 @@ var BADGE_DEFINITIONS = [
98
97
  { 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" },
99
98
  { 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" },
100
99
  // ===================================================================
101
- // MULTI-AGENT (2 existing + 4 new = 6)
100
+ // MULTI-AGENT (6)
102
101
  // ===================================================================
103
102
  { 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" },
104
103
  { id: "hive_mind", name: "Hive Mind", icon: "\u{1F41D}", description: "Spawn subagents total", category: "multi_agent", stat: "totalSubagents", tiers: [25, 250, 1e3, 5e3, 25e3], trigger: "Total SubagentStart events across all sessions" },
105
- // New Multi-Agent
106
- { 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" },
104
+ { id: "swarm_intelligence", name: "Swarm Intelligence", icon: "\u{1F41C}", description: "You've built an army.", category: "multi_agent", stat: "maxConcurrentSubagents", tiers: [3, 5, 8, 12, 20], trigger: "Max concurrent subagents active at any point" },
107
105
  { 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" },
108
- { 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, 1e4, 5e4], trigger: "Total subagent spawns across all sessions" },
109
- { 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" },
106
+ { id: "the_orchestrator", name: "The Orchestrator", icon: "\u{1F3BC}", description: "You don't code. You conduct.", category: "multi_agent", stat: "totalSubagentSpawns", tiers: [50, 250, 1e3, 5e3, 25e3], trigger: "Total subagent spawns across all sessions" },
107
+ { id: "agent_smith", name: "Agent Smith", icon: "\u{1F576}", description: "They're multiplying.", category: "multi_agent", stat: "maxSubagentsInSession", tiers: [10, 25, 50, 100, 250], trigger: "Max SubagentStart events in a single session" },
108
+ // --- Cross-agent badges (new) ---
109
+ { id: "polyglot_agent", name: "Polyglot Agent", icon: "\u{1F30F}", description: "A tool for every occasion.", category: "multi_agent", stat: "distinctAgentsUsed", tiers: [2, 3, 4, 4, 4], trigger: "Distinct CLI agents used (Claude Code, Gemini, Copilot, OpenCode)" },
110
+ { id: "gemini_whisperer", name: "Gemini Whisperer", icon: "\u264A", description: "The stars aligned for your Gemini sessions.", category: "multi_agent", stat: "geminiSessions", tiers: [10, 50, 200, 1e3, 5e3], trigger: "Sessions completed in Gemini CLI" },
111
+ { id: "copilot_rider", name: "Copilot Rider", icon: "\u2708", description: "Your copilot is always on duty.", category: "multi_agent", stat: "copilotSessions", tiers: [10, 50, 200, 1e3, 5e3], trigger: "Sessions completed in Copilot CLI" },
112
+ { id: "open_source_spirit", name: "Open Source Spirit", icon: "\u{1F4A1}", description: "Freedom in every keystroke.", category: "multi_agent", stat: "opencodeSessions", tiers: [10, 50, 200, 1e3, 5e3], trigger: "Sessions completed in OpenCode" },
113
+ { id: "agent_hopper", name: "Agent Hopper", icon: "\u{1F407}", description: "Can't pick a favorite? Neither can we.", category: "multi_agent", stat: "agentSwitchDays", tiers: [2, 4, 6, 8, 10], trigger: "Days where you used 2+ different CLI agents" },
114
+ { id: "double_agent", name: "Double Agent", icon: "\u{1F575}", description: "Playing both sides. Respect.", category: "multi_agent", stat: "doubleAgentDays", tiers: [5, 25, 100, 250, 500], trigger: "Days with sessions in 2+ different CLI agents" },
110
115
  // ===================================================================
111
- // HUMOR & META (7 existing + 8 new = 15)
116
+ // WILD CARD (12)
112
117
  // ===================================================================
113
- { 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, trigger: "Prompts containing 'please' or 'thank'" },
114
- { 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, trigger: "Prompts over 5,000 characters" },
115
- { 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" },
116
- { 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" },
117
- { 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, trigger: "Total duplicate prompts submitted" },
118
- { 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" },
119
- { 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, trigger: "Count of your single most repeated prompt" },
120
- // New Humor & Meta
121
- { id: "deja_vu", name: "D\xE9j\xE0 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" },
122
- { 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, 1e3], humor: true, trigger: "Read immediately after Write on the same file" },
123
- { 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, 1e3], humor: true, trigger: "Prompts with numbered step-by-step instructions (e.g. '1.' '2.')" },
124
- { 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, 1e3], humor: true, trigger: "Prompts containing 'try again' or 'one more time'" },
125
- { id: "rubber_stamp", name: "Rubber Stamp", icon: "\u2705", description: "Yes. Yes. Yes. Approved.", category: "humor", stat: "maxConsecutivePermissions", tiers: [25, 50, 100, 250, 500], humor: true, trigger: "Max consecutive PermissionRequest events" },
126
- { 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)" },
127
- { id: "inbox_zero", name: "Inbox Zero", icon: "\u2728", description: "No errors. No warnings. Just vibes.", category: "humor", stat: "longestErrorFreeStreak", tiers: [50, 100, 200, 500, 1e3], humor: true, trigger: "Longest streak of consecutive successful tool calls" },
128
- { 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, 1e3], humor: true, trigger: "Bash success (exit code 0) after a previous Bash failure" },
118
+ { 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: "wild_card", stat: "politePromptCount", tiers: [10, 50, 200, 1e3, 5e3], trigger: "Prompts containing 'please' or 'thank'" },
119
+ { id: "wall_of_text", name: "Wall of Text", icon: "\u{1F4DC}", description: "Claude read your entire novel and didn't even complain.", category: "wild_card", stat: "hugePromptCount", tiers: [1, 10, 50, 200, 1e3], trigger: "Prompts over 5,000 characters" },
120
+ { id: "the_fixer", name: "The Fixer", icon: "\u{1F6E0}", description: "At this point just rewrite the whole thing.", category: "wild_card", stat: "maxSameFileEdits", tiers: [10, 20, 50, 100, 200], trigger: "Max Edit calls to a single file" },
121
+ { id: "what_day_is_it", name: "What Day Is It?", icon: "\u{1F62B}", description: "Your chair is now a part of you.", category: "wild_card", stat: "longSessionCount", tiers: [1, 5, 25, 100, 500], trigger: "Sessions exceeding 8 hours" },
122
+ { id: "copy_pasta", name: "Copy Pasta", icon: "\u{1F35D}", description: "Maybe if I ask again it'll work differently.", category: "wild_card", stat: "repeatedPromptCount", tiers: [3, 10, 50, 200, 1e3], trigger: "Total duplicate prompts submitted" },
123
+ { id: "error_magnet", name: "Error Magnet", icon: "\u{1F9F2}", description: "At this point, the errors are a feature.", category: "wild_card", stat: "maxErrorsInSession", tiers: [10, 25, 50, 100, 200], trigger: "Max errors in a single session" },
124
+ { id: "deja_vu", name: "D\xE9j\xE0 Vu", icon: "\u{1F408}", description: "Didn't we just do this?", category: "wild_card", stat: "dejaVuCount", tiers: [1, 5, 25, 100, 500], trigger: "Same prompt submitted twice within 5 minutes" },
125
+ { id: "trust_issues", name: "Trust Issues", icon: "\u{1F50E}", description: "You read the file Claude just wrote.", category: "wild_card", stat: "trustIssueCount", tiers: [1, 10, 50, 200, 1e3], trigger: "Read immediately after Write on the same file" },
126
+ { id: "backseat_driver", name: "Backseat Driver", icon: "\u{1F697}", description: "Let me tell you exactly how to do your job.", category: "wild_card", stat: "backseatDriverCount", tiers: [1, 10, 50, 200, 1e3], trigger: "Prompts with numbered step-by-step instructions (e.g. '1.' '2.')" },
127
+ { id: "the_negotiator", name: "The Negotiator", icon: "\u{1F91C}", description: "Can you try again but better?", category: "wild_card", stat: "negotiatorCount", tiers: [1, 10, 50, 200, 1e3], trigger: "Prompts containing 'try again' or 'one more time'" },
128
+ { id: "rubber_stamp", name: "Rubber Stamp", icon: "\u2705", description: "Yes. Yes. Yes. Approved.", category: "wild_card", stat: "maxConsecutivePermissions", tiers: [25, 50, 100, 250, 500], trigger: "Max consecutive PermissionRequest events" },
129
+ { id: "it_works_on_my_machine", name: "It Works On My Machine", icon: "\u{1F937}", description: "The classic excuse.", category: "wild_card", stat: "bashRetrySuccessCount", tiers: [1, 10, 50, 200, 1e3], trigger: "Bash success (exit code 0) after a previous Bash failure" },
129
130
  // ===================================================================
130
- // TOKEN USAGE (10 new)
131
+ // TOKEN USAGE (10)
131
132
  // ===================================================================
132
- { id: "token_burner", name: "Token Burner", icon: "\u{1F525}", description: "Consume tokens across all sessions", category: "token_usage", stat: "totalTokens", tiers: [1e8, 1e9, 5e9, 2e10, 1e11], trigger: "Total tokens consumed (input + output + cache read + cache creation)" },
133
- { id: "output_machine", name: "Output Machine", icon: "\u{1F5A8}", description: "Generate output tokens from Claude", category: "token_usage", stat: "totalOutputTokens", tiers: [5e5, 5e6, 25e6, 1e8, 5e8], trigger: "Total output tokens generated by Claude across all sessions" },
134
- { id: "cache_royalty", name: "Cache Royalty", icon: "\u{1F451}", description: "Read tokens from prompt cache", category: "token_usage", stat: "totalCacheReadTokens", tiers: [1e8, 1e9, 5e9, 2e10, 1e11], trigger: "Total cache read tokens (cached context reused across turns)" },
135
- { id: "context_crafter", name: "Context Crafter", icon: "\u{1F9F1}", description: "Create new cache entries", category: "token_usage", stat: "totalCacheCreationTokens", tiers: [1e7, 1e8, 1e9, 5e9, 25e9], trigger: "Total cache creation tokens (new context written to cache)" },
136
- { id: "token_whale", name: "Token Whale", icon: "\u{1F40B}", description: "Massive token consumption in a single session", category: "token_usage", stat: "mostTokensInSession", tiers: [5e6, 25e6, 1e8, 5e8, 2e9], trigger: "Most total tokens consumed in any single session" },
137
- { 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, 1e4], trigger: "Sessions with 1,000,000+ total tokens" },
133
+ { id: "token_burner", name: "Token Burner", icon: "\u{1F525}", description: "Consume tokens across all sessions", category: "token_usage", stat: "totalTokens", tiers: [5e7, 5e8, 5e9, 5e10, 5e11], trigger: "Total tokens consumed (input + output + cache read + cache creation)" },
134
+ { id: "output_machine", name: "Output Machine", icon: "\u{1F5A8}", description: "Generate output tokens from Claude", category: "token_usage", stat: "totalOutputTokens", tiers: [1e6, 1e7, 1e8, 5e8, 2e9], trigger: "Total output tokens generated by Claude across all sessions" },
135
+ { id: "cache_royalty", name: "Cache Royalty", icon: "\u{1F451}", description: "Read tokens from prompt cache", category: "token_usage", stat: "totalCacheReadTokens", tiers: [5e7, 5e8, 5e9, 5e10, 5e11], trigger: "Total cache read tokens (cached context reused across turns)" },
136
+ { id: "context_crafter", name: "Context Crafter", icon: "\u{1F9F1}", description: "Create new cache entries", category: "token_usage", stat: "totalCacheCreationTokens", tiers: [1e7, 1e8, 1e9, 1e10, 1e11], trigger: "Total cache creation tokens (new context written to cache)" },
137
+ { id: "token_whale", name: "Token Whale", icon: "\u{1F40B}", description: "Massive token consumption in a single session", category: "token_usage", stat: "mostTokensInSession", tiers: [1e7, 5e7, 2e8, 5e8, 1e9], trigger: "Most total tokens consumed in any single session" },
138
+ { id: "heavy_hitter", name: "Heavy Hitter", icon: "\u{1F4AA}", description: "Sessions exceeding 1M total tokens", category: "token_usage", stat: "heavyTokenSessions", tiers: [10, 50, 250, 1e3, 5e3], trigger: "Sessions with 1,000,000+ total tokens" },
138
139
  { 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, 1e3], trigger: "Sessions under 50,000 total tokens with at least 1 tool call" },
139
- { id: "token_velocity", name: "Token Velocity", icon: "\u26A1", description: "High average tokens per session", category: "token_usage", stat: "avgTokensPerSession", tiers: [25e5, 5e6, 1e7, 3e7, 1e8], trigger: "Average total tokens per session across all sessions" },
140
- { id: "prolific_session", name: "Prolific", icon: "\u270D", description: "Most output generated in one session", category: "token_usage", stat: "maxOutputInSession", tiers: [5e4, 25e4, 1e6, 5e6, 2e7], trigger: "Most output tokens generated by Claude in a single session" },
141
- { id: "input_flood", name: "Input Flood", icon: "\u{1F30A}", description: "Total raw input tokens sent to the API", category: "token_usage", stat: "totalInputTokens", tiers: [5e5, 25e5, 1e7, 5e7, 25e7], trigger: "Total non-cached input tokens (the small uncached portion of each request)" },
140
+ { id: "token_velocity", name: "Token Velocity", icon: "\u26A1", description: "High average tokens per session", category: "token_usage", stat: "avgTokensPerSession", tiers: [5e6, 1e7, 25e6, 5e7, 1e8], trigger: "Average total tokens per session across all sessions" },
141
+ { id: "prolific_session", name: "Prolific", icon: "\u270D", description: "Most output generated in one session", category: "token_usage", stat: "maxOutputInSession", tiers: [1e5, 5e5, 2e6, 1e7, 5e7], trigger: "Most output tokens generated by Claude in a single session" },
142
+ { id: "input_flood", name: "Input Flood", icon: "\u{1F30A}", description: "Total raw input tokens sent to the API", category: "token_usage", stat: "totalInputTokens", tiers: [1e6, 1e7, 1e8, 5e8, 2e9], trigger: "Total non-cached input tokens (the small uncached portion of each request)" },
142
143
  // ===================================================================
143
- // ASPIRATIONAL (6 existing + 5 new + 1 token = 12) - Obsidian-only
144
+ // ASPIRATIONAL (12) - Singularity-only
144
145
  // ===================================================================
145
146
  { 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, trigger: "Reach 100,000 total tool calls" },
146
147
  { 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" },
147
148
  { 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, trigger: "Type 10 million characters in prompts" },
148
149
  { 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, trigger: "Complete 10,000 sessions" },
149
150
  { 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, trigger: "Earn 100,000 total XP" },
150
- { 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" },
151
- // New Aspirational
151
+ { 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 Singularity tier" },
152
152
  { id: "ten_thousand_hours", name: "10,000 Hours", icon: "\u23F0", description: "Malcolm Gladwell would be proud.", category: "aspirational", stat: "totalSessionHours", tiers: [1e4, 1e4, 1e4, 1e4, 1e4], aspirational: true, trigger: "Spend 10,000 hours in sessions" },
153
153
  { id: "master_architect", name: "The Architect", icon: "\u{1F3DB}", description: "You've built more than most companies ship.", category: "aspirational", stat: "totalFilesCreated", tiers: [1e3, 1e3, 1e3, 1e3, 1e3], aspirational: true, trigger: "Create 1,000+ files with the Write tool" },
154
154
  { 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" },
@@ -156,7 +156,7 @@ var BADGE_DEFINITIONS = [
156
156
  { id: "centimillionaire", name: "Centimillionaire", icon: "\u2328", description: "100 million characters. Your keyboard weeps.", category: "aspirational", stat: "totalCharsTyped", tiers: [1e8, 1e8, 1e8, 1e8, 1e8], aspirational: true, trigger: "Type 100 million characters in prompts" },
157
157
  { 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: [1e9, 1e9, 1e9, 1e9, 1e9], aspirational: true, trigger: "Consume 1 billion total tokens" },
158
158
  // ===================================================================
159
- // SECRET (10 existing + 6 new + 1 token = 17)
159
+ // SECRET (17)
160
160
  // ===================================================================
161
161
  { 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" },
162
162
  { 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" },
@@ -168,7 +168,6 @@ var BADGE_DEFINITIONS = [
168
168
  { 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" },
169
169
  { 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" },
170
170
  { 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" },
171
- // New Secret
172
171
  { 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" },
173
172
  { 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)" },
174
173
  { 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" },
@@ -177,14 +176,63 @@ var BADGE_DEFINITIONS = [
177
176
  { 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" },
178
177
  { 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" }
179
178
  ];
180
- var RANK_THRESHOLDS = [
181
- { rank: "Obsidian", xp: 1e5 },
182
- { rank: "Diamond", xp: 25e3 },
183
- { rank: "Gold", xp: 5e3 },
184
- { rank: "Silver", xp: 1e3 },
185
- { rank: "Bronze", xp: 0 }
179
+ var RANK_TIER_BRACKETS = [
180
+ { tier: "System Anomaly", minRank: 500, maxRank: 500 },
181
+ { tier: "Obsidian", minRank: 401, maxRank: 499 },
182
+ { tier: "Diamond", minRank: 301, maxRank: 400 },
183
+ { tier: "Gold", minRank: 201, maxRank: 300 },
184
+ { tier: "Silver", minRank: 101, maxRank: 200 },
185
+ { tier: "Bronze", minRank: 1, maxRank: 100 }
186
186
  ];
187
+ function xpForRank(rank) {
188
+ if (rank <= 0) return 0;
189
+ return Math.floor(10 * Math.pow(rank, 2.2));
190
+ }
191
+ function rankTierForRank(rank) {
192
+ for (const bracket of RANK_TIER_BRACKETS) {
193
+ if (rank >= bracket.minRank && rank <= bracket.maxRank) {
194
+ return bracket.tier;
195
+ }
196
+ }
197
+ return "Unranked";
198
+ }
187
199
  var TIER_XP = [0, 50, 100, 200, 500, 1e3];
200
+ var ACTIVITY_MULTIPLIERS = {
201
+ 1: 1,
202
+ 2: 1.1,
203
+ 3: 1.2,
204
+ 4: 1.3,
205
+ 5: 1.5,
206
+ 6: 1.75,
207
+ 7: 2
208
+ };
209
+ var WEEKLY_CHALLENGES = [
210
+ { id: "wk_diverse_tools", description: "Use 5+ distinct tools in 5 sessions", stat: "diverseToolSessions", threshold: 5, xpReward: 200, weekScoped: true },
211
+ { id: "wk_200_prompts", description: "Submit 200 prompts this week", stat: "totalPrompts", threshold: 200, xpReward: 250, weekScoped: true },
212
+ { id: "wk_quick_sessions", description: "Complete 3 sessions under 2 minutes", stat: "quickDrawSessions", threshold: 3, xpReward: 150, weekScoped: true },
213
+ { id: "wk_5_day_commits", description: "Make git commits on 5 different days", stat: "commitDays", threshold: 5, xpReward: 300, weekScoped: true },
214
+ { id: "wk_7_day_streak", description: "Be active all 7 days this week", stat: "daysActive", threshold: 7, xpReward: 400, weekScoped: true },
215
+ { id: "wk_subagent_power", description: "Spawn 8+ subagents in a single session", stat: "maxSubagentsInSession", threshold: 8, xpReward: 200, weekScoped: true },
216
+ { id: "wk_night_code", description: "Code after midnight on 3 different nights", stat: "nightOwlDays", threshold: 3, xpReward: 150, weekScoped: true },
217
+ { id: "wk_2000_tools", description: "Make 2,000 tool calls this week", stat: "totalToolCalls", threshold: 2e3, xpReward: 250, weekScoped: true },
218
+ { id: "wk_10_sessions", description: "Complete 10 sessions this week", stat: "totalSessions", threshold: 10, xpReward: 200, weekScoped: true },
219
+ { id: "wk_20_sessions", description: "Complete 20 sessions this week", stat: "totalSessions", threshold: 20, xpReward: 350, weekScoped: true },
220
+ { id: "wk_500_reads", description: "Read 500 files this week", stat: "totalFilesRead", threshold: 500, xpReward: 250, weekScoped: true },
221
+ { id: "wk_100_edits", description: "Edit 100 files this week", stat: "totalFilesEdited", threshold: 100, xpReward: 250, weekScoped: true },
222
+ { id: "wk_20_creates", description: "Create 20 new files this week", stat: "totalFilesCreated", threshold: 20, xpReward: 200, weekScoped: true },
223
+ { id: "wk_3_projects", description: "Work on 3+ different projects this week", stat: "uniqueProjects", threshold: 3, xpReward: 200, weekScoped: true },
224
+ { id: "wk_3_marathons", description: "Have 3 sessions lasting over 1 hour", stat: "extendedSessionCount", threshold: 3, xpReward: 250, weekScoped: true },
225
+ { id: "wk_5_clean", description: "Complete 5 sessions with zero errors", stat: "cleanSessions", threshold: 5, xpReward: 200, weekScoped: true },
226
+ { id: "wk_15_hours", description: "Spend 15+ total hours in sessions", stat: "totalHours", threshold: 15, xpReward: 300, weekScoped: true },
227
+ { id: "wk_200_bash", description: "Run 200 Bash commands this week", stat: "totalBashCommands", threshold: 200, xpReward: 200, weekScoped: true },
228
+ { id: "wk_250_searches", description: "Perform 250 searches (Grep/Glob)", stat: "totalSearches", threshold: 250, xpReward: 200, weekScoped: true },
229
+ { id: "wk_10_long_prompts", description: "Write 10 prompts over 1,000 characters", stat: "longPromptCount", threshold: 10, xpReward: 150, weekScoped: true },
230
+ { id: "wk_5_commits", description: "Make 5 git commits this week", stat: "totalCommits", threshold: 5, xpReward: 200, weekScoped: true },
231
+ { id: "wk_2_prs", description: "Create 2 pull requests this week", stat: "totalPRs", threshold: 2, xpReward: 250, weekScoped: true },
232
+ { id: "wk_5000_tools", description: "Make 5,000 tool calls this week", stat: "totalToolCalls", threshold: 5e3, xpReward: 400, weekScoped: true },
233
+ { id: "wk_weekend_warrior", description: "Code on both Saturday and Sunday", stat: "weekendDays", threshold: 2, xpReward: 200, weekScoped: true },
234
+ { id: "wk_3_early_birds", description: "Submit a prompt before 8am on 3 days", stat: "earlyBirdDays", threshold: 3, xpReward: 200, weekScoped: true }
235
+ ];
188
236
  var DATA_DIR = ".bashstats";
189
237
  var DB_FILENAME = "bashstats.db";
190
238
  var DEFAULT_PORT = 17900;
@@ -265,6 +313,21 @@ CREATE TABLE IF NOT EXISTS metadata (
265
313
  value TEXT
266
314
  );
267
315
 
316
+ CREATE TABLE IF NOT EXISTS weekly_goals (
317
+ week_start TEXT NOT NULL,
318
+ challenge_id TEXT NOT NULL,
319
+ completed INTEGER DEFAULT 0,
320
+ xp_reward INTEGER NOT NULL,
321
+ PRIMARY KEY (week_start, challenge_id)
322
+ );
323
+
324
+ CREATE TABLE IF NOT EXISTS weekly_xp (
325
+ week_start TEXT PRIMARY KEY,
326
+ base_xp INTEGER DEFAULT 0,
327
+ multiplier REAL DEFAULT 1.0,
328
+ bonus_xp INTEGER DEFAULT 0
329
+ );
330
+
268
331
  CREATE INDEX IF NOT EXISTS idx_events_session ON events(session_id);
269
332
  CREATE INDEX IF NOT EXISTS idx_events_hook_type ON events(hook_type);
270
333
  CREATE INDEX IF NOT EXISTS idx_events_tool_name ON events(tool_name);
@@ -470,6 +533,28 @@ var BashStatsDB = class {
470
533
  const row = this.db.prepare("SELECT value FROM metadata WHERE key = ?").get(key);
471
534
  return row?.value ?? null;
472
535
  }
536
+ // === Weekly Goals ===
537
+ insertWeeklyGoal(weekStart, challengeId, xpReward) {
538
+ this.db.prepare(`
539
+ INSERT OR IGNORE INTO weekly_goals (week_start, challenge_id, xp_reward) VALUES (?, ?, ?)
540
+ `).run(weekStart, challengeId, xpReward);
541
+ }
542
+ completeWeeklyGoal(weekStart, challengeId) {
543
+ this.db.prepare("UPDATE weekly_goals SET completed = 1 WHERE week_start = ? AND challenge_id = ?").run(weekStart, challengeId);
544
+ }
545
+ getWeeklyGoals(weekStart) {
546
+ return this.db.prepare("SELECT * FROM weekly_goals WHERE week_start = ?").all(weekStart);
547
+ }
548
+ // === Weekly XP ===
549
+ upsertWeeklyXP(weekStart, baseXP, multiplier, bonusXP) {
550
+ this.db.prepare(`
551
+ INSERT INTO weekly_xp (week_start, base_xp, multiplier, bonus_xp) VALUES (?, ?, ?, ?)
552
+ ON CONFLICT(week_start) DO UPDATE SET base_xp = ?, multiplier = ?, bonus_xp = ?
553
+ `).run(weekStart, baseXP, multiplier, bonusXP, baseXP, multiplier, bonusXP);
554
+ }
555
+ getWeeklyXP(weekStart) {
556
+ return this.db.prepare("SELECT * FROM weekly_xp WHERE week_start = ?").get(weekStart);
557
+ }
473
558
  // === Raw DB access for stats engine ===
474
559
  prepare(sql) {
475
560
  return this.db.prepare(sql);
@@ -601,15 +686,397 @@ function isInstalled() {
601
686
  }
602
687
  }
603
688
 
604
- // src/db/writer.ts
689
+ // src/installer/gemini.ts
690
+ import fs2 from "fs";
605
691
  import path2 from "path";
692
+ import os2 from "os";
693
+ var GEMINI_HOOK_SCRIPTS = {
694
+ SessionStart: "session-start.js",
695
+ SessionEnd: "stop.js",
696
+ BeforeAgent: "user-prompt-submit.js",
697
+ BeforeTool: "pre-tool-use.js",
698
+ AfterTool: "post-tool-use.js",
699
+ AfterModel: "stop.js",
700
+ PreCompress: "pre-compact.js",
701
+ Notification: "notification.js"
702
+ };
703
+ var MARKER2 = "# bashstats-managed";
704
+ var DEFAULT_TIMEOUT = 10;
705
+ function mergeGeminiHooks(settings, hooksDir) {
706
+ const result = { ...settings };
707
+ if (!result.hooks) {
708
+ result.hooks = {};
709
+ }
710
+ for (const [event, scriptFile] of Object.entries(GEMINI_HOOK_SCRIPTS)) {
711
+ const command = `node "${path2.join(hooksDir, scriptFile)}" ${MARKER2}`;
712
+ const existing = result.hooks[event] ?? [];
713
+ const nonBashstats = existing.filter((entry) => {
714
+ return !entry.hooks?.some((h) => h.command?.includes(MARKER2));
715
+ });
716
+ const bashstatsEntry = {
717
+ hooks: [{ name: "bashstats", type: "command", command, timeout: DEFAULT_TIMEOUT }]
718
+ };
719
+ result.hooks[event] = [...nonBashstats, bashstatsEntry];
720
+ }
721
+ return result;
722
+ }
723
+ function getGeminiSettingsPath() {
724
+ return path2.join(os2.homedir(), ".gemini", "settings.json");
725
+ }
726
+ function isGeminiAvailable() {
727
+ try {
728
+ const geminiDir = path2.join(os2.homedir(), ".gemini");
729
+ return fs2.existsSync(geminiDir) && fs2.statSync(geminiDir).isDirectory();
730
+ } catch {
731
+ return false;
732
+ }
733
+ }
734
+ function installGemini() {
735
+ try {
736
+ const dataDir = path2.join(os2.homedir(), DATA_DIR);
737
+ fs2.mkdirSync(dataDir, { recursive: true });
738
+ const dbPath = path2.join(dataDir, DB_FILENAME);
739
+ const db = new BashStatsDB(dbPath);
740
+ const now = (/* @__PURE__ */ new Date()).toISOString();
741
+ db.setMetadata("installed_at", now);
742
+ if (!db.getMetadata("first_run")) {
743
+ db.setMetadata("first_run", now);
744
+ }
745
+ db.close();
746
+ const geminiDir = path2.join(os2.homedir(), ".gemini");
747
+ fs2.mkdirSync(geminiDir, { recursive: true });
748
+ const settingsPath = getGeminiSettingsPath();
749
+ let settings = {};
750
+ if (fs2.existsSync(settingsPath)) {
751
+ const raw = fs2.readFileSync(settingsPath, "utf-8");
752
+ settings = JSON.parse(raw);
753
+ }
754
+ const hooksDir = getHooksDir();
755
+ settings = mergeGeminiHooks(settings, hooksDir);
756
+ fs2.writeFileSync(settingsPath, JSON.stringify(settings, null, 2), "utf-8");
757
+ return { success: true, message: "bashstats Gemini hooks installed successfully." };
758
+ } catch (err) {
759
+ const message = err instanceof Error ? err.message : String(err);
760
+ return { success: false, message: `Gemini installation failed: ${message}` };
761
+ }
762
+ }
763
+ function uninstallGemini() {
764
+ try {
765
+ const settingsPath = getGeminiSettingsPath();
766
+ if (!fs2.existsSync(settingsPath)) {
767
+ return { success: true, message: "No Gemini settings.json found; nothing to uninstall." };
768
+ }
769
+ const raw = fs2.readFileSync(settingsPath, "utf-8");
770
+ const settings = JSON.parse(raw);
771
+ if (settings.hooks) {
772
+ for (const event of Object.keys(settings.hooks)) {
773
+ settings.hooks[event] = settings.hooks[event].filter((entry) => {
774
+ return !entry.hooks?.some((h) => h.command?.includes(MARKER2));
775
+ });
776
+ if (settings.hooks[event].length === 0) {
777
+ delete settings.hooks[event];
778
+ }
779
+ }
780
+ if (Object.keys(settings.hooks).length === 0) {
781
+ delete settings.hooks;
782
+ }
783
+ }
784
+ fs2.writeFileSync(settingsPath, JSON.stringify(settings, null, 2), "utf-8");
785
+ return { success: true, message: "bashstats Gemini hooks removed successfully." };
786
+ } catch (err) {
787
+ const message = err instanceof Error ? err.message : String(err);
788
+ return { success: false, message: `Gemini uninstall failed: ${message}` };
789
+ }
790
+ }
791
+
792
+ // src/installer/copilot.ts
793
+ import fs3 from "fs";
794
+ import path3 from "path";
795
+ import os3 from "os";
796
+ import { execSync } from "child_process";
797
+ var COPILOT_HOOK_SCRIPTS = {
798
+ sessionStart: "session-start.js",
799
+ sessionEnd: "stop.js",
800
+ userPromptSubmitted: "user-prompt-submit.js",
801
+ preToolUse: "pre-tool-use.js",
802
+ postToolUse: "post-tool-use.js",
803
+ errorOccurred: "post-tool-failure.js"
804
+ };
805
+ var COMMENT_MARKER = "bashstats-managed";
806
+ function buildCopilotHooksConfig(hooksDir) {
807
+ const hooks = {};
808
+ for (const [event, scriptFile] of Object.entries(COPILOT_HOOK_SCRIPTS)) {
809
+ const scriptPath = path3.join(hooksDir, scriptFile);
810
+ hooks[event] = [
811
+ {
812
+ type: "command",
813
+ bash: `node "${scriptPath}"`,
814
+ powershell: `node "${scriptPath}"`,
815
+ timeoutSec: 30,
816
+ comment: COMMENT_MARKER
817
+ }
818
+ ];
819
+ }
820
+ return { version: 1, hooks };
821
+ }
822
+ function getCopilotHooksPath() {
823
+ return path3.join(os3.homedir(), ".copilot", "hooks", "bashstats-hooks.json");
824
+ }
825
+ function installCopilot() {
826
+ try {
827
+ const hooksPath = getCopilotHooksPath();
828
+ const hooksDir = path3.dirname(hooksPath);
829
+ fs3.mkdirSync(hooksDir, { recursive: true });
830
+ const distHooksDir = getHooksDir();
831
+ const config = buildCopilotHooksConfig(distHooksDir);
832
+ fs3.writeFileSync(hooksPath, JSON.stringify(config, null, 2), "utf-8");
833
+ return { success: true, message: "Copilot bashstats hooks installed successfully." };
834
+ } catch (err) {
835
+ const message = err instanceof Error ? err.message : String(err);
836
+ return { success: false, message: `Copilot installation failed: ${message}` };
837
+ }
838
+ }
839
+ function uninstallCopilot() {
840
+ try {
841
+ const hooksPath = getCopilotHooksPath();
842
+ if (!fs3.existsSync(hooksPath)) {
843
+ return { success: true, message: "No bashstats-hooks.json found; nothing to uninstall." };
844
+ }
845
+ fs3.unlinkSync(hooksPath);
846
+ return { success: true, message: "Copilot bashstats hooks removed successfully." };
847
+ } catch (err) {
848
+ const message = err instanceof Error ? err.message : String(err);
849
+ return { success: false, message: `Copilot uninstall failed: ${message}` };
850
+ }
851
+ }
852
+ function isCopilotAvailable() {
853
+ try {
854
+ execSync("copilot --version", { stdio: "ignore", timeout: 5e3 });
855
+ return true;
856
+ } catch {
857
+ return false;
858
+ }
859
+ }
860
+
861
+ // src/installer/opencode.ts
862
+ import fs4 from "fs";
863
+ import path4 from "path";
864
+ import os4 from "os";
865
+ function getOpenCodePluginsDir() {
866
+ return path4.join(os4.homedir(), ".config", "opencode", "plugins");
867
+ }
868
+ function getOpenCodePluginPath() {
869
+ return path4.join(getOpenCodePluginsDir(), "bashstats.ts");
870
+ }
871
+ function isOpenCodeAvailable() {
872
+ try {
873
+ return fs4.existsSync(path4.join(os4.homedir(), ".config", "opencode"));
874
+ } catch {
875
+ return false;
876
+ }
877
+ }
878
+ function getOpenCodePluginContent(dbPath) {
879
+ return `// bashstats plugin for OpenCode -- auto-generated, do not edit
880
+ // Tracks coding sessions, tool usage, and prompts for bashstats achievements.
881
+
882
+ import Database from "better-sqlite3";
883
+
884
+ const DB_PATH = ${JSON.stringify(dbPath)};
885
+
886
+ function today(): string {
887
+ const d = new Date();
888
+ const pad = (n: number) => String(n).padStart(2, "0");
889
+ return \`\${d.getFullYear()}-\${pad(d.getMonth() + 1)}-\${pad(d.getDate())}\`;
890
+ }
891
+
892
+ function localNow(): string {
893
+ const d = new Date();
894
+ const pad = (n: number) => String(n).padStart(2, "0");
895
+ const ms = String(d.getMilliseconds()).padStart(3, "0");
896
+ return \`\${d.getFullYear()}-\${pad(d.getMonth() + 1)}-\${pad(d.getDate())}T\${pad(d.getHours())}:\${pad(d.getMinutes())}:\${pad(d.getSeconds())}.\${ms}\`;
897
+ }
898
+
899
+ function withDb<T>(fn: (db: InstanceType<typeof Database>) => T): T | undefined {
900
+ let db: InstanceType<typeof Database> | undefined;
901
+ try {
902
+ db = new Database(DB_PATH);
903
+ db.pragma("journal_mode = WAL");
904
+ db.pragma("busy_timeout = 5000");
905
+ return fn(db);
906
+ } catch {
907
+ // silently ignore DB errors so we never break the host
908
+ } finally {
909
+ try { db?.close(); } catch {}
910
+ }
911
+ return undefined;
912
+ }
913
+
914
+ function insertEvent(db: InstanceType<typeof Database>, sessionId: string, hookType: string, toolName?: string, toolInput?: string, project?: string): void {
915
+ db.prepare(
916
+ "INSERT INTO events (session_id, hook_type, tool_name, tool_input, cwd, project, timestamp) VALUES (?, ?, ?, ?, ?, ?, ?)"
917
+ ).run(sessionId, hookType, toolName ?? null, toolInput ?? null, null, project ?? null, localNow());
918
+ }
919
+
920
+ function incrementDaily(db: InstanceType<typeof Database>, increments: { sessions?: number; prompts?: number; tool_calls?: number; errors?: number; duration_seconds?: number }): void {
921
+ const date = today();
922
+ db.prepare(
923
+ \`INSERT INTO daily_activity (date, sessions, prompts, tool_calls, errors, duration_seconds, input_tokens, output_tokens, cache_creation_input_tokens, cache_read_input_tokens)
924
+ VALUES (?, ?, ?, ?, ?, ?, 0, 0, 0, 0)
925
+ ON CONFLICT(date) DO UPDATE SET
926
+ sessions = sessions + excluded.sessions,
927
+ prompts = prompts + excluded.prompts,
928
+ tool_calls = tool_calls + excluded.tool_calls,
929
+ errors = errors + excluded.errors,
930
+ duration_seconds = duration_seconds + excluded.duration_seconds\`
931
+ ).run(
932
+ date,
933
+ increments.sessions ?? 0,
934
+ increments.prompts ?? 0,
935
+ increments.tool_calls ?? 0,
936
+ increments.errors ?? 0,
937
+ increments.duration_seconds ?? 0,
938
+ );
939
+ }
940
+
941
+ export default async ({ project, directory }: { project?: string; directory?: string }) => {
942
+ const sessionId = \`opencode-\${Date.now()}\`;
943
+ const startedAt = localNow();
944
+ const projectName = project ?? directory ?? null;
945
+
946
+ // Create session on plugin load
947
+ withDb((db) => {
948
+ db.prepare(
949
+ "INSERT OR IGNORE INTO sessions (id, agent, started_at, project) VALUES (?, ?, ?, ?)"
950
+ ).run(sessionId, "opencode", startedAt, projectName);
951
+ incrementDaily(db, { sessions: 1 });
952
+ });
953
+
954
+ return {
955
+ event: async ({ event }: { event: { type: string; properties?: Record<string, unknown> } }) => {
956
+ const eventType = event.type;
957
+ const props = event.properties ?? {};
958
+
959
+ if (eventType === "session.created") {
960
+ withDb((db) => {
961
+ db.prepare(
962
+ "INSERT OR IGNORE INTO sessions (id, agent, started_at, project) VALUES (?, ?, ?, ?)"
963
+ ).run(sessionId, "opencode", startedAt, projectName);
964
+ incrementDaily(db, { sessions: 1 });
965
+ });
966
+ }
967
+
968
+ else if (eventType === "session.idle" || eventType === "session.deleted") {
969
+ withDb((db) => {
970
+ const endedAt = localNow();
971
+ const startMs = new Date(startedAt).getTime();
972
+ const endMs = new Date(endedAt).getTime();
973
+ const durationSeconds = Math.max(0, Math.floor((endMs - startMs) / 1000));
974
+ const stopReason = eventType === "session.idle" ? "idle" : "deleted";
975
+ db.prepare(
976
+ "UPDATE sessions SET ended_at = ?, stop_reason = ?, duration_seconds = ? WHERE id = ?"
977
+ ).run(endedAt, stopReason, durationSeconds, sessionId);
978
+ incrementDaily(db, { duration_seconds: durationSeconds });
979
+ });
980
+ }
981
+
982
+ else if (eventType === "tool.execute.before") {
983
+ withDb((db) => {
984
+ const toolName = (props.tool as string) ?? "unknown";
985
+ const toolInput = props.input ? JSON.stringify(props.input) : null;
986
+ insertEvent(db, sessionId, "PreToolUse", toolName, toolInput, projectName ?? undefined);
987
+ });
988
+ }
989
+
990
+ else if (eventType === "tool.execute.after") {
991
+ withDb((db) => {
992
+ const toolName = (props.tool as string) ?? "unknown";
993
+ const toolInput = props.input ? JSON.stringify(props.input) : null;
994
+ insertEvent(db, sessionId, "PostToolUse", toolName, toolInput, projectName ?? undefined);
995
+ db.prepare(
996
+ "UPDATE sessions SET tool_count = tool_count + 1 WHERE id = ?"
997
+ ).run(sessionId);
998
+ incrementDaily(db, { tool_calls: 1 });
999
+ });
1000
+ }
1001
+
1002
+ else if (eventType === "session.error") {
1003
+ withDb((db) => {
1004
+ const toolName = (props.tool as string) ?? null;
1005
+ insertEvent(db, sessionId, "PostToolUseFailure", toolName ?? undefined, undefined, projectName ?? undefined);
1006
+ db.prepare(
1007
+ "UPDATE sessions SET error_count = error_count + 1 WHERE id = ?"
1008
+ ).run(sessionId);
1009
+ incrementDaily(db, { errors: 1 });
1010
+ });
1011
+ }
1012
+
1013
+ else if (eventType === "session.compacted") {
1014
+ withDb((db) => {
1015
+ insertEvent(db, sessionId, "PreCompact", undefined, undefined, projectName ?? undefined);
1016
+ });
1017
+ }
1018
+
1019
+ else if (eventType === "message.updated") {
1020
+ const role = props.role as string | undefined;
1021
+ if (role === "user") {
1022
+ withDb((db) => {
1023
+ const content = (props.content as string) ?? "";
1024
+ const charCount = content.length;
1025
+ const wordCount = content.trim() === "" ? 0 : content.trim().split(/\\s+/).length;
1026
+ db.prepare(
1027
+ "INSERT INTO prompts (session_id, content, char_count, word_count, timestamp) VALUES (?, ?, ?, ?, ?)"
1028
+ ).run(sessionId, content, charCount, wordCount, localNow());
1029
+ db.prepare(
1030
+ "UPDATE sessions SET prompt_count = prompt_count + 1 WHERE id = ?"
1031
+ ).run(sessionId);
1032
+ incrementDaily(db, { prompts: 1 });
1033
+ });
1034
+ }
1035
+ }
1036
+ },
1037
+ };
1038
+ };
1039
+ `;
1040
+ }
1041
+ function installOpenCode() {
1042
+ try {
1043
+ const dataDir = path4.join(os4.homedir(), DATA_DIR);
1044
+ fs4.mkdirSync(dataDir, { recursive: true });
1045
+ const dbPath = path4.join(dataDir, DB_FILENAME);
1046
+ const pluginsDir = getOpenCodePluginsDir();
1047
+ fs4.mkdirSync(pluginsDir, { recursive: true });
1048
+ const pluginPath = getOpenCodePluginPath();
1049
+ const content = getOpenCodePluginContent(dbPath);
1050
+ fs4.writeFileSync(pluginPath, content, "utf-8");
1051
+ return { success: true, message: `bashstats plugin installed at ${pluginPath}` };
1052
+ } catch (err) {
1053
+ const message = err instanceof Error ? err.message : String(err);
1054
+ return { success: false, message: `OpenCode plugin install failed: ${message}` };
1055
+ }
1056
+ }
1057
+ function uninstallOpenCode() {
1058
+ try {
1059
+ const pluginPath = getOpenCodePluginPath();
1060
+ if (fs4.existsSync(pluginPath)) {
1061
+ fs4.unlinkSync(pluginPath);
1062
+ return { success: true, message: "bashstats plugin removed from OpenCode." };
1063
+ }
1064
+ return { success: true, message: "No bashstats plugin found; nothing to uninstall." };
1065
+ } catch (err) {
1066
+ const message = err instanceof Error ? err.message : String(err);
1067
+ return { success: false, message: `OpenCode plugin uninstall failed: ${message}` };
1068
+ }
1069
+ }
1070
+
1071
+ // src/db/writer.ts
1072
+ import path5 from "path";
606
1073
  var BashStatsWriter = class {
607
1074
  db;
608
1075
  constructor(db) {
609
1076
  this.db = db;
610
1077
  }
611
1078
  extractProject(cwd) {
612
- return path2.basename(cwd);
1079
+ return path5.basename(cwd);
613
1080
  }
614
1081
  today() {
615
1082
  const d = /* @__PURE__ */ new Date();
@@ -792,17 +1259,17 @@ var BashStatsWriter = class {
792
1259
  };
793
1260
 
794
1261
  // src/hooks/handler.ts
795
- import path3 from "path";
796
- import os2 from "os";
797
- import fs3 from "fs";
1262
+ import path6 from "path";
1263
+ import os5 from "os";
1264
+ import fs6 from "fs";
798
1265
 
799
1266
  // src/hooks/transcript.ts
800
- import fs2 from "fs";
1267
+ import fs5 from "fs";
801
1268
  import readline from "readline";
802
1269
  async function extractTokenUsage(transcriptPath) {
803
1270
  try {
804
- if (!fs2.existsSync(transcriptPath)) return null;
805
- const stream = fs2.createReadStream(transcriptPath, { encoding: "utf-8" });
1271
+ if (!fs5.existsSync(transcriptPath)) return null;
1272
+ const stream = fs5.createReadStream(transcriptPath, { encoding: "utf-8" });
806
1273
  const rl = readline.createInterface({ input: stream, crlfDelay: Infinity });
807
1274
  const seenMessages = /* @__PURE__ */ new Map();
808
1275
  for await (const line of rl) {
@@ -844,9 +1311,145 @@ async function extractTokenUsage(transcriptPath) {
844
1311
  }
845
1312
  }
846
1313
 
1314
+ // src/hooks/normalizers/gemini.ts
1315
+ function normalizeGeminiEvent(geminiEvent, raw) {
1316
+ switch (geminiEvent) {
1317
+ case "SessionStart":
1318
+ return { hookType: "SessionStart", payload: raw };
1319
+ case "SessionEnd":
1320
+ return {
1321
+ hookType: "Stop",
1322
+ payload: { ...raw, stop_hook_active: false }
1323
+ };
1324
+ case "BeforeAgent":
1325
+ return { hookType: "UserPromptSubmit", payload: raw };
1326
+ case "BeforeTool":
1327
+ return { hookType: "PreToolUse", payload: raw };
1328
+ case "AfterTool":
1329
+ return { hookType: "PostToolUse", payload: raw };
1330
+ case "AfterModel": {
1331
+ const usageMetadata = raw.llm_response?.usageMetadata;
1332
+ const totalTokenCount = typeof usageMetadata?.totalTokenCount === "number" ? usageMetadata.totalTokenCount : void 0;
1333
+ return {
1334
+ hookType: "AfterModel",
1335
+ payload: raw,
1336
+ ...totalTokenCount !== void 0 ? { tokenData: { totalTokenCount } } : {}
1337
+ };
1338
+ }
1339
+ case "PreCompress":
1340
+ return { hookType: "PreCompact", payload: raw };
1341
+ case "Notification":
1342
+ return { hookType: "Notification", payload: raw };
1343
+ default:
1344
+ return null;
1345
+ }
1346
+ }
1347
+
1348
+ // src/hooks/normalizers/copilot.ts
1349
+ function parseToolArgs(toolArgs) {
1350
+ if (!toolArgs || typeof toolArgs !== "string") return {};
1351
+ try {
1352
+ return JSON.parse(toolArgs);
1353
+ } catch {
1354
+ return {};
1355
+ }
1356
+ }
1357
+ function deriveSessionId() {
1358
+ const date = (/* @__PURE__ */ new Date()).toISOString().slice(0, 10);
1359
+ const ppid = process.ppid ?? process.pid;
1360
+ return `copilot-${ppid}-${date}`;
1361
+ }
1362
+ function mapSource(source) {
1363
+ if (source === "resume") return "resume";
1364
+ return "startup";
1365
+ }
1366
+ function isToolFailure(toolResult) {
1367
+ if (!toolResult) return false;
1368
+ const resultType = toolResult.resultType;
1369
+ return resultType === "failure" || resultType === "denied";
1370
+ }
1371
+ function normalizeCopilotEvent(copilotEvent, raw) {
1372
+ const sessionId = deriveSessionId();
1373
+ switch (copilotEvent) {
1374
+ case "sessionStart": {
1375
+ return {
1376
+ hookType: "SessionStart",
1377
+ payload: {
1378
+ session_id: sessionId,
1379
+ source: mapSource(raw.source),
1380
+ ...raw.model !== void 0 ? { model: raw.model } : {}
1381
+ }
1382
+ };
1383
+ }
1384
+ case "sessionEnd": {
1385
+ return {
1386
+ hookType: "Stop",
1387
+ payload: {
1388
+ session_id: sessionId,
1389
+ ...raw,
1390
+ stop_hook_active: false
1391
+ }
1392
+ };
1393
+ }
1394
+ case "userPromptSubmitted": {
1395
+ return {
1396
+ hookType: "UserPromptSubmit",
1397
+ payload: {
1398
+ session_id: sessionId,
1399
+ prompt: raw.prompt
1400
+ }
1401
+ };
1402
+ }
1403
+ case "preToolUse": {
1404
+ return {
1405
+ hookType: "PreToolUse",
1406
+ payload: {
1407
+ session_id: sessionId,
1408
+ tool_name: raw.toolName,
1409
+ tool_input: parseToolArgs(raw.toolArgs)
1410
+ }
1411
+ };
1412
+ }
1413
+ case "postToolUse": {
1414
+ const toolResult = raw.toolResult ?? {};
1415
+ const failed = isToolFailure(
1416
+ raw.toolResult
1417
+ );
1418
+ return {
1419
+ hookType: failed ? "PostToolUseFailure" : "PostToolUse",
1420
+ payload: {
1421
+ session_id: sessionId,
1422
+ tool_name: raw.toolName,
1423
+ tool_input: parseToolArgs(raw.toolArgs),
1424
+ tool_response: toolResult,
1425
+ exit_code: failed ? 1 : 0
1426
+ }
1427
+ };
1428
+ }
1429
+ case "errorOccurred": {
1430
+ const error = raw.error ?? {};
1431
+ return {
1432
+ hookType: "PostToolUseFailure",
1433
+ payload: {
1434
+ session_id: sessionId,
1435
+ tool_name: "_error",
1436
+ tool_input: {},
1437
+ tool_response: {
1438
+ error_message: error.message ?? "",
1439
+ error_name: error.name ?? ""
1440
+ },
1441
+ exit_code: 1
1442
+ }
1443
+ };
1444
+ }
1445
+ default:
1446
+ return null;
1447
+ }
1448
+ }
1449
+
847
1450
  // src/hooks/handler.ts
848
1451
  function detectAgent() {
849
- if (process.env.GEMINI_CLI || process.env.GEMINI_API_KEY) return "gemini-cli";
1452
+ if (process.env.GEMINI_SESSION_ID || process.env.GEMINI_PROJECT_DIR || process.env.GEMINI_CLI) return "gemini-cli";
850
1453
  if (process.env.GITHUB_COPILOT_CLI) return "copilot-cli";
851
1454
  if (process.env.OPENCODE) return "opencode";
852
1455
  return "claude-code";
@@ -860,10 +1463,10 @@ function parseHookEvent(input) {
860
1463
  }
861
1464
  }
862
1465
  function getDataDir() {
863
- return path3.join(os2.homedir(), DATA_DIR);
1466
+ return path6.join(os5.homedir(), DATA_DIR);
864
1467
  }
865
1468
  function getDbPath() {
866
- return path3.join(os2.homedir(), DATA_DIR, DB_FILENAME);
1469
+ return path6.join(os5.homedir(), DATA_DIR, DB_FILENAME);
867
1470
  }
868
1471
  async function readStdin() {
869
1472
  if (process.env.CLAUDE_HOOK_EVENT) {
@@ -885,78 +1488,120 @@ async function handleHookEvent(hookType) {
885
1488
  const event = parseHookEvent(raw);
886
1489
  if (!event) return;
887
1490
  const dataDir = getDataDir();
888
- fs3.mkdirSync(dataDir, { recursive: true });
1491
+ fs6.mkdirSync(dataDir, { recursive: true });
889
1492
  const dbPath = getDbPath();
890
1493
  const db = new BashStatsDB(dbPath);
891
1494
  const writer = new BashStatsWriter(db);
892
1495
  try {
893
- const sessionId = event.session_id ?? "";
894
- const cwd = event.cwd ?? "";
895
- switch (hookType) {
1496
+ const agent = detectAgent();
1497
+ let effectiveHookType = hookType;
1498
+ let payload = event;
1499
+ if (agent === "gemini-cli") {
1500
+ const normalized = normalizeGeminiEvent(hookType, event);
1501
+ if (!normalized) return;
1502
+ if (normalized.hookType === "AfterModel") {
1503
+ if (normalized.tokenData) {
1504
+ const sessionId2 = event.session_id ?? "";
1505
+ const metaKey = `gemini_tokens_${sessionId2}`;
1506
+ const existing = db.getMetadata(metaKey);
1507
+ const prev = existing ? parseInt(existing, 10) : 0;
1508
+ db.setMetadata(metaKey, String(prev + normalized.tokenData.totalTokenCount));
1509
+ }
1510
+ return;
1511
+ }
1512
+ effectiveHookType = normalized.hookType;
1513
+ payload = normalized.payload;
1514
+ } else if (agent === "copilot-cli") {
1515
+ const normalized = normalizeCopilotEvent(hookType, event);
1516
+ if (!normalized) return;
1517
+ effectiveHookType = normalized.hookType;
1518
+ payload = normalized.payload;
1519
+ }
1520
+ const sessionId = payload.session_id ?? "";
1521
+ const cwd = payload.cwd ?? "";
1522
+ switch (effectiveHookType) {
896
1523
  case "SessionStart": {
897
- const source = event.source ?? "startup";
898
- const agent = detectAgent();
1524
+ const source = payload.source ?? "startup";
899
1525
  writer.recordSessionStart(sessionId, cwd, source, agent);
900
1526
  break;
901
1527
  }
902
1528
  case "UserPromptSubmit": {
903
- const prompt = event.prompt ?? "";
1529
+ const prompt = payload.prompt ?? "";
904
1530
  writer.recordPrompt(sessionId, prompt);
905
1531
  break;
906
1532
  }
907
1533
  case "PreToolUse": {
908
- const toolName = event.tool_name ?? "";
909
- const toolInput = event.tool_input ?? {};
1534
+ const toolName = payload.tool_name ?? "";
1535
+ const toolInput = payload.tool_input ?? {};
910
1536
  writer.recordToolUse(sessionId, "PreToolUse", toolName, toolInput, {}, 0, cwd);
911
1537
  break;
912
1538
  }
913
1539
  case "PostToolUse": {
914
- const toolName = event.tool_name ?? "";
915
- const toolInput = event.tool_input ?? {};
916
- const toolResponse = event.tool_response ?? {};
917
- const exitCode = event.exit_code ?? 0;
1540
+ const toolName = payload.tool_name ?? "";
1541
+ const toolInput = payload.tool_input ?? {};
1542
+ const toolResponse = payload.tool_response ?? {};
1543
+ const exitCode = payload.exit_code ?? 0;
918
1544
  writer.recordToolUse(sessionId, "PostToolUse", toolName, toolInput, toolResponse, exitCode, cwd);
919
1545
  break;
920
1546
  }
921
1547
  case "PostToolUseFailure": {
922
- const toolName = event.tool_name ?? "";
923
- const toolInput = event.tool_input ?? {};
924
- const toolResponse = event.tool_response ?? {};
1548
+ const toolName = payload.tool_name ?? "";
1549
+ const toolInput = payload.tool_input ?? {};
1550
+ const toolResponse = payload.tool_response ?? {};
925
1551
  writer.recordToolUse(sessionId, "PostToolUseFailure", toolName, toolInput, toolResponse, 1, cwd);
926
1552
  break;
927
1553
  }
928
1554
  case "Stop": {
929
- const rawPath = event.transcript_path ?? "";
930
- const transcriptPath = rawPath && rawPath.endsWith(".jsonl") ? path3.resolve(rawPath) : "";
931
- const tokens = transcriptPath ? await extractTokenUsage(transcriptPath) : null;
1555
+ let tokens = null;
1556
+ if (agent === "gemini-cli") {
1557
+ const metaKey = `gemini_tokens_${sessionId}`;
1558
+ const stored = db.getMetadata(metaKey);
1559
+ if (stored) {
1560
+ const totalTokens = parseInt(stored, 10);
1561
+ const inputTokens = Math.round(totalTokens * 0.7);
1562
+ const outputTokens = totalTokens - inputTokens;
1563
+ tokens = {
1564
+ input_tokens: inputTokens,
1565
+ output_tokens: outputTokens,
1566
+ cache_creation_input_tokens: 0,
1567
+ cache_read_input_tokens: 0
1568
+ };
1569
+ }
1570
+ } else if (agent === "copilot-cli") {
1571
+ tokens = null;
1572
+ } else {
1573
+ const rawPath = payload.transcript_path ?? "";
1574
+ const transcriptPath = rawPath && rawPath.endsWith(".jsonl") ? path6.resolve(rawPath) : "";
1575
+ tokens = transcriptPath ? await extractTokenUsage(transcriptPath) : null;
1576
+ }
932
1577
  writer.recordSessionEnd(sessionId, "stopped", tokens);
933
1578
  break;
934
1579
  }
935
1580
  case "Notification": {
936
- const message = event.message ?? "";
937
- const notificationType = event.notification_type ?? "";
1581
+ const message = payload.message ?? "";
1582
+ const notificationType = payload.notification_type ?? "";
938
1583
  writer.recordNotification(sessionId, message, notificationType);
939
1584
  break;
940
1585
  }
941
1586
  case "SubagentStart": {
942
- const agentId = event.agent_id ?? "";
943
- const agentType = event.agent_type ?? "";
1587
+ const agentId = payload.agent_id ?? "";
1588
+ const agentType = payload.agent_type ?? "";
944
1589
  writer.recordSubagent(sessionId, "SubagentStart", agentId, agentType);
945
1590
  break;
946
1591
  }
947
1592
  case "SubagentStop": {
948
- const agentId = event.agent_id ?? "";
1593
+ const agentId = payload.agent_id ?? "";
949
1594
  writer.recordSubagent(sessionId, "SubagentStop", agentId);
950
1595
  break;
951
1596
  }
952
1597
  case "PreCompact": {
953
- const trigger = event.trigger ?? "manual";
1598
+ const trigger = payload.trigger ?? "manual";
954
1599
  writer.recordCompaction(sessionId, trigger);
955
1600
  break;
956
1601
  }
957
1602
  case "PermissionRequest": {
958
- const toolName = event.tool_name ?? "";
959
- const toolInput = event.tool_input ?? {};
1603
+ const toolName = payload.tool_name ?? "";
1604
+ const toolInput = payload.tool_input ?? {};
960
1605
  writer.recordToolUse(sessionId, "PermissionRequest", toolName, toolInput, {}, 0, cwd);
961
1606
  break;
962
1607
  }
@@ -970,6 +1615,25 @@ async function handleHookEvent(hookType) {
970
1615
  }
971
1616
 
972
1617
  // src/stats/engine.ts
1618
+ function localDateStr(d = /* @__PURE__ */ new Date()) {
1619
+ const pad = (n) => String(n).padStart(2, "0");
1620
+ return `${d.getFullYear()}-${pad(d.getMonth() + 1)}-${pad(d.getDate())}`;
1621
+ }
1622
+ function selectWeekChallenges(weekStart) {
1623
+ let hash = 0;
1624
+ for (const c of weekStart) {
1625
+ hash = hash * 31 + c.charCodeAt(0) | 0;
1626
+ }
1627
+ hash = Math.abs(hash);
1628
+ const pool = [...WEEKLY_CHALLENGES];
1629
+ const selected = [];
1630
+ for (let i = 0; i < 3 && pool.length > 0; i++) {
1631
+ const idx = hash % pool.length;
1632
+ selected.push(pool.splice(idx, 1)[0]);
1633
+ hash = Math.abs(hash * 127 + 63 | 0);
1634
+ }
1635
+ return selected;
1636
+ }
973
1637
  var StatsEngine = class {
974
1638
  db;
975
1639
  constructor(db) {
@@ -980,58 +1644,87 @@ var StatsEngine = class {
980
1644
  if (!row) return 0;
981
1645
  return Object.values(row)[0] ?? 0;
982
1646
  }
983
- getLifetimeStats() {
984
- const totalSessions = this.queryScalar("SELECT COUNT(*) as c FROM sessions");
985
- const totalPrompts = this.queryScalar("SELECT COUNT(*) as c FROM prompts");
986
- const totalCharsTyped = this.queryScalar("SELECT COALESCE(SUM(char_count), 0) as c FROM prompts");
1647
+ /** Returns a WHERE/AND clause filtering sessions by agent */
1648
+ agentWhere(agent, alias) {
1649
+ if (!agent) return { clause: "", params: [] };
1650
+ const col = alias ? `${alias}.agent` : "agent";
1651
+ return { clause: ` AND ${col} = ?`, params: [agent] };
1652
+ }
1653
+ /** Returns a subquery filter for events/prompts tables */
1654
+ agentSessionFilter(agent) {
1655
+ if (!agent) return { clause: "", params: [] };
1656
+ return { clause: ` AND session_id IN (SELECT id FROM sessions WHERE agent = ?)`, params: [agent] };
1657
+ }
1658
+ getLifetimeStats(agent) {
1659
+ const sf = this.agentWhere(agent);
1660
+ const ef = this.agentSessionFilter(agent);
1661
+ const totalSessions = this.queryScalar("SELECT COUNT(*) as c FROM sessions WHERE 1=1" + sf.clause, ...sf.params);
1662
+ const totalPrompts = this.queryScalar("SELECT COUNT(*) as c FROM prompts WHERE 1=1" + ef.clause, ...ef.params);
1663
+ const totalCharsTyped = this.queryScalar("SELECT COALESCE(SUM(char_count), 0) as c FROM prompts WHERE 1=1" + ef.clause, ...ef.params);
987
1664
  const totalToolCalls = this.queryScalar(
988
- "SELECT COUNT(*) as c FROM events WHERE hook_type IN ('PostToolUse', 'PostToolUseFailure')"
1665
+ "SELECT COUNT(*) as c FROM events WHERE hook_type IN ('PostToolUse', 'PostToolUseFailure')" + ef.clause,
1666
+ ...ef.params
989
1667
  );
990
1668
  const totalDurationSeconds = this.queryScalar(
991
- "SELECT COALESCE(SUM(duration_seconds), 0) as c FROM sessions"
1669
+ "SELECT COALESCE(SUM(duration_seconds), 0) as c FROM sessions WHERE 1=1" + sf.clause,
1670
+ ...sf.params
992
1671
  );
993
1672
  const totalFilesRead = this.queryScalar(
994
- "SELECT COUNT(*) as c FROM events WHERE tool_name = 'Read' AND hook_type = 'PostToolUse'"
1673
+ "SELECT COUNT(*) as c FROM events WHERE tool_name = 'Read' AND hook_type = 'PostToolUse'" + ef.clause,
1674
+ ...ef.params
995
1675
  );
996
1676
  const totalFilesWritten = this.queryScalar(
997
- "SELECT COUNT(*) as c FROM events WHERE tool_name = 'Write' AND hook_type IN ('PostToolUse', 'PostToolUseFailure')"
1677
+ "SELECT COUNT(*) as c FROM events WHERE tool_name = 'Write' AND hook_type IN ('PostToolUse', 'PostToolUseFailure')" + ef.clause,
1678
+ ...ef.params
998
1679
  );
999
1680
  const totalFilesEdited = this.queryScalar(
1000
- "SELECT COUNT(*) as c FROM events WHERE tool_name = 'Edit' AND hook_type IN ('PostToolUse', 'PostToolUseFailure')"
1681
+ "SELECT COUNT(*) as c FROM events WHERE tool_name = 'Edit' AND hook_type IN ('PostToolUse', 'PostToolUseFailure')" + ef.clause,
1682
+ ...ef.params
1001
1683
  );
1002
1684
  const totalFilesCreated = totalFilesWritten;
1003
1685
  const totalBashCommands = this.queryScalar(
1004
- "SELECT COUNT(*) as c FROM events WHERE tool_name = 'Bash' AND hook_type IN ('PostToolUse', 'PostToolUseFailure')"
1686
+ "SELECT COUNT(*) as c FROM events WHERE tool_name = 'Bash' AND hook_type IN ('PostToolUse', 'PostToolUseFailure')" + ef.clause,
1687
+ ...ef.params
1005
1688
  );
1006
1689
  const totalWebSearches = this.queryScalar(
1007
- "SELECT COUNT(*) as c FROM events WHERE tool_name = 'WebSearch' AND hook_type IN ('PostToolUse', 'PostToolUseFailure')"
1690
+ "SELECT COUNT(*) as c FROM events WHERE tool_name = 'WebSearch' AND hook_type IN ('PostToolUse', 'PostToolUseFailure')" + ef.clause,
1691
+ ...ef.params
1008
1692
  );
1009
1693
  const totalWebFetches = this.queryScalar(
1010
- "SELECT COUNT(*) as c FROM events WHERE tool_name = 'WebFetch' AND hook_type IN ('PostToolUse', 'PostToolUseFailure')"
1694
+ "SELECT COUNT(*) as c FROM events WHERE tool_name = 'WebFetch' AND hook_type IN ('PostToolUse', 'PostToolUseFailure')" + ef.clause,
1695
+ ...ef.params
1011
1696
  );
1012
1697
  const totalSubagents = this.queryScalar(
1013
- "SELECT COUNT(*) as c FROM events WHERE hook_type = 'SubagentStart'"
1698
+ "SELECT COUNT(*) as c FROM events WHERE hook_type = 'SubagentStart'" + ef.clause,
1699
+ ...ef.params
1014
1700
  );
1015
1701
  const totalCompactions = this.queryScalar(
1016
- "SELECT COUNT(*) as c FROM events WHERE hook_type = 'PreCompact'"
1702
+ "SELECT COUNT(*) as c FROM events WHERE hook_type = 'PreCompact'" + ef.clause,
1703
+ ...ef.params
1017
1704
  );
1018
1705
  const totalErrors = this.queryScalar(
1019
- `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"%'))`
1706
+ `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"%')))` + ef.clause,
1707
+ ...ef.params
1020
1708
  );
1021
1709
  const totalRateLimits = this.queryScalar(
1022
- "SELECT COUNT(*) as c FROM events WHERE hook_type = 'Notification' AND tool_input LIKE '%rate_limit%'"
1710
+ "SELECT COUNT(*) as c FROM events WHERE hook_type = 'Notification' AND tool_input LIKE '%rate_limit%'" + ef.clause,
1711
+ ...ef.params
1023
1712
  );
1024
1713
  const totalInputTokens = this.queryScalar(
1025
- "SELECT COALESCE(SUM(input_tokens), 0) as c FROM sessions"
1714
+ "SELECT COALESCE(SUM(input_tokens), 0) as c FROM sessions WHERE 1=1" + sf.clause,
1715
+ ...sf.params
1026
1716
  );
1027
1717
  const totalOutputTokens = this.queryScalar(
1028
- "SELECT COALESCE(SUM(output_tokens), 0) as c FROM sessions"
1718
+ "SELECT COALESCE(SUM(output_tokens), 0) as c FROM sessions WHERE 1=1" + sf.clause,
1719
+ ...sf.params
1029
1720
  );
1030
1721
  const totalCacheCreationTokens = this.queryScalar(
1031
- "SELECT COALESCE(SUM(cache_creation_input_tokens), 0) as c FROM sessions"
1722
+ "SELECT COALESCE(SUM(cache_creation_input_tokens), 0) as c FROM sessions WHERE 1=1" + sf.clause,
1723
+ ...sf.params
1032
1724
  );
1033
1725
  const totalCacheReadTokens = this.queryScalar(
1034
- "SELECT COALESCE(SUM(cache_read_input_tokens), 0) as c FROM sessions"
1726
+ "SELECT COALESCE(SUM(cache_read_input_tokens), 0) as c FROM sessions WHERE 1=1" + sf.clause,
1727
+ ...sf.params
1035
1728
  );
1036
1729
  const totalTokens = totalInputTokens + totalOutputTokens + totalCacheCreationTokens + totalCacheReadTokens;
1037
1730
  return {
@@ -1058,17 +1751,20 @@ var StatsEngine = class {
1058
1751
  totalTokens
1059
1752
  };
1060
1753
  }
1061
- getToolBreakdown() {
1754
+ getToolBreakdown(agent) {
1755
+ const ef = this.agentSessionFilter(agent);
1062
1756
  const rows = this.db.prepare(
1063
- "SELECT tool_name, COUNT(*) as cnt FROM events WHERE hook_type = 'PostToolUse' AND tool_name IS NOT NULL GROUP BY tool_name"
1064
- ).all();
1757
+ "SELECT tool_name, COUNT(*) as cnt FROM events WHERE hook_type = 'PostToolUse' AND tool_name IS NOT NULL" + ef.clause + " GROUP BY tool_name"
1758
+ ).all(...ef.params);
1065
1759
  const breakdown = {};
1066
1760
  for (const row of rows) {
1067
1761
  breakdown[row.tool_name] = row.cnt;
1068
1762
  }
1069
1763
  return breakdown;
1070
1764
  }
1071
- getTimeStats() {
1765
+ getTimeStats(agent) {
1766
+ const sf = this.agentWhere(agent);
1767
+ const ef = this.agentSessionFilter(agent);
1072
1768
  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();
1073
1769
  let longestStreak = 0;
1074
1770
  let currentStreak = 0;
@@ -1086,48 +1782,50 @@ var StatsEngine = class {
1086
1782
  }
1087
1783
  }
1088
1784
  longestStreak = Math.max(longestStreak, streak);
1089
- const today = /* @__PURE__ */ new Date();
1090
- const todayStr = today.toISOString().slice(0, 10);
1091
1785
  const activeDates = new Set(dailyRows.map((r) => r.date));
1092
- let checkDate = /* @__PURE__ */ new Date(todayStr + "T00:00:00Z");
1786
+ const todayStr = localDateStr();
1787
+ let checkDate = /* @__PURE__ */ new Date();
1093
1788
  currentStreak = 0;
1094
1789
  if (activeDates.has(todayStr)) {
1095
1790
  currentStreak = 1;
1096
- checkDate.setUTCDate(checkDate.getUTCDate() - 1);
1097
- while (activeDates.has(checkDate.toISOString().slice(0, 10))) {
1791
+ checkDate.setDate(checkDate.getDate() - 1);
1792
+ while (activeDates.has(localDateStr(checkDate))) {
1098
1793
  currentStreak++;
1099
- checkDate.setUTCDate(checkDate.getUTCDate() - 1);
1794
+ checkDate.setDate(checkDate.getDate() - 1);
1100
1795
  }
1101
1796
  } else {
1102
- checkDate.setUTCDate(checkDate.getUTCDate() - 1);
1103
- const yesterdayStr = checkDate.toISOString().slice(0, 10);
1797
+ checkDate.setDate(checkDate.getDate() - 1);
1798
+ const yesterdayStr = localDateStr(checkDate);
1104
1799
  if (activeDates.has(yesterdayStr)) {
1105
1800
  currentStreak = 1;
1106
- checkDate.setUTCDate(checkDate.getUTCDate() - 1);
1107
- while (activeDates.has(checkDate.toISOString().slice(0, 10))) {
1801
+ checkDate.setDate(checkDate.getDate() - 1);
1802
+ while (activeDates.has(localDateStr(checkDate))) {
1108
1803
  currentStreak++;
1109
- checkDate.setUTCDate(checkDate.getUTCDate() - 1);
1804
+ checkDate.setDate(checkDate.getDate() - 1);
1110
1805
  }
1111
1806
  }
1112
1807
  }
1113
1808
  }
1114
1809
  const peakHourRow = this.db.prepare(
1115
- "SELECT CAST(strftime('%H', timestamp) AS INTEGER) as hour, COUNT(*) as cnt FROM prompts GROUP BY hour ORDER BY cnt DESC LIMIT 1"
1116
- ).get();
1810
+ "SELECT CAST(strftime('%H', timestamp) AS INTEGER) as hour, COUNT(*) as cnt FROM prompts WHERE 1=1" + ef.clause + " GROUP BY hour ORDER BY cnt DESC LIMIT 1"
1811
+ ).get(...ef.params);
1117
1812
  const peakHour = peakHourRow?.hour ?? 0;
1118
1813
  const peakHourCount = peakHourRow?.cnt ?? 0;
1119
1814
  const nightOwlCount = this.queryScalar(
1120
- "SELECT COUNT(*) as c FROM prompts WHERE CAST(strftime('%H', timestamp) AS INTEGER) < 5"
1815
+ "SELECT COUNT(*) as c FROM prompts WHERE CAST(strftime('%H', timestamp) AS INTEGER) < 5" + ef.clause,
1816
+ ...ef.params
1121
1817
  );
1122
1818
  const earlyBirdCount = this.queryScalar(
1123
- "SELECT COUNT(*) as c FROM prompts WHERE CAST(strftime('%H', timestamp) AS INTEGER) BETWEEN 5 AND 7"
1819
+ "SELECT COUNT(*) as c FROM prompts WHERE CAST(strftime('%H', timestamp) AS INTEGER) BETWEEN 5 AND 7" + ef.clause,
1820
+ ...ef.params
1124
1821
  );
1125
1822
  const weekendSessions = this.queryScalar(
1126
- "SELECT COUNT(*) as c FROM sessions WHERE CAST(strftime('%w', started_at) AS INTEGER) IN (0, 6)"
1823
+ "SELECT COUNT(*) as c FROM sessions WHERE CAST(strftime('%w', started_at) AS INTEGER) IN (0, 6)" + sf.clause,
1824
+ ...sf.params
1127
1825
  );
1128
1826
  const mostActiveDayRow = this.db.prepare(
1129
- "SELECT CAST(strftime('%w', started_at) AS INTEGER) as dow, COUNT(*) as cnt FROM sessions GROUP BY dow ORDER BY cnt DESC LIMIT 1"
1130
- ).get();
1827
+ "SELECT CAST(strftime('%w', started_at) AS INTEGER) as dow, COUNT(*) as cnt FROM sessions WHERE 1=1" + sf.clause + " GROUP BY dow ORDER BY cnt DESC LIMIT 1"
1828
+ ).get(...sf.params);
1131
1829
  const mostActiveDay = mostActiveDayRow?.dow ?? 0;
1132
1830
  const busiestDateRow = this.db.prepare(
1133
1831
  "SELECT date, (sessions + prompts + tool_calls) as total FROM daily_activity ORDER BY total DESC LIMIT 1"
@@ -1147,33 +1845,43 @@ var StatsEngine = class {
1147
1845
  busiestDateCount
1148
1846
  };
1149
1847
  }
1150
- getSessionRecords() {
1848
+ getSessionRecords(agent) {
1849
+ const sf = this.agentWhere(agent);
1151
1850
  const longestSessionSeconds = this.queryScalar(
1152
- "SELECT COALESCE(MAX(duration_seconds), 0) as c FROM sessions"
1851
+ "SELECT COALESCE(MAX(duration_seconds), 0) as c FROM sessions WHERE 1=1" + sf.clause,
1852
+ ...sf.params
1153
1853
  );
1154
1854
  const mostToolsInSession = this.queryScalar(
1155
- "SELECT COALESCE(MAX(tool_count), 0) as c FROM sessions"
1855
+ "SELECT COALESCE(MAX(tool_count), 0) as c FROM sessions WHERE 1=1" + sf.clause,
1856
+ ...sf.params
1156
1857
  );
1157
1858
  const mostPromptsInSession = this.queryScalar(
1158
- "SELECT COALESCE(MAX(prompt_count), 0) as c FROM sessions"
1859
+ "SELECT COALESCE(MAX(prompt_count), 0) as c FROM sessions WHERE 1=1" + sf.clause,
1860
+ ...sf.params
1159
1861
  );
1160
1862
  const fastestSessionSeconds = this.queryScalar(
1161
- "SELECT COALESCE(MIN(duration_seconds), 0) as c FROM sessions WHERE duration_seconds IS NOT NULL AND duration_seconds > 0"
1863
+ "SELECT COALESCE(MIN(duration_seconds), 0) as c FROM sessions WHERE duration_seconds IS NOT NULL AND duration_seconds > 0" + sf.clause,
1864
+ ...sf.params
1162
1865
  );
1163
1866
  const avgDurationSeconds = this.queryScalar(
1164
- "SELECT COALESCE(AVG(duration_seconds), 0) as c FROM sessions WHERE duration_seconds IS NOT NULL"
1867
+ "SELECT COALESCE(AVG(duration_seconds), 0) as c FROM sessions WHERE duration_seconds IS NOT NULL" + sf.clause,
1868
+ ...sf.params
1165
1869
  );
1166
1870
  const avgPromptsPerSession = this.queryScalar(
1167
- "SELECT COALESCE(AVG(prompt_count), 0) as c FROM sessions"
1871
+ "SELECT COALESCE(AVG(prompt_count), 0) as c FROM sessions WHERE 1=1" + sf.clause,
1872
+ ...sf.params
1168
1873
  );
1169
1874
  const avgToolsPerSession = this.queryScalar(
1170
- "SELECT COALESCE(AVG(tool_count), 0) as c FROM sessions"
1875
+ "SELECT COALESCE(AVG(tool_count), 0) as c FROM sessions WHERE 1=1" + sf.clause,
1876
+ ...sf.params
1171
1877
  );
1172
1878
  const mostTokensInSession = this.queryScalar(
1173
- "SELECT COALESCE(MAX(COALESCE(input_tokens, 0) + COALESCE(output_tokens, 0) + COALESCE(cache_creation_input_tokens, 0) + COALESCE(cache_read_input_tokens, 0)), 0) as c FROM sessions"
1879
+ "SELECT COALESCE(MAX(COALESCE(input_tokens, 0) + COALESCE(output_tokens, 0) + COALESCE(cache_creation_input_tokens, 0) + COALESCE(cache_read_input_tokens, 0)), 0) as c FROM sessions WHERE 1=1" + sf.clause,
1880
+ ...sf.params
1174
1881
  );
1175
1882
  const avgTokensPerSession = this.queryScalar(
1176
- "SELECT COALESCE(AVG(COALESCE(input_tokens, 0) + COALESCE(output_tokens, 0) + COALESCE(cache_creation_input_tokens, 0) + COALESCE(cache_read_input_tokens, 0)), 0) as c FROM sessions"
1883
+ "SELECT COALESCE(AVG(COALESCE(input_tokens, 0) + COALESCE(output_tokens, 0) + COALESCE(cache_creation_input_tokens, 0) + COALESCE(cache_read_input_tokens, 0)), 0) as c FROM sessions WHERE 1=1" + sf.clause,
1884
+ ...sf.params
1177
1885
  );
1178
1886
  return {
1179
1887
  longestSessionSeconds,
@@ -1187,13 +1895,15 @@ var StatsEngine = class {
1187
1895
  avgTokensPerSession: Math.round(avgTokensPerSession)
1188
1896
  };
1189
1897
  }
1190
- getProjectStats() {
1898
+ getProjectStats(agent) {
1899
+ const sf = this.agentWhere(agent);
1191
1900
  const uniqueProjects = this.queryScalar(
1192
- "SELECT COUNT(DISTINCT project) as c FROM sessions WHERE project IS NOT NULL"
1901
+ "SELECT COUNT(DISTINCT project) as c FROM sessions WHERE project IS NOT NULL" + sf.clause,
1902
+ ...sf.params
1193
1903
  );
1194
1904
  const projectRows = this.db.prepare(
1195
- "SELECT project, COUNT(*) as cnt FROM sessions WHERE project IS NOT NULL GROUP BY project ORDER BY cnt DESC"
1196
- ).all();
1905
+ "SELECT project, COUNT(*) as cnt FROM sessions WHERE project IS NOT NULL" + sf.clause + " GROUP BY project ORDER BY cnt DESC"
1906
+ ).all(...sf.params);
1197
1907
  const mostVisitedProject = projectRows.length > 0 ? projectRows[0].project : "";
1198
1908
  const mostVisitedProjectCount = projectRows.length > 0 ? projectRows[0].cnt : 0;
1199
1909
  const projectBreakdown = {};
@@ -1207,15 +1917,125 @@ var StatsEngine = class {
1207
1917
  projectBreakdown
1208
1918
  };
1209
1919
  }
1210
- getAllStats() {
1920
+ getAllStats(agent) {
1921
+ return {
1922
+ lifetime: this.getLifetimeStats(agent),
1923
+ tools: this.getToolBreakdown(agent),
1924
+ time: this.getTimeStats(agent),
1925
+ sessions: this.getSessionRecords(agent),
1926
+ projects: this.getProjectStats(agent)
1927
+ };
1928
+ }
1929
+ getAgentBreakdown() {
1930
+ const rows = this.db.prepare(
1931
+ "SELECT agent, COUNT(*) as cnt, COALESCE(SUM(duration_seconds), 0) as total_seconds FROM sessions GROUP BY agent ORDER BY cnt DESC"
1932
+ ).all();
1933
+ const sessionsPerAgent = {};
1934
+ const hoursPerAgent = {};
1935
+ for (const row of rows) {
1936
+ sessionsPerAgent[row.agent] = row.cnt;
1937
+ hoursPerAgent[row.agent] = Math.round(row.total_seconds / 3600 * 10) / 10;
1938
+ }
1211
1939
  return {
1212
- lifetime: this.getLifetimeStats(),
1213
- tools: this.getToolBreakdown(),
1214
- time: this.getTimeStats(),
1215
- sessions: this.getSessionRecords(),
1216
- projects: this.getProjectStats()
1940
+ favoriteAgent: rows.length > 0 ? rows[0].agent : "unknown",
1941
+ sessionsPerAgent,
1942
+ hoursPerAgent,
1943
+ distinctAgents: rows.length
1217
1944
  };
1218
1945
  }
1946
+ getWeeklyGoalsPayload() {
1947
+ const today = /* @__PURE__ */ new Date();
1948
+ const dayOfWeek = today.getDay();
1949
+ const mondayOffset = dayOfWeek === 0 ? -6 : 1 - dayOfWeek;
1950
+ const monday = new Date(today);
1951
+ monday.setDate(monday.getDate() + mondayOffset);
1952
+ const sunday = new Date(monday);
1953
+ sunday.setDate(sunday.getDate() + 6);
1954
+ const nextMonday = new Date(monday);
1955
+ nextMonday.setDate(nextMonday.getDate() + 7);
1956
+ const weekStart = localDateStr(monday);
1957
+ const weekEnd = localDateStr(sunday);
1958
+ const nextWeekStart = localDateStr(nextMonday);
1959
+ const challenges = selectWeekChallenges(weekStart);
1960
+ const daysActive = this.queryScalar(
1961
+ "SELECT COUNT(*) as c FROM daily_activity WHERE date >= ? AND date <= ? AND (sessions > 0 OR prompts > 0)",
1962
+ weekStart,
1963
+ weekEnd
1964
+ );
1965
+ const multiplier = ACTIVITY_MULTIPLIERS[Math.min(7, Math.max(1, daysActive))] ?? 1;
1966
+ const results = challenges.map((c) => {
1967
+ const current = this.computeWeeklyStat(c.stat, weekStart, weekEnd, nextWeekStart);
1968
+ return {
1969
+ id: c.id,
1970
+ description: c.description,
1971
+ xpReward: c.xpReward,
1972
+ completed: current >= c.threshold,
1973
+ progress: Math.min(1, current / Math.max(1, c.threshold)),
1974
+ threshold: c.threshold,
1975
+ current
1976
+ };
1977
+ });
1978
+ return {
1979
+ weekStart,
1980
+ daysActive,
1981
+ multiplier,
1982
+ challenges: results
1983
+ };
1984
+ }
1985
+ computeWeeklyStat(stat, weekStart, weekEnd, nextWeekStart) {
1986
+ switch (stat) {
1987
+ case "totalPrompts":
1988
+ return this.queryScalar("SELECT COALESCE(SUM(prompts), 0) as c FROM daily_activity WHERE date >= ? AND date <= ?", weekStart, weekEnd);
1989
+ case "totalToolCalls":
1990
+ return this.queryScalar("SELECT COALESCE(SUM(tool_calls), 0) as c FROM daily_activity WHERE date >= ? AND date <= ?", weekStart, weekEnd);
1991
+ case "totalSessions":
1992
+ return this.queryScalar("SELECT COALESCE(SUM(sessions), 0) as c FROM daily_activity WHERE date >= ? AND date <= ?", weekStart, weekEnd);
1993
+ case "daysActive":
1994
+ return this.queryScalar("SELECT COUNT(*) as c FROM daily_activity WHERE date >= ? AND date <= ? AND (sessions > 0 OR prompts > 0)", weekStart, weekEnd);
1995
+ case "totalHours": {
1996
+ const secs = this.queryScalar("SELECT COALESCE(SUM(duration_seconds), 0) as c FROM daily_activity WHERE date >= ? AND date <= ?", weekStart, weekEnd);
1997
+ return Math.round(secs / 3600 * 10) / 10;
1998
+ }
1999
+ case "totalFilesRead":
2000
+ return this.queryScalar("SELECT COUNT(*) as c FROM events WHERE tool_name = 'Read' AND hook_type = 'PostToolUse' AND timestamp >= ? AND timestamp < ?", weekStart, nextWeekStart);
2001
+ case "totalFilesEdited":
2002
+ return this.queryScalar("SELECT COUNT(*) as c FROM events WHERE tool_name = 'Edit' AND hook_type IN ('PostToolUse', 'PostToolUseFailure') AND timestamp >= ? AND timestamp < ?", weekStart, nextWeekStart);
2003
+ case "totalFilesCreated":
2004
+ return this.queryScalar("SELECT COUNT(*) as c FROM events WHERE tool_name = 'Write' AND hook_type IN ('PostToolUse', 'PostToolUseFailure') AND timestamp >= ? AND timestamp < ?", weekStart, nextWeekStart);
2005
+ case "totalBashCommands":
2006
+ return this.queryScalar("SELECT COUNT(*) as c FROM events WHERE tool_name = 'Bash' AND hook_type IN ('PostToolUse', 'PostToolUseFailure') AND timestamp >= ? AND timestamp < ?", weekStart, nextWeekStart);
2007
+ case "totalSearches":
2008
+ return this.queryScalar("SELECT COUNT(*) as c FROM events WHERE tool_name IN ('Grep', 'Glob') AND hook_type = 'PostToolUse' AND timestamp >= ? AND timestamp < ?", weekStart, nextWeekStart);
2009
+ case "totalCommits":
2010
+ return this.queryScalar("SELECT COUNT(*) as c FROM events WHERE tool_name = 'Bash' AND hook_type = 'PostToolUse' AND tool_input LIKE '%git commit%' AND timestamp >= ? AND timestamp < ?", weekStart, nextWeekStart);
2011
+ case "totalPRs":
2012
+ return this.queryScalar("SELECT COUNT(*) as c FROM events WHERE tool_name = 'Bash' AND hook_type = 'PostToolUse' AND tool_input LIKE '%gh pr create%' AND timestamp >= ? AND timestamp < ?", weekStart, nextWeekStart);
2013
+ case "commitDays":
2014
+ return this.queryScalar("SELECT COUNT(DISTINCT substr(timestamp, 1, 10)) as c FROM events WHERE tool_name = 'Bash' AND hook_type = 'PostToolUse' AND tool_input LIKE '%git commit%' AND timestamp >= ? AND timestamp < ?", weekStart, nextWeekStart);
2015
+ case "longPromptCount":
2016
+ return this.queryScalar("SELECT COUNT(*) as c FROM prompts WHERE char_count > 1000 AND timestamp >= ? AND timestamp < ?", weekStart, nextWeekStart);
2017
+ case "nightOwlDays":
2018
+ return this.queryScalar("SELECT COUNT(DISTINCT substr(timestamp, 1, 10)) as c FROM prompts WHERE CAST(strftime('%H', timestamp) AS INTEGER) < 5 AND timestamp >= ? AND timestamp < ?", weekStart, nextWeekStart);
2019
+ case "earlyBirdDays":
2020
+ return this.queryScalar("SELECT COUNT(DISTINCT substr(timestamp, 1, 10)) as c FROM prompts WHERE CAST(strftime('%H', timestamp) AS INTEGER) BETWEEN 5 AND 7 AND timestamp >= ? AND timestamp < ?", weekStart, nextWeekStart);
2021
+ case "weekendDays":
2022
+ return this.queryScalar("SELECT COUNT(*) as c FROM daily_activity WHERE date >= ? AND date <= ? AND (sessions > 0 OR prompts > 0) AND CAST(strftime('%w', date) AS INTEGER) IN (0, 6)", weekStart, weekEnd);
2023
+ case "uniqueProjects":
2024
+ return this.queryScalar("SELECT COUNT(DISTINCT project) as c FROM sessions WHERE project IS NOT NULL AND started_at >= ? AND started_at < ?", weekStart, nextWeekStart);
2025
+ case "cleanSessions":
2026
+ return this.queryScalar("SELECT COUNT(*) as c FROM sessions WHERE error_count = 0 AND tool_count > 0 AND started_at >= ? AND started_at < ?", weekStart, nextWeekStart);
2027
+ case "extendedSessionCount":
2028
+ return this.queryScalar("SELECT COUNT(*) as c FROM sessions WHERE duration_seconds > 3600 AND prompt_count >= 15 AND started_at >= ? AND started_at < ?", weekStart, nextWeekStart);
2029
+ case "quickDrawSessions":
2030
+ return this.queryScalar("SELECT COUNT(*) as c FROM sessions WHERE duration_seconds IS NOT NULL AND duration_seconds < 120 AND tool_count > 0 AND started_at >= ? AND started_at < ?", weekStart, nextWeekStart);
2031
+ case "diverseToolSessions":
2032
+ return this.queryScalar("SELECT COUNT(*) as c FROM (SELECT session_id FROM events WHERE hook_type = 'PostToolUse' AND tool_name IS NOT NULL AND session_id IN (SELECT id FROM sessions WHERE started_at >= ? AND started_at < ?) GROUP BY session_id HAVING COUNT(DISTINCT tool_name) >= 5)", weekStart, nextWeekStart);
2033
+ case "maxSubagentsInSession":
2034
+ return this.queryScalar("SELECT COALESCE(MAX(cnt), 0) as c FROM (SELECT COUNT(*) as cnt FROM events WHERE hook_type = 'SubagentStart' AND session_id IN (SELECT id FROM sessions WHERE started_at >= ? AND started_at < ?) GROUP BY session_id)", weekStart, nextWeekStart);
2035
+ default:
2036
+ return 0;
2037
+ }
2038
+ }
1219
2039
  };
1220
2040
 
1221
2041
  // src/types.ts
@@ -1232,7 +2052,7 @@ var TIER_NAMES = {
1232
2052
  2: "Silver",
1233
2053
  3: "Gold",
1234
2054
  4: "Diamond",
1235
- 5: "Obsidian"
2055
+ 5: "Singularity"
1236
2056
  };
1237
2057
 
1238
2058
  // src/achievements/compute.ts
@@ -1243,8 +2063,8 @@ var AchievementEngine = class {
1243
2063
  this.db = db;
1244
2064
  this.stats = stats;
1245
2065
  }
1246
- computeBadges() {
1247
- const allStats = this.stats.getAllStats();
2066
+ computeBadges(agent) {
2067
+ const allStats = this.stats.getAllStats(agent);
1248
2068
  const flat = this.flattenStats(allStats);
1249
2069
  return BADGE_DEFINITIONS.map((badge) => {
1250
2070
  const value = flat[badge.stat] ?? 0;
@@ -1309,9 +2129,9 @@ var AchievementEngine = class {
1309
2129
  };
1310
2130
  });
1311
2131
  }
1312
- computeXP() {
1313
- const allStats = this.stats.getAllStats();
1314
- const badges = this.computeBadges();
2132
+ computeXP(agent) {
2133
+ const allStats = this.stats.getAllStats(agent);
2134
+ const badges = this.computeBadges(agent);
1315
2135
  let totalXP = 0;
1316
2136
  totalXP += allStats.lifetime.totalPrompts * 1;
1317
2137
  totalXP += allStats.lifetime.totalSessions * 5;
@@ -1322,38 +2142,39 @@ var AchievementEngine = class {
1322
2142
  totalXP += TIER_XP[badge.tier] ?? 0;
1323
2143
  }
1324
2144
  }
1325
- let rank = "Bronze";
1326
- let nextRankXP = RANK_THRESHOLDS[RANK_THRESHOLDS.length - 2]?.xp ?? 1e3;
1327
- for (const threshold of RANK_THRESHOLDS) {
1328
- if (totalXP >= threshold.xp) {
1329
- rank = threshold.rank;
2145
+ let rankNumber = 0;
2146
+ for (let r = 500; r >= 1; r--) {
2147
+ if (totalXP >= xpForRank(r)) {
2148
+ rankNumber = r;
1330
2149
  break;
1331
2150
  }
1332
2151
  }
1333
- let progress = 0;
1334
- const rankIndex = RANK_THRESHOLDS.findIndex((t) => t.rank === rank);
1335
- if (rankIndex <= 0) {
1336
- nextRankXP = RANK_THRESHOLDS[0].xp;
2152
+ let nextRankXP;
2153
+ let progress;
2154
+ if (rankNumber >= 500) {
2155
+ nextRankXP = xpForRank(500);
1337
2156
  progress = 1;
1338
2157
  } else {
1339
- const currentThreshold = RANK_THRESHOLDS[rankIndex].xp;
1340
- nextRankXP = RANK_THRESHOLDS[rankIndex - 1].xp;
2158
+ const nextRank = rankNumber + 1;
2159
+ nextRankXP = xpForRank(nextRank);
2160
+ const currentThreshold = rankNumber > 0 ? xpForRank(rankNumber) : 0;
1341
2161
  const range = nextRankXP - currentThreshold;
1342
2162
  progress = range > 0 ? Math.min((totalXP - currentThreshold) / range, 0.99) : 0;
1343
2163
  }
1344
2164
  return {
1345
2165
  totalXP,
1346
- rank,
2166
+ rankNumber,
2167
+ rankTier: rankTierForRank(rankNumber || 1),
1347
2168
  nextRankXP,
1348
2169
  progress
1349
2170
  };
1350
2171
  }
1351
- getAchievementsPayload() {
1352
- const allStats = this.stats.getAllStats();
2172
+ getAchievementsPayload(agent) {
2173
+ const allStats = this.stats.getAllStats(agent);
1353
2174
  return {
1354
2175
  stats: allStats,
1355
- badges: this.computeBadges(),
1356
- xp: this.computeXP()
2176
+ badges: this.computeBadges(agent),
2177
+ xp: this.computeXP(agent)
1357
2178
  };
1358
2179
  }
1359
2180
  flattenStats(allStats) {
@@ -1443,6 +2264,14 @@ var AchievementEngine = class {
1443
2264
  flat.quickSubagentStops = this.queryQuickSubagentStops();
1444
2265
  flat.totalSubagentSpawns = allStats.lifetime.totalSubagents;
1445
2266
  flat.maxSubagentsInSession = this.queryMaxSubagentsInSession();
2267
+ flat.distinctAgentsUsed = this.queryScalar("SELECT COUNT(DISTINCT agent) FROM sessions");
2268
+ flat.geminiSessions = this.queryScalar("SELECT COUNT(*) FROM sessions WHERE agent = 'gemini-cli'");
2269
+ flat.copilotSessions = this.queryScalar("SELECT COUNT(*) FROM sessions WHERE agent = 'copilot-cli'");
2270
+ flat.opencodeSessions = this.queryScalar("SELECT COUNT(*) FROM sessions WHERE agent = 'opencode'");
2271
+ flat.doubleAgentDays = this.queryScalar(
2272
+ "SELECT COUNT(*) FROM (SELECT substr(started_at, 1, 10) as d FROM sessions GROUP BY d HAVING COUNT(DISTINCT agent) >= 2)"
2273
+ );
2274
+ flat.agentSwitchDays = flat.doubleAgentDays;
1446
2275
  flat.dejaVuCount = this.queryDejaVuCount();
1447
2276
  flat.trustIssueCount = this.queryTrustIssueCount();
1448
2277
  flat.backseatDriverCount = this.queryBackseatDriverCount();
@@ -2097,8 +2926,12 @@ var AchievementEngine = class {
2097
2926
 
2098
2927
  export {
2099
2928
  BADGE_DEFINITIONS,
2100
- RANK_THRESHOLDS,
2929
+ RANK_TIER_BRACKETS,
2930
+ xpForRank,
2931
+ rankTierForRank,
2101
2932
  TIER_XP,
2933
+ ACTIVITY_MULTIPLIERS,
2934
+ WEEKLY_CHALLENGES,
2102
2935
  DATA_DIR,
2103
2936
  DB_FILENAME,
2104
2937
  DEFAULT_PORT,
@@ -2106,6 +2939,15 @@ export {
2106
2939
  install,
2107
2940
  uninstall,
2108
2941
  isInstalled,
2942
+ isGeminiAvailable,
2943
+ installGemini,
2944
+ uninstallGemini,
2945
+ installCopilot,
2946
+ uninstallCopilot,
2947
+ isCopilotAvailable,
2948
+ isOpenCodeAvailable,
2949
+ installOpenCode,
2950
+ uninstallOpenCode,
2109
2951
  BashStatsWriter,
2110
2952
  detectAgent,
2111
2953
  parseHookEvent,
@@ -2116,4 +2958,4 @@ export {
2116
2958
  TIER_NAMES,
2117
2959
  AchievementEngine
2118
2960
  };
2119
- //# sourceMappingURL=chunk-4HVDBCTU.js.map
2961
+ //# sourceMappingURL=chunk-YAJO5WNW.js.map