agentfit 0.1.0 → 0.1.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.
Files changed (74) hide show
  1. package/.github/workflows/release.yml +111 -0
  2. package/README.md +41 -38
  3. package/app/(dashboard)/daily/page.tsx +1 -1
  4. package/app/(dashboard)/data-management/page.tsx +180 -0
  5. package/app/(dashboard)/flow/page.tsx +17 -0
  6. package/app/(dashboard)/layout.tsx +2 -0
  7. package/app/(dashboard)/page.tsx +24 -5
  8. package/app/(dashboard)/reports/[id]/page.tsx +72 -0
  9. package/app/(dashboard)/reports/page.tsx +132 -0
  10. package/app/(dashboard)/sessions/[id]/page.tsx +167 -0
  11. package/app/api/backup/route.ts +215 -0
  12. package/app/api/check/route.ts +11 -1
  13. package/app/api/command-insights/route.ts +13 -0
  14. package/app/api/commands/route.ts +55 -1
  15. package/app/api/images-analysis/route.ts +3 -4
  16. package/app/api/reports/[id]/route.ts +23 -0
  17. package/app/api/reports/route.ts +50 -0
  18. package/app/api/reset/route.ts +21 -0
  19. package/app/api/session/route.ts +40 -0
  20. package/app/api/usage/route.ts +26 -1
  21. package/app/layout.tsx +1 -1
  22. package/bin/agentfit.mjs +2 -2
  23. package/components/agent-coach.tsx +256 -129
  24. package/components/app-sidebar.tsx +45 -10
  25. package/components/backup-section.tsx +236 -0
  26. package/components/daily-chart.tsx +447 -83
  27. package/components/dashboard-shell.tsx +29 -31
  28. package/components/data-provider.tsx +88 -8
  29. package/components/fitness-score.tsx +95 -54
  30. package/components/overview-cards.tsx +148 -41
  31. package/components/report-view.tsx +307 -0
  32. package/components/screenshots-analysis.tsx +51 -46
  33. package/components/session-chatlog.tsx +124 -0
  34. package/components/session-timeline.tsx +184 -0
  35. package/components/session-workflow.tsx +183 -0
  36. package/components/sessions-table.tsx +9 -1
  37. package/components/tool-flow-graph.tsx +144 -0
  38. package/components/ui/carousel.tsx +242 -0
  39. package/components/ui/sidebar.tsx +1 -1
  40. package/components/ui/sonner.tsx +51 -0
  41. package/electron/entitlements.mac.plist +16 -0
  42. package/electron/init-db.mjs +37 -0
  43. package/electron/main.mjs +203 -0
  44. package/generated/prisma/browser.ts +5 -0
  45. package/generated/prisma/client.ts +5 -0
  46. package/generated/prisma/internal/class.ts +14 -4
  47. package/generated/prisma/internal/prismaNamespace.ts +97 -2
  48. package/generated/prisma/internal/prismaNamespaceBrowser.ts +21 -1
  49. package/generated/prisma/models/Report.ts +1219 -0
  50. package/generated/prisma/models/Session.ts +221 -1
  51. package/generated/prisma/models.ts +1 -0
  52. package/lib/coach.ts +571 -211
  53. package/lib/command-insights.ts +231 -0
  54. package/lib/db.ts +2 -2
  55. package/lib/parse-codex.ts +6 -0
  56. package/lib/parse-logs.ts +80 -1
  57. package/lib/queries-codex.ts +24 -0
  58. package/lib/queries.ts +45 -0
  59. package/lib/report.ts +156 -0
  60. package/lib/session-detail.ts +382 -0
  61. package/lib/sync.ts +87 -0
  62. package/lib/tool-flow.ts +71 -0
  63. package/next.config.mjs +6 -1
  64. package/package.json +17 -2
  65. package/plugins/cost-heatmap/component.tsx +72 -50
  66. package/prisma/migrations/20260401144555_add_system_prompt_edits/migration.sql +80 -0
  67. package/prisma/schema.prisma +18 -0
  68. package/prisma/schema.sql +81 -0
  69. package/.claude/settings.local.json +0 -26
  70. package/CONTRIBUTING.md +0 -209
  71. package/prisma/migrations/20260328152517_init/migration.sql +0 -41
  72. package/prisma/migrations/20260328153801_add_image_model/migration.sql +0 -18
  73. package/prisma.config.ts +0 -14
  74. 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,8 +3,8 @@ 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')
7
- const adapter = new PrismaLibSql({ url: `file:${dbPath}` })
6
+ const dbUrl = process.env.DATABASE_URL || `file:${path.resolve(process.cwd(), 'agentfit.db')}`
7
+ const adapter = new PrismaLibSql({ url: dbUrl })
8
8
  return new PrismaClient({ adapter })
9
9
  }
10
10
 
@@ -158,6 +158,12 @@ 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
+ systemPromptEdits: 0,
166
+ permissionModes: {},
161
167
  }
162
168
  } catch {
163
169
  return null
package/lib/parse-logs.ts CHANGED
@@ -27,6 +27,13 @@ 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
36
+ systemPromptEdits: number // edits/writes to CLAUDE.md, AGENTS.md, agent.md
30
37
  }
31
38
 
32
39
  export interface ProjectSummary {
@@ -44,6 +51,8 @@ export interface DailyUsage {
44
51
  date: string
45
52
  sessions: number
46
53
  messages: number
54
+ userMessages: number
55
+ assistantMessages: number
47
56
  inputTokens: number
48
57
  outputTokens: number
49
58
  cacheCreationTokens: number
@@ -51,6 +60,9 @@ export interface DailyUsage {
51
60
  totalTokens: number
52
61
  costUSD: number
53
62
  toolCalls: number
63
+ toolCallsDetail: Record<string, number>
64
+ interruptions: number
65
+ rateLimitErrors: number
54
66
  }
55
67
 
56
68
  export interface OverviewStats {
@@ -67,7 +79,13 @@ export interface OverviewStats {
67
79
  totalCostUSD: number
68
80
  totalDurationMinutes: number
69
81
  totalToolCalls: number
82
+ totalApiErrors: number
83
+ totalRateLimitDays: number
84
+ totalUserInterruptions: number
85
+ totalSystemPromptEdits: number
70
86
  models: Record<string, number>
87
+ skillUsage: Record<string, number>
88
+ permissionModes: Record<string, number>
71
89
  }
72
90
 
73
91
  export interface UsageData {
@@ -85,7 +103,23 @@ function decodeProjectPath(dirName: string): string {
85
103
  }
86
104
 
87
105
  function getProjectName(projectPath: string): string {
106
+ // The decoded path may be wrong if the actual folder name contains dashes,
107
+ // since decodeProjectPath replaces ALL dashes with slashes.
108
+ // Try merging trailing segments to find the real directory on disk.
109
+ if (fs.existsSync(projectPath)) {
110
+ return path.basename(projectPath)
111
+ }
88
112
  const parts = projectPath.split('/')
113
+ for (let merge = 2; merge <= Math.min(parts.length, 6); merge++) {
114
+ const parentParts = parts.slice(0, -merge)
115
+ const nameParts = parts.slice(-merge)
116
+ const candidateName = nameParts.join('-')
117
+ const candidatePath = [...parentParts, candidateName].join('/')
118
+ if (fs.existsSync(candidatePath)) {
119
+ return candidateName
120
+ }
121
+ }
122
+ // Fallback: last segment
89
123
  return parts[parts.length - 1] || parts[parts.length - 2] || projectPath
90
124
  }
91
125
 
@@ -125,6 +159,8 @@ function parseSessionFile(
125
159
  let startTime = ''
126
160
  let endTime = ''
127
161
  const toolCalls: Record<string, number> = {}
162
+ const permissionModes: Record<string, number> = {}
163
+ let systemPromptEdits = 0
128
164
 
129
165
  for (const line of lines) {
130
166
  if (!line.trim()) continue
@@ -143,6 +179,11 @@ function parseSessionFile(
143
179
 
144
180
  if (entry.type === 'user') {
145
181
  userMessages++
182
+ // Track permission mode
183
+ const pm = (entry as Record<string, unknown>).permissionMode as string | undefined
184
+ if (pm) {
185
+ permissionModes[pm] = (permissionModes[pm] || 0) + 1
186
+ }
146
187
  } else if (entry.type === 'assistant' && entry.message) {
147
188
  assistantMessages++
148
189
  const msg = entry.message
@@ -174,9 +215,17 @@ function parseSessionFile(
174
215
  'type' in block &&
175
216
  (block as Record<string, unknown>).type === 'tool_use'
176
217
  ) {
177
- const toolName = (block as Record<string, unknown>).name as string
218
+ const b = block as Record<string, unknown>
219
+ const toolName = b.name as string
178
220
  if (toolName) {
179
221
  toolCalls[toolName] = (toolCalls[toolName] || 0) + 1
222
+ // Detect system prompt file edits (CLAUDE.md, AGENTS.md, agent.md)
223
+ if ((toolName === 'Edit' || toolName === 'Write') && b.input && typeof b.input === 'object') {
224
+ const filePath = (b.input as Record<string, unknown>).file_path as string
225
+ if (filePath && /\/(CLAUDE|AGENTS|agent)\.md$/i.test(filePath)) {
226
+ systemPromptEdits++
227
+ }
228
+ }
180
229
  }
181
230
  }
182
231
  }
@@ -212,6 +261,12 @@ function parseSessionFile(
212
261
  model: model || 'unknown',
213
262
  toolCalls,
214
263
  toolCallsTotal: Object.values(toolCalls).reduce((a, b) => a + b, 0),
264
+ skillCalls: {},
265
+ apiErrors: 0,
266
+ rateLimitErrors: 0,
267
+ userInterruptions: 0,
268
+ permissionModes,
269
+ systemPromptEdits,
215
270
  }
216
271
  } catch {
217
272
  return null
@@ -280,6 +335,8 @@ export async function parseAllLogs(): Promise<UsageData> {
280
335
  date,
281
336
  sessions: 0,
282
337
  messages: 0,
338
+ userMessages: 0,
339
+ assistantMessages: 0,
283
340
  inputTokens: 0,
284
341
  outputTokens: 0,
285
342
  cacheCreationTokens: 0,
@@ -287,11 +344,16 @@ export async function parseAllLogs(): Promise<UsageData> {
287
344
  totalTokens: 0,
288
345
  costUSD: 0,
289
346
  toolCalls: 0,
347
+ toolCallsDetail: {},
348
+ interruptions: 0,
349
+ rateLimitErrors: 0,
290
350
  })
291
351
  }
292
352
  const daily = dailyMap.get(date)!
293
353
  daily.sessions++
294
354
  daily.messages += session.totalMessages
355
+ daily.userMessages += session.userMessages
356
+ daily.assistantMessages += session.assistantMessages
295
357
  daily.inputTokens += session.inputTokens
296
358
  daily.outputTokens += session.outputTokens
297
359
  daily.cacheCreationTokens += session.cacheCreationTokens
@@ -299,6 +361,11 @@ export async function parseAllLogs(): Promise<UsageData> {
299
361
  daily.totalTokens += session.totalTokens
300
362
  daily.costUSD += session.costUSD
301
363
  daily.toolCalls += session.toolCallsTotal
364
+ daily.interruptions += session.userInterruptions
365
+ daily.rateLimitErrors += session.rateLimitErrors
366
+ for (const [tool, count] of Object.entries(session.toolCalls)) {
367
+ daily.toolCallsDetail[tool] = (daily.toolCallsDetail[tool] || 0) + count
368
+ }
302
369
  }
303
370
 
304
371
  // Aggregate tool usage
@@ -337,7 +404,13 @@ export async function parseAllLogs(): Promise<UsageData> {
337
404
  totalCostUSD: sessions.reduce((a, s) => a + s.costUSD, 0),
338
405
  totalDurationMinutes: sessions.reduce((a, s) => a + s.durationMinutes, 0),
339
406
  totalToolCalls: sessions.reduce((a, s) => a + s.toolCallsTotal, 0),
407
+ totalApiErrors: 0,
408
+ totalRateLimitDays: 0,
409
+ totalUserInterruptions: 0,
410
+ totalSystemPromptEdits: sessions.reduce((a, s) => a + s.systemPromptEdits, 0),
340
411
  models,
412
+ skillUsage: {},
413
+ permissionModes: {},
341
414
  }
342
415
 
343
416
  return { overview, sessions, projects, daily, toolUsage }
@@ -359,7 +432,13 @@ function emptyUsageData(): UsageData {
359
432
  totalCostUSD: 0,
360
433
  totalDurationMinutes: 0,
361
434
  totalToolCalls: 0,
435
+ totalApiErrors: 0,
436
+ totalRateLimitDays: 0,
437
+ totalUserInterruptions: 0,
438
+ totalSystemPromptEdits: 0,
362
439
  models: {},
440
+ skillUsage: {},
441
+ permissionModes: {},
363
442
  },
364
443
  sessions: [],
365
444
  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,13 @@ 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,
116
+ totalSystemPromptEdits: 0,
101
117
  models,
118
+ skillUsage: {},
119
+ permissionModes: {},
102
120
  }
103
121
 
104
122
  return { overview, sessions, projects, daily, toolUsage }
@@ -120,7 +138,13 @@ function emptyUsageData(): UsageData {
120
138
  totalCostUSD: 0,
121
139
  totalDurationMinutes: 0,
122
140
  totalToolCalls: 0,
141
+ totalApiErrors: 0,
142
+ totalRateLimitDays: 0,
143
+ totalUserInterruptions: 0,
144
+ totalSystemPromptEdits: 0,
123
145
  models: {},
146
+ skillUsage: {},
147
+ permissionModes: {},
124
148
  },
125
149
  sessions: [],
126
150
  projects: [],
package/lib/queries.ts CHANGED
@@ -36,6 +36,13 @@ 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>,
45
+ systemPromptEdits: s.systemPromptEdits,
39
46
  }))
40
47
 
41
48
  // Aggregate projects
@@ -74,6 +81,8 @@ export async function getUsageData(): Promise<UsageData> {
74
81
  date,
75
82
  sessions: 0,
76
83
  messages: 0,
84
+ userMessages: 0,
85
+ assistantMessages: 0,
77
86
  inputTokens: 0,
78
87
  outputTokens: 0,
79
88
  cacheCreationTokens: 0,
@@ -81,11 +90,16 @@ export async function getUsageData(): Promise<UsageData> {
81
90
  totalTokens: 0,
82
91
  costUSD: 0,
83
92
  toolCalls: 0,
93
+ toolCallsDetail: {},
94
+ interruptions: 0,
95
+ rateLimitErrors: 0,
84
96
  })
85
97
  }
86
98
  const daily = dailyMap.get(date)!
87
99
  daily.sessions++
88
100
  daily.messages += s.totalMessages
101
+ daily.userMessages += s.userMessages
102
+ daily.assistantMessages += s.assistantMessages
89
103
  daily.inputTokens += s.inputTokens
90
104
  daily.outputTokens += s.outputTokens
91
105
  daily.cacheCreationTokens += s.cacheCreationTokens
@@ -93,6 +107,11 @@ export async function getUsageData(): Promise<UsageData> {
93
107
  daily.totalTokens += s.totalTokens
94
108
  daily.costUSD += s.costUSD
95
109
  daily.toolCalls += s.toolCallsTotal
110
+ daily.interruptions += s.userInterruptions
111
+ daily.rateLimitErrors += s.rateLimitErrors
112
+ for (const [tool, count] of Object.entries(s.toolCalls)) {
113
+ daily.toolCallsDetail[tool] = (daily.toolCallsDetail[tool] || 0) + count
114
+ }
96
115
 
97
116
  // Tool usage
98
117
  for (const [tool, count] of Object.entries(s.toolCalls)) {
@@ -104,8 +123,16 @@ export async function getUsageData(): Promise<UsageData> {
104
123
  const projects = Array.from(projectMap.values()).sort((a, b) => b.totalCost - a.totalCost)
105
124
 
106
125
  const models: Record<string, number> = {}
126
+ const skillUsage: Record<string, number> = {}
127
+ const permissionModes: Record<string, number> = {}
107
128
  for (const s of sessions) {
108
129
  models[s.model] = (models[s.model] || 0) + 1
130
+ for (const [skill, count] of Object.entries(s.skillCalls)) {
131
+ skillUsage[skill] = (skillUsage[skill] || 0) + count
132
+ }
133
+ for (const [mode, count] of Object.entries(s.permissionModes)) {
134
+ permissionModes[mode] = (permissionModes[mode] || 0) + count
135
+ }
109
136
  }
110
137
 
111
138
  const overview: OverviewStats = {
@@ -122,7 +149,19 @@ export async function getUsageData(): Promise<UsageData> {
122
149
  totalCostUSD: sessions.reduce((a, s) => a + s.costUSD, 0),
123
150
  totalDurationMinutes: sessions.reduce((a, s) => a + s.durationMinutes, 0),
124
151
  totalToolCalls: sessions.reduce((a, s) => a + s.toolCallsTotal, 0),
152
+ totalApiErrors: sessions.reduce((a, s) => a + s.apiErrors, 0),
153
+ totalRateLimitDays: (() => {
154
+ const days = new Set<string>()
155
+ for (const s of sessions) {
156
+ if (s.rateLimitErrors > 0) days.add(s.startTime.slice(0, 10))
157
+ }
158
+ return days.size
159
+ })(),
160
+ totalUserInterruptions: sessions.reduce((a, s) => a + s.userInterruptions, 0),
161
+ totalSystemPromptEdits: sessions.reduce((a, s) => a + s.systemPromptEdits, 0),
125
162
  models,
163
+ skillUsage,
164
+ permissionModes,
126
165
  }
127
166
 
128
167
  return { overview, sessions, projects, daily: dailyArr, toolUsage }
@@ -144,7 +183,13 @@ function emptyUsageData(): UsageData {
144
183
  totalCostUSD: 0,
145
184
  totalDurationMinutes: 0,
146
185
  totalToolCalls: 0,
186
+ totalApiErrors: 0,
187
+ totalRateLimitDays: 0,
188
+ totalUserInterruptions: 0,
189
+ totalSystemPromptEdits: 0,
147
190
  models: {},
191
+ skillUsage: {},
192
+ permissionModes: {},
148
193
  },
149
194
  sessions: [],
150
195
  projects: [],