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.
- package/NUL +0 -10
- package/README.md +159 -46
- package/dist/{chunk-4HVDBCTU.js → chunk-YAJO5WNW.js} +1021 -179
- package/dist/chunk-YAJO5WNW.js.map +1 -0
- package/dist/cli.js +103 -40
- package/dist/cli.js.map +1 -1
- package/dist/hooks/{chunk-37VUNTM4.js → chunk-CLSVLWCR.js} +243 -28
- package/dist/hooks/chunk-CLSVLWCR.js.map +1 -0
- package/dist/hooks/notification.js +1 -1
- package/dist/hooks/permission-request.js +1 -1
- package/dist/hooks/post-tool-failure.js +1 -1
- package/dist/hooks/post-tool-use.js +1 -1
- package/dist/hooks/pre-compact.js +1 -1
- package/dist/hooks/pre-tool-use.js +1 -1
- package/dist/hooks/session-start.js +1 -1
- package/dist/hooks/setup.js +1 -1
- package/dist/hooks/stop.js +1 -1
- package/dist/hooks/subagent-start.js +1 -1
- package/dist/hooks/subagent-stop.js +1 -1
- package/dist/hooks/user-prompt-submit.js +1 -1
- package/dist/index.d.ts +145 -17
- package/dist/index.js +31 -5
- package/dist/static/index.html +806 -55
- package/package.json +1 -1
- package/dist/chunk-4HVDBCTU.js.map +0 -1
- package/dist/hooks/chunk-37VUNTM4.js.map +0 -1
|
@@ -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,
|
|
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 (
|
|
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: [
|
|
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,
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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 (
|
|
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
|
-
|
|
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: [
|
|
109
|
-
{ id: "agent_smith", name: "Agent Smith", icon: "\u{1F576}", description: "They're multiplying.", category: "multi_agent", stat: "maxSubagentsInSession", tiers: [25, 50, 100, 250
|
|
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
|
-
//
|
|
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: "
|
|
114
|
-
{ id: "wall_of_text", name: "Wall of Text", icon: "\u{1F4DC}", description: "Claude read your entire novel and didn't even complain.", category: "
|
|
115
|
-
{ id: "the_fixer", name: "The Fixer", icon: "\u{1F6E0}", description: "At this point just rewrite the whole thing.", category: "
|
|
116
|
-
{ id: "what_day_is_it", name: "What Day Is It?", icon: "\u{1F62B}", description: "Your chair is now a part of you.", category: "
|
|
117
|
-
{ id: "copy_pasta", name: "Copy Pasta", icon: "\u{1F35D}", description: "Maybe if I ask again it'll work differently.", category: "
|
|
118
|
-
{ id: "error_magnet", name: "Error Magnet", icon: "\u{1F9F2}", description: "At this point, the errors are a feature.", category: "
|
|
119
|
-
{ id: "
|
|
120
|
-
|
|
121
|
-
{ id: "
|
|
122
|
-
{ id: "
|
|
123
|
-
{ id: "
|
|
124
|
-
{ id: "
|
|
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
|
|
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: [
|
|
133
|
-
{ id: "output_machine", name: "Output Machine", icon: "\u{1F5A8}", description: "Generate output tokens from Claude", category: "token_usage", stat: "totalOutputTokens", tiers: [
|
|
134
|
-
{ id: "cache_royalty", name: "Cache Royalty", icon: "\u{1F451}", description: "Read tokens from prompt cache", category: "token_usage", stat: "totalCacheReadTokens", tiers: [
|
|
135
|
-
{ id: "context_crafter", name: "Context Crafter", icon: "\u{1F9F1}", description: "Create new cache entries", category: "token_usage", stat: "totalCacheCreationTokens", tiers: [1e7, 1e8, 1e9,
|
|
136
|
-
{ id: "token_whale", name: "Token Whale", icon: "\u{1F40B}", description: "Massive token consumption in a single session", category: "token_usage", stat: "mostTokensInSession", tiers: [
|
|
137
|
-
{ id: "heavy_hitter", name: "Heavy Hitter", icon: "\u{1F4AA}", description: "Sessions exceeding 1M total tokens", category: "token_usage", stat: "heavyTokenSessions", tiers: [
|
|
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: [
|
|
140
|
-
{ id: "prolific_session", name: "Prolific", icon: "\u270D", description: "Most output generated in one session", category: "token_usage", stat: "maxOutputInSession", tiers: [
|
|
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: [
|
|
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 (
|
|
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
|
|
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 (
|
|
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
|
|
181
|
-
{
|
|
182
|
-
{
|
|
183
|
-
{
|
|
184
|
-
{
|
|
185
|
-
{
|
|
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/
|
|
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
|
|
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
|
|
796
|
-
import
|
|
797
|
-
import
|
|
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
|
|
1267
|
+
import fs5 from "fs";
|
|
801
1268
|
import readline from "readline";
|
|
802
1269
|
async function extractTokenUsage(transcriptPath) {
|
|
803
1270
|
try {
|
|
804
|
-
if (!
|
|
805
|
-
const stream =
|
|
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.
|
|
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
|
|
1466
|
+
return path6.join(os5.homedir(), DATA_DIR);
|
|
864
1467
|
}
|
|
865
1468
|
function getDbPath() {
|
|
866
|
-
return
|
|
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
|
-
|
|
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
|
|
894
|
-
|
|
895
|
-
|
|
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 =
|
|
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 =
|
|
1529
|
+
const prompt = payload.prompt ?? "";
|
|
904
1530
|
writer.recordPrompt(sessionId, prompt);
|
|
905
1531
|
break;
|
|
906
1532
|
}
|
|
907
1533
|
case "PreToolUse": {
|
|
908
|
-
const toolName =
|
|
909
|
-
const toolInput =
|
|
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 =
|
|
915
|
-
const toolInput =
|
|
916
|
-
const toolResponse =
|
|
917
|
-
const exitCode =
|
|
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 =
|
|
923
|
-
const toolInput =
|
|
924
|
-
const toolResponse =
|
|
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
|
-
|
|
930
|
-
|
|
931
|
-
|
|
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 =
|
|
937
|
-
const notificationType =
|
|
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 =
|
|
943
|
-
const agentType =
|
|
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 =
|
|
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 =
|
|
1598
|
+
const trigger = payload.trigger ?? "manual";
|
|
954
1599
|
writer.recordCompaction(sessionId, trigger);
|
|
955
1600
|
break;
|
|
956
1601
|
}
|
|
957
1602
|
case "PermissionRequest": {
|
|
958
|
-
const toolName =
|
|
959
|
-
const toolInput =
|
|
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
|
-
|
|
984
|
-
|
|
985
|
-
|
|
986
|
-
const
|
|
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
|
-
|
|
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.
|
|
1097
|
-
while (activeDates.has(checkDate
|
|
1791
|
+
checkDate.setDate(checkDate.getDate() - 1);
|
|
1792
|
+
while (activeDates.has(localDateStr(checkDate))) {
|
|
1098
1793
|
currentStreak++;
|
|
1099
|
-
checkDate.
|
|
1794
|
+
checkDate.setDate(checkDate.getDate() - 1);
|
|
1100
1795
|
}
|
|
1101
1796
|
} else {
|
|
1102
|
-
checkDate.
|
|
1103
|
-
const yesterdayStr = checkDate
|
|
1797
|
+
checkDate.setDate(checkDate.getDate() - 1);
|
|
1798
|
+
const yesterdayStr = localDateStr(checkDate);
|
|
1104
1799
|
if (activeDates.has(yesterdayStr)) {
|
|
1105
1800
|
currentStreak = 1;
|
|
1106
|
-
checkDate.
|
|
1107
|
-
while (activeDates.has(checkDate
|
|
1801
|
+
checkDate.setDate(checkDate.getDate() - 1);
|
|
1802
|
+
while (activeDates.has(localDateStr(checkDate))) {
|
|
1108
1803
|
currentStreak++;
|
|
1109
|
-
checkDate.
|
|
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
|
-
|
|
1213
|
-
|
|
1214
|
-
|
|
1215
|
-
|
|
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: "
|
|
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
|
|
1326
|
-
let
|
|
1327
|
-
|
|
1328
|
-
|
|
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
|
|
1334
|
-
|
|
1335
|
-
if (
|
|
1336
|
-
nextRankXP =
|
|
2152
|
+
let nextRankXP;
|
|
2153
|
+
let progress;
|
|
2154
|
+
if (rankNumber >= 500) {
|
|
2155
|
+
nextRankXP = xpForRank(500);
|
|
1337
2156
|
progress = 1;
|
|
1338
2157
|
} else {
|
|
1339
|
-
const
|
|
1340
|
-
nextRankXP =
|
|
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
|
-
|
|
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
|
-
|
|
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-
|
|
2961
|
+
//# sourceMappingURL=chunk-YAJO5WNW.js.map
|