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,14 @@
1
+
2
+ /* !!! This is code generated by Prisma. Do not edit directly. !!! */
3
+ /* eslint-disable */
4
+ // biome-ignore-all lint: generated file
5
+ // @ts-nocheck
6
+ /*
7
+ * This is a barrel export file for all models and their related types.
8
+ *
9
+ * 🟢 You can import this file directly.
10
+ */
11
+ export type * from './models/Session'
12
+ export type * from './models/Image'
13
+ export type * from './models/SyncLog'
14
+ export type * from './commonInputTypes'
package/hooks/.gitkeep ADDED
File without changes
@@ -0,0 +1,19 @@
1
+ import * as React from "react"
2
+
3
+ const MOBILE_BREAKPOINT = 768
4
+
5
+ export function useIsMobile() {
6
+ const [isMobile, setIsMobile] = React.useState<boolean | undefined>(undefined)
7
+
8
+ React.useEffect(() => {
9
+ const mql = window.matchMedia(`(max-width: ${MOBILE_BREAKPOINT - 1}px)`)
10
+ const onChange = () => {
11
+ setIsMobile(window.innerWidth < MOBILE_BREAKPOINT)
12
+ }
13
+ mql.addEventListener("change", onChange)
14
+ setIsMobile(window.innerWidth < MOBILE_BREAKPOINT)
15
+ return () => mql.removeEventListener("change", onChange)
16
+ }, [])
17
+
18
+ return !!isMobile
19
+ }
@@ -0,0 +1,60 @@
1
+ import { useState, useMemo } from 'react'
2
+
3
+ export interface PaginationState<T> {
4
+ page: number
5
+ pageSize: number
6
+ totalPages: number
7
+ totalItems: number
8
+ pageItems: T[]
9
+ setPage: (page: number) => void
10
+ setPageSize: (size: number) => void
11
+ canPrevious: boolean
12
+ canNext: boolean
13
+ previous: () => void
14
+ next: () => void
15
+ startIndex: number
16
+ endIndex: number
17
+ }
18
+
19
+ export function usePagination<T>(
20
+ items: T[],
21
+ defaultPageSize = 20
22
+ ): PaginationState<T> {
23
+ const [page, setPage] = useState(1)
24
+ const [pageSize, setPageSizeState] = useState(defaultPageSize)
25
+
26
+ const totalItems = items.length
27
+ const totalPages = Math.max(1, Math.ceil(totalItems / pageSize))
28
+
29
+ // Reset to page 1 if current page is out of bounds
30
+ const safePage = Math.min(page, totalPages)
31
+
32
+ const startIndex = (safePage - 1) * pageSize
33
+ const endIndex = Math.min(startIndex + pageSize, totalItems)
34
+
35
+ const pageItems = useMemo(
36
+ () => items.slice(startIndex, endIndex),
37
+ [items, startIndex, endIndex]
38
+ )
39
+
40
+ const setPageSize = (size: number) => {
41
+ setPageSizeState(size)
42
+ setPage(1)
43
+ }
44
+
45
+ return {
46
+ page: safePage,
47
+ pageSize,
48
+ totalPages,
49
+ totalItems,
50
+ pageItems,
51
+ setPage,
52
+ setPageSize,
53
+ canPrevious: safePage > 1,
54
+ canNext: safePage < totalPages,
55
+ previous: () => setPage(Math.max(1, safePage - 1)),
56
+ next: () => setPage(Math.min(totalPages, safePage + 1)),
57
+ startIndex: startIndex + 1,
58
+ endIndex,
59
+ }
60
+ }
package/lib/.gitkeep ADDED
File without changes
package/lib/coach.ts ADDED
@@ -0,0 +1,425 @@
1
+ // ─── Agent Coach ─────────────────────────────────────────────────────
2
+ // Analyzes usage data and generates actionable coaching insights,
3
+ // like a Garmin coach for your AI coding agent.
4
+
5
+ import type { UsageData, SessionSummary } from './parse-logs'
6
+
7
+ export type InsightSeverity = 'tip' | 'warning' | 'achievement'
8
+ export type InsightCategory =
9
+ | 'cost'
10
+ | 'efficiency'
11
+ | 'tools'
12
+ | 'context'
13
+ | 'model'
14
+ | 'habits'
15
+ | 'discovery'
16
+ | 'streak'
17
+
18
+ export interface CoachInsight {
19
+ id: string
20
+ title: string
21
+ description: string
22
+ category: InsightCategory
23
+ severity: InsightSeverity
24
+ metric?: string // e.g., "$142.30" or "26.8%"
25
+ recommendation?: string
26
+ }
27
+
28
+ export interface CoachSummary {
29
+ score: number // 0-100 overall "fitness score"
30
+ scoreLabel: string
31
+ insights: CoachInsight[]
32
+ stats: {
33
+ avgCostPerSession: number
34
+ avgDurationMinutes: number
35
+ avgMessagesPerSession: number
36
+ avgToolCallsPerSession: number
37
+ mostUsedModel: string
38
+ mostActiveDay: string
39
+ longestStreak: number
40
+ currentStreak: number
41
+ totalDays: number
42
+ peakHour: number
43
+ }
44
+ }
45
+
46
+ // ─── Streak Calculation ──────────────────────────────────────────────
47
+
48
+ function calculateStreaks(sessions: SessionSummary[]): { longest: number; current: number } {
49
+ const dates = new Set(sessions.map(s => s.startTime.slice(0, 10)))
50
+ const sorted = Array.from(dates).sort()
51
+ if (sorted.length === 0) return { longest: 0, current: 0 }
52
+
53
+ let longest = 1
54
+ let current = 1
55
+ let streak = 1
56
+
57
+ for (let i = 1; i < sorted.length; i++) {
58
+ const prev = new Date(sorted[i - 1])
59
+ const curr = new Date(sorted[i])
60
+ const diffDays = (curr.getTime() - prev.getTime()) / (1000 * 60 * 60 * 24)
61
+
62
+ if (diffDays === 1) {
63
+ streak++
64
+ longest = Math.max(longest, streak)
65
+ } else {
66
+ streak = 1
67
+ }
68
+ }
69
+
70
+ // Check if current streak is still active (last active day is today or yesterday)
71
+ const lastDate = new Date(sorted[sorted.length - 1])
72
+ const today = new Date()
73
+ today.setHours(0, 0, 0, 0)
74
+ const diffFromToday = (today.getTime() - lastDate.getTime()) / (1000 * 60 * 60 * 24)
75
+ current = diffFromToday <= 1 ? streak : 0
76
+
77
+ return { longest, current }
78
+ }
79
+
80
+ // ─── Peak Hour ───────────────────────────────────────────────────────
81
+
82
+ function findPeakHour(sessions: SessionSummary[]): number {
83
+ const hourCounts = new Array(24).fill(0)
84
+ for (const s of sessions) {
85
+ if (s.startTime) {
86
+ const hour = new Date(s.startTime).getHours()
87
+ hourCounts[hour]++
88
+ }
89
+ }
90
+ return hourCounts.indexOf(Math.max(...hourCounts))
91
+ }
92
+
93
+ // ─── Most Active Day ─────────────────────────────────────────────────
94
+
95
+ function findMostActiveDay(sessions: SessionSummary[]): string {
96
+ const days = ['Sunday', 'Monday', 'Tuesday', 'Wednesday', 'Thursday', 'Friday', 'Saturday']
97
+ const dayCounts = new Array(7).fill(0)
98
+ for (const s of sessions) {
99
+ if (s.startTime) {
100
+ dayCounts[new Date(s.startTime).getDay()]++
101
+ }
102
+ }
103
+ return days[dayCounts.indexOf(Math.max(...dayCounts))]
104
+ }
105
+
106
+ // ─── Insight Generation ──────────────────────────────────────────────
107
+
108
+ export function generateCoachInsights(data: UsageData): CoachSummary {
109
+ const { sessions, overview, toolUsage } = data
110
+ const insights: CoachInsight[] = []
111
+
112
+ if (sessions.length === 0) {
113
+ return {
114
+ score: 0,
115
+ scoreLabel: 'No data',
116
+ insights: [{ id: 'no-data', title: 'No sessions found', description: 'Start using your coding agent to get coaching insights.', category: 'habits', severity: 'tip' }],
117
+ stats: { avgCostPerSession: 0, avgDurationMinutes: 0, avgMessagesPerSession: 0, avgToolCallsPerSession: 0, mostUsedModel: 'N/A', mostActiveDay: 'N/A', longestStreak: 0, currentStreak: 0, totalDays: 0, peakHour: 0 },
118
+ }
119
+ }
120
+
121
+ // Basic stats
122
+ const avgCost = overview.totalCostUSD / sessions.length
123
+ const avgDuration = overview.totalDurationMinutes / sessions.length
124
+ const avgMessages = overview.totalMessages / sessions.length
125
+ const avgToolCalls = overview.totalToolCalls / sessions.length
126
+ const modelEntries = Object.entries(overview.models).sort((a, b) => b[1] - a[1])
127
+ const mostUsedModel = modelEntries[0]?.[0] || 'unknown'
128
+ const streaks = calculateStreaks(sessions)
129
+ const peakHour = findPeakHour(sessions)
130
+ const mostActiveDay = findMostActiveDay(sessions)
131
+ const uniqueDates = new Set(sessions.map(s => s.startTime.slice(0, 10)))
132
+
133
+ // ── Cost insights ──
134
+
135
+ // Model cost optimization
136
+ const opusSessions = sessions.filter(s => s.model.includes('opus'))
137
+ const opusRatio = opusSessions.length / sessions.length
138
+ if (opusRatio > 0.8 && sessions.length > 10) {
139
+ const opusCost = opusSessions.reduce((sum, s) => sum + s.costUSD, 0)
140
+ insights.push({
141
+ id: 'model-diversity',
142
+ title: 'Heavy Opus usage detected',
143
+ description: `${Math.round(opusRatio * 100)}% of your sessions use Opus, costing $${opusCost.toFixed(0)} total. Many routine tasks (git operations, simple edits, file reading) can be handled by Haiku or Sonnet at a fraction of the cost.`,
144
+ category: 'cost',
145
+ severity: 'warning',
146
+ metric: `${Math.round(opusRatio * 100)}% Opus`,
147
+ recommendation: 'Use /model to switch to Sonnet or Haiku for routine tasks. Reserve Opus for complex architecture decisions and debugging.',
148
+ })
149
+ }
150
+
151
+ // High cost sessions
152
+ const expensiveSessions = sessions.filter(s => s.costUSD > avgCost * 3)
153
+ if (expensiveSessions.length > 3) {
154
+ insights.push({
155
+ id: 'expensive-sessions',
156
+ title: 'Cost spikes detected',
157
+ description: `${expensiveSessions.length} sessions cost 3x+ your average ($${avgCost.toFixed(2)}). These outliers often indicate context overflow or runaway tool loops.`,
158
+ category: 'cost',
159
+ severity: 'warning',
160
+ metric: `${expensiveSessions.length} expensive sessions`,
161
+ recommendation: 'Use /compact proactively during long sessions. Watch for repeated tool call patterns that indicate the agent is stuck.',
162
+ })
163
+ }
164
+
165
+ // Cost trend (last 7 days vs previous 7 days)
166
+ const sortedByDate = [...sessions].sort((a, b) => b.startTime.localeCompare(a.startTime))
167
+ const recentSessions = sortedByDate.filter(s => {
168
+ const d = new Date(s.startTime)
169
+ const now = new Date()
170
+ return (now.getTime() - d.getTime()) / (1000 * 60 * 60 * 24) <= 7
171
+ })
172
+ const olderSessions = sortedByDate.filter(s => {
173
+ const d = new Date(s.startTime)
174
+ const now = new Date()
175
+ const daysAgo = (now.getTime() - d.getTime()) / (1000 * 60 * 60 * 24)
176
+ return daysAgo > 7 && daysAgo <= 14
177
+ })
178
+ if (recentSessions.length > 3 && olderSessions.length > 3) {
179
+ const recentCost = recentSessions.reduce((sum, s) => sum + s.costUSD, 0)
180
+ const olderCost = olderSessions.reduce((sum, s) => sum + s.costUSD, 0)
181
+ if (recentCost < olderCost * 0.7) {
182
+ insights.push({
183
+ id: 'cost-down',
184
+ title: 'Costs trending down',
185
+ description: `Your spending dropped ${Math.round((1 - recentCost / olderCost) * 100)}% this week vs last week. You're becoming more efficient with your agent.`,
186
+ category: 'cost',
187
+ severity: 'achievement',
188
+ metric: `-${Math.round((1 - recentCost / olderCost) * 100)}%`,
189
+ })
190
+ } else if (recentCost > olderCost * 1.5) {
191
+ insights.push({
192
+ id: 'cost-up',
193
+ title: 'Cost spike this week',
194
+ description: `Spending is up ${Math.round((recentCost / olderCost - 1) * 100)}% vs last week ($${recentCost.toFixed(0)} vs $${olderCost.toFixed(0)}).`,
195
+ category: 'cost',
196
+ severity: 'warning',
197
+ metric: `+${Math.round((recentCost / olderCost - 1) * 100)}%`,
198
+ recommendation: 'Check if long-running sessions or model upgrades are driving the increase.',
199
+ })
200
+ }
201
+ }
202
+
203
+ // ── Efficiency insights ──
204
+
205
+ // Long sessions
206
+ const longSessions = sessions.filter(s => s.durationMinutes > 120)
207
+ if (longSessions.length > 5) {
208
+ const longAvgCost = longSessions.reduce((sum, s) => sum + s.costUSD, 0) / longSessions.length
209
+ insights.push({
210
+ id: 'long-sessions',
211
+ title: 'Many long sessions (2h+)',
212
+ description: `${longSessions.length} sessions exceeded 2 hours. Long sessions often hit context limits and lose coherence. Average cost per long session: $${longAvgCost.toFixed(2)}.`,
213
+ category: 'efficiency',
214
+ severity: 'tip',
215
+ metric: `${longSessions.length} sessions > 2h`,
216
+ recommendation: 'Break complex tasks into focused 30-60 min sessions. Use /clear between distinct tasks. Use /compact if you need to continue.',
217
+ })
218
+ }
219
+
220
+ // Read-before-Edit ratio
221
+ const readCalls = (toolUsage['Read'] || 0) + (toolUsage['Grep'] || 0) + (toolUsage['Glob'] || 0)
222
+ const editCalls = (toolUsage['Edit'] || 0) + (toolUsage['Write'] || 0)
223
+ if (editCalls > 0) {
224
+ const ratio = readCalls / editCalls
225
+ if (ratio < 0.5 && editCalls > 50) {
226
+ insights.push({
227
+ id: 'read-before-edit',
228
+ title: 'Low read-before-edit ratio',
229
+ description: `Your agent reads ${ratio.toFixed(1)}x for every edit. A ratio below 1.0 suggests the agent may be editing without fully understanding the code first, leading to more iterations.`,
230
+ category: 'efficiency',
231
+ severity: 'warning',
232
+ metric: `${ratio.toFixed(1)}x ratio`,
233
+ recommendation: 'Encourage the agent to read files before modifying them. Add "always read the file first" to your CLAUDE.md.',
234
+ })
235
+ } else if (ratio > 2.0) {
236
+ insights.push({
237
+ id: 'good-read-ratio',
238
+ title: 'Strong read-before-edit habit',
239
+ description: `Your agent reads ${ratio.toFixed(1)}x for every edit — a sign of careful, well-informed modifications. This typically leads to fewer iterations and less rework.`,
240
+ category: 'efficiency',
241
+ severity: 'achievement',
242
+ metric: `${ratio.toFixed(1)}x ratio`,
243
+ })
244
+ }
245
+ }
246
+
247
+ // ── Context insights ──
248
+
249
+ // High-token sessions (proxy for context overflow)
250
+ const highTokenSessions = sessions.filter(s => s.totalTokens > 200000)
251
+ const overflowRate = highTokenSessions.length / sessions.length
252
+ if (overflowRate > 0.2) {
253
+ insights.push({
254
+ id: 'context-overflow',
255
+ title: 'Frequent high-token sessions',
256
+ description: `${Math.round(overflowRate * 100)}% of sessions exceed 200K tokens, suggesting frequent context pressure. This increases cost and can reduce response quality.`,
257
+ category: 'context',
258
+ severity: 'warning',
259
+ metric: `${Math.round(overflowRate * 100)}% overflow rate`,
260
+ recommendation: 'Use /compact with focus instructions to preserve important context. Use /context to monitor usage. Start fresh sessions for new tasks.',
261
+ })
262
+ }
263
+
264
+ // ── Tool insights ──
265
+
266
+ // Bash overuse
267
+ const bashCalls = toolUsage['Bash'] || 0
268
+ const totalToolCalls = overview.totalToolCalls
269
+ const bashRatio = totalToolCalls > 0 ? bashCalls / totalToolCalls : 0
270
+ if (bashRatio > 0.4 && totalToolCalls > 100) {
271
+ insights.push({
272
+ id: 'bash-heavy',
273
+ title: 'Heavy Bash usage',
274
+ description: `${Math.round(bashRatio * 100)}% of tool calls are Bash commands. Some of these may be better handled by dedicated tools (Read instead of cat, Edit instead of sed, Grep instead of grep).`,
275
+ category: 'tools',
276
+ severity: 'tip',
277
+ metric: `${Math.round(bashRatio * 100)}% Bash`,
278
+ recommendation: 'Dedicated tools are faster, safer, and easier to review. Add guidance to CLAUDE.md to prefer Read/Edit/Grep over Bash equivalents.',
279
+ })
280
+ }
281
+
282
+ // Agent/subagent usage
283
+ const agentCalls = toolUsage['Agent'] || 0
284
+ if (agentCalls === 0 && totalToolCalls > 500) {
285
+ insights.push({
286
+ id: 'no-agents',
287
+ title: 'Not using subagents',
288
+ description: 'You haven\'t used the Agent tool for parallel research or exploration. Subagents can significantly speed up tasks that require searching across multiple files or investigating multiple approaches.',
289
+ category: 'tools',
290
+ severity: 'tip',
291
+ recommendation: 'Try asking Claude to "use an agent to research X while you work on Y". This parallelizes work and keeps the main context clean.',
292
+ })
293
+ } else if (agentCalls > 50) {
294
+ insights.push({
295
+ id: 'good-agents',
296
+ title: 'Effective subagent usage',
297
+ description: `${agentCalls} subagent calls show you're effectively parallelizing work. This keeps the main context lean while delegating research tasks.`,
298
+ category: 'tools',
299
+ severity: 'achievement',
300
+ metric: `${agentCalls} agent calls`,
301
+ })
302
+ }
303
+
304
+ // ── Habit insights ──
305
+
306
+ // Streaks
307
+ if (streaks.current >= 7) {
308
+ insights.push({
309
+ id: 'streak-hot',
310
+ title: `${streaks.current}-day coding streak!`,
311
+ description: `You've been coding with your agent every day for ${streaks.current} days straight. Consistency is the key to mastering human-AI collaboration.`,
312
+ category: 'streak',
313
+ severity: 'achievement',
314
+ metric: `${streaks.current} days`,
315
+ })
316
+ } else if (streaks.current >= 3) {
317
+ insights.push({
318
+ id: 'streak-building',
319
+ title: `${streaks.current}-day streak building`,
320
+ description: `You're on a ${streaks.current}-day streak. Your longest was ${streaks.longest} days — keep going!`,
321
+ category: 'streak',
322
+ severity: 'achievement',
323
+ metric: `${streaks.current} / ${streaks.longest} days`,
324
+ })
325
+ } else if (streaks.current === 0 && streaks.longest > 3) {
326
+ insights.push({
327
+ id: 'streak-broken',
328
+ title: 'Streak broken',
329
+ description: `Your ${streaks.longest}-day streak ended. Start a new one today!`,
330
+ category: 'streak',
331
+ severity: 'tip',
332
+ metric: `Best: ${streaks.longest} days`,
333
+ })
334
+ }
335
+
336
+ // Peak productivity time
337
+ insights.push({
338
+ id: 'peak-hour',
339
+ title: `Peak hour: ${peakHour}:00`,
340
+ description: `You start the most sessions around ${peakHour}:00. Your most active day is ${mostActiveDay}. Schedule your most complex tasks during these peak windows.`,
341
+ category: 'habits',
342
+ severity: 'tip',
343
+ metric: `${peakHour}:00 ${mostActiveDay}s`,
344
+ })
345
+
346
+ // Session volume trend
347
+ if (recentSessions.length > 0 && olderSessions.length > 0) {
348
+ const recentPerDay = recentSessions.length / 7
349
+ const olderPerDay = olderSessions.length / 7
350
+ if (recentPerDay > olderPerDay * 1.5) {
351
+ insights.push({
352
+ id: 'usage-up',
353
+ title: 'Usage ramping up',
354
+ description: `You're averaging ${recentPerDay.toFixed(1)} sessions/day this week, up from ${olderPerDay.toFixed(1)} last week. You're leaning more into AI-assisted development.`,
355
+ category: 'habits',
356
+ severity: 'achievement',
357
+ metric: `${recentPerDay.toFixed(1)}/day`,
358
+ })
359
+ }
360
+ }
361
+
362
+ // ── Discovery insights ──
363
+
364
+ // Skill/command usage from tool calls
365
+ const skillCalls = toolUsage['Skill'] || 0
366
+ if (skillCalls === 0 && totalToolCalls > 200) {
367
+ insights.push({
368
+ id: 'no-skills',
369
+ title: 'Unused: Custom Skills',
370
+ description: 'You haven\'t used any custom skills yet. Skills are reusable prompt templates that automate repetitive workflows — like /simplify for code review or custom deploy scripts.',
371
+ category: 'discovery',
372
+ severity: 'tip',
373
+ recommendation: 'Try /skills to see available skills. Create your own in .claude/skills/ for tasks you repeat often.',
374
+ })
375
+ }
376
+
377
+ // ── Compute fitness score ──
378
+
379
+ let score = 50 // baseline
380
+ const achievements = insights.filter(i => i.severity === 'achievement').length
381
+ const warnings = insights.filter(i => i.severity === 'warning').length
382
+
383
+ score += achievements * 8
384
+ score -= warnings * 6
385
+
386
+ // Bonus for streaks
387
+ score += Math.min(streaks.current, 10) * 2
388
+
389
+ // Bonus for good read ratio
390
+ if (readCalls > 0 && editCalls > 0 && readCalls / editCalls > 1.5) score += 5
391
+
392
+ // Penalty for high overflow rate
393
+ if (overflowRate > 0.3) score -= 10
394
+
395
+ // Bonus for tool diversity
396
+ const uniqueTools = Object.keys(toolUsage).length
397
+ if (uniqueTools > 10) score += 5
398
+
399
+ score = Math.max(0, Math.min(100, score))
400
+
401
+ const scoreLabel =
402
+ score >= 85 ? 'Elite' :
403
+ score >= 70 ? 'Strong' :
404
+ score >= 55 ? 'Building' :
405
+ score >= 40 ? 'Getting Started' :
406
+ 'Needs Attention'
407
+
408
+ return {
409
+ score,
410
+ scoreLabel,
411
+ insights,
412
+ stats: {
413
+ avgCostPerSession: avgCost,
414
+ avgDurationMinutes: avgDuration,
415
+ avgMessagesPerSession: avgMessages,
416
+ avgToolCallsPerSession: avgToolCalls,
417
+ mostUsedModel,
418
+ mostActiveDay,
419
+ longestStreak: streaks.longest,
420
+ currentStreak: streaks.current,
421
+ totalDays: uniqueDates.size,
422
+ peakHour,
423
+ },
424
+ }
425
+ }