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.
Files changed (74) hide show
  1. package/.github/workflows/release.yml +111 -0
  2. package/README.md +41 -38
  3. package/app/(dashboard)/daily/page.tsx +1 -1
  4. package/app/(dashboard)/data-management/page.tsx +180 -0
  5. package/app/(dashboard)/flow/page.tsx +17 -0
  6. package/app/(dashboard)/layout.tsx +2 -0
  7. package/app/(dashboard)/page.tsx +24 -5
  8. package/app/(dashboard)/reports/[id]/page.tsx +72 -0
  9. package/app/(dashboard)/reports/page.tsx +132 -0
  10. package/app/(dashboard)/sessions/[id]/page.tsx +167 -0
  11. package/app/api/backup/route.ts +215 -0
  12. package/app/api/check/route.ts +11 -1
  13. package/app/api/command-insights/route.ts +13 -0
  14. package/app/api/commands/route.ts +55 -1
  15. package/app/api/images-analysis/route.ts +3 -4
  16. package/app/api/reports/[id]/route.ts +23 -0
  17. package/app/api/reports/route.ts +50 -0
  18. package/app/api/reset/route.ts +21 -0
  19. package/app/api/session/route.ts +40 -0
  20. package/app/api/usage/route.ts +26 -1
  21. package/app/layout.tsx +1 -1
  22. package/bin/agentfit.mjs +2 -2
  23. package/components/agent-coach.tsx +256 -129
  24. package/components/app-sidebar.tsx +45 -10
  25. package/components/backup-section.tsx +236 -0
  26. package/components/daily-chart.tsx +447 -83
  27. package/components/dashboard-shell.tsx +29 -31
  28. package/components/data-provider.tsx +88 -8
  29. package/components/fitness-score.tsx +95 -54
  30. package/components/overview-cards.tsx +148 -41
  31. package/components/report-view.tsx +307 -0
  32. package/components/screenshots-analysis.tsx +51 -46
  33. package/components/session-chatlog.tsx +124 -0
  34. package/components/session-timeline.tsx +184 -0
  35. package/components/session-workflow.tsx +183 -0
  36. package/components/sessions-table.tsx +9 -1
  37. package/components/tool-flow-graph.tsx +144 -0
  38. package/components/ui/carousel.tsx +242 -0
  39. package/components/ui/sidebar.tsx +1 -1
  40. package/components/ui/sonner.tsx +51 -0
  41. package/electron/entitlements.mac.plist +16 -0
  42. package/electron/init-db.mjs +37 -0
  43. package/electron/main.mjs +203 -0
  44. package/generated/prisma/browser.ts +5 -0
  45. package/generated/prisma/client.ts +5 -0
  46. package/generated/prisma/internal/class.ts +14 -4
  47. package/generated/prisma/internal/prismaNamespace.ts +97 -2
  48. package/generated/prisma/internal/prismaNamespaceBrowser.ts +21 -1
  49. package/generated/prisma/models/Report.ts +1219 -0
  50. package/generated/prisma/models/Session.ts +221 -1
  51. package/generated/prisma/models.ts +1 -0
  52. package/lib/coach.ts +571 -211
  53. package/lib/command-insights.ts +231 -0
  54. package/lib/db.ts +2 -2
  55. package/lib/parse-codex.ts +6 -0
  56. package/lib/parse-logs.ts +80 -1
  57. package/lib/queries-codex.ts +24 -0
  58. package/lib/queries.ts +45 -0
  59. package/lib/report.ts +156 -0
  60. package/lib/session-detail.ts +382 -0
  61. package/lib/sync.ts +87 -0
  62. package/lib/tool-flow.ts +71 -0
  63. package/next.config.mjs +6 -1
  64. package/package.json +17 -2
  65. package/plugins/cost-heatmap/component.tsx +72 -50
  66. package/prisma/migrations/20260401144555_add_system_prompt_edits/migration.sql +80 -0
  67. package/prisma/schema.prisma +18 -0
  68. package/prisma/schema.sql +81 -0
  69. package/.claude/settings.local.json +0 -26
  70. package/CONTRIBUTING.md +0 -209
  71. package/prisma/migrations/20260328152517_init/migration.sql +0 -41
  72. package/prisma/migrations/20260328153801_add_image_model/migration.sql +0 -18
  73. package/prisma.config.ts +0 -14
  74. package/setup.sh +0 -73
@@ -1,6 +1,6 @@
1
1
  'use client'
2
2
 
3
- import { useState } from 'react'
3
+ import { useState, type ReactNode } from 'react'
4
4
  import Link from 'next/link'
5
5
  import { usePathname } from 'next/navigation'
6
6
  import {
@@ -9,11 +9,14 @@ import {
9
9
  Camera,
10
10
  ChevronRight,
11
11
  Coins,
12
+ FileText,
12
13
  FolderOpen,
14
+ GitBranch,
13
15
  HeartPulse,
14
16
  LayoutDashboard,
15
17
  ListTree,
16
18
  Puzzle,
19
+ Settings,
17
20
  Terminal,
18
21
  Wrench,
19
22
  } from 'lucide-react'
@@ -24,6 +27,7 @@ import {
24
27
  SidebarGroup,
25
28
  SidebarGroupContent,
26
29
  SidebarGroupLabel,
30
+ SidebarFooter,
27
31
  SidebarHeader,
28
32
  SidebarMenu,
29
33
  SidebarMenuButton,
@@ -37,7 +41,7 @@ import { getPlugins } from '@/lib/plugins'
37
41
  import '@/plugins'
38
42
 
39
43
  interface NavItem {
40
- title: string
44
+ title: ReactNode
41
45
  icon: ComponentType<{ className?: string }>
42
46
  href: string
43
47
  }
@@ -50,11 +54,18 @@ interface NavGroup {
50
54
 
51
55
  const topItems: NavItem[] = [
52
56
  { title: 'Dashboard', icon: LayoutDashboard, href: '/' },
53
- { title: 'Agent Coach', icon: HeartPulse, href: '/coach' },
54
- { title: 'Projects', icon: FolderOpen, href: '/projects' },
57
+ { title: 'CRAFT Coach', icon: HeartPulse, href: '/coach' },
55
58
  ]
56
59
 
57
60
  const navGroups: NavGroup[] = [
61
+ {
62
+ title: 'Projects',
63
+ icon: FolderOpen,
64
+ items: [
65
+ { title: 'Overview', icon: FolderOpen, href: '/projects' },
66
+ { title: 'Sessions', icon: ListTree, href: '/sessions' },
67
+ ],
68
+ },
58
69
  {
59
70
  title: 'Usage',
60
71
  icon: BarChart3,
@@ -62,6 +73,7 @@ const navGroups: NavGroup[] = [
62
73
  { title: 'Daily Usage', icon: BarChart3, href: '/daily' },
63
74
  { title: 'Token Breakdown', icon: Coins, href: '/tokens' },
64
75
  { title: 'Tool Usage', icon: Wrench, href: '/tools' },
76
+ { title: 'Command Usage', icon: Terminal, href: '/commands' },
65
77
  ],
66
78
  },
67
79
  {
@@ -69,8 +81,9 @@ const navGroups: NavGroup[] = [
69
81
  icon: Brain,
70
82
  items: [
71
83
  { title: 'Personality Fit', icon: Brain, href: '/personality' },
72
- { title: 'Command Usage', icon: Terminal, href: '/commands' },
73
- { title: 'Images', icon: Camera, href: '/images' },
84
+ { title: 'Session Flow', icon: GitBranch, href: '/flow' },
85
+ { title: 'Image Analysis', icon: Camera, href: '/images' },
86
+ { title: 'Reports', icon: FileText, href: '/reports' },
74
87
  ],
75
88
  },
76
89
  ]
@@ -115,10 +128,7 @@ export function AppSidebar() {
115
128
  <SidebarHeader>
116
129
  <div className="flex items-center gap-2 px-2 py-2">
117
130
  <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>
131
+ <div className="text-sm font-semibold">AgentFit</div>
122
132
  </div>
123
133
  </SidebarHeader>
124
134
  <SidebarContent>
@@ -151,10 +161,35 @@ export function AppSidebar() {
151
161
  </SidebarMenuButton>
152
162
  </SidebarMenuItem>
153
163
  )}
164
+ <SidebarMenuItem>
165
+ <SidebarMenuButton
166
+ render={<Link href="/data-management" />}
167
+ isActive={pathname === '/data-management'}
168
+ >
169
+ <Settings className="h-4 w-4" />
170
+ <span>Data Management</span>
171
+ </SidebarMenuButton>
172
+ </SidebarMenuItem>
154
173
  </SidebarMenu>
155
174
  </SidebarGroupContent>
156
175
  </SidebarGroup>
157
176
  </SidebarContent>
177
+
178
+ <SidebarFooter>
179
+ <div className="flex items-center justify-center px-3 py-2 text-xs text-muted-foreground">
180
+ <a
181
+ href="https://github.com/harrywang/agentfit"
182
+ target="_blank"
183
+ rel="noopener noreferrer"
184
+ className="flex items-center gap-1.5 hover:text-foreground transition-colors"
185
+ >
186
+ <svg className="h-4 w-4" viewBox="0 0 24 24" fill="currentColor">
187
+ <path d="M12 0C5.37 0 0 5.37 0 12c0 5.31 3.435 9.795 8.205 11.385.6.105.825-.255.825-.57 0-.285-.015-1.23-.015-2.235-3.015.555-3.795-.735-4.035-1.41-.135-.345-.72-1.41-1.23-1.695-.42-.225-1.02-.78-.015-.795.945-.015 1.62.87 1.845 1.23 1.08 1.815 2.805 1.305 3.495.99.105-.78.42-1.305.765-1.605-2.67-.3-5.46-1.335-5.46-5.925 0-1.305.465-2.385 1.23-3.225-.12-.3-.54-1.53.12-3.18 0 0 1.005-.315 3.3 1.23.96-.27 1.98-.405 3-.405s2.04.135 3 .405c2.295-1.56 3.3-1.23 3.3-1.23.66 1.65.24 2.88.12 3.18.765.84 1.23 1.905 1.23 3.225 0 4.605-2.805 5.625-5.475 5.925.435.375.81 1.095.81 2.22 0 1.605-.015 2.895-.015 3.3 0 .315.225.69.825.57A12.02 12.02 0 0024 12c0-6.63-5.37-12-12-12z" />
188
+ </svg>
189
+ GitHub
190
+ </a>
191
+ </div>
192
+ </SidebarFooter>
158
193
  <SidebarRail />
159
194
  </Sidebar>
160
195
  )
@@ -0,0 +1,236 @@
1
+ 'use client'
2
+
3
+ import { useState, useEffect, useCallback } from 'react'
4
+ import { toast } from 'sonner'
5
+ import { Card, CardContent, CardDescription, CardHeader, CardTitle } from '@/components/ui/card'
6
+ import { Button } from '@/components/ui/button'
7
+ import { Badge } from '@/components/ui/badge'
8
+ import {
9
+ Cloud,
10
+ CloudOff,
11
+ Check,
12
+ Loader2,
13
+ Upload,
14
+ } from 'lucide-react'
15
+
16
+ interface FolderStatus {
17
+ exists: boolean
18
+ hasGit: boolean
19
+ hasRemote: boolean
20
+ isDirty: boolean
21
+ }
22
+
23
+ interface BackupStatus {
24
+ ghInstalled: boolean
25
+ ghAuthenticated: boolean
26
+ ghUser: string
27
+ claude: FolderStatus
28
+ codex: FolderStatus
29
+ }
30
+
31
+ export function BackupCard() {
32
+ const [status, setStatus] = useState<BackupStatus | null>(null)
33
+ const [loading, setLoading] = useState(false)
34
+ const [syncing, setSyncing] = useState<Record<string, boolean>>({})
35
+
36
+ const fetchStatus = useCallback(async () => {
37
+ setLoading(true)
38
+ try {
39
+ const res = await fetch('/api/backup')
40
+ setStatus(await res.json())
41
+ } catch {
42
+ // ignore
43
+ } finally {
44
+ setLoading(false)
45
+ }
46
+ }, [])
47
+
48
+ useEffect(() => {
49
+ fetchStatus()
50
+ }, [fetchStatus])
51
+
52
+ const handleInit = async (folder: 'claude' | 'codex') => {
53
+ setSyncing((s) => ({ ...s, [folder]: true }))
54
+ try {
55
+ const res = await fetch('/api/backup', {
56
+ method: 'POST',
57
+ headers: { 'Content-Type': 'application/json' },
58
+ body: JSON.stringify({ action: 'init', folder }),
59
+ })
60
+ const data = await res.json()
61
+ if (!res.ok) {
62
+ toast.error('Backup failed', { description: data.error })
63
+ return
64
+ }
65
+ toast.success(`~/.${folder} backed up`, {
66
+ description: (
67
+ <a href={data.repoUrl} target="_blank" rel="noopener noreferrer" className="underline">
68
+ {data.repoUrl}
69
+ </a>
70
+ ),
71
+ })
72
+ await fetchStatus()
73
+ } catch (e) {
74
+ toast.error('Backup failed', { description: (e as Error).message })
75
+ } finally {
76
+ setSyncing((s) => ({ ...s, [folder]: false }))
77
+ }
78
+ }
79
+
80
+ const handleSync = async (folder: 'claude' | 'codex') => {
81
+ setSyncing((s) => ({ ...s, [folder]: true }))
82
+ try {
83
+ const res = await fetch('/api/backup', {
84
+ method: 'POST',
85
+ headers: { 'Content-Type': 'application/json' },
86
+ body: JSON.stringify({ action: 'sync', folder }),
87
+ })
88
+ const data = await res.json()
89
+ if (!res.ok) {
90
+ toast.error('Sync failed', { description: data.error })
91
+ return
92
+ }
93
+ toast.success(`~/.${folder} synced`, { description: data.message })
94
+ await fetchStatus()
95
+ } catch (e) {
96
+ toast.error('Sync failed', { description: (e as Error).message })
97
+ } finally {
98
+ setSyncing((s) => ({ ...s, [folder]: false }))
99
+ }
100
+ }
101
+
102
+ if (loading) {
103
+ return (
104
+ <Card>
105
+ <CardHeader>
106
+ <div className="flex items-center gap-2">
107
+ <Cloud className="h-5 w-5 text-muted-foreground" />
108
+ <div>
109
+ <CardTitle>GitHub Backup</CardTitle>
110
+ <CardDescription>Checking GitHub status...</CardDescription>
111
+ </div>
112
+ </div>
113
+ </CardHeader>
114
+ <CardContent>
115
+ <div className="flex items-center gap-2 text-sm text-muted-foreground">
116
+ <Loader2 className="h-4 w-4 animate-spin" />
117
+ Loading...
118
+ </div>
119
+ </CardContent>
120
+ </Card>
121
+ )
122
+ }
123
+
124
+ if (!status || !status.ghInstalled) {
125
+ return (
126
+ <Card>
127
+ <CardHeader>
128
+ <div className="flex items-center gap-2">
129
+ <CloudOff className="h-5 w-5 text-muted-foreground" />
130
+ <div>
131
+ <CardTitle>GitHub Backup</CardTitle>
132
+ <CardDescription>Back up your agent logs to a private GitHub repo</CardDescription>
133
+ </div>
134
+ </div>
135
+ </CardHeader>
136
+ <CardContent>
137
+ <div className="space-y-2 text-sm text-muted-foreground">
138
+ <p>GitHub CLI is not installed.</p>
139
+ <a href="https://cli.github.com" target="_blank" rel="noopener noreferrer" className="text-primary underline">
140
+ Install GitHub CLI
141
+ </a>
142
+ </div>
143
+ </CardContent>
144
+ </Card>
145
+ )
146
+ }
147
+
148
+ if (!status.ghAuthenticated) {
149
+ return (
150
+ <Card>
151
+ <CardHeader>
152
+ <div className="flex items-center gap-2">
153
+ <CloudOff className="h-5 w-5 text-muted-foreground" />
154
+ <div>
155
+ <CardTitle>GitHub Backup</CardTitle>
156
+ <CardDescription>Back up your agent logs to a private GitHub repo</CardDescription>
157
+ </div>
158
+ </div>
159
+ </CardHeader>
160
+ <CardContent>
161
+ <div className="space-y-2 text-sm text-muted-foreground">
162
+ <p>Not logged in to GitHub.</p>
163
+ <code className="block rounded bg-muted px-2 py-1.5 text-xs font-mono">gh auth login</code>
164
+ </div>
165
+ </CardContent>
166
+ </Card>
167
+ )
168
+ }
169
+
170
+ const folders = [
171
+ { key: 'claude' as const, label: '~/.claude', status: status.claude },
172
+ { key: 'codex' as const, label: '~/.codex', status: status.codex },
173
+ ].filter((f) => f.status.exists)
174
+
175
+ return (
176
+ <Card>
177
+ <CardHeader>
178
+ <div className="flex items-center gap-2">
179
+ <Cloud className="h-5 w-5 text-muted-foreground" />
180
+ <div>
181
+ <CardTitle>GitHub Backup</CardTitle>
182
+ <CardDescription>
183
+ Back up agent logs to private GitHub repos — logged in as <strong>{status.ghUser}</strong>
184
+ </CardDescription>
185
+ </div>
186
+ </div>
187
+ </CardHeader>
188
+ <CardContent className="space-y-3">
189
+ {folders.map(({ key, label, status: fs }) => {
190
+ const isSetUp = fs.hasGit && fs.hasRemote
191
+ const isSyncing = syncing[key]
192
+
193
+ return (
194
+ <div key={key} className="flex items-center justify-between rounded-lg border p-4">
195
+ <div className="flex items-center gap-3">
196
+ <code className="text-sm font-medium">{label}</code>
197
+ {isSetUp && !fs.isDirty && (
198
+ <Check className="h-4 w-4 text-green-500" />
199
+ )}
200
+ {isSetUp && fs.isDirty && (
201
+ <Badge variant="secondary" className="text-xs">new changes</Badge>
202
+ )}
203
+ </div>
204
+ {!isSetUp ? (
205
+ <Button
206
+ variant="outline"
207
+ size="sm"
208
+ onClick={() => handleInit(key)}
209
+ disabled={isSyncing}
210
+ className="gap-2"
211
+ >
212
+ {isSyncing ? <Loader2 className="h-4 w-4 animate-spin" /> : <Upload className="h-4 w-4" />}
213
+ Backup
214
+ </Button>
215
+ ) : (
216
+ <Button
217
+ variant="outline"
218
+ size="sm"
219
+ onClick={() => handleSync(key)}
220
+ disabled={isSyncing}
221
+ className="gap-2"
222
+ >
223
+ {isSyncing ? <Loader2 className="h-4 w-4 animate-spin" /> : <Cloud className="h-4 w-4" />}
224
+ Sync
225
+ </Button>
226
+ )}
227
+ </div>
228
+ )
229
+ })}
230
+ {folders.length === 0 && (
231
+ <p className="text-sm text-muted-foreground">No agent log directories found.</p>
232
+ )}
233
+ </CardContent>
234
+ </Card>
235
+ )
236
+ }