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.
- package/.claude/settings.local.json +26 -0
- package/.prettierignore +7 -0
- package/.prettierrc +11 -0
- package/CONTRIBUTING.md +209 -0
- package/LICENSE +21 -0
- package/README.md +109 -0
- package/app/(dashboard)/coach/page.tsx +11 -0
- package/app/(dashboard)/commands/page.tsx +7 -0
- package/app/(dashboard)/community/[slug]/page.tsx +23 -0
- package/app/(dashboard)/community/page.tsx +71 -0
- package/app/(dashboard)/daily/page.tsx +19 -0
- package/app/(dashboard)/images/page.tsx +5 -0
- package/app/(dashboard)/layout.tsx +12 -0
- package/app/(dashboard)/page.tsx +23 -0
- package/app/(dashboard)/personality/page.tsx +11 -0
- package/app/(dashboard)/projects/page.tsx +11 -0
- package/app/(dashboard)/sessions/page.tsx +11 -0
- package/app/(dashboard)/tokens/page.tsx +11 -0
- package/app/(dashboard)/tools/page.tsx +11 -0
- package/app/api/check/route.ts +13 -0
- package/app/api/commands/route.ts +16 -0
- package/app/api/images/[...path]/route.ts +33 -0
- package/app/api/images-analysis/route.ts +177 -0
- package/app/api/sync/route.ts +14 -0
- package/app/api/usage/route.ts +117 -0
- package/app/favicon.ico +0 -0
- package/app/globals.css +144 -0
- package/app/icon.svg +3 -0
- package/app/layout.tsx +35 -0
- package/bin/agentfit.mjs +69 -0
- package/components/.gitkeep +0 -0
- package/components/agent-coach.tsx +248 -0
- package/components/app-sidebar.tsx +161 -0
- package/components/command-usage.tsx +294 -0
- package/components/daily-chart.tsx +118 -0
- package/components/daily-table.tsx +115 -0
- package/components/dashboard-shell.tsx +149 -0
- package/components/data-provider.tsx +213 -0
- package/components/fitness-score.tsx +95 -0
- package/components/overview-cards.tsx +198 -0
- package/components/pagination-controls.tsx +104 -0
- package/components/personality-fit.tsx +446 -0
- package/components/projects-table.tsx +70 -0
- package/components/screenshots-analysis.tsx +359 -0
- package/components/sessions-table.tsx +97 -0
- package/components/theme-provider.tsx +71 -0
- package/components/token-breakdown.tsx +179 -0
- package/components/tool-usage-chart.tsx +63 -0
- package/components/ui/badge.tsx +52 -0
- package/components/ui/button.tsx +60 -0
- package/components/ui/card.tsx +103 -0
- package/components/ui/chart.tsx +373 -0
- package/components/ui/dialog.tsx +160 -0
- package/components/ui/input.tsx +20 -0
- package/components/ui/scroll-area.tsx +55 -0
- package/components/ui/select.tsx +201 -0
- package/components/ui/separator.tsx +25 -0
- package/components/ui/sheet.tsx +138 -0
- package/components/ui/sidebar.tsx +723 -0
- package/components/ui/skeleton.tsx +13 -0
- package/components/ui/table.tsx +116 -0
- package/components/ui/tabs.tsx +82 -0
- package/components/ui/tooltip.tsx +66 -0
- package/components.json +25 -0
- package/generated/prisma/browser.ts +34 -0
- package/generated/prisma/client.ts +58 -0
- package/generated/prisma/commonInputTypes.ts +237 -0
- package/generated/prisma/enums.ts +15 -0
- package/generated/prisma/internal/class.ts +224 -0
- package/generated/prisma/internal/prismaNamespace.ts +920 -0
- package/generated/prisma/internal/prismaNamespaceBrowser.ts +130 -0
- package/generated/prisma/models/Image.ts +1310 -0
- package/generated/prisma/models/Session.ts +1695 -0
- package/generated/prisma/models/SyncLog.ts +1203 -0
- package/generated/prisma/models.ts +14 -0
- package/hooks/.gitkeep +0 -0
- package/hooks/use-mobile.ts +19 -0
- package/hooks/use-pagination.ts +60 -0
- package/lib/.gitkeep +0 -0
- package/lib/coach.ts +425 -0
- package/lib/commands.ts +239 -0
- package/lib/db.ts +15 -0
- package/lib/format.ts +26 -0
- package/lib/parse-codex.ts +201 -0
- package/lib/parse-logs.ts +369 -0
- package/lib/personality.ts +481 -0
- package/lib/plugins.ts +107 -0
- package/lib/pricing.ts +112 -0
- package/lib/queries-codex.ts +130 -0
- package/lib/queries.ts +154 -0
- package/lib/resolve-icon.ts +12 -0
- package/lib/sync.ts +335 -0
- package/lib/utils.ts +6 -0
- package/next.config.mjs +4 -0
- package/package.json +73 -0
- package/plugins/cost-heatmap/component.test.tsx +52 -0
- package/plugins/cost-heatmap/component.tsx +227 -0
- package/plugins/cost-heatmap/manifest.ts +13 -0
- package/plugins/index.ts +18 -0
- package/prisma/migrations/20260328152517_init/migration.sql +41 -0
- package/prisma/migrations/20260328153801_add_image_model/migration.sql +18 -0
- package/prisma/migrations/migration_lock.toml +3 -0
- package/prisma/schema.prisma +57 -0
- package/prisma.config.ts +14 -0
- package/public/.gitkeep +0 -0
- package/public/logo.svg +3 -0
- 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
|
+
}
|