agentfit 0.1.0 → 0.1.1

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 (68) hide show
  1. package/README.md +30 -34
  2. package/app/(dashboard)/daily/page.tsx +1 -1
  3. package/app/(dashboard)/flow/page.tsx +17 -0
  4. package/app/(dashboard)/layout.tsx +2 -0
  5. package/app/(dashboard)/page.tsx +24 -5
  6. package/app/(dashboard)/reports/[id]/page.tsx +72 -0
  7. package/app/(dashboard)/reports/page.tsx +132 -0
  8. package/app/(dashboard)/sessions/[id]/page.tsx +167 -0
  9. package/app/(dashboard)/settings/page.tsx +180 -0
  10. package/app/api/backup/route.ts +215 -0
  11. package/app/api/check/route.ts +11 -1
  12. package/app/api/command-insights/route.ts +13 -0
  13. package/app/api/images-analysis/route.ts +3 -4
  14. package/app/api/reports/[id]/route.ts +23 -0
  15. package/app/api/reports/route.ts +50 -0
  16. package/app/api/reset/route.ts +21 -0
  17. package/app/api/session/route.ts +40 -0
  18. package/app/api/usage/route.ts +25 -1
  19. package/app/layout.tsx +1 -1
  20. package/bin/agentfit.mjs +2 -2
  21. package/components/agent-coach.tsx +256 -129
  22. package/components/app-sidebar.tsx +258 -8
  23. package/components/backup-section.tsx +236 -0
  24. package/components/daily-chart.tsx +404 -83
  25. package/components/dashboard-shell.tsx +9 -24
  26. package/components/data-provider.tsx +66 -2
  27. package/components/fitness-score.tsx +95 -54
  28. package/components/overview-cards.tsx +148 -41
  29. package/components/report-view.tsx +307 -0
  30. package/components/screenshots-analysis.tsx +51 -46
  31. package/components/session-chatlog.tsx +124 -0
  32. package/components/session-timeline.tsx +184 -0
  33. package/components/session-workflow.tsx +183 -0
  34. package/components/sessions-table.tsx +9 -1
  35. package/components/tool-flow-graph.tsx +144 -0
  36. package/components/ui/carousel.tsx +242 -0
  37. package/components/ui/sidebar.tsx +1 -1
  38. package/components/ui/sonner.tsx +51 -0
  39. package/generated/prisma/browser.ts +5 -0
  40. package/generated/prisma/client.ts +5 -0
  41. package/generated/prisma/internal/class.ts +14 -4
  42. package/generated/prisma/internal/prismaNamespace.ts +96 -2
  43. package/generated/prisma/internal/prismaNamespaceBrowser.ts +20 -1
  44. package/generated/prisma/models/Report.ts +1219 -0
  45. package/generated/prisma/models/Session.ts +187 -1
  46. package/generated/prisma/models.ts +1 -0
  47. package/lib/coach.ts +530 -211
  48. package/lib/command-insights.ts +231 -0
  49. package/lib/db.ts +1 -1
  50. package/lib/parse-codex.ts +5 -0
  51. package/lib/parse-logs.ts +65 -0
  52. package/lib/queries-codex.ts +22 -0
  53. package/lib/queries.ts +42 -0
  54. package/lib/report.ts +156 -0
  55. package/lib/session-detail.ts +382 -0
  56. package/lib/sync.ts +77 -0
  57. package/lib/tool-flow.ts +71 -0
  58. package/next.config.mjs +6 -1
  59. package/package.json +16 -2
  60. package/plugins/cost-heatmap/component.tsx +72 -50
  61. package/prisma/schema.prisma +17 -0
  62. package/.claude/settings.local.json +0 -26
  63. package/CONTRIBUTING.md +0 -209
  64. package/prisma/migrations/20260328152517_init/migration.sql +0 -41
  65. package/prisma/migrations/20260328153801_add_image_model/migration.sql +0 -18
  66. package/prisma/migrations/migration_lock.toml +0 -3
  67. package/prisma.config.ts +0 -14
  68. package/setup.sh +0 -73
@@ -0,0 +1,180 @@
1
+ 'use client'
2
+
3
+ import { useState, useEffect } from 'react'
4
+ import { useData } from '@/components/data-provider'
5
+ import { Card, CardContent, CardDescription, CardHeader, CardTitle } from '@/components/ui/card'
6
+ import { Button } from '@/components/ui/button'
7
+ import {
8
+ Dialog,
9
+ DialogContent,
10
+ DialogFooter,
11
+ DialogHeader,
12
+ DialogTitle,
13
+ } from '@/components/ui/dialog'
14
+ import { Badge } from '@/components/ui/badge'
15
+ import {
16
+ RefreshCw,
17
+ RotateCcw,
18
+ AlertTriangle,
19
+ Database,
20
+ HardDrive,
21
+ FolderSync,
22
+ } from 'lucide-react'
23
+ import { BackupCard } from '@/components/backup-section'
24
+
25
+ export default function SettingsPage() {
26
+ const { syncing, resetting, lastSyncResult, lastSyncTime, handleSync, handleReset } = useData()
27
+ const [resetOpen, setResetOpen] = useState(false)
28
+ const [paths, setPaths] = useState<{ database: string; images: string; claudeLogs: string; codexLogs: string } | null>(null)
29
+
30
+ useEffect(() => {
31
+ fetch('/api/check')
32
+ .then((r) => r.json())
33
+ .then((d) => { if (d.paths) setPaths(d.paths) })
34
+ .catch(() => {})
35
+ }, [])
36
+
37
+ return (
38
+ <div className="space-y-6 max-w-2xl">
39
+ {/* Local Data */}
40
+ <Card>
41
+ <CardHeader>
42
+ <div className="flex items-center gap-2">
43
+ <Database className="h-5 w-5 text-muted-foreground" />
44
+ <div>
45
+ <CardTitle>Local Data</CardTitle>
46
+ <CardDescription>Manage your synced session data</CardDescription>
47
+ </div>
48
+ </div>
49
+ </CardHeader>
50
+ <CardContent className="space-y-4">
51
+ {/* Sync */}
52
+ <div className="flex items-center justify-between rounded-lg border p-4">
53
+ <div className="space-y-1">
54
+ <div className="flex items-center gap-2">
55
+ <FolderSync className="h-4 w-4 text-muted-foreground" />
56
+ <span className="text-sm font-medium">Sync Logs</span>
57
+ </div>
58
+ <p className="text-xs text-muted-foreground">
59
+ Import new sessions from ~/.claude and ~/.codex into the local database.
60
+ </p>
61
+ {lastSyncTime && (
62
+ <p className="text-xs text-muted-foreground">
63
+ Last synced: {lastSyncTime.toLocaleString()}
64
+ {lastSyncResult && (
65
+ <span> — {lastSyncResult.sessionsAdded} added, {lastSyncResult.sessionsSkipped} skipped</span>
66
+ )}
67
+ </p>
68
+ )}
69
+ </div>
70
+ <Button
71
+ variant="outline"
72
+ onClick={handleSync}
73
+ disabled={syncing || resetting}
74
+ className="gap-2 shrink-0"
75
+ >
76
+ <RefreshCw className={`h-4 w-4 ${syncing ? 'animate-spin' : ''}`} />
77
+ {syncing ? 'Syncing...' : 'Sync'}
78
+ </Button>
79
+ </div>
80
+
81
+ {/* Reset */}
82
+ <div className="flex items-center justify-between rounded-lg border border-destructive/20 p-4">
83
+ <div className="space-y-1">
84
+ <div className="flex items-center gap-2">
85
+ <RotateCcw className="h-4 w-4 text-destructive" />
86
+ <span className="text-sm font-medium text-destructive">Reset Database</span>
87
+ </div>
88
+ <p className="text-xs text-muted-foreground">
89
+ Delete all synced data and re-import from log files on disk. Use if data seems corrupted.
90
+ </p>
91
+ </div>
92
+ <Button
93
+ variant="destructive"
94
+ onClick={() => setResetOpen(true)}
95
+ disabled={syncing || resetting}
96
+ className="gap-2 shrink-0"
97
+ >
98
+ <RotateCcw className={`h-4 w-4 ${resetting ? 'animate-spin' : ''}`} />
99
+ {resetting ? 'Resetting...' : 'Reset'}
100
+ </Button>
101
+ </div>
102
+ </CardContent>
103
+ </Card>
104
+
105
+ {/* Database Info */}
106
+ <Card>
107
+ <CardHeader>
108
+ <div className="flex items-center gap-2">
109
+ <HardDrive className="h-5 w-5 text-muted-foreground" />
110
+ <div>
111
+ <CardTitle>Storage</CardTitle>
112
+ <CardDescription>Where your data lives</CardDescription>
113
+ </div>
114
+ </div>
115
+ </CardHeader>
116
+ <CardContent>
117
+ <div className="space-y-3 text-sm">
118
+ <div className="flex items-center justify-between">
119
+ <span className="text-muted-foreground">Database</span>
120
+ <code className="rounded bg-muted px-2 py-0.5 text-xs">{paths?.database ?? 'agentfit.db'}</code>
121
+ </div>
122
+ <div className="flex items-center justify-between">
123
+ <span className="text-muted-foreground">Claude Code logs</span>
124
+ <code className="rounded bg-muted px-2 py-0.5 text-xs">{paths?.claudeLogs ?? '~/.claude/projects/'}</code>
125
+ </div>
126
+ <div className="flex items-center justify-between">
127
+ <span className="text-muted-foreground">Codex logs</span>
128
+ <code className="rounded bg-muted px-2 py-0.5 text-xs">{paths?.codexLogs ?? '~/.codex/sessions/'}</code>
129
+ </div>
130
+ <div className="flex items-center justify-between">
131
+ <span className="text-muted-foreground">Extracted images</span>
132
+ <code className="rounded bg-muted px-2 py-0.5 text-xs">{paths?.images ?? 'data/images/'}</code>
133
+ </div>
134
+ </div>
135
+ </CardContent>
136
+ </Card>
137
+
138
+ {/* GitHub Backup */}
139
+ <BackupCard />
140
+
141
+ {/* Reset confirmation dialog */}
142
+ <Dialog open={resetOpen} onOpenChange={setResetOpen}>
143
+ <DialogContent className="sm:max-w-md">
144
+ <DialogHeader>
145
+ <DialogTitle className="flex items-center gap-2 text-destructive">
146
+ <AlertTriangle className="h-5 w-5" />
147
+ Destructive Action
148
+ </DialogTitle>
149
+ </DialogHeader>
150
+ <div className="space-y-3 text-sm text-muted-foreground">
151
+ <div>
152
+ This will <strong className="text-foreground">permanently delete all synced data</strong> from the database and re-import from log files currently on disk.
153
+ </div>
154
+ <div className="rounded-md border border-destructive/30 bg-destructive/5 p-3 text-sm text-destructive">
155
+ <strong>Warning:</strong> If your coding agent (Claude Code, Codex, etc.) has purged old log files, that data will be permanently lost. The database may contain sessions that no longer exist on disk.
156
+ </div>
157
+ <div>
158
+ This cannot be undone. Consider backing up <code className="rounded bg-muted px-1 py-0.5 text-xs">agentfit.db</code> first.
159
+ </div>
160
+ </div>
161
+ <DialogFooter className="gap-3">
162
+ <Button variant="outline" onClick={() => setResetOpen(false)}>
163
+ Cancel
164
+ </Button>
165
+ <Button
166
+ variant="destructive"
167
+ disabled={resetting}
168
+ onClick={async () => {
169
+ await handleReset()
170
+ setResetOpen(false)
171
+ }}
172
+ >
173
+ I understand, reset everything
174
+ </Button>
175
+ </DialogFooter>
176
+ </DialogContent>
177
+ </Dialog>
178
+ </div>
179
+ )
180
+ }
@@ -0,0 +1,215 @@
1
+ import { NextResponse } from 'next/server'
2
+ import { execSync } from 'child_process'
3
+ import { existsSync } from 'fs'
4
+ import { homedir } from 'os'
5
+ import path from 'path'
6
+
7
+ interface FolderStatus {
8
+ exists: boolean
9
+ hasGit: boolean
10
+ hasRemote: boolean
11
+ remoteUrl: string | null
12
+ isDirty: boolean
13
+ lastCommit: string | null
14
+ }
15
+
16
+ interface BackupStatus {
17
+ ghInstalled: boolean
18
+ ghAuthenticated: boolean
19
+ ghUser: string | null
20
+ claude: FolderStatus
21
+ codex: FolderStatus
22
+ }
23
+
24
+ function run(cmd: string, cwd: string): string {
25
+ try {
26
+ return execSync(cmd, { cwd, encoding: 'utf-8', timeout: 30000 }).trim()
27
+ } catch {
28
+ return ''
29
+ }
30
+ }
31
+
32
+ function getFolderStatus(folderPath: string): FolderStatus {
33
+ if (!existsSync(folderPath)) {
34
+ return { exists: false, hasGit: false, hasRemote: false, remoteUrl: null, isDirty: false, lastCommit: null }
35
+ }
36
+
37
+ const hasGit = existsSync(path.join(folderPath, '.git'))
38
+ let hasRemote = false
39
+ let remoteUrl: string | null = null
40
+ let isDirty = false
41
+ let lastCommit: string | null = null
42
+
43
+ if (hasGit) {
44
+ remoteUrl = run('git remote get-url origin', folderPath) || null
45
+ hasRemote = !!remoteUrl
46
+ const status = run('git status --porcelain', folderPath)
47
+ isDirty = status.length > 0
48
+ lastCommit = run('git log -1 --format=%ci', folderPath) || null
49
+ }
50
+
51
+ return { exists: true, hasGit, hasRemote, remoteUrl, isDirty, lastCommit }
52
+ }
53
+
54
+ export async function GET() {
55
+ const home = homedir()
56
+ const ghInstalled = !!run('which gh', home)
57
+ let ghAuthenticated = false
58
+ let ghUser: string | null = null
59
+
60
+ if (ghInstalled) {
61
+ ghUser = run('gh api user -q .login', home) || null
62
+ ghAuthenticated = !!ghUser
63
+ }
64
+
65
+ const status: BackupStatus = {
66
+ ghInstalled,
67
+ ghAuthenticated,
68
+ ghUser,
69
+ claude: getFolderStatus(path.join(home, '.claude')),
70
+ codex: getFolderStatus(path.join(home, '.codex')),
71
+ }
72
+
73
+ return NextResponse.json(status)
74
+ }
75
+
76
+ const CLAUDE_GITIGNORE = `# Sensitive / ephemeral files
77
+ *.lock
78
+ *.lock.lock
79
+ cache/
80
+ chrome/
81
+ debug/
82
+ downloads/
83
+ ide/
84
+ image-cache/
85
+ paste-cache/
86
+ session-env/
87
+ telemetry/
88
+ mcp-needs-auth-cache.json
89
+ .DS_Store
90
+ `
91
+
92
+ const CODEX_GITIGNORE = `# Sensitive files
93
+ auth.json
94
+ *.lock
95
+ tmp/
96
+ .DS_Store
97
+ `
98
+
99
+ function initGit(folderPath: string, gitignoreContent: string) {
100
+ const gitignorePath = path.join(folderPath, '.gitignore')
101
+ if (!existsSync(gitignorePath)) {
102
+ require('fs').writeFileSync(gitignorePath, gitignoreContent)
103
+ }
104
+ if (!existsSync(path.join(folderPath, '.git'))) {
105
+ run('git init -b main', folderPath)
106
+ }
107
+ }
108
+
109
+ function commitAll(folderPath: string, message: string): boolean {
110
+ run('git add -A', folderPath)
111
+ const status = run('git status --porcelain', folderPath)
112
+ if (!status) return false
113
+ execSync(`git commit -m "${message}"`, { cwd: folderPath, encoding: 'utf-8', timeout: 30000 })
114
+ return true
115
+ }
116
+
117
+ export async function POST(request: Request) {
118
+ const home = homedir()
119
+ const body = await request.json()
120
+ const { action, folder } = body as { action: string; folder?: 'claude' | 'codex' }
121
+
122
+ if (action === 'init') {
123
+ // Initialize and push to GitHub for the first time
124
+ if (!folder) return NextResponse.json({ error: 'folder required' }, { status: 400 })
125
+
126
+ const ghUser = run('gh api user -q .login', home)
127
+ if (!ghUser) {
128
+ return NextResponse.json({ error: 'Not authenticated with GitHub. Run: gh auth login' }, { status: 401 })
129
+ }
130
+
131
+ const folderPath = path.join(home, `.${folder}`)
132
+ if (!existsSync(folderPath)) {
133
+ return NextResponse.json({ error: `~/.${folder} does not exist` }, { status: 404 })
134
+ }
135
+
136
+ const repoName = `my-${folder}-backup`
137
+ const gitignore = folder === 'claude' ? CLAUDE_GITIGNORE : CODEX_GITIGNORE
138
+
139
+ // Init git and .gitignore
140
+ initGit(folderPath, gitignore)
141
+
142
+ // Create private repo if it doesn't exist
143
+ const existing = run(`gh repo view ${ghUser}/${repoName} --json name -q .name`, home)
144
+ if (!existing) {
145
+ try {
146
+ execSync(`gh repo create ${repoName} --private --description "Backup of ~/.${folder}"`, {
147
+ cwd: home, encoding: 'utf-8', timeout: 30000,
148
+ })
149
+ } catch (e) {
150
+ return NextResponse.json({ error: `Failed to create repo: ${(e as Error).message}` }, { status: 500 })
151
+ }
152
+ }
153
+
154
+ // Set remote
155
+ const currentRemote = run('git remote get-url origin', folderPath)
156
+ const repoUrl = `https://github.com/${ghUser}/${repoName}.git`
157
+ if (!currentRemote) {
158
+ run(`git remote add origin ${repoUrl}`, folderPath)
159
+ } else if (currentRemote !== repoUrl) {
160
+ run(`git remote set-url origin ${repoUrl}`, folderPath)
161
+ }
162
+
163
+ // Commit and push
164
+ const timestamp = new Date().toISOString().slice(0, 19).replace('T', ' ')
165
+ commitAll(folderPath, `backup: ${timestamp}`)
166
+
167
+ try {
168
+ execSync('git push -u origin main', { cwd: folderPath, encoding: 'utf-8', timeout: 60000 })
169
+ } catch {
170
+ // Try force push on first init (empty remote)
171
+ try {
172
+ execSync('git push -u origin main --force-with-lease', { cwd: folderPath, encoding: 'utf-8', timeout: 60000 })
173
+ } catch (e) {
174
+ return NextResponse.json({ error: `Push failed: ${(e as Error).message}` }, { status: 500 })
175
+ }
176
+ }
177
+
178
+ return NextResponse.json({
179
+ success: true,
180
+ repoUrl: `https://github.com/${ghUser}/${repoName}`,
181
+ })
182
+ }
183
+
184
+ if (action === 'sync') {
185
+ // Sync changes to existing remote
186
+ if (!folder) return NextResponse.json({ error: 'folder required' }, { status: 400 })
187
+
188
+ const folderPath = path.join(home, `.${folder}`)
189
+ if (!existsSync(path.join(folderPath, '.git'))) {
190
+ return NextResponse.json({ error: 'Not initialized. Set up backup first.' }, { status: 400 })
191
+ }
192
+
193
+ const remote = run('git remote get-url origin', folderPath)
194
+ if (!remote) {
195
+ return NextResponse.json({ error: 'No remote configured. Set up backup first.' }, { status: 400 })
196
+ }
197
+
198
+ const timestamp = new Date().toISOString().slice(0, 19).replace('T', ' ')
199
+ const hadChanges = commitAll(folderPath, `backup: ${timestamp}`)
200
+
201
+ if (!hadChanges) {
202
+ return NextResponse.json({ success: true, message: 'Already up to date' })
203
+ }
204
+
205
+ try {
206
+ execSync('git push origin main', { cwd: folderPath, encoding: 'utf-8', timeout: 60000 })
207
+ } catch (e) {
208
+ return NextResponse.json({ error: `Push failed: ${(e as Error).message}` }, { status: 500 })
209
+ }
210
+
211
+ return NextResponse.json({ success: true, message: 'Synced to GitHub' })
212
+ }
213
+
214
+ return NextResponse.json({ error: 'Unknown action' }, { status: 400 })
215
+ }
@@ -1,4 +1,6 @@
1
1
  import { NextResponse } from 'next/server'
2
+ import path from 'path'
3
+ import os from 'os'
2
4
  import { checkForNewSessions } from '@/lib/sync'
3
5
 
4
6
  export const dynamic = 'force-dynamic'
@@ -6,7 +8,15 @@ export const dynamic = 'force-dynamic'
6
8
  export async function GET() {
7
9
  try {
8
10
  const newSessions = await checkForNewSessions()
9
- return NextResponse.json({ newSessions })
11
+ return NextResponse.json({
12
+ newSessions,
13
+ paths: {
14
+ database: path.resolve(process.cwd(), 'agentfit.db'),
15
+ images: path.resolve(process.cwd(), 'data', 'images'),
16
+ claudeLogs: path.join(os.homedir(), '.claude', 'projects'),
17
+ codexLogs: path.join(os.homedir(), '.codex', 'sessions'),
18
+ },
19
+ })
10
20
  } catch (error) {
11
21
  return NextResponse.json({ newSessions: 0, error: (error as Error).message }, { status: 500 })
12
22
  }
@@ -0,0 +1,13 @@
1
+ import { NextResponse } from 'next/server'
2
+ import { generateCommandInsights } from '@/lib/command-insights'
3
+
4
+ export const dynamic = 'force-dynamic'
5
+
6
+ export async function GET() {
7
+ try {
8
+ const insights = generateCommandInsights()
9
+ return NextResponse.json(insights)
10
+ } catch (error) {
11
+ return NextResponse.json([], { status: 500 })
12
+ }
13
+ }
@@ -113,9 +113,8 @@ export async function GET() {
113
113
  }
114
114
  })
115
115
 
116
- // --- Recent images (for gallery) ---
117
- const recentImages = images
118
- .slice(-20)
116
+ // --- All images (newest first, for gallery with pagination) ---
117
+ const allImages = [...images]
119
118
  .reverse()
120
119
  .map((img) => ({
121
120
  filename: img.filename,
@@ -168,7 +167,7 @@ export async function GET() {
168
167
  under5Percent: gaps.length ? Math.round((under5 / gaps.length) * 100) : 0,
169
168
  },
170
169
  topSessions,
171
- recentImages,
170
+ allImages,
172
171
  })
173
172
  } catch (error) {
174
173
  console.error('Failed to analyze images:', error)
@@ -0,0 +1,23 @@
1
+ import { NextRequest, NextResponse } from 'next/server'
2
+ import { prisma } from '@/lib/db'
3
+
4
+ export const dynamic = 'force-dynamic'
5
+
6
+ export async function GET(
7
+ request: NextRequest,
8
+ { params }: { params: Promise<{ id: string }> }
9
+ ) {
10
+ try {
11
+ const { id } = await params
12
+ const report = await prisma.report.findUnique({ where: { id } })
13
+ if (!report) {
14
+ return NextResponse.json({ error: 'Report not found' }, { status: 404 })
15
+ }
16
+ return NextResponse.json({
17
+ ...report,
18
+ contentJson: JSON.parse(report.contentJson),
19
+ })
20
+ } catch (error) {
21
+ return NextResponse.json({ error: (error as Error).message }, { status: 500 })
22
+ }
23
+ }
@@ -0,0 +1,50 @@
1
+ import { NextResponse } from 'next/server'
2
+ import { prisma } from '@/lib/db'
3
+ import { generateReport } from '@/lib/report'
4
+
5
+ export const dynamic = 'force-dynamic'
6
+
7
+ export async function GET() {
8
+ try {
9
+ const reports = await prisma.report.findMany({
10
+ orderBy: { generatedAt: 'desc' },
11
+ select: { id: true, title: true, generatedAt: true, sessionCount: true },
12
+ })
13
+ return NextResponse.json(reports)
14
+ } catch (error) {
15
+ return NextResponse.json({ error: (error as Error).message }, { status: 500 })
16
+ }
17
+ }
18
+
19
+ export async function POST() {
20
+ try {
21
+ const { title, contentJson, sessionCount } = await generateReport()
22
+
23
+ // Check if data has changed since last report
24
+ const lastReport = await prisma.report.findFirst({
25
+ orderBy: { generatedAt: 'desc' },
26
+ select: { sessionCount: true },
27
+ })
28
+
29
+ if (lastReport && lastReport.sessionCount === sessionCount) {
30
+ return NextResponse.json(
31
+ { error: 'No new data since the last report. Sync new sessions first.' },
32
+ { status: 409 }
33
+ )
34
+ }
35
+
36
+ const report = await prisma.report.create({
37
+ data: {
38
+ title,
39
+ contentJson: JSON.stringify(contentJson),
40
+ sessionCount,
41
+ },
42
+ })
43
+ return NextResponse.json({
44
+ ...report,
45
+ contentJson: JSON.parse(report.contentJson),
46
+ })
47
+ } catch (error) {
48
+ return NextResponse.json({ error: (error as Error).message }, { status: 500 })
49
+ }
50
+ }
@@ -0,0 +1,21 @@
1
+ import { NextResponse } from 'next/server'
2
+ import { prisma } from '@/lib/db'
3
+ import { syncLogs } from '@/lib/sync'
4
+
5
+ export const dynamic = 'force-dynamic'
6
+
7
+ export async function POST() {
8
+ try {
9
+ // Delete all records in order (images reference sessions)
10
+ await prisma.image.deleteMany()
11
+ await prisma.session.deleteMany()
12
+ await prisma.syncLog.deleteMany()
13
+
14
+ // Re-sync from disk
15
+ const result = await syncLogs()
16
+ return NextResponse.json(result)
17
+ } catch (error) {
18
+ console.error('Reset failed:', error)
19
+ return NextResponse.json({ error: 'Reset failed' }, { status: 500 })
20
+ }
21
+ }
@@ -0,0 +1,40 @@
1
+ import { NextRequest, NextResponse } from 'next/server'
2
+ import path from 'path'
3
+ import os from 'os'
4
+ import fs from 'fs'
5
+ import { parseSessionDetail } from '@/lib/session-detail'
6
+
7
+ export const dynamic = 'force-dynamic'
8
+
9
+ export async function GET(request: NextRequest) {
10
+ const sessionId = request.nextUrl.searchParams.get('id')
11
+ if (!sessionId) {
12
+ return NextResponse.json({ error: 'Missing session id' }, { status: 400 })
13
+ }
14
+
15
+ try {
16
+ // Search for the JSONL file across all project directories
17
+ const projectsDir = path.join(os.homedir(), '.claude', 'projects')
18
+ let filePath: string | null = null
19
+
20
+ if (fs.existsSync(projectsDir)) {
21
+ const dirs = fs.readdirSync(projectsDir)
22
+ for (const dir of dirs) {
23
+ const candidate = path.join(projectsDir, dir, `${sessionId}.jsonl`)
24
+ if (fs.existsSync(candidate)) {
25
+ filePath = candidate
26
+ break
27
+ }
28
+ }
29
+ }
30
+
31
+ if (!filePath) {
32
+ return NextResponse.json({ error: 'Session not found' }, { status: 404 })
33
+ }
34
+
35
+ const detail = parseSessionDetail(filePath, sessionId)
36
+ return NextResponse.json(detail)
37
+ } catch (error) {
38
+ return NextResponse.json({ error: (error as Error).message }, { status: 500 })
39
+ }
40
+ }
@@ -65,6 +65,8 @@ function mergeUsageData(a: UsageData, b: UsageData): UsageData {
65
65
  if (existing) {
66
66
  existing.sessions += day.sessions
67
67
  existing.messages += day.messages
68
+ existing.userMessages += day.userMessages
69
+ existing.assistantMessages += day.assistantMessages
68
70
  existing.inputTokens += day.inputTokens
69
71
  existing.outputTokens += day.outputTokens
70
72
  existing.cacheCreationTokens += day.cacheCreationTokens
@@ -72,8 +74,13 @@ function mergeUsageData(a: UsageData, b: UsageData): UsageData {
72
74
  existing.totalTokens += day.totalTokens
73
75
  existing.costUSD += day.costUSD
74
76
  existing.toolCalls += day.toolCalls
77
+ existing.interruptions += day.interruptions
78
+ existing.rateLimitErrors += day.rateLimitErrors
79
+ for (const [tool, count] of Object.entries(day.toolCallsDetail)) {
80
+ existing.toolCallsDetail[tool] = (existing.toolCallsDetail[tool] || 0) + count
81
+ }
75
82
  } else {
76
- dailyMap.set(day.date, { ...day })
83
+ dailyMap.set(day.date, { ...day, toolCallsDetail: { ...day.toolCallsDetail } })
77
84
  }
78
85
  }
79
86
 
@@ -107,7 +114,24 @@ function mergeUsageData(a: UsageData, b: UsageData): UsageData {
107
114
  totalCostUSD: a.overview.totalCostUSD + b.overview.totalCostUSD,
108
115
  totalDurationMinutes: a.overview.totalDurationMinutes + b.overview.totalDurationMinutes,
109
116
  totalToolCalls: a.overview.totalToolCalls + b.overview.totalToolCalls,
117
+ totalApiErrors: a.overview.totalApiErrors + b.overview.totalApiErrors,
118
+ totalRateLimitDays: a.overview.totalRateLimitDays + b.overview.totalRateLimitDays,
119
+ totalUserInterruptions: a.overview.totalUserInterruptions + b.overview.totalUserInterruptions,
110
120
  models,
121
+ skillUsage: (() => {
122
+ const skills: Record<string, number> = { ...a.overview.skillUsage }
123
+ for (const [s, c] of Object.entries(b.overview.skillUsage)) {
124
+ skills[s] = (skills[s] || 0) + c
125
+ }
126
+ return skills
127
+ })(),
128
+ permissionModes: (() => {
129
+ const modes: Record<string, number> = { ...(a.overview.permissionModes || {}) }
130
+ for (const [m, c] of Object.entries(b.overview.permissionModes || {})) {
131
+ modes[m] = (modes[m] || 0) + c
132
+ }
133
+ return modes
134
+ })(),
111
135
  },
112
136
  sessions,
113
137
  projects,
package/app/layout.tsx CHANGED
@@ -12,7 +12,7 @@ const fontMono = Geist_Mono({
12
12
  })
13
13
 
14
14
  export const metadata = {
15
- title: 'AgentFit — Coding Agent Fitness Tracker',
15
+ title: 'AgentFit — Your AI Coding Fitness Tracker',
16
16
  description: 'Track your AI coding agent\'s fitness — usage, costs, personality, and behavioral patterns across Claude Code, Codex, and more.',
17
17
  }
18
18
 
package/bin/agentfit.mjs CHANGED
@@ -26,7 +26,7 @@ function run(cmd, opts = {}) {
26
26
  // ─── Ensure .env exists ─────────────────────────────────────────────
27
27
  const envPath = path.join(ROOT, '.env')
28
28
  if (!existsSync(envPath)) {
29
- writeFileSync(envPath, 'DATABASE_URL="file:./dev.db"\n')
29
+ writeFileSync(envPath, 'DATABASE_URL="file:./agentfit.db"\n')
30
30
  }
31
31
 
32
32
  // ─── First-run setup: prisma generate + migrate ─────────────────────
@@ -36,7 +36,7 @@ if (!existsSync(generatedClient)) {
36
36
  run('npx prisma generate')
37
37
  }
38
38
 
39
- const dbPath = path.join(ROOT, 'dev.db')
39
+ const dbPath = path.join(ROOT, 'agentfit.db')
40
40
  if (!existsSync(dbPath)) {
41
41
  info('Creating database...')
42
42
  run('npx prisma migrate deploy')