agentfit 0.1.0 → 0.1.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (68) hide show
  1. package/README.md +30 -34
  2. package/app/(dashboard)/daily/page.tsx +1 -1
  3. package/app/(dashboard)/flow/page.tsx +17 -0
  4. package/app/(dashboard)/layout.tsx +2 -0
  5. package/app/(dashboard)/page.tsx +24 -5
  6. package/app/(dashboard)/reports/[id]/page.tsx +72 -0
  7. package/app/(dashboard)/reports/page.tsx +132 -0
  8. package/app/(dashboard)/sessions/[id]/page.tsx +167 -0
  9. package/app/(dashboard)/settings/page.tsx +180 -0
  10. package/app/api/backup/route.ts +215 -0
  11. package/app/api/check/route.ts +11 -1
  12. package/app/api/command-insights/route.ts +13 -0
  13. package/app/api/images-analysis/route.ts +3 -4
  14. package/app/api/reports/[id]/route.ts +23 -0
  15. package/app/api/reports/route.ts +50 -0
  16. package/app/api/reset/route.ts +21 -0
  17. package/app/api/session/route.ts +40 -0
  18. package/app/api/usage/route.ts +25 -1
  19. package/app/layout.tsx +1 -1
  20. package/bin/agentfit.mjs +2 -2
  21. package/components/agent-coach.tsx +256 -129
  22. package/components/app-sidebar.tsx +258 -8
  23. package/components/backup-section.tsx +236 -0
  24. package/components/daily-chart.tsx +404 -83
  25. package/components/dashboard-shell.tsx +9 -24
  26. package/components/data-provider.tsx +66 -2
  27. package/components/fitness-score.tsx +95 -54
  28. package/components/overview-cards.tsx +148 -41
  29. package/components/report-view.tsx +307 -0
  30. package/components/screenshots-analysis.tsx +51 -46
  31. package/components/session-chatlog.tsx +124 -0
  32. package/components/session-timeline.tsx +184 -0
  33. package/components/session-workflow.tsx +183 -0
  34. package/components/sessions-table.tsx +9 -1
  35. package/components/tool-flow-graph.tsx +144 -0
  36. package/components/ui/carousel.tsx +242 -0
  37. package/components/ui/sidebar.tsx +1 -1
  38. package/components/ui/sonner.tsx +51 -0
  39. package/generated/prisma/browser.ts +5 -0
  40. package/generated/prisma/client.ts +5 -0
  41. package/generated/prisma/internal/class.ts +14 -4
  42. package/generated/prisma/internal/prismaNamespace.ts +96 -2
  43. package/generated/prisma/internal/prismaNamespaceBrowser.ts +20 -1
  44. package/generated/prisma/models/Report.ts +1219 -0
  45. package/generated/prisma/models/Session.ts +187 -1
  46. package/generated/prisma/models.ts +1 -0
  47. package/lib/coach.ts +530 -211
  48. package/lib/command-insights.ts +231 -0
  49. package/lib/db.ts +1 -1
  50. package/lib/parse-codex.ts +5 -0
  51. package/lib/parse-logs.ts +65 -0
  52. package/lib/queries-codex.ts +22 -0
  53. package/lib/queries.ts +42 -0
  54. package/lib/report.ts +156 -0
  55. package/lib/session-detail.ts +382 -0
  56. package/lib/sync.ts +77 -0
  57. package/lib/tool-flow.ts +71 -0
  58. package/next.config.mjs +6 -1
  59. package/package.json +16 -2
  60. package/plugins/cost-heatmap/component.tsx +72 -50
  61. package/prisma/schema.prisma +17 -0
  62. package/.claude/settings.local.json +0 -26
  63. package/CONTRIBUTING.md +0 -209
  64. package/prisma/migrations/20260328152517_init/migration.sql +0 -41
  65. package/prisma/migrations/20260328153801_add_image_model/migration.sql +0 -18
  66. package/prisma/migrations/migration_lock.toml +0 -3
  67. package/prisma.config.ts +0 -14
  68. package/setup.sh +0 -73
@@ -0,0 +1,231 @@
1
+ // ─── Command Discovery Insights ──────────────────────────────────────
2
+ // Analyzes ~/.claude/history.jsonl command patterns to generate
3
+ // actionable coaching insights about command adoption.
4
+
5
+ import fs from 'fs'
6
+ import path from 'path'
7
+ import os from 'os'
8
+
9
+ export interface CommandInsight {
10
+ id: string
11
+ title: string
12
+ description: string
13
+ severity: 'tip' | 'achievement'
14
+ metric?: string
15
+ recommendation?: string
16
+ }
17
+
18
+ interface CommandTimeline {
19
+ command: string
20
+ firstUsed: string // ISO date
21
+ totalUses: number
22
+ recentUses: number // last 7 days
23
+ isBuiltIn: boolean
24
+ }
25
+
26
+ // High-value built-in commands that many users don't know about
27
+ const POWER_COMMANDS: Record<string, { description: string; benefit: string }> = {
28
+ '/compact': {
29
+ description: 'Compress conversation context with optional focus instructions',
30
+ benefit: 'Reduces context overflow by ~50%. Use when context exceeds 80% but you\'re still working on the same task.',
31
+ },
32
+ '/rewind': {
33
+ description: 'Rewind conversation and/or code to a previous point',
34
+ benefit: 'Undo mistakes without restarting. Saves 5-10 minutes per recovery vs starting fresh.',
35
+ },
36
+ '/simplify': {
37
+ description: 'Review changed code for reuse, quality, and efficiency',
38
+ benefit: 'Catches code quality issues automatically. Run after completing a feature to clean up.',
39
+ },
40
+ '/diff': {
41
+ description: 'Interactive diff viewer for uncommitted changes',
42
+ benefit: 'Review all changes at a glance before committing. Catches unintended modifications.',
43
+ },
44
+ '/btw': {
45
+ description: 'Ask a side question without polluting the main conversation',
46
+ benefit: 'Keeps your main context clean. Quick lookups without losing your place.',
47
+ },
48
+ '/branch': {
49
+ description: 'Create a branch of the current conversation',
50
+ benefit: 'Try risky approaches without losing your current progress. Fork, experiment, come back.',
51
+ },
52
+ '/plan': {
53
+ description: 'Enter plan mode for complex tasks',
54
+ benefit: 'Forces the agent to think before acting. Reduces wasted iterations on complex problems.',
55
+ },
56
+ '/context': {
57
+ description: 'Visualize current context usage as a colored grid',
58
+ benefit: 'See exactly what\'s consuming your context window. Know when to /compact.',
59
+ },
60
+ '/security-review': {
61
+ description: 'Analyze pending changes for security vulnerabilities',
62
+ benefit: 'Catches injection, auth, and data exposure issues before they ship.',
63
+ },
64
+ '/insights': {
65
+ description: 'Generate a report analyzing your Claude Code sessions',
66
+ benefit: 'Built-in analytics about your interaction patterns and friction points.',
67
+ },
68
+ '/frontend-design': {
69
+ description: 'Create distinctive, production-grade frontend interfaces',
70
+ benefit: 'Generates polished UI components with creative design choices, not generic AI slop.',
71
+ },
72
+ '/rename': {
73
+ description: 'Name your session for easy recall later',
74
+ benefit: 'Find important sessions quickly with /resume instead of scrolling through UUIDs.',
75
+ },
76
+ '/export': {
77
+ description: 'Export the current conversation as plain text',
78
+ benefit: 'Save important conversations for documentation, sharing, or review.',
79
+ },
80
+ }
81
+
82
+ function parseCommandHistory(): CommandTimeline[] {
83
+ const historyPath = path.join(os.homedir(), '.claude', 'history.jsonl')
84
+ if (!fs.existsSync(historyPath)) return []
85
+
86
+ const content = fs.readFileSync(historyPath, 'utf-8')
87
+ const lines = content.trim().split('\n')
88
+
89
+ const commandData = new Map<string, { firstUsed: number; uses: number; recentUses: number }>()
90
+ const now = Date.now()
91
+ const sevenDaysAgo = now - 7 * 24 * 60 * 60 * 1000
92
+
93
+ for (const line of lines) {
94
+ if (!line.trim()) continue
95
+ try {
96
+ const entry = JSON.parse(line)
97
+ const text = (entry.display || '').trim()
98
+ const ts = entry.timestamp || 0
99
+ const match = text.match(/^\/([a-zA-Z_-]+)/)
100
+ if (!match) continue
101
+ const cmd = '/' + match[1]
102
+
103
+ if (!commandData.has(cmd)) {
104
+ commandData.set(cmd, { firstUsed: ts, uses: 0, recentUses: 0 })
105
+ }
106
+ const d = commandData.get(cmd)!
107
+ d.uses++
108
+ if (d.firstUsed > ts) d.firstUsed = ts
109
+ if (ts > sevenDaysAgo) d.recentUses++
110
+ } catch {
111
+ continue
112
+ }
113
+ }
114
+
115
+ return Array.from(commandData.entries()).map(([cmd, data]) => ({
116
+ command: cmd,
117
+ firstUsed: new Date(data.firstUsed).toISOString().slice(0, 10),
118
+ totalUses: data.uses,
119
+ recentUses: data.recentUses,
120
+ isBuiltIn: cmd in POWER_COMMANDS,
121
+ }))
122
+ }
123
+
124
+ export function generateCommandInsights(): CommandInsight[] {
125
+ const timeline = parseCommandHistory()
126
+ const insights: CommandInsight[] = []
127
+ const usedCommands = new Set(timeline.map(t => t.command))
128
+
129
+ // 1. Power commands you discovered and now use frequently
130
+ const powerHits = timeline
131
+ .filter(t => POWER_COMMANDS[t.command] && t.totalUses >= 5)
132
+ .sort((a, b) => b.totalUses - a.totalUses)
133
+
134
+ if (powerHits.length > 0) {
135
+ const top = powerHits.slice(0, 3)
136
+ insights.push({
137
+ id: 'power-commands-adopted',
138
+ title: `Power commands mastered: ${top.map(t => t.command).join(', ')}`,
139
+ description: `You've adopted ${powerHits.length} power commands. ${top[0].command} is your most-used (${top[0].totalUses}x). These commands significantly improve your workflow efficiency.`,
140
+ severity: 'achievement',
141
+ metric: `${powerHits.length} mastered`,
142
+ })
143
+ }
144
+
145
+ // 2. Recently discovered commands gaining traction
146
+ const recentDiscoveries = timeline
147
+ .filter(t => {
148
+ const daysSinceFirst = (Date.now() - new Date(t.firstUsed).getTime()) / (1000 * 60 * 60 * 24)
149
+ return daysSinceFirst < 14 && t.totalUses >= 3
150
+ })
151
+ .sort((a, b) => b.totalUses - a.totalUses)
152
+
153
+ for (const disc of recentDiscoveries.slice(0, 2)) {
154
+ const info = POWER_COMMANDS[disc.command]
155
+ insights.push({
156
+ id: `recent-discovery-${disc.command}`,
157
+ title: `New favorite: ${disc.command} (${disc.totalUses}x in ${Math.ceil((Date.now() - new Date(disc.firstUsed).getTime()) / (1000 * 60 * 60 * 24))} days)`,
158
+ description: info
159
+ ? `${info.description}. ${info.benefit}`
160
+ : `You discovered ${disc.command} recently and it's becoming a regular part of your workflow.`,
161
+ severity: 'achievement',
162
+ metric: `${disc.totalUses}x`,
163
+ })
164
+ }
165
+
166
+ // 3. High-value commands you haven't tried yet
167
+ const untried = Object.entries(POWER_COMMANDS)
168
+ .filter(([cmd]) => !usedCommands.has(cmd))
169
+ .sort(() => Math.random() - 0.5) // randomize so different tips each time
170
+
171
+ for (const [cmd, info] of untried.slice(0, 3)) {
172
+ insights.push({
173
+ id: `try-${cmd}`,
174
+ title: `Try ${cmd}`,
175
+ description: info.description,
176
+ severity: 'tip',
177
+ recommendation: info.benefit,
178
+ })
179
+ }
180
+
181
+ // 4. Commands you used to use but stopped
182
+ const abandoned = timeline
183
+ .filter(t => t.totalUses >= 3 && t.recentUses === 0 && POWER_COMMANDS[t.command])
184
+ .sort((a, b) => b.totalUses - a.totalUses)
185
+
186
+ for (const cmd of abandoned.slice(0, 2)) {
187
+ const info = POWER_COMMANDS[cmd.command]!
188
+ insights.push({
189
+ id: `revive-${cmd.command}`,
190
+ title: `Revisit ${cmd.command}?`,
191
+ description: `You used ${cmd.command} ${cmd.totalUses} times but haven't used it recently.`,
192
+ severity: 'tip',
193
+ recommendation: info.benefit,
194
+ })
195
+ }
196
+
197
+ // 5. Command diversity insight
198
+ const weeklyDiversity = timeline.filter(t => t.recentUses > 0).length
199
+ if (weeklyDiversity >= 8) {
200
+ insights.push({
201
+ id: 'diverse-commands',
202
+ title: `${weeklyDiversity} commands used this week`,
203
+ description: 'You have a diverse command vocabulary. This means you\'re leveraging the full power of your coding agent instead of just chatting.',
204
+ severity: 'achievement',
205
+ metric: `${weeklyDiversity} active`,
206
+ })
207
+ } else if (weeklyDiversity <= 3 && timeline.length > 5) {
208
+ insights.push({
209
+ id: 'low-diversity',
210
+ title: 'Low command diversity this week',
211
+ description: `Only ${weeklyDiversity} commands used in the last 7 days. Slash commands save time by encoding common workflows into single keystrokes.`,
212
+ severity: 'tip',
213
+ recommendation: 'Type / in Claude Code to see all available commands. Try using at least one new command today.',
214
+ })
215
+ }
216
+
217
+ // 6. Custom skills/plugins adoption
218
+ const customCommands = timeline.filter(t => !Object.keys(POWER_COMMANDS).includes(t.command) && t.totalUses >= 3)
219
+ if (customCommands.length >= 3) {
220
+ const topCustom = customCommands.sort((a, b) => b.totalUses - a.totalUses).slice(0, 3)
221
+ insights.push({
222
+ id: 'custom-skills',
223
+ title: `${customCommands.length} custom skills in your toolkit`,
224
+ description: `Your most-used custom commands: ${topCustom.map(c => `${c.command} (${c.totalUses}x)`).join(', ')}. Custom skills automate your specific workflows.`,
225
+ severity: 'achievement',
226
+ metric: `${customCommands.length} skills`,
227
+ })
228
+ }
229
+
230
+ return insights
231
+ }
package/lib/db.ts CHANGED
@@ -3,7 +3,7 @@ import { PrismaLibSql } from '@prisma/adapter-libsql'
3
3
  import path from 'path'
4
4
 
5
5
  function createPrisma() {
6
- const dbPath = path.resolve(process.cwd(), 'dev.db')
6
+ const dbPath = path.resolve(process.cwd(), 'agentfit.db')
7
7
  const adapter = new PrismaLibSql({ url: `file:${dbPath}` })
8
8
  return new PrismaClient({ adapter })
9
9
  }
@@ -158,6 +158,11 @@ export function parseCodexSession(filePath: string): SessionSummary | null {
158
158
  model: model || 'gpt-5',
159
159
  toolCalls,
160
160
  toolCallsTotal: Object.values(toolCalls).reduce((a, b) => a + b, 0),
161
+ skillCalls: {},
162
+ apiErrors: 0,
163
+ rateLimitErrors: 0,
164
+ userInterruptions: 0,
165
+ permissionModes: {},
161
166
  }
162
167
  } catch {
163
168
  return null
package/lib/parse-logs.ts CHANGED
@@ -27,6 +27,12 @@ export interface SessionSummary {
27
27
  model: string
28
28
  toolCalls: Record<string, number>
29
29
  toolCallsTotal: number
30
+ skillCalls: Record<string, number>
31
+ messageTimestamps?: string[] // ISO timestamps of each message
32
+ apiErrors: number
33
+ rateLimitErrors: number
34
+ userInterruptions: number
35
+ permissionModes: Record<string, number> // default, acceptEdits, bypassPermissions, plan
30
36
  }
31
37
 
32
38
  export interface ProjectSummary {
@@ -44,6 +50,8 @@ export interface DailyUsage {
44
50
  date: string
45
51
  sessions: number
46
52
  messages: number
53
+ userMessages: number
54
+ assistantMessages: number
47
55
  inputTokens: number
48
56
  outputTokens: number
49
57
  cacheCreationTokens: number
@@ -51,6 +59,9 @@ export interface DailyUsage {
51
59
  totalTokens: number
52
60
  costUSD: number
53
61
  toolCalls: number
62
+ toolCallsDetail: Record<string, number>
63
+ interruptions: number
64
+ rateLimitErrors: number
54
65
  }
55
66
 
56
67
  export interface OverviewStats {
@@ -67,7 +78,12 @@ export interface OverviewStats {
67
78
  totalCostUSD: number
68
79
  totalDurationMinutes: number
69
80
  totalToolCalls: number
81
+ totalApiErrors: number
82
+ totalRateLimitDays: number
83
+ totalUserInterruptions: number
70
84
  models: Record<string, number>
85
+ skillUsage: Record<string, number>
86
+ permissionModes: Record<string, number>
71
87
  }
72
88
 
73
89
  export interface UsageData {
@@ -85,7 +101,23 @@ function decodeProjectPath(dirName: string): string {
85
101
  }
86
102
 
87
103
  function getProjectName(projectPath: string): string {
104
+ // The decoded path may be wrong if the actual folder name contains dashes,
105
+ // since decodeProjectPath replaces ALL dashes with slashes.
106
+ // Try merging trailing segments to find the real directory on disk.
107
+ if (fs.existsSync(projectPath)) {
108
+ return path.basename(projectPath)
109
+ }
88
110
  const parts = projectPath.split('/')
111
+ for (let merge = 2; merge <= Math.min(parts.length, 6); merge++) {
112
+ const parentParts = parts.slice(0, -merge)
113
+ const nameParts = parts.slice(-merge)
114
+ const candidateName = nameParts.join('-')
115
+ const candidatePath = [...parentParts, candidateName].join('/')
116
+ if (fs.existsSync(candidatePath)) {
117
+ return candidateName
118
+ }
119
+ }
120
+ // Fallback: last segment
89
121
  return parts[parts.length - 1] || parts[parts.length - 2] || projectPath
90
122
  }
91
123
 
@@ -125,6 +157,7 @@ function parseSessionFile(
125
157
  let startTime = ''
126
158
  let endTime = ''
127
159
  const toolCalls: Record<string, number> = {}
160
+ const permissionModes: Record<string, number> = {}
128
161
 
129
162
  for (const line of lines) {
130
163
  if (!line.trim()) continue
@@ -143,6 +176,11 @@ function parseSessionFile(
143
176
 
144
177
  if (entry.type === 'user') {
145
178
  userMessages++
179
+ // Track permission mode
180
+ const pm = (entry as Record<string, unknown>).permissionMode as string | undefined
181
+ if (pm) {
182
+ permissionModes[pm] = (permissionModes[pm] || 0) + 1
183
+ }
146
184
  } else if (entry.type === 'assistant' && entry.message) {
147
185
  assistantMessages++
148
186
  const msg = entry.message
@@ -212,6 +250,11 @@ function parseSessionFile(
212
250
  model: model || 'unknown',
213
251
  toolCalls,
214
252
  toolCallsTotal: Object.values(toolCalls).reduce((a, b) => a + b, 0),
253
+ skillCalls: {},
254
+ apiErrors: 0,
255
+ rateLimitErrors: 0,
256
+ userInterruptions: 0,
257
+ permissionModes,
215
258
  }
216
259
  } catch {
217
260
  return null
@@ -280,6 +323,8 @@ export async function parseAllLogs(): Promise<UsageData> {
280
323
  date,
281
324
  sessions: 0,
282
325
  messages: 0,
326
+ userMessages: 0,
327
+ assistantMessages: 0,
283
328
  inputTokens: 0,
284
329
  outputTokens: 0,
285
330
  cacheCreationTokens: 0,
@@ -287,11 +332,16 @@ export async function parseAllLogs(): Promise<UsageData> {
287
332
  totalTokens: 0,
288
333
  costUSD: 0,
289
334
  toolCalls: 0,
335
+ toolCallsDetail: {},
336
+ interruptions: 0,
337
+ rateLimitErrors: 0,
290
338
  })
291
339
  }
292
340
  const daily = dailyMap.get(date)!
293
341
  daily.sessions++
294
342
  daily.messages += session.totalMessages
343
+ daily.userMessages += session.userMessages
344
+ daily.assistantMessages += session.assistantMessages
295
345
  daily.inputTokens += session.inputTokens
296
346
  daily.outputTokens += session.outputTokens
297
347
  daily.cacheCreationTokens += session.cacheCreationTokens
@@ -299,6 +349,11 @@ export async function parseAllLogs(): Promise<UsageData> {
299
349
  daily.totalTokens += session.totalTokens
300
350
  daily.costUSD += session.costUSD
301
351
  daily.toolCalls += session.toolCallsTotal
352
+ daily.interruptions += session.userInterruptions
353
+ daily.rateLimitErrors += session.rateLimitErrors
354
+ for (const [tool, count] of Object.entries(session.toolCalls)) {
355
+ daily.toolCallsDetail[tool] = (daily.toolCallsDetail[tool] || 0) + count
356
+ }
302
357
  }
303
358
 
304
359
  // Aggregate tool usage
@@ -337,7 +392,12 @@ export async function parseAllLogs(): Promise<UsageData> {
337
392
  totalCostUSD: sessions.reduce((a, s) => a + s.costUSD, 0),
338
393
  totalDurationMinutes: sessions.reduce((a, s) => a + s.durationMinutes, 0),
339
394
  totalToolCalls: sessions.reduce((a, s) => a + s.toolCallsTotal, 0),
395
+ totalApiErrors: 0,
396
+ totalRateLimitDays: 0,
397
+ totalUserInterruptions: 0,
340
398
  models,
399
+ skillUsage: {},
400
+ permissionModes: {},
341
401
  }
342
402
 
343
403
  return { overview, sessions, projects, daily, toolUsage }
@@ -359,7 +419,12 @@ function emptyUsageData(): UsageData {
359
419
  totalCostUSD: 0,
360
420
  totalDurationMinutes: 0,
361
421
  totalToolCalls: 0,
422
+ totalApiErrors: 0,
423
+ totalRateLimitDays: 0,
424
+ totalUserInterruptions: 0,
362
425
  models: {},
426
+ skillUsage: {},
427
+ permissionModes: {},
363
428
  },
364
429
  sessions: [],
365
430
  projects: [],
@@ -52,6 +52,8 @@ export function getCodexUsageData(): UsageData {
52
52
  date,
53
53
  sessions: 0,
54
54
  messages: 0,
55
+ userMessages: 0,
56
+ assistantMessages: 0,
55
57
  inputTokens: 0,
56
58
  outputTokens: 0,
57
59
  cacheCreationTokens: 0,
@@ -59,16 +61,26 @@ export function getCodexUsageData(): UsageData {
59
61
  totalTokens: 0,
60
62
  costUSD: 0,
61
63
  toolCalls: 0,
64
+ toolCallsDetail: {},
65
+ interruptions: 0,
66
+ rateLimitErrors: 0,
62
67
  })
63
68
  }
64
69
  const daily = dailyMap.get(date)!
65
70
  daily.sessions++
66
71
  daily.messages += s.totalMessages
72
+ daily.userMessages += s.userMessages
73
+ daily.assistantMessages += s.assistantMessages
67
74
  daily.inputTokens += s.inputTokens
68
75
  daily.outputTokens += s.outputTokens
69
76
  daily.totalTokens += s.totalTokens
70
77
  daily.costUSD += s.costUSD
71
78
  daily.toolCalls += s.toolCallsTotal
79
+ daily.interruptions += s.userInterruptions
80
+ daily.rateLimitErrors += s.rateLimitErrors
81
+ for (const [tool, count] of Object.entries(s.toolCalls)) {
82
+ daily.toolCallsDetail[tool] = (daily.toolCallsDetail[tool] || 0) + count
83
+ }
72
84
 
73
85
  // Tool usage
74
86
  for (const [tool, count] of Object.entries(s.toolCalls)) {
@@ -98,7 +110,12 @@ export function getCodexUsageData(): UsageData {
98
110
  totalCostUSD: sessions.reduce((a, s) => a + s.costUSD, 0),
99
111
  totalDurationMinutes: sessions.reduce((a, s) => a + s.durationMinutes, 0),
100
112
  totalToolCalls: sessions.reduce((a, s) => a + s.toolCallsTotal, 0),
113
+ totalApiErrors: 0,
114
+ totalRateLimitDays: 0,
115
+ totalUserInterruptions: 0,
101
116
  models,
117
+ skillUsage: {},
118
+ permissionModes: {},
102
119
  }
103
120
 
104
121
  return { overview, sessions, projects, daily, toolUsage }
@@ -120,7 +137,12 @@ function emptyUsageData(): UsageData {
120
137
  totalCostUSD: 0,
121
138
  totalDurationMinutes: 0,
122
139
  totalToolCalls: 0,
140
+ totalApiErrors: 0,
141
+ totalRateLimitDays: 0,
142
+ totalUserInterruptions: 0,
123
143
  models: {},
144
+ skillUsage: {},
145
+ permissionModes: {},
124
146
  },
125
147
  sessions: [],
126
148
  projects: [],
package/lib/queries.ts CHANGED
@@ -36,6 +36,12 @@ export async function getUsageData(): Promise<UsageData> {
36
36
  model: s.model,
37
37
  toolCalls: JSON.parse(s.toolCallsJson) as Record<string, number>,
38
38
  toolCallsTotal: s.toolCallsTotal,
39
+ messageTimestamps: JSON.parse(s.messageTimestamps) as string[],
40
+ skillCalls: JSON.parse(s.skillCallsJson) as Record<string, number>,
41
+ apiErrors: s.apiErrors,
42
+ rateLimitErrors: s.rateLimitErrors,
43
+ userInterruptions: s.userInterruptions,
44
+ permissionModes: JSON.parse(s.permissionModesJson || '{}') as Record<string, number>,
39
45
  }))
40
46
 
41
47
  // Aggregate projects
@@ -74,6 +80,8 @@ export async function getUsageData(): Promise<UsageData> {
74
80
  date,
75
81
  sessions: 0,
76
82
  messages: 0,
83
+ userMessages: 0,
84
+ assistantMessages: 0,
77
85
  inputTokens: 0,
78
86
  outputTokens: 0,
79
87
  cacheCreationTokens: 0,
@@ -81,11 +89,16 @@ export async function getUsageData(): Promise<UsageData> {
81
89
  totalTokens: 0,
82
90
  costUSD: 0,
83
91
  toolCalls: 0,
92
+ toolCallsDetail: {},
93
+ interruptions: 0,
94
+ rateLimitErrors: 0,
84
95
  })
85
96
  }
86
97
  const daily = dailyMap.get(date)!
87
98
  daily.sessions++
88
99
  daily.messages += s.totalMessages
100
+ daily.userMessages += s.userMessages
101
+ daily.assistantMessages += s.assistantMessages
89
102
  daily.inputTokens += s.inputTokens
90
103
  daily.outputTokens += s.outputTokens
91
104
  daily.cacheCreationTokens += s.cacheCreationTokens
@@ -93,6 +106,11 @@ export async function getUsageData(): Promise<UsageData> {
93
106
  daily.totalTokens += s.totalTokens
94
107
  daily.costUSD += s.costUSD
95
108
  daily.toolCalls += s.toolCallsTotal
109
+ daily.interruptions += s.userInterruptions
110
+ daily.rateLimitErrors += s.rateLimitErrors
111
+ for (const [tool, count] of Object.entries(s.toolCalls)) {
112
+ daily.toolCallsDetail[tool] = (daily.toolCallsDetail[tool] || 0) + count
113
+ }
96
114
 
97
115
  // Tool usage
98
116
  for (const [tool, count] of Object.entries(s.toolCalls)) {
@@ -104,8 +122,16 @@ export async function getUsageData(): Promise<UsageData> {
104
122
  const projects = Array.from(projectMap.values()).sort((a, b) => b.totalCost - a.totalCost)
105
123
 
106
124
  const models: Record<string, number> = {}
125
+ const skillUsage: Record<string, number> = {}
126
+ const permissionModes: Record<string, number> = {}
107
127
  for (const s of sessions) {
108
128
  models[s.model] = (models[s.model] || 0) + 1
129
+ for (const [skill, count] of Object.entries(s.skillCalls)) {
130
+ skillUsage[skill] = (skillUsage[skill] || 0) + count
131
+ }
132
+ for (const [mode, count] of Object.entries(s.permissionModes)) {
133
+ permissionModes[mode] = (permissionModes[mode] || 0) + count
134
+ }
109
135
  }
110
136
 
111
137
  const overview: OverviewStats = {
@@ -122,7 +148,18 @@ export async function getUsageData(): Promise<UsageData> {
122
148
  totalCostUSD: sessions.reduce((a, s) => a + s.costUSD, 0),
123
149
  totalDurationMinutes: sessions.reduce((a, s) => a + s.durationMinutes, 0),
124
150
  totalToolCalls: sessions.reduce((a, s) => a + s.toolCallsTotal, 0),
151
+ totalApiErrors: sessions.reduce((a, s) => a + s.apiErrors, 0),
152
+ totalRateLimitDays: (() => {
153
+ const days = new Set<string>()
154
+ for (const s of sessions) {
155
+ if (s.rateLimitErrors > 0) days.add(s.startTime.slice(0, 10))
156
+ }
157
+ return days.size
158
+ })(),
159
+ totalUserInterruptions: sessions.reduce((a, s) => a + s.userInterruptions, 0),
125
160
  models,
161
+ skillUsage,
162
+ permissionModes,
126
163
  }
127
164
 
128
165
  return { overview, sessions, projects, daily: dailyArr, toolUsage }
@@ -144,7 +181,12 @@ function emptyUsageData(): UsageData {
144
181
  totalCostUSD: 0,
145
182
  totalDurationMinutes: 0,
146
183
  totalToolCalls: 0,
184
+ totalApiErrors: 0,
185
+ totalRateLimitDays: 0,
186
+ totalUserInterruptions: 0,
147
187
  models: {},
188
+ skillUsage: {},
189
+ permissionModes: {},
148
190
  },
149
191
  sessions: [],
150
192
  projects: [],