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
@@ -0,0 +1,132 @@
1
+ 'use client'
2
+
3
+ import { useEffect, useState } from 'react'
4
+ import Link from 'next/link'
5
+ import { Card, CardContent } from '@/components/ui/card'
6
+ import { Button } from '@/components/ui/button'
7
+ import { Badge } from '@/components/ui/badge'
8
+ import { Tooltip, TooltipContent, TooltipTrigger } from '@/components/ui/tooltip'
9
+ import { FileText, Plus, Loader2, ChevronRight } from 'lucide-react'
10
+ import { toast } from 'sonner'
11
+ import { useData } from '@/components/data-provider'
12
+
13
+ interface ReportSummary {
14
+ id: string
15
+ title: string
16
+ generatedAt: string
17
+ sessionCount: number
18
+ }
19
+
20
+ export default function ReportsPage() {
21
+ const { data } = useData()
22
+ const [reports, setReports] = useState<ReportSummary[]>([])
23
+ const [loading, setLoading] = useState(true)
24
+ const [generating, setGenerating] = useState(false)
25
+
26
+ async function fetchReports() {
27
+ try {
28
+ const res = await fetch('/api/reports')
29
+ setReports(await res.json())
30
+ } catch {
31
+ // ignore
32
+ } finally {
33
+ setLoading(false)
34
+ }
35
+ }
36
+
37
+ useEffect(() => { fetchReports() }, [])
38
+
39
+ const currentSessionCount = data?.overview.totalSessions || 0
40
+ const lastReportSessionCount = reports[0]?.sessionCount || 0
41
+ const hasNewData = reports.length === 0 || currentSessionCount !== lastReportSessionCount
42
+
43
+ async function handleGenerate() {
44
+ setGenerating(true)
45
+ try {
46
+ const res = await fetch('/api/reports', { method: 'POST' })
47
+ const report = await res.json()
48
+ if (res.status === 409) {
49
+ toast.info('No new data', { description: report.error })
50
+ return
51
+ }
52
+ if (!res.ok) throw new Error(report.error || 'Failed to generate report')
53
+ toast.success('Report generated', { description: report.title })
54
+ await fetchReports()
55
+ } catch (e) {
56
+ toast.error('Failed to generate report', { description: (e as Error).message })
57
+ } finally {
58
+ setGenerating(false)
59
+ }
60
+ }
61
+
62
+ const buttonDisabled = generating || !hasNewData
63
+
64
+ return (
65
+ <div className="space-y-6 max-w-3xl">
66
+ <div className="flex items-center justify-between">
67
+ <div>
68
+ <h2 className="text-lg font-semibold">Insights Reports</h2>
69
+ <p className="text-sm text-muted-foreground">
70
+ Generate point-in-time snapshots of your coding agent fitness
71
+ </p>
72
+ </div>
73
+ <Tooltip>
74
+ <TooltipTrigger
75
+ render={<span />}
76
+ >
77
+ <Button onClick={handleGenerate} disabled={buttonDisabled} className="gap-2">
78
+ {generating ? <Loader2 className="h-4 w-4 animate-spin" /> : <Plus className="h-4 w-4" />}
79
+ {generating ? 'Generating...' : 'Generate Report'}
80
+ </Button>
81
+ </TooltipTrigger>
82
+ {!hasNewData && (
83
+ <TooltipContent>
84
+ No new sessions since last report. Sync new data first.
85
+ </TooltipContent>
86
+ )}
87
+ </Tooltip>
88
+ </div>
89
+
90
+ {loading ? (
91
+ <div className="flex items-center gap-2 py-8 text-muted-foreground">
92
+ <Loader2 className="h-4 w-4 animate-spin" /> Loading reports...
93
+ </div>
94
+ ) : reports.length === 0 ? (
95
+ <Card>
96
+ <CardContent className="py-12 text-center">
97
+ <FileText className="h-10 w-10 text-muted-foreground/30 mx-auto mb-3" />
98
+ <p className="text-sm text-muted-foreground">
99
+ No reports yet. Click "Generate Report" to create your first fitness snapshot.
100
+ </p>
101
+ </CardContent>
102
+ </Card>
103
+ ) : (
104
+ <div className="space-y-2">
105
+ {reports.map(report => (
106
+ <Link key={report.id} href={`/reports/${report.id}`}>
107
+ <Card className="hover:bg-muted/50 transition-colors cursor-pointer">
108
+ <CardContent className="flex items-center justify-between py-4">
109
+ <div className="flex items-center gap-3">
110
+ <FileText className="h-5 w-5 text-muted-foreground" />
111
+ <div>
112
+ <div className="text-sm font-medium">{report.title}</div>
113
+ <div className="text-xs text-muted-foreground">
114
+ {new Date(report.generatedAt).toLocaleString()}
115
+ </div>
116
+ </div>
117
+ </div>
118
+ <div className="flex items-center gap-3">
119
+ <Badge variant="secondary" className="text-xs">
120
+ {report.sessionCount} sessions
121
+ </Badge>
122
+ <ChevronRight className="h-4 w-4 text-muted-foreground" />
123
+ </div>
124
+ </CardContent>
125
+ </Card>
126
+ </Link>
127
+ ))}
128
+ </div>
129
+ )}
130
+ </div>
131
+ )
132
+ }
@@ -0,0 +1,167 @@
1
+ 'use client'
2
+
3
+ import { useEffect, useState } from 'react'
4
+ import { useParams } from 'next/navigation'
5
+ import Link from 'next/link'
6
+ import { Card, CardContent, CardDescription, CardHeader, CardTitle } from '@/components/ui/card'
7
+ import { ArrowLeft, Clock, Coins, MessageSquare, Wrench, CheckCircle, XCircle } from 'lucide-react'
8
+ import { SessionWorkflow } from '@/components/session-workflow'
9
+ import { SessionChatLog } from '@/components/session-chatlog'
10
+ import type { SessionDetail } from '@/lib/session-detail'
11
+
12
+ export default function SessionDetailPage() {
13
+ const params = useParams()
14
+ const sessionId = params.id as string
15
+ const [detail, setDetail] = useState<SessionDetail | null>(null)
16
+ const [loading, setLoading] = useState(true)
17
+ const [error, setError] = useState<string | null>(null)
18
+ const [activeTab, setActiveTab] = useState<'workflow' | 'dialog'>('dialog')
19
+
20
+ useEffect(() => {
21
+ async function load() {
22
+ try {
23
+ const res = await fetch(`/api/session?id=${sessionId}`)
24
+ if (!res.ok) throw new Error('Session not found')
25
+ const data = await res.json()
26
+ setDetail(data)
27
+ } catch (e) {
28
+ setError((e as Error).message)
29
+ } finally {
30
+ setLoading(false)
31
+ }
32
+ }
33
+ load()
34
+ }, [sessionId])
35
+
36
+ if (loading) {
37
+ return <div className="text-muted-foreground">Loading session...</div>
38
+ }
39
+
40
+ if (error || !detail) {
41
+ return (
42
+ <div className="space-y-4">
43
+ <Link href="/sessions" className="flex items-center gap-1 text-sm text-primary hover:underline">
44
+ <ArrowLeft className="h-4 w-4" /> Back to sessions
45
+ </Link>
46
+ <div className="text-destructive">{error || 'Session not found'}</div>
47
+ </div>
48
+ )
49
+ }
50
+
51
+ const s = detail.stats
52
+
53
+ return (
54
+ <div className="space-y-4">
55
+ <Link href="/sessions" className="flex items-center gap-1 text-sm text-muted-foreground hover:text-foreground">
56
+ <ArrowLeft className="h-4 w-4" /> Back to sessions
57
+ </Link>
58
+
59
+ <div className="grid gap-4 sm:grid-cols-2 lg:grid-cols-6">
60
+ <Card>
61
+ <CardHeader className="flex flex-row items-center justify-between pb-2">
62
+ <CardTitle className="text-sm font-medium text-muted-foreground">Duration</CardTitle>
63
+ <Clock className="h-4 w-4 text-muted-foreground" />
64
+ </CardHeader>
65
+ <CardContent>
66
+ <div className="text-2xl font-bold tracking-tight">{s.duration}</div>
67
+ </CardContent>
68
+ </Card>
69
+ <Card>
70
+ <CardHeader className="flex flex-row items-center justify-between pb-2">
71
+ <CardTitle className="text-sm font-medium text-muted-foreground">Tokens</CardTitle>
72
+ <Coins className="h-4 w-4 text-muted-foreground" />
73
+ </CardHeader>
74
+ <CardContent>
75
+ <div className="text-2xl font-bold tracking-tight">{s.tokens.toLocaleString()}</div>
76
+ </CardContent>
77
+ </Card>
78
+ <Card>
79
+ <CardHeader className="flex flex-row items-center justify-between pb-2">
80
+ <CardTitle className="text-sm font-medium text-muted-foreground">Messages</CardTitle>
81
+ <MessageSquare className="h-4 w-4 text-muted-foreground" />
82
+ </CardHeader>
83
+ <CardContent>
84
+ <div className="text-2xl font-bold tracking-tight">{s.totalMessages}</div>
85
+ <p className="text-xs text-muted-foreground">{s.userTurns} user / {s.assistantTurns} assistant</p>
86
+ </CardContent>
87
+ </Card>
88
+ <Card>
89
+ <CardHeader className="flex flex-row items-center justify-between pb-2">
90
+ <CardTitle className="text-sm font-medium text-muted-foreground">Tool Calls</CardTitle>
91
+ <Wrench className="h-4 w-4 text-muted-foreground" />
92
+ </CardHeader>
93
+ <CardContent>
94
+ <div className="text-2xl font-bold tracking-tight">{s.toolCalls}</div>
95
+ </CardContent>
96
+ </Card>
97
+ <Card>
98
+ <CardHeader className="flex flex-row items-center justify-between pb-2">
99
+ <CardTitle className="text-sm font-medium text-muted-foreground">Success</CardTitle>
100
+ <CheckCircle className="h-4 w-4 text-muted-foreground" />
101
+ </CardHeader>
102
+ <CardContent>
103
+ <div className="text-2xl font-bold tracking-tight">{s.successCount}</div>
104
+ </CardContent>
105
+ </Card>
106
+ <Card>
107
+ <CardHeader className="flex flex-row items-center justify-between pb-2">
108
+ <CardTitle className="text-sm font-medium text-muted-foreground">Failures</CardTitle>
109
+ <XCircle className="h-4 w-4 text-muted-foreground" />
110
+ </CardHeader>
111
+ <CardContent>
112
+ <div className={`text-2xl font-bold tracking-tight ${s.failureCount > 0 ? 'text-destructive' : ''}`}>{s.failureCount}</div>
113
+ </CardContent>
114
+ </Card>
115
+ </div>
116
+
117
+ <div className="flex gap-1.5 border-b pb-1">
118
+ <button
119
+ onClick={() => setActiveTab('dialog')}
120
+ className={`rounded-md px-3 py-1.5 text-sm font-medium transition-colors ${
121
+ activeTab === 'dialog'
122
+ ? 'bg-primary text-primary-foreground'
123
+ : 'text-muted-foreground hover:text-foreground'
124
+ }`}
125
+ >
126
+ Dialog
127
+ </button>
128
+ <button
129
+ onClick={() => setActiveTab('workflow')}
130
+ className={`rounded-md px-3 py-1.5 text-sm font-medium transition-colors ${
131
+ activeTab === 'workflow'
132
+ ? 'bg-primary text-primary-foreground'
133
+ : 'text-muted-foreground hover:text-foreground'
134
+ }`}
135
+ >
136
+ Workflow
137
+ </button>
138
+ </div>
139
+
140
+ {activeTab === 'dialog' && (
141
+ <Card>
142
+ <CardHeader>
143
+ <CardTitle>Chat Log</CardTitle>
144
+ <CardDescription>{detail.chatLog.length} records</CardDescription>
145
+ </CardHeader>
146
+ <CardContent>
147
+ <SessionChatLog messages={detail.chatLog} />
148
+ </CardContent>
149
+ </Card>
150
+ )}
151
+
152
+ {activeTab === 'workflow' && (
153
+ <Card>
154
+ <CardHeader>
155
+ <CardTitle>Workflow</CardTitle>
156
+ <CardDescription>
157
+ Tool calls, reasoning, and execution flow — {detail.workflowNodes.length} nodes
158
+ </CardDescription>
159
+ </CardHeader>
160
+ <CardContent>
161
+ <SessionWorkflow nodes={detail.workflowNodes} edges={detail.workflowEdges} />
162
+ </CardContent>
163
+ </Card>
164
+ )}
165
+ </div>
166
+ )
167
+ }
@@ -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
+ }
@@ -1,12 +1,66 @@
1
1
  import { NextResponse } from 'next/server'
2
2
  import { analyzeCommands } from '@/lib/commands'
3
+ import { prisma } from '@/lib/db'
3
4
 
4
5
  export const dynamic = 'force-dynamic'
5
6
 
6
7
  export async function GET() {
7
8
  try {
8
9
  const analysis = analyzeCommands()
9
- return NextResponse.json(analysis)
10
+
11
+ // Merge skill invocations from session data (Skill tool calls tracked in DB)
12
+ // These capture cases where the user asked Claude to invoke a skill
13
+ // (e.g. "use /doc-writer to...") rather than typing the slash command directly
14
+ const dbSessions = await prisma.session.findMany({
15
+ select: { skillCallsJson: true },
16
+ })
17
+
18
+ const dbSkillCounts = new Map<string, number>()
19
+ for (const s of dbSessions) {
20
+ const skills = JSON.parse(s.skillCallsJson) as Record<string, number>
21
+ for (const [skill, count] of Object.entries(skills)) {
22
+ const cmd = skill.startsWith('/') ? skill : `/${skill}`
23
+ dbSkillCounts.set(cmd, (dbSkillCounts.get(cmd) || 0) + count)
24
+ }
25
+ }
26
+
27
+ // Build a map of history.jsonl counts for custom commands
28
+ const historyMap = new Map<string, number>()
29
+ for (const c of analysis.customCommands) {
30
+ historyMap.set(c.command, c.count)
31
+ }
32
+
33
+ // Merge DB skill counts into customCommands with breakdown
34
+ const customMap = new Map<string, { command: string; count: number; historyCount: number; sessionCount: number }>()
35
+ for (const c of analysis.customCommands) {
36
+ customMap.set(c.command, { command: c.command, count: c.count, historyCount: c.count, sessionCount: 0 })
37
+ }
38
+ for (const [cmd, count] of dbSkillCounts) {
39
+ const existing = analysis.commands.find(c => c.command === cmd)
40
+ if (existing) {
41
+ existing.count += count
42
+ existing.used = existing.count > 0
43
+ } else {
44
+ const prev = customMap.get(cmd)
45
+ if (prev) {
46
+ prev.sessionCount += count
47
+ prev.count += count
48
+ } else {
49
+ customMap.set(cmd, { command: cmd, count, historyCount: 0, sessionCount: count })
50
+ }
51
+ }
52
+ }
53
+
54
+ analysis.customCommands = Array.from(customMap.values())
55
+ .sort((a, b) => b.count - a.count)
56
+
57
+ // Expose session skill counts so the chart can show breakdown
58
+ const responseData = {
59
+ ...analysis,
60
+ dbSkillCounts: Object.fromEntries(dbSkillCounts),
61
+ }
62
+
63
+ return NextResponse.json(responseData)
10
64
  } catch (error) {
11
65
  return NextResponse.json(
12
66
  { error: (error as Error).message },
@@ -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
+ }