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.
- package/.github/workflows/release.yml +111 -0
- package/README.md +41 -38
- package/app/(dashboard)/daily/page.tsx +1 -1
- package/app/(dashboard)/data-management/page.tsx +180 -0
- package/app/(dashboard)/flow/page.tsx +17 -0
- package/app/(dashboard)/layout.tsx +2 -0
- package/app/(dashboard)/page.tsx +24 -5
- package/app/(dashboard)/reports/[id]/page.tsx +72 -0
- package/app/(dashboard)/reports/page.tsx +132 -0
- package/app/(dashboard)/sessions/[id]/page.tsx +167 -0
- package/app/api/backup/route.ts +215 -0
- package/app/api/check/route.ts +11 -1
- package/app/api/command-insights/route.ts +13 -0
- package/app/api/commands/route.ts +55 -1
- package/app/api/images-analysis/route.ts +3 -4
- package/app/api/reports/[id]/route.ts +23 -0
- package/app/api/reports/route.ts +50 -0
- package/app/api/reset/route.ts +21 -0
- package/app/api/session/route.ts +40 -0
- package/app/api/usage/route.ts +26 -1
- package/app/layout.tsx +1 -1
- package/bin/agentfit.mjs +2 -2
- package/components/agent-coach.tsx +256 -129
- package/components/app-sidebar.tsx +45 -10
- package/components/backup-section.tsx +236 -0
- package/components/daily-chart.tsx +447 -83
- package/components/dashboard-shell.tsx +29 -31
- package/components/data-provider.tsx +88 -8
- package/components/fitness-score.tsx +95 -54
- package/components/overview-cards.tsx +148 -41
- package/components/report-view.tsx +307 -0
- package/components/screenshots-analysis.tsx +51 -46
- package/components/session-chatlog.tsx +124 -0
- package/components/session-timeline.tsx +184 -0
- package/components/session-workflow.tsx +183 -0
- package/components/sessions-table.tsx +9 -1
- package/components/tool-flow-graph.tsx +144 -0
- package/components/ui/carousel.tsx +242 -0
- package/components/ui/sidebar.tsx +1 -1
- package/components/ui/sonner.tsx +51 -0
- package/electron/entitlements.mac.plist +16 -0
- package/electron/init-db.mjs +37 -0
- package/electron/main.mjs +203 -0
- package/generated/prisma/browser.ts +5 -0
- package/generated/prisma/client.ts +5 -0
- package/generated/prisma/internal/class.ts +14 -4
- package/generated/prisma/internal/prismaNamespace.ts +97 -2
- package/generated/prisma/internal/prismaNamespaceBrowser.ts +21 -1
- package/generated/prisma/models/Report.ts +1219 -0
- package/generated/prisma/models/Session.ts +221 -1
- package/generated/prisma/models.ts +1 -0
- package/lib/coach.ts +571 -211
- package/lib/command-insights.ts +231 -0
- package/lib/db.ts +2 -2
- package/lib/parse-codex.ts +6 -0
- package/lib/parse-logs.ts +80 -1
- package/lib/queries-codex.ts +24 -0
- package/lib/queries.ts +45 -0
- package/lib/report.ts +156 -0
- package/lib/session-detail.ts +382 -0
- package/lib/sync.ts +87 -0
- package/lib/tool-flow.ts +71 -0
- package/next.config.mjs +6 -1
- package/package.json +17 -2
- package/plugins/cost-heatmap/component.tsx +72 -50
- package/prisma/migrations/20260401144555_add_system_prompt_edits/migration.sql +80 -0
- package/prisma/schema.prisma +18 -0
- package/prisma/schema.sql +81 -0
- package/.claude/settings.local.json +0 -26
- package/CONTRIBUTING.md +0 -209
- package/prisma/migrations/20260328152517_init/migration.sql +0 -41
- package/prisma/migrations/20260328153801_add_image_model/migration.sql +0 -18
- package/prisma.config.ts +0 -14
- 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
|
|
7
|
-
const adapter = new PrismaLibSql({ url:
|
|
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
|
|
package/lib/parse-codex.ts
CHANGED
|
@@ -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
|
|
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: [],
|
package/lib/queries-codex.ts
CHANGED
|
@@ -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: [],
|