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,248 @@
1
+ 'use client'
2
+
3
+ import { useMemo } from 'react'
4
+ import type { UsageData } from '@/lib/parse-logs'
5
+ import { generateCoachInsights, type CoachInsight, type InsightCategory, type InsightSeverity } from '@/lib/coach'
6
+ import { Card, CardContent, CardDescription, CardHeader, CardTitle } from '@/components/ui/card'
7
+ import { formatCost, formatDuration, formatNumber } from '@/lib/format'
8
+ import {
9
+ Trophy,
10
+ AlertTriangle,
11
+ Lightbulb,
12
+ DollarSign,
13
+ Zap,
14
+ Wrench,
15
+ Monitor,
16
+ Cpu,
17
+ Flame,
18
+ Calendar,
19
+ Search,
20
+ Clock,
21
+ TrendingUp,
22
+ MessageSquare,
23
+ } from 'lucide-react'
24
+
25
+ const CATEGORY_ICONS: Record<InsightCategory, typeof Trophy> = {
26
+ cost: DollarSign,
27
+ efficiency: Zap,
28
+ tools: Wrench,
29
+ context: Monitor,
30
+ model: Cpu,
31
+ habits: Calendar,
32
+ discovery: Search,
33
+ streak: Flame,
34
+ }
35
+
36
+ const CATEGORY_LABELS: Record<InsightCategory, string> = {
37
+ cost: 'Cost',
38
+ efficiency: 'Efficiency',
39
+ tools: 'Tools',
40
+ context: 'Context',
41
+ model: 'Model',
42
+ habits: 'Habits',
43
+ discovery: 'Discovery',
44
+ streak: 'Streak',
45
+ }
46
+
47
+ const SEVERITY_STYLES: Record<InsightSeverity, { border: string; icon: typeof Trophy; iconClass: string }> = {
48
+ achievement: { border: 'border-l-chart-2', icon: Trophy, iconClass: 'text-chart-2' },
49
+ warning: { border: 'border-l-chart-5', icon: AlertTriangle, iconClass: 'text-chart-5' },
50
+ tip: { border: 'border-l-chart-1', icon: Lightbulb, iconClass: 'text-chart-1' },
51
+ }
52
+
53
+ function FitnessRing({ score, label }: { score: number; label: string }) {
54
+ const radius = 70
55
+ const circumference = 2 * Math.PI * radius
56
+ const offset = circumference - (score / 100) * circumference
57
+
58
+ return (
59
+ <div className="flex flex-col items-center gap-2">
60
+ <div className="relative">
61
+ <svg width="180" height="180" viewBox="0 0 180 180">
62
+ <circle
63
+ cx="90" cy="90" r={radius}
64
+ fill="none"
65
+ stroke="var(--muted)"
66
+ strokeWidth="10"
67
+ />
68
+ <circle
69
+ cx="90" cy="90" r={radius}
70
+ fill="none"
71
+ stroke="var(--chart-2)"
72
+ strokeWidth="10"
73
+ strokeLinecap="round"
74
+ strokeDasharray={circumference}
75
+ strokeDashoffset={offset}
76
+ transform="rotate(-90 90 90)"
77
+ className="transition-all duration-1000"
78
+ />
79
+ </svg>
80
+ <div className="absolute inset-0 flex flex-col items-center justify-center">
81
+ <span className="text-4xl font-bold">{score}</span>
82
+ <span className="text-xs text-muted-foreground">{label}</span>
83
+ </div>
84
+ </div>
85
+ </div>
86
+ )
87
+ }
88
+
89
+ function InsightCard({ insight }: { insight: CoachInsight }) {
90
+ const style = SEVERITY_STYLES[insight.severity]
91
+ const CategoryIcon = CATEGORY_ICONS[insight.category]
92
+ const SeverityIcon = style.icon
93
+
94
+ return (
95
+ <div className={`rounded-lg border border-l-4 ${style.border} p-4`}>
96
+ <div className="flex items-start gap-3">
97
+ <SeverityIcon className={`h-5 w-5 shrink-0 mt-0.5 ${style.iconClass}`} />
98
+ <div className="flex-1 space-y-1.5">
99
+ <div className="flex items-center justify-between gap-2">
100
+ <h4 className="text-sm font-semibold">{insight.title}</h4>
101
+ <div className="flex items-center gap-1.5 shrink-0">
102
+ {insight.metric && (
103
+ <span className="rounded-md bg-muted px-2 py-0.5 text-xs font-mono font-medium">
104
+ {insight.metric}
105
+ </span>
106
+ )}
107
+ <span className="flex items-center gap-1 text-xs text-muted-foreground">
108
+ <CategoryIcon className="h-3 w-3" />
109
+ {CATEGORY_LABELS[insight.category]}
110
+ </span>
111
+ </div>
112
+ </div>
113
+ <p className="text-sm text-muted-foreground">{insight.description}</p>
114
+ {insight.recommendation && (
115
+ <div className="rounded-md bg-muted/50 p-2.5 text-sm">
116
+ <span className="font-medium">Recommendation: </span>
117
+ {insight.recommendation}
118
+ </div>
119
+ )}
120
+ </div>
121
+ </div>
122
+ </div>
123
+ )
124
+ }
125
+
126
+ export function AgentCoach({ data }: { data: UsageData }) {
127
+ const coach = useMemo(() => generateCoachInsights(data), [data])
128
+
129
+ const achievements = coach.insights.filter(i => i.severity === 'achievement')
130
+ const warnings = coach.insights.filter(i => i.severity === 'warning')
131
+ const tips = coach.insights.filter(i => i.severity === 'tip')
132
+
133
+ return (
134
+ <div className="space-y-6">
135
+ {/* Fitness Score + Quick Stats */}
136
+ <div className="grid gap-4 lg:grid-cols-3">
137
+ <Card className="lg:row-span-2">
138
+ <CardHeader>
139
+ <CardTitle>Agent Fitness Score</CardTitle>
140
+ <CardDescription>Overall health of your human-AI collaboration</CardDescription>
141
+ </CardHeader>
142
+ <CardContent className="flex justify-center pb-6">
143
+ <FitnessRing score={coach.score} label={coach.scoreLabel} />
144
+ </CardContent>
145
+ </Card>
146
+
147
+ <Card>
148
+ <CardHeader className="flex flex-row items-center justify-between pb-2">
149
+ <CardTitle className="text-sm font-medium text-muted-foreground">Avg Cost / Session</CardTitle>
150
+ <DollarSign className="h-4 w-4 text-muted-foreground" />
151
+ </CardHeader>
152
+ <CardContent>
153
+ <div className="text-2xl font-bold">{formatCost(coach.stats.avgCostPerSession)}</div>
154
+ </CardContent>
155
+ </Card>
156
+
157
+ <Card>
158
+ <CardHeader className="flex flex-row items-center justify-between pb-2">
159
+ <CardTitle className="text-sm font-medium text-muted-foreground">Avg Duration</CardTitle>
160
+ <Clock className="h-4 w-4 text-muted-foreground" />
161
+ </CardHeader>
162
+ <CardContent>
163
+ <div className="text-2xl font-bold">{formatDuration(coach.stats.avgDurationMinutes)}</div>
164
+ </CardContent>
165
+ </Card>
166
+
167
+ <Card>
168
+ <CardHeader className="flex flex-row items-center justify-between pb-2">
169
+ <CardTitle className="text-sm font-medium text-muted-foreground">Avg Messages</CardTitle>
170
+ <MessageSquare className="h-4 w-4 text-muted-foreground" />
171
+ </CardHeader>
172
+ <CardContent>
173
+ <div className="text-2xl font-bold">{formatNumber(Math.round(coach.stats.avgMessagesPerSession))}</div>
174
+ </CardContent>
175
+ </Card>
176
+
177
+ <Card>
178
+ <CardHeader className="flex flex-row items-center justify-between pb-2">
179
+ <CardTitle className="text-sm font-medium text-muted-foreground">Coding Streak</CardTitle>
180
+ <Flame className="h-4 w-4 text-muted-foreground" />
181
+ </CardHeader>
182
+ <CardContent>
183
+ <div className="text-2xl font-bold">
184
+ {coach.stats.currentStreak > 0 ? `${coach.stats.currentStreak}d` : '—'}
185
+ <span className="text-sm font-normal text-muted-foreground ml-2">
186
+ best: {coach.stats.longestStreak}d
187
+ </span>
188
+ </div>
189
+ </CardContent>
190
+ </Card>
191
+ </div>
192
+
193
+ {/* Achievements */}
194
+ {achievements.length > 0 && (
195
+ <Card>
196
+ <CardHeader>
197
+ <div className="flex items-center gap-2">
198
+ <Trophy className="h-5 w-5 text-chart-2" />
199
+ <div>
200
+ <CardTitle>Achievements</CardTitle>
201
+ <CardDescription>Things you're doing well</CardDescription>
202
+ </div>
203
+ </div>
204
+ </CardHeader>
205
+ <CardContent className="space-y-3">
206
+ {achievements.map(i => <InsightCard key={i.id} insight={i} />)}
207
+ </CardContent>
208
+ </Card>
209
+ )}
210
+
211
+ {/* Warnings */}
212
+ {warnings.length > 0 && (
213
+ <Card>
214
+ <CardHeader>
215
+ <div className="flex items-center gap-2">
216
+ <AlertTriangle className="h-5 w-5 text-chart-5" />
217
+ <div>
218
+ <CardTitle>Areas to Improve</CardTitle>
219
+ <CardDescription>Issues that may be costing you time or money</CardDescription>
220
+ </div>
221
+ </div>
222
+ </CardHeader>
223
+ <CardContent className="space-y-3">
224
+ {warnings.map(i => <InsightCard key={i.id} insight={i} />)}
225
+ </CardContent>
226
+ </Card>
227
+ )}
228
+
229
+ {/* Tips */}
230
+ {tips.length > 0 && (
231
+ <Card>
232
+ <CardHeader>
233
+ <div className="flex items-center gap-2">
234
+ <Lightbulb className="h-5 w-5 text-chart-1" />
235
+ <div>
236
+ <CardTitle>Tips & Suggestions</CardTitle>
237
+ <CardDescription>Ways to get more out of your coding agent</CardDescription>
238
+ </div>
239
+ </div>
240
+ </CardHeader>
241
+ <CardContent className="space-y-3">
242
+ {tips.map(i => <InsightCard key={i.id} insight={i} />)}
243
+ </CardContent>
244
+ </Card>
245
+ )}
246
+ </div>
247
+ )
248
+ }
@@ -0,0 +1,161 @@
1
+ 'use client'
2
+
3
+ import { useState } from 'react'
4
+ import Link from 'next/link'
5
+ import { usePathname } from 'next/navigation'
6
+ import {
7
+ BarChart3,
8
+ Brain,
9
+ Camera,
10
+ ChevronRight,
11
+ Coins,
12
+ FolderOpen,
13
+ HeartPulse,
14
+ LayoutDashboard,
15
+ ListTree,
16
+ Puzzle,
17
+ Terminal,
18
+ Wrench,
19
+ } from 'lucide-react'
20
+ import type { ComponentType } from 'react'
21
+ import {
22
+ Sidebar,
23
+ SidebarContent,
24
+ SidebarGroup,
25
+ SidebarGroupContent,
26
+ SidebarGroupLabel,
27
+ SidebarHeader,
28
+ SidebarMenu,
29
+ SidebarMenuButton,
30
+ SidebarMenuItem,
31
+ SidebarMenuSub,
32
+ SidebarMenuSubButton,
33
+ SidebarMenuSubItem,
34
+ SidebarRail,
35
+ } from '@/components/ui/sidebar'
36
+ import { getPlugins } from '@/lib/plugins'
37
+ import '@/plugins'
38
+
39
+ interface NavItem {
40
+ title: string
41
+ icon: ComponentType<{ className?: string }>
42
+ href: string
43
+ }
44
+
45
+ interface NavGroup {
46
+ title: string
47
+ icon: ComponentType<{ className?: string }>
48
+ items: NavItem[]
49
+ }
50
+
51
+ const topItems: NavItem[] = [
52
+ { title: 'Dashboard', icon: LayoutDashboard, href: '/' },
53
+ { title: 'Agent Coach', icon: HeartPulse, href: '/coach' },
54
+ { title: 'Projects', icon: FolderOpen, href: '/projects' },
55
+ ]
56
+
57
+ const navGroups: NavGroup[] = [
58
+ {
59
+ title: 'Usage',
60
+ icon: BarChart3,
61
+ items: [
62
+ { title: 'Daily Usage', icon: BarChart3, href: '/daily' },
63
+ { title: 'Token Breakdown', icon: Coins, href: '/tokens' },
64
+ { title: 'Tool Usage', icon: Wrench, href: '/tools' },
65
+ ],
66
+ },
67
+ {
68
+ title: 'Insights',
69
+ icon: Brain,
70
+ items: [
71
+ { title: 'Personality Fit', icon: Brain, href: '/personality' },
72
+ { title: 'Command Usage', icon: Terminal, href: '/commands' },
73
+ { title: 'Images', icon: Camera, href: '/images' },
74
+ ],
75
+ },
76
+ ]
77
+
78
+ function CollapsibleGroup({ group, pathname }: { group: NavGroup; pathname: string }) {
79
+ const isActive = group.items.some((item) => pathname === item.href)
80
+ const [open, setOpen] = useState(isActive)
81
+
82
+ return (
83
+ <SidebarMenuItem>
84
+ <SidebarMenuButton onClick={() => setOpen(!open)}>
85
+ <group.icon className="h-4 w-4" />
86
+ <span>{group.title}</span>
87
+ <ChevronRight
88
+ className={`ml-auto h-4 w-4 transition-transform ${open ? 'rotate-90' : ''}`}
89
+ />
90
+ </SidebarMenuButton>
91
+ {open && (
92
+ <SidebarMenuSub>
93
+ {group.items.map((item) => (
94
+ <SidebarMenuSubItem key={item.href}>
95
+ <SidebarMenuSubButton
96
+ render={<Link href={item.href} />}
97
+ isActive={pathname === item.href}
98
+ >
99
+ <span>{item.title}</span>
100
+ </SidebarMenuSubButton>
101
+ </SidebarMenuSubItem>
102
+ ))}
103
+ </SidebarMenuSub>
104
+ )}
105
+ </SidebarMenuItem>
106
+ )
107
+ }
108
+
109
+ export function AppSidebar() {
110
+ const pathname = usePathname()
111
+ const plugins = getPlugins()
112
+
113
+ return (
114
+ <Sidebar>
115
+ <SidebarHeader>
116
+ <div className="flex items-center gap-2 px-2 py-2">
117
+ <img src="/logo.svg" alt="AgentFit" className="h-8 w-8" />
118
+ <div>
119
+ <div className="text-sm font-semibold">AgentFit</div>
120
+ <div className="text-xs text-muted-foreground">Coding Agent Fitness Tracker</div>
121
+ </div>
122
+ </div>
123
+ </SidebarHeader>
124
+ <SidebarContent>
125
+ <SidebarGroup>
126
+ <SidebarGroupLabel className="sr-only">Navigation</SidebarGroupLabel>
127
+ <SidebarGroupContent>
128
+ <SidebarMenu>
129
+ {topItems.map((item) => (
130
+ <SidebarMenuItem key={item.href}>
131
+ <SidebarMenuButton
132
+ render={<Link href={item.href} />}
133
+ isActive={pathname === item.href}
134
+ >
135
+ <item.icon className="h-4 w-4" />
136
+ <span>{item.title}</span>
137
+ </SidebarMenuButton>
138
+ </SidebarMenuItem>
139
+ ))}
140
+ {navGroups.map((group) => (
141
+ <CollapsibleGroup key={group.title} group={group} pathname={pathname} />
142
+ ))}
143
+ {plugins.length > 0 && (
144
+ <SidebarMenuItem>
145
+ <SidebarMenuButton
146
+ render={<Link href="/community" />}
147
+ isActive={pathname.startsWith('/community')}
148
+ >
149
+ <Puzzle className="h-4 w-4" />
150
+ <span>Community</span>
151
+ </SidebarMenuButton>
152
+ </SidebarMenuItem>
153
+ )}
154
+ </SidebarMenu>
155
+ </SidebarGroupContent>
156
+ </SidebarGroup>
157
+ </SidebarContent>
158
+ <SidebarRail />
159
+ </Sidebar>
160
+ )
161
+ }