agentfit 0.1.0

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 (107) hide show
  1. package/.claude/settings.local.json +26 -0
  2. package/.prettierignore +7 -0
  3. package/.prettierrc +11 -0
  4. package/CONTRIBUTING.md +209 -0
  5. package/LICENSE +21 -0
  6. package/README.md +109 -0
  7. package/app/(dashboard)/coach/page.tsx +11 -0
  8. package/app/(dashboard)/commands/page.tsx +7 -0
  9. package/app/(dashboard)/community/[slug]/page.tsx +23 -0
  10. package/app/(dashboard)/community/page.tsx +71 -0
  11. package/app/(dashboard)/daily/page.tsx +19 -0
  12. package/app/(dashboard)/images/page.tsx +5 -0
  13. package/app/(dashboard)/layout.tsx +12 -0
  14. package/app/(dashboard)/page.tsx +23 -0
  15. package/app/(dashboard)/personality/page.tsx +11 -0
  16. package/app/(dashboard)/projects/page.tsx +11 -0
  17. package/app/(dashboard)/sessions/page.tsx +11 -0
  18. package/app/(dashboard)/tokens/page.tsx +11 -0
  19. package/app/(dashboard)/tools/page.tsx +11 -0
  20. package/app/api/check/route.ts +13 -0
  21. package/app/api/commands/route.ts +16 -0
  22. package/app/api/images/[...path]/route.ts +33 -0
  23. package/app/api/images-analysis/route.ts +177 -0
  24. package/app/api/sync/route.ts +14 -0
  25. package/app/api/usage/route.ts +117 -0
  26. package/app/favicon.ico +0 -0
  27. package/app/globals.css +144 -0
  28. package/app/icon.svg +3 -0
  29. package/app/layout.tsx +35 -0
  30. package/bin/agentfit.mjs +69 -0
  31. package/components/.gitkeep +0 -0
  32. package/components/agent-coach.tsx +248 -0
  33. package/components/app-sidebar.tsx +161 -0
  34. package/components/command-usage.tsx +294 -0
  35. package/components/daily-chart.tsx +118 -0
  36. package/components/daily-table.tsx +115 -0
  37. package/components/dashboard-shell.tsx +149 -0
  38. package/components/data-provider.tsx +213 -0
  39. package/components/fitness-score.tsx +95 -0
  40. package/components/overview-cards.tsx +198 -0
  41. package/components/pagination-controls.tsx +104 -0
  42. package/components/personality-fit.tsx +446 -0
  43. package/components/projects-table.tsx +70 -0
  44. package/components/screenshots-analysis.tsx +359 -0
  45. package/components/sessions-table.tsx +97 -0
  46. package/components/theme-provider.tsx +71 -0
  47. package/components/token-breakdown.tsx +179 -0
  48. package/components/tool-usage-chart.tsx +63 -0
  49. package/components/ui/badge.tsx +52 -0
  50. package/components/ui/button.tsx +60 -0
  51. package/components/ui/card.tsx +103 -0
  52. package/components/ui/chart.tsx +373 -0
  53. package/components/ui/dialog.tsx +160 -0
  54. package/components/ui/input.tsx +20 -0
  55. package/components/ui/scroll-area.tsx +55 -0
  56. package/components/ui/select.tsx +201 -0
  57. package/components/ui/separator.tsx +25 -0
  58. package/components/ui/sheet.tsx +138 -0
  59. package/components/ui/sidebar.tsx +723 -0
  60. package/components/ui/skeleton.tsx +13 -0
  61. package/components/ui/table.tsx +116 -0
  62. package/components/ui/tabs.tsx +82 -0
  63. package/components/ui/tooltip.tsx +66 -0
  64. package/components.json +25 -0
  65. package/generated/prisma/browser.ts +34 -0
  66. package/generated/prisma/client.ts +58 -0
  67. package/generated/prisma/commonInputTypes.ts +237 -0
  68. package/generated/prisma/enums.ts +15 -0
  69. package/generated/prisma/internal/class.ts +224 -0
  70. package/generated/prisma/internal/prismaNamespace.ts +920 -0
  71. package/generated/prisma/internal/prismaNamespaceBrowser.ts +130 -0
  72. package/generated/prisma/models/Image.ts +1310 -0
  73. package/generated/prisma/models/Session.ts +1695 -0
  74. package/generated/prisma/models/SyncLog.ts +1203 -0
  75. package/generated/prisma/models.ts +14 -0
  76. package/hooks/.gitkeep +0 -0
  77. package/hooks/use-mobile.ts +19 -0
  78. package/hooks/use-pagination.ts +60 -0
  79. package/lib/.gitkeep +0 -0
  80. package/lib/coach.ts +425 -0
  81. package/lib/commands.ts +239 -0
  82. package/lib/db.ts +15 -0
  83. package/lib/format.ts +26 -0
  84. package/lib/parse-codex.ts +201 -0
  85. package/lib/parse-logs.ts +369 -0
  86. package/lib/personality.ts +481 -0
  87. package/lib/plugins.ts +107 -0
  88. package/lib/pricing.ts +112 -0
  89. package/lib/queries-codex.ts +130 -0
  90. package/lib/queries.ts +154 -0
  91. package/lib/resolve-icon.ts +12 -0
  92. package/lib/sync.ts +335 -0
  93. package/lib/utils.ts +6 -0
  94. package/next.config.mjs +4 -0
  95. package/package.json +73 -0
  96. package/plugins/cost-heatmap/component.test.tsx +52 -0
  97. package/plugins/cost-heatmap/component.tsx +227 -0
  98. package/plugins/cost-heatmap/manifest.ts +13 -0
  99. package/plugins/index.ts +18 -0
  100. package/prisma/migrations/20260328152517_init/migration.sql +41 -0
  101. package/prisma/migrations/20260328153801_add_image_model/migration.sql +18 -0
  102. package/prisma/migrations/migration_lock.toml +3 -0
  103. package/prisma/schema.prisma +57 -0
  104. package/prisma.config.ts +14 -0
  105. package/public/.gitkeep +0 -0
  106. package/public/logo.svg +3 -0
  107. package/setup.sh +73 -0
@@ -0,0 +1,369 @@
1
+ import fs from 'fs'
2
+ import path from 'path'
3
+ import os from 'os'
4
+ import { loadPricing, calculateCost, type ModelPricing } from './pricing'
5
+
6
+ const CLAUDE_DIR = path.join(os.homedir(), '.claude')
7
+ const PROJECTS_DIR = path.join(CLAUDE_DIR, 'projects')
8
+
9
+ // ─── Types ───────────────────────────────────────────────────────────
10
+
11
+ export interface SessionSummary {
12
+ sessionId: string
13
+ project: string
14
+ projectPath: string // original path decoded from dir name
15
+ startTime: string
16
+ endTime: string
17
+ durationMinutes: number
18
+ userMessages: number
19
+ assistantMessages: number
20
+ totalMessages: number
21
+ inputTokens: number
22
+ outputTokens: number
23
+ cacheCreationTokens: number
24
+ cacheReadTokens: number
25
+ totalTokens: number
26
+ costUSD: number
27
+ model: string
28
+ toolCalls: Record<string, number>
29
+ toolCallsTotal: number
30
+ }
31
+
32
+ export interface ProjectSummary {
33
+ name: string
34
+ path: string
35
+ sessions: number
36
+ totalMessages: number
37
+ totalTokens: number
38
+ totalCost: number
39
+ totalDurationMinutes: number
40
+ toolCalls: Record<string, number>
41
+ }
42
+
43
+ export interface DailyUsage {
44
+ date: string
45
+ sessions: number
46
+ messages: number
47
+ inputTokens: number
48
+ outputTokens: number
49
+ cacheCreationTokens: number
50
+ cacheReadTokens: number
51
+ totalTokens: number
52
+ costUSD: number
53
+ toolCalls: number
54
+ }
55
+
56
+ export interface OverviewStats {
57
+ totalSessions: number
58
+ totalProjects: number
59
+ totalMessages: number
60
+ totalUserMessages: number
61
+ totalAssistantMessages: number
62
+ totalInputTokens: number
63
+ totalOutputTokens: number
64
+ totalCacheCreationTokens: number
65
+ totalCacheReadTokens: number
66
+ totalTokens: number
67
+ totalCostUSD: number
68
+ totalDurationMinutes: number
69
+ totalToolCalls: number
70
+ models: Record<string, number>
71
+ }
72
+
73
+ export interface UsageData {
74
+ overview: OverviewStats
75
+ sessions: SessionSummary[]
76
+ projects: ProjectSummary[]
77
+ daily: DailyUsage[]
78
+ toolUsage: Record<string, number>
79
+ }
80
+
81
+ // ─── Parsing ─────────────────────────────────────────────────────────
82
+
83
+ function decodeProjectPath(dirName: string): string {
84
+ return dirName.replace(/-/g, '/')
85
+ }
86
+
87
+ function getProjectName(projectPath: string): string {
88
+ const parts = projectPath.split('/')
89
+ return parts[parts.length - 1] || parts[parts.length - 2] || projectPath
90
+ }
91
+
92
+ interface LogEntry {
93
+ type?: string
94
+ message?: {
95
+ role?: string
96
+ content?: unknown[]
97
+ model?: string
98
+ usage?: {
99
+ input_tokens?: number
100
+ output_tokens?: number
101
+ cache_creation_input_tokens?: number
102
+ cache_read_input_tokens?: number
103
+ }
104
+ }
105
+ timestamp?: string
106
+ uuid?: string
107
+ }
108
+
109
+ function parseSessionFile(
110
+ filePath: string,
111
+ allPricing: Record<string, ModelPricing>
112
+ ): SessionSummary | null {
113
+ try {
114
+ const content = fs.readFileSync(filePath, 'utf-8')
115
+ const lines = content.trim().split('\n')
116
+
117
+ let userMessages = 0
118
+ let assistantMessages = 0
119
+ let inputTokens = 0
120
+ let outputTokens = 0
121
+ let cacheCreationTokens = 0
122
+ let cacheReadTokens = 0
123
+ let costUSD = 0
124
+ let model = ''
125
+ let startTime = ''
126
+ let endTime = ''
127
+ const toolCalls: Record<string, number> = {}
128
+
129
+ for (const line of lines) {
130
+ if (!line.trim()) continue
131
+ let entry: LogEntry
132
+ try {
133
+ entry = JSON.parse(line)
134
+ } catch {
135
+ continue
136
+ }
137
+
138
+ // Track timestamps
139
+ if (entry.timestamp) {
140
+ if (!startTime) startTime = entry.timestamp
141
+ endTime = entry.timestamp
142
+ }
143
+
144
+ if (entry.type === 'user') {
145
+ userMessages++
146
+ } else if (entry.type === 'assistant' && entry.message) {
147
+ assistantMessages++
148
+ const msg = entry.message
149
+
150
+ // Model
151
+ if (msg.model && msg.model !== '<synthetic>') {
152
+ model = msg.model
153
+ }
154
+
155
+ // Usage
156
+ if (msg.usage) {
157
+ const u = msg.usage
158
+ inputTokens += u.input_tokens || 0
159
+ outputTokens += u.output_tokens || 0
160
+ cacheCreationTokens += u.cache_creation_input_tokens || 0
161
+ cacheReadTokens += u.cache_read_input_tokens || 0
162
+
163
+ if (model) {
164
+ costUSD += calculateCost(model, u, allPricing)
165
+ }
166
+ }
167
+
168
+ // Tool calls
169
+ if (Array.isArray(msg.content)) {
170
+ for (const block of msg.content) {
171
+ if (
172
+ block &&
173
+ typeof block === 'object' &&
174
+ 'type' in block &&
175
+ (block as Record<string, unknown>).type === 'tool_use'
176
+ ) {
177
+ const toolName = (block as Record<string, unknown>).name as string
178
+ if (toolName) {
179
+ toolCalls[toolName] = (toolCalls[toolName] || 0) + 1
180
+ }
181
+ }
182
+ }
183
+ }
184
+ }
185
+ }
186
+
187
+ if (userMessages === 0 && assistantMessages === 0) return null
188
+
189
+ const durationMinutes =
190
+ startTime && endTime
191
+ ? (new Date(endTime).getTime() - new Date(startTime).getTime()) / 60000
192
+ : 0
193
+
194
+ const sessionId = path.basename(filePath, '.jsonl')
195
+
196
+ return {
197
+ sessionId,
198
+ project: '',
199
+ projectPath: '',
200
+ startTime,
201
+ endTime,
202
+ durationMinutes: Math.max(0, durationMinutes),
203
+ userMessages,
204
+ assistantMessages,
205
+ totalMessages: userMessages + assistantMessages,
206
+ inputTokens,
207
+ outputTokens,
208
+ cacheCreationTokens,
209
+ cacheReadTokens,
210
+ totalTokens: inputTokens + outputTokens + cacheCreationTokens + cacheReadTokens,
211
+ costUSD,
212
+ model: model || 'unknown',
213
+ toolCalls,
214
+ toolCallsTotal: Object.values(toolCalls).reduce((a, b) => a + b, 0),
215
+ }
216
+ } catch {
217
+ return null
218
+ }
219
+ }
220
+
221
+ export async function parseAllLogs(): Promise<UsageData> {
222
+ const allPricing = await loadPricing()
223
+
224
+ if (!fs.existsSync(PROJECTS_DIR)) {
225
+ return emptyUsageData()
226
+ }
227
+
228
+ const projectDirs = fs.readdirSync(PROJECTS_DIR).filter((d) => {
229
+ return fs.statSync(path.join(PROJECTS_DIR, d)).isDirectory()
230
+ })
231
+
232
+ const sessions: SessionSummary[] = []
233
+ const projectMap = new Map<string, ProjectSummary>()
234
+ const dailyMap = new Map<string, DailyUsage>()
235
+ const toolUsage: Record<string, number> = {}
236
+
237
+ for (const dir of projectDirs) {
238
+ const projectPath = decodeProjectPath(dir)
239
+ const projectName = getProjectName(projectPath)
240
+ const dirPath = path.join(PROJECTS_DIR, dir)
241
+
242
+ const jsonlFiles = fs.readdirSync(dirPath).filter((f) => f.endsWith('.jsonl'))
243
+
244
+ for (const file of jsonlFiles) {
245
+ const session = parseSessionFile(path.join(dirPath, file), allPricing)
246
+ if (!session) continue
247
+
248
+ session.project = projectName
249
+ session.projectPath = projectPath
250
+ sessions.push(session)
251
+
252
+ // Aggregate project stats
253
+ if (!projectMap.has(projectName)) {
254
+ projectMap.set(projectName, {
255
+ name: projectName,
256
+ path: projectPath,
257
+ sessions: 0,
258
+ totalMessages: 0,
259
+ totalTokens: 0,
260
+ totalCost: 0,
261
+ totalDurationMinutes: 0,
262
+ toolCalls: {},
263
+ })
264
+ }
265
+ const proj = projectMap.get(projectName)!
266
+ proj.sessions++
267
+ proj.totalMessages += session.totalMessages
268
+ proj.totalTokens += session.totalTokens
269
+ proj.totalCost += session.costUSD
270
+ proj.totalDurationMinutes += session.durationMinutes
271
+ for (const [tool, count] of Object.entries(session.toolCalls)) {
272
+ proj.toolCalls[tool] = (proj.toolCalls[tool] || 0) + count
273
+ }
274
+
275
+ // Aggregate daily stats
276
+ if (session.startTime) {
277
+ const date = session.startTime.slice(0, 10)
278
+ if (!dailyMap.has(date)) {
279
+ dailyMap.set(date, {
280
+ date,
281
+ sessions: 0,
282
+ messages: 0,
283
+ inputTokens: 0,
284
+ outputTokens: 0,
285
+ cacheCreationTokens: 0,
286
+ cacheReadTokens: 0,
287
+ totalTokens: 0,
288
+ costUSD: 0,
289
+ toolCalls: 0,
290
+ })
291
+ }
292
+ const daily = dailyMap.get(date)!
293
+ daily.sessions++
294
+ daily.messages += session.totalMessages
295
+ daily.inputTokens += session.inputTokens
296
+ daily.outputTokens += session.outputTokens
297
+ daily.cacheCreationTokens += session.cacheCreationTokens
298
+ daily.cacheReadTokens += session.cacheReadTokens
299
+ daily.totalTokens += session.totalTokens
300
+ daily.costUSD += session.costUSD
301
+ daily.toolCalls += session.toolCallsTotal
302
+ }
303
+
304
+ // Aggregate tool usage
305
+ for (const [tool, count] of Object.entries(session.toolCalls)) {
306
+ toolUsage[tool] = (toolUsage[tool] || 0) + count
307
+ }
308
+ }
309
+ }
310
+
311
+ // Sort sessions by start time desc
312
+ sessions.sort((a, b) => (b.startTime || '').localeCompare(a.startTime || ''))
313
+
314
+ // Build daily array sorted by date
315
+ const daily = Array.from(dailyMap.values()).sort((a, b) => a.date.localeCompare(b.date))
316
+
317
+ // Build projects array sorted by cost desc
318
+ const projects = Array.from(projectMap.values()).sort((a, b) => b.totalCost - a.totalCost)
319
+
320
+ // Build overview
321
+ const models: Record<string, number> = {}
322
+ for (const s of sessions) {
323
+ models[s.model] = (models[s.model] || 0) + 1
324
+ }
325
+
326
+ const overview: OverviewStats = {
327
+ totalSessions: sessions.length,
328
+ totalProjects: projects.length,
329
+ totalMessages: sessions.reduce((a, s) => a + s.totalMessages, 0),
330
+ totalUserMessages: sessions.reduce((a, s) => a + s.userMessages, 0),
331
+ totalAssistantMessages: sessions.reduce((a, s) => a + s.assistantMessages, 0),
332
+ totalInputTokens: sessions.reduce((a, s) => a + s.inputTokens, 0),
333
+ totalOutputTokens: sessions.reduce((a, s) => a + s.outputTokens, 0),
334
+ totalCacheCreationTokens: sessions.reduce((a, s) => a + s.cacheCreationTokens, 0),
335
+ totalCacheReadTokens: sessions.reduce((a, s) => a + s.cacheReadTokens, 0),
336
+ totalTokens: sessions.reduce((a, s) => a + s.totalTokens, 0),
337
+ totalCostUSD: sessions.reduce((a, s) => a + s.costUSD, 0),
338
+ totalDurationMinutes: sessions.reduce((a, s) => a + s.durationMinutes, 0),
339
+ totalToolCalls: sessions.reduce((a, s) => a + s.toolCallsTotal, 0),
340
+ models,
341
+ }
342
+
343
+ return { overview, sessions, projects, daily, toolUsage }
344
+ }
345
+
346
+ function emptyUsageData(): UsageData {
347
+ return {
348
+ overview: {
349
+ totalSessions: 0,
350
+ totalProjects: 0,
351
+ totalMessages: 0,
352
+ totalUserMessages: 0,
353
+ totalAssistantMessages: 0,
354
+ totalInputTokens: 0,
355
+ totalOutputTokens: 0,
356
+ totalCacheCreationTokens: 0,
357
+ totalCacheReadTokens: 0,
358
+ totalTokens: 0,
359
+ totalCostUSD: 0,
360
+ totalDurationMinutes: 0,
361
+ totalToolCalls: 0,
362
+ models: {},
363
+ },
364
+ sessions: [],
365
+ projects: [],
366
+ daily: [],
367
+ toolUsage: {},
368
+ }
369
+ }