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,33 @@
|
|
|
1
|
+
import { NextRequest, NextResponse } from 'next/server'
|
|
2
|
+
import fs from 'fs'
|
|
3
|
+
import path from 'path'
|
|
4
|
+
|
|
5
|
+
const IMAGES_DIR = path.resolve(process.cwd(), 'data', 'images')
|
|
6
|
+
|
|
7
|
+
export async function GET(
|
|
8
|
+
_request: NextRequest,
|
|
9
|
+
{ params }: { params: Promise<{ path: string[] }> }
|
|
10
|
+
) {
|
|
11
|
+
const { path: segments } = await params
|
|
12
|
+
const filePath = path.join(IMAGES_DIR, ...segments)
|
|
13
|
+
|
|
14
|
+
// Prevent directory traversal
|
|
15
|
+
if (!filePath.startsWith(IMAGES_DIR)) {
|
|
16
|
+
return NextResponse.json({ error: 'Forbidden' }, { status: 403 })
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
if (!fs.existsSync(filePath)) {
|
|
20
|
+
return NextResponse.json({ error: 'Not found' }, { status: 404 })
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
const buffer = fs.readFileSync(filePath)
|
|
24
|
+
const ext = path.extname(filePath).slice(1)
|
|
25
|
+
const contentType = ext === 'png' ? 'image/png' : ext === 'jpg' || ext === 'jpeg' ? 'image/jpeg' : 'image/png'
|
|
26
|
+
|
|
27
|
+
return new NextResponse(buffer, {
|
|
28
|
+
headers: {
|
|
29
|
+
'Content-Type': contentType,
|
|
30
|
+
'Cache-Control': 'public, max-age=31536000, immutable',
|
|
31
|
+
},
|
|
32
|
+
})
|
|
33
|
+
}
|
|
@@ -0,0 +1,177 @@
|
|
|
1
|
+
import { NextResponse } from 'next/server'
|
|
2
|
+
import { prisma } from '@/lib/db'
|
|
3
|
+
|
|
4
|
+
export const dynamic = 'force-dynamic'
|
|
5
|
+
|
|
6
|
+
export async function GET() {
|
|
7
|
+
try {
|
|
8
|
+
const images = await prisma.image.findMany({
|
|
9
|
+
orderBy: { timestamp: 'asc' },
|
|
10
|
+
})
|
|
11
|
+
const sessions = await prisma.session.findMany()
|
|
12
|
+
const sessionMap = new Map(sessions.map((s) => [s.sessionId, s]))
|
|
13
|
+
|
|
14
|
+
// --- Basic stats ---
|
|
15
|
+
const totalImages = images.length
|
|
16
|
+
const sessionsWithImages = new Set(images.map((i) => i.sessionId)).size
|
|
17
|
+
const totalSessions = sessions.length
|
|
18
|
+
const byMediaType: Record<string, number> = {}
|
|
19
|
+
let totalBytes = 0
|
|
20
|
+
for (const img of images) {
|
|
21
|
+
byMediaType[img.mediaType] = (byMediaType[img.mediaType] || 0) + 1
|
|
22
|
+
totalBytes += img.sizeBytes
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
// --- By project ---
|
|
26
|
+
const byProject: Record<string, number> = {}
|
|
27
|
+
for (const img of images) {
|
|
28
|
+
const session = sessionMap.get(img.sessionId)
|
|
29
|
+
if (session) {
|
|
30
|
+
byProject[session.project] = (byProject[session.project] || 0) + 1
|
|
31
|
+
}
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
// --- By hour (user's local timezone approximation via UTC) ---
|
|
35
|
+
const byHour: number[] = new Array(24).fill(0)
|
|
36
|
+
for (const img of images) {
|
|
37
|
+
const h = new Date(img.timestamp).getUTCHours()
|
|
38
|
+
byHour[h]++
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
// --- By date ---
|
|
42
|
+
const byDate: Record<string, number> = {}
|
|
43
|
+
for (const img of images) {
|
|
44
|
+
const d = img.timestamp.toISOString().slice(0, 10)
|
|
45
|
+
byDate[d] = (byDate[d] || 0) + 1
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
// --- Sessions with vs without images comparison ---
|
|
49
|
+
const withImageSessionIds = new Set(images.map((i) => i.sessionId))
|
|
50
|
+
let withImgStats = { count: 0, messages: 0, cost: 0, duration: 0, tools: 0 }
|
|
51
|
+
let noImgStats = { count: 0, messages: 0, cost: 0, duration: 0, tools: 0 }
|
|
52
|
+
for (const s of sessions) {
|
|
53
|
+
const target = withImageSessionIds.has(s.sessionId) ? withImgStats : noImgStats
|
|
54
|
+
target.count++
|
|
55
|
+
target.messages += s.totalMessages
|
|
56
|
+
target.cost += s.costUSD
|
|
57
|
+
target.duration += s.durationMinutes
|
|
58
|
+
target.tools += s.toolCallsTotal
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
// --- Images per session distribution ---
|
|
62
|
+
const imagesPerSession: Record<string, number> = {}
|
|
63
|
+
for (const img of images) {
|
|
64
|
+
imagesPerSession[img.sessionId] = (imagesPerSession[img.sessionId] || 0) + 1
|
|
65
|
+
}
|
|
66
|
+
const countDistribution: Record<number, { sessions: number; avgCost: number; avgMessages: number }> = {}
|
|
67
|
+
for (const [sid, count] of Object.entries(imagesPerSession)) {
|
|
68
|
+
const session = sessionMap.get(sid)
|
|
69
|
+
if (!session) continue
|
|
70
|
+
if (!countDistribution[count]) {
|
|
71
|
+
countDistribution[count] = { sessions: 0, avgCost: 0, avgMessages: 0 }
|
|
72
|
+
}
|
|
73
|
+
countDistribution[count].sessions++
|
|
74
|
+
countDistribution[count].avgCost += session.costUSD
|
|
75
|
+
countDistribution[count].avgMessages += session.totalMessages
|
|
76
|
+
}
|
|
77
|
+
for (const d of Object.values(countDistribution)) {
|
|
78
|
+
d.avgCost /= d.sessions
|
|
79
|
+
d.avgMessages /= d.sessions
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
// --- Screenshot frequency (time gaps between images in same session) ---
|
|
83
|
+
const imagesBySession = new Map<string, Date[]>()
|
|
84
|
+
for (const img of images) {
|
|
85
|
+
if (!imagesBySession.has(img.sessionId)) imagesBySession.set(img.sessionId, [])
|
|
86
|
+
imagesBySession.get(img.sessionId)!.push(new Date(img.timestamp))
|
|
87
|
+
}
|
|
88
|
+
const gaps: number[] = []
|
|
89
|
+
for (const timestamps of imagesBySession.values()) {
|
|
90
|
+
timestamps.sort((a, b) => a.getTime() - b.getTime())
|
|
91
|
+
for (let i = 1; i < timestamps.length; i++) {
|
|
92
|
+
const gap = (timestamps[i].getTime() - timestamps[i - 1].getTime()) / 60000
|
|
93
|
+
if (gap > 0) gaps.push(gap)
|
|
94
|
+
}
|
|
95
|
+
}
|
|
96
|
+
gaps.sort((a, b) => a - b)
|
|
97
|
+
const rapidFire = gaps.filter((g) => g < 2).length
|
|
98
|
+
const under5 = gaps.filter((g) => g < 5).length
|
|
99
|
+
|
|
100
|
+
// --- Top screenshot-heavy sessions ---
|
|
101
|
+
const topSessions = Object.entries(imagesPerSession)
|
|
102
|
+
.sort((a, b) => b[1] - a[1])
|
|
103
|
+
.slice(0, 10)
|
|
104
|
+
.map(([sid, count]) => {
|
|
105
|
+
const session = sessionMap.get(sid)
|
|
106
|
+
return {
|
|
107
|
+
sessionId: sid,
|
|
108
|
+
imageCount: count,
|
|
109
|
+
project: session?.project || 'unknown',
|
|
110
|
+
messages: session?.totalMessages || 0,
|
|
111
|
+
cost: session?.costUSD || 0,
|
|
112
|
+
date: session?.startTime.toISOString().slice(0, 10) || '',
|
|
113
|
+
}
|
|
114
|
+
})
|
|
115
|
+
|
|
116
|
+
// --- Recent images (for gallery) ---
|
|
117
|
+
const recentImages = images
|
|
118
|
+
.slice(-20)
|
|
119
|
+
.reverse()
|
|
120
|
+
.map((img) => ({
|
|
121
|
+
filename: img.filename,
|
|
122
|
+
sessionId: img.sessionId,
|
|
123
|
+
timestamp: img.timestamp.toISOString(),
|
|
124
|
+
sizeBytes: img.sizeBytes,
|
|
125
|
+
project: sessionMap.get(img.sessionId)?.project || 'unknown',
|
|
126
|
+
}))
|
|
127
|
+
|
|
128
|
+
return NextResponse.json({
|
|
129
|
+
overview: {
|
|
130
|
+
totalImages,
|
|
131
|
+
sessionsWithImages,
|
|
132
|
+
totalSessions,
|
|
133
|
+
percentWithImages: Math.round((sessionsWithImages / totalSessions) * 100),
|
|
134
|
+
totalSizeMB: Math.round(totalBytes / 1024 / 1024),
|
|
135
|
+
avgSizeKB: Math.round(totalBytes / totalImages / 1024),
|
|
136
|
+
byMediaType,
|
|
137
|
+
},
|
|
138
|
+
byProject: Object.entries(byProject)
|
|
139
|
+
.sort((a, b) => b[1] - a[1])
|
|
140
|
+
.map(([name, count]) => ({ name, count })),
|
|
141
|
+
byHour: byHour.map((count, hour) => ({ hour, count })),
|
|
142
|
+
byDate: Object.entries(byDate)
|
|
143
|
+
.sort((a, b) => a[0].localeCompare(b[0]))
|
|
144
|
+
.map(([date, count]) => ({ date: date.slice(5), count })),
|
|
145
|
+
comparison: {
|
|
146
|
+
withImages: {
|
|
147
|
+
sessions: withImgStats.count,
|
|
148
|
+
avgMessages: Math.round(withImgStats.messages / withImgStats.count),
|
|
149
|
+
avgCost: Number((withImgStats.cost / withImgStats.count).toFixed(2)),
|
|
150
|
+
avgDuration: Math.round(withImgStats.duration / withImgStats.count),
|
|
151
|
+
avgTools: Math.round(withImgStats.tools / withImgStats.count),
|
|
152
|
+
},
|
|
153
|
+
withoutImages: {
|
|
154
|
+
sessions: noImgStats.count,
|
|
155
|
+
avgMessages: Math.round(noImgStats.messages / noImgStats.count),
|
|
156
|
+
avgCost: Number((noImgStats.cost / noImgStats.count).toFixed(2)),
|
|
157
|
+
avgDuration: Math.round(noImgStats.duration / noImgStats.count),
|
|
158
|
+
avgTools: Math.round(noImgStats.tools / noImgStats.count),
|
|
159
|
+
},
|
|
160
|
+
},
|
|
161
|
+
screenshotFrequency: {
|
|
162
|
+
totalGaps: gaps.length,
|
|
163
|
+
medianMinutes: gaps.length ? Number(gaps[Math.floor(gaps.length / 2)].toFixed(1)) : 0,
|
|
164
|
+
meanMinutes: gaps.length ? Number((gaps.reduce((a, b) => a + b, 0) / gaps.length).toFixed(1)) : 0,
|
|
165
|
+
rapidFireCount: rapidFire,
|
|
166
|
+
rapidFirePercent: gaps.length ? Math.round((rapidFire / gaps.length) * 100) : 0,
|
|
167
|
+
under5Count: under5,
|
|
168
|
+
under5Percent: gaps.length ? Math.round((under5 / gaps.length) * 100) : 0,
|
|
169
|
+
},
|
|
170
|
+
topSessions,
|
|
171
|
+
recentImages,
|
|
172
|
+
})
|
|
173
|
+
} catch (error) {
|
|
174
|
+
console.error('Failed to analyze images:', error)
|
|
175
|
+
return NextResponse.json({ error: 'Failed to analyze images' }, { status: 500 })
|
|
176
|
+
}
|
|
177
|
+
}
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
import { NextResponse } from 'next/server'
|
|
2
|
+
import { syncLogs } from '@/lib/sync'
|
|
3
|
+
|
|
4
|
+
export const dynamic = 'force-dynamic'
|
|
5
|
+
|
|
6
|
+
export async function POST() {
|
|
7
|
+
try {
|
|
8
|
+
const result = await syncLogs()
|
|
9
|
+
return NextResponse.json(result)
|
|
10
|
+
} catch (error) {
|
|
11
|
+
console.error('Sync failed:', error)
|
|
12
|
+
return NextResponse.json({ error: 'Sync failed' }, { status: 500 })
|
|
13
|
+
}
|
|
14
|
+
}
|
|
@@ -0,0 +1,117 @@
|
|
|
1
|
+
import { NextRequest, NextResponse } from 'next/server'
|
|
2
|
+
import { getUsageData } from '@/lib/queries'
|
|
3
|
+
import { getCodexUsageData } from '@/lib/queries-codex'
|
|
4
|
+
|
|
5
|
+
export const dynamic = 'force-dynamic'
|
|
6
|
+
|
|
7
|
+
export async function GET(request: NextRequest) {
|
|
8
|
+
try {
|
|
9
|
+
const agent = request.nextUrl.searchParams.get('agent') || 'claude'
|
|
10
|
+
|
|
11
|
+
if (agent === 'codex') {
|
|
12
|
+
const data = getCodexUsageData()
|
|
13
|
+
return NextResponse.json(data)
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
if (agent === 'combined') {
|
|
17
|
+
const [claudeData, codexData] = await Promise.all([
|
|
18
|
+
getUsageData(),
|
|
19
|
+
Promise.resolve(getCodexUsageData()),
|
|
20
|
+
])
|
|
21
|
+
const merged = mergeUsageData(claudeData, codexData)
|
|
22
|
+
return NextResponse.json(merged)
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
// Default: claude
|
|
26
|
+
const data = await getUsageData()
|
|
27
|
+
return NextResponse.json(data)
|
|
28
|
+
} catch (error) {
|
|
29
|
+
console.error('Failed to query usage data:', error)
|
|
30
|
+
return NextResponse.json({ error: 'Failed to query usage data' }, { status: 500 })
|
|
31
|
+
}
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
// ─── Merge two UsageData objects ─────────────────────────────────────
|
|
35
|
+
|
|
36
|
+
import type { UsageData } from '@/lib/parse-logs'
|
|
37
|
+
|
|
38
|
+
function mergeUsageData(a: UsageData, b: UsageData): UsageData {
|
|
39
|
+
const sessions = [...a.sessions, ...b.sessions].sort(
|
|
40
|
+
(x, y) => (y.startTime || '').localeCompare(x.startTime || '')
|
|
41
|
+
)
|
|
42
|
+
|
|
43
|
+
// Merge projects
|
|
44
|
+
const projectMap = new Map<string, (typeof a.projects)[0]>()
|
|
45
|
+
for (const proj of [...a.projects, ...b.projects]) {
|
|
46
|
+
const existing = projectMap.get(proj.name)
|
|
47
|
+
if (existing) {
|
|
48
|
+
existing.sessions += proj.sessions
|
|
49
|
+
existing.totalMessages += proj.totalMessages
|
|
50
|
+
existing.totalTokens += proj.totalTokens
|
|
51
|
+
existing.totalCost += proj.totalCost
|
|
52
|
+
existing.totalDurationMinutes += proj.totalDurationMinutes
|
|
53
|
+
for (const [tool, count] of Object.entries(proj.toolCalls)) {
|
|
54
|
+
existing.toolCalls[tool] = (existing.toolCalls[tool] || 0) + count
|
|
55
|
+
}
|
|
56
|
+
} else {
|
|
57
|
+
projectMap.set(proj.name, { ...proj, toolCalls: { ...proj.toolCalls } })
|
|
58
|
+
}
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
// Merge daily
|
|
62
|
+
const dailyMap = new Map<string, (typeof a.daily)[0]>()
|
|
63
|
+
for (const day of [...a.daily, ...b.daily]) {
|
|
64
|
+
const existing = dailyMap.get(day.date)
|
|
65
|
+
if (existing) {
|
|
66
|
+
existing.sessions += day.sessions
|
|
67
|
+
existing.messages += day.messages
|
|
68
|
+
existing.inputTokens += day.inputTokens
|
|
69
|
+
existing.outputTokens += day.outputTokens
|
|
70
|
+
existing.cacheCreationTokens += day.cacheCreationTokens
|
|
71
|
+
existing.cacheReadTokens += day.cacheReadTokens
|
|
72
|
+
existing.totalTokens += day.totalTokens
|
|
73
|
+
existing.costUSD += day.costUSD
|
|
74
|
+
existing.toolCalls += day.toolCalls
|
|
75
|
+
} else {
|
|
76
|
+
dailyMap.set(day.date, { ...day })
|
|
77
|
+
}
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
// Merge tool usage
|
|
81
|
+
const toolUsage: Record<string, number> = { ...a.toolUsage }
|
|
82
|
+
for (const [tool, count] of Object.entries(b.toolUsage)) {
|
|
83
|
+
toolUsage[tool] = (toolUsage[tool] || 0) + count
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
// Merge models
|
|
87
|
+
const models: Record<string, number> = { ...a.overview.models }
|
|
88
|
+
for (const [m, count] of Object.entries(b.overview.models)) {
|
|
89
|
+
models[m] = (models[m] || 0) + count
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
const projects = Array.from(projectMap.values()).sort((x, y) => y.totalCost - x.totalCost)
|
|
93
|
+
const daily = Array.from(dailyMap.values()).sort((x, y) => x.date.localeCompare(y.date))
|
|
94
|
+
|
|
95
|
+
return {
|
|
96
|
+
overview: {
|
|
97
|
+
totalSessions: a.overview.totalSessions + b.overview.totalSessions,
|
|
98
|
+
totalProjects: projects.length,
|
|
99
|
+
totalMessages: a.overview.totalMessages + b.overview.totalMessages,
|
|
100
|
+
totalUserMessages: a.overview.totalUserMessages + b.overview.totalUserMessages,
|
|
101
|
+
totalAssistantMessages: a.overview.totalAssistantMessages + b.overview.totalAssistantMessages,
|
|
102
|
+
totalInputTokens: a.overview.totalInputTokens + b.overview.totalInputTokens,
|
|
103
|
+
totalOutputTokens: a.overview.totalOutputTokens + b.overview.totalOutputTokens,
|
|
104
|
+
totalCacheCreationTokens: a.overview.totalCacheCreationTokens + b.overview.totalCacheCreationTokens,
|
|
105
|
+
totalCacheReadTokens: a.overview.totalCacheReadTokens + b.overview.totalCacheReadTokens,
|
|
106
|
+
totalTokens: a.overview.totalTokens + b.overview.totalTokens,
|
|
107
|
+
totalCostUSD: a.overview.totalCostUSD + b.overview.totalCostUSD,
|
|
108
|
+
totalDurationMinutes: a.overview.totalDurationMinutes + b.overview.totalDurationMinutes,
|
|
109
|
+
totalToolCalls: a.overview.totalToolCalls + b.overview.totalToolCalls,
|
|
110
|
+
models,
|
|
111
|
+
},
|
|
112
|
+
sessions,
|
|
113
|
+
projects,
|
|
114
|
+
daily,
|
|
115
|
+
toolUsage,
|
|
116
|
+
}
|
|
117
|
+
}
|
package/app/favicon.ico
ADDED
|
Binary file
|
package/app/globals.css
ADDED
|
@@ -0,0 +1,144 @@
|
|
|
1
|
+
@import "tailwindcss";
|
|
2
|
+
@import "tw-animate-css";
|
|
3
|
+
@import "shadcn/tailwind.css";
|
|
4
|
+
|
|
5
|
+
@custom-variant dark (&:is(.dark *));
|
|
6
|
+
|
|
7
|
+
@theme inline {
|
|
8
|
+
--font-heading: var(--font-sans);
|
|
9
|
+
--font-sans: var(--font-sans);
|
|
10
|
+
--color-sidebar-ring: var(--sidebar-ring);
|
|
11
|
+
--color-sidebar-border: var(--sidebar-border);
|
|
12
|
+
--color-sidebar-accent-foreground: var(--sidebar-accent-foreground);
|
|
13
|
+
--color-sidebar-accent: var(--sidebar-accent);
|
|
14
|
+
--color-sidebar-primary-foreground: var(--sidebar-primary-foreground);
|
|
15
|
+
--color-sidebar-primary: var(--sidebar-primary);
|
|
16
|
+
--color-sidebar-foreground: var(--sidebar-foreground);
|
|
17
|
+
--color-sidebar: var(--sidebar);
|
|
18
|
+
--color-chart-10: var(--chart-10);
|
|
19
|
+
--color-chart-9: var(--chart-9);
|
|
20
|
+
--color-chart-8: var(--chart-8);
|
|
21
|
+
--color-chart-7: var(--chart-7);
|
|
22
|
+
--color-chart-6: var(--chart-6);
|
|
23
|
+
--color-chart-5: var(--chart-5);
|
|
24
|
+
--color-chart-4: var(--chart-4);
|
|
25
|
+
--color-chart-3: var(--chart-3);
|
|
26
|
+
--color-chart-2: var(--chart-2);
|
|
27
|
+
--color-chart-1: var(--chart-1);
|
|
28
|
+
--color-ring: var(--ring);
|
|
29
|
+
--color-input: var(--input);
|
|
30
|
+
--color-border: var(--border);
|
|
31
|
+
--color-destructive: var(--destructive);
|
|
32
|
+
--color-accent-foreground: var(--accent-foreground);
|
|
33
|
+
--color-accent: var(--accent);
|
|
34
|
+
--color-muted-foreground: var(--muted-foreground);
|
|
35
|
+
--color-muted: var(--muted);
|
|
36
|
+
--color-secondary-foreground: var(--secondary-foreground);
|
|
37
|
+
--color-secondary: var(--secondary);
|
|
38
|
+
--color-primary-foreground: var(--primary-foreground);
|
|
39
|
+
--color-primary: var(--primary);
|
|
40
|
+
--color-popover-foreground: var(--popover-foreground);
|
|
41
|
+
--color-popover: var(--popover);
|
|
42
|
+
--color-card-foreground: var(--card-foreground);
|
|
43
|
+
--color-card: var(--card);
|
|
44
|
+
--color-foreground: var(--foreground);
|
|
45
|
+
--color-background: var(--background);
|
|
46
|
+
--radius-sm: calc(var(--radius) * 0.6);
|
|
47
|
+
--radius-md: calc(var(--radius) * 0.8);
|
|
48
|
+
--radius-lg: var(--radius);
|
|
49
|
+
--radius-xl: calc(var(--radius) * 1.4);
|
|
50
|
+
--radius-2xl: calc(var(--radius) * 1.8);
|
|
51
|
+
--radius-3xl: calc(var(--radius) * 2.2);
|
|
52
|
+
--radius-4xl: calc(var(--radius) * 2.6);
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
:root {
|
|
56
|
+
--background: oklch(1 0 0);
|
|
57
|
+
--foreground: oklch(0.145 0 0);
|
|
58
|
+
--card: oklch(1 0 0);
|
|
59
|
+
--card-foreground: oklch(0.145 0 0);
|
|
60
|
+
--popover: oklch(1 0 0);
|
|
61
|
+
--popover-foreground: oklch(0.145 0 0);
|
|
62
|
+
--primary: oklch(0.205 0 0);
|
|
63
|
+
--primary-foreground: oklch(0.985 0 0);
|
|
64
|
+
--secondary: oklch(0.97 0 0);
|
|
65
|
+
--secondary-foreground: oklch(0.205 0 0);
|
|
66
|
+
--muted: oklch(0.97 0 0);
|
|
67
|
+
--muted-foreground: oklch(0.556 0 0);
|
|
68
|
+
--accent: oklch(0.97 0 0);
|
|
69
|
+
--accent-foreground: oklch(0.205 0 0);
|
|
70
|
+
--destructive: oklch(0.577 0.245 27.325);
|
|
71
|
+
--border: oklch(0.922 0 0);
|
|
72
|
+
--input: oklch(0.922 0 0);
|
|
73
|
+
--ring: oklch(0.708 0 0);
|
|
74
|
+
--chart-1: oklch(0.70 0.12 250);
|
|
75
|
+
--chart-2: oklch(0.72 0.11 170);
|
|
76
|
+
--chart-3: oklch(0.68 0.10 300);
|
|
77
|
+
--chart-4: oklch(0.78 0.10 75);
|
|
78
|
+
--chart-5: oklch(0.72 0.10 25);
|
|
79
|
+
--chart-6: oklch(0.66 0.11 210);
|
|
80
|
+
--chart-7: oklch(0.75 0.10 130);
|
|
81
|
+
--chart-8: oklch(0.71 0.11 340);
|
|
82
|
+
--chart-9: oklch(0.73 0.10 50);
|
|
83
|
+
--chart-10: oklch(0.67 0.12 280);
|
|
84
|
+
--radius: 0.625rem;
|
|
85
|
+
--sidebar: oklch(0.985 0 0);
|
|
86
|
+
--sidebar-foreground: oklch(0.145 0 0);
|
|
87
|
+
--sidebar-primary: oklch(0.205 0 0);
|
|
88
|
+
--sidebar-primary-foreground: oklch(0.985 0 0);
|
|
89
|
+
--sidebar-accent: oklch(0.97 0 0);
|
|
90
|
+
--sidebar-accent-foreground: oklch(0.205 0 0);
|
|
91
|
+
--sidebar-border: oklch(0.922 0 0);
|
|
92
|
+
--sidebar-ring: oklch(0.708 0 0);
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
.dark {
|
|
96
|
+
--background: oklch(0.145 0 0);
|
|
97
|
+
--foreground: oklch(0.985 0 0);
|
|
98
|
+
--card: oklch(0.205 0 0);
|
|
99
|
+
--card-foreground: oklch(0.985 0 0);
|
|
100
|
+
--popover: oklch(0.205 0 0);
|
|
101
|
+
--popover-foreground: oklch(0.985 0 0);
|
|
102
|
+
--primary: oklch(0.922 0 0);
|
|
103
|
+
--primary-foreground: oklch(0.205 0 0);
|
|
104
|
+
--secondary: oklch(0.269 0 0);
|
|
105
|
+
--secondary-foreground: oklch(0.985 0 0);
|
|
106
|
+
--muted: oklch(0.269 0 0);
|
|
107
|
+
--muted-foreground: oklch(0.708 0 0);
|
|
108
|
+
--accent: oklch(0.269 0 0);
|
|
109
|
+
--accent-foreground: oklch(0.985 0 0);
|
|
110
|
+
--destructive: oklch(0.704 0.191 22.216);
|
|
111
|
+
--border: oklch(1 0 0 / 10%);
|
|
112
|
+
--input: oklch(1 0 0 / 15%);
|
|
113
|
+
--ring: oklch(0.556 0 0);
|
|
114
|
+
--chart-1: oklch(0.72 0.12 250);
|
|
115
|
+
--chart-2: oklch(0.74 0.11 170);
|
|
116
|
+
--chart-3: oklch(0.70 0.10 300);
|
|
117
|
+
--chart-4: oklch(0.80 0.10 75);
|
|
118
|
+
--chart-5: oklch(0.74 0.10 25);
|
|
119
|
+
--chart-6: oklch(0.68 0.11 210);
|
|
120
|
+
--chart-7: oklch(0.77 0.10 130);
|
|
121
|
+
--chart-8: oklch(0.73 0.11 340);
|
|
122
|
+
--chart-9: oklch(0.75 0.10 50);
|
|
123
|
+
--chart-10: oklch(0.69 0.12 280);
|
|
124
|
+
--sidebar: oklch(0.205 0 0);
|
|
125
|
+
--sidebar-foreground: oklch(0.985 0 0);
|
|
126
|
+
--sidebar-primary: oklch(0.488 0.243 264.376);
|
|
127
|
+
--sidebar-primary-foreground: oklch(0.985 0 0);
|
|
128
|
+
--sidebar-accent: oklch(0.269 0 0);
|
|
129
|
+
--sidebar-accent-foreground: oklch(0.985 0 0);
|
|
130
|
+
--sidebar-border: oklch(1 0 0 / 10%);
|
|
131
|
+
--sidebar-ring: oklch(0.556 0 0);
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
@layer base {
|
|
135
|
+
* {
|
|
136
|
+
@apply border-border outline-ring/50;
|
|
137
|
+
}
|
|
138
|
+
body {
|
|
139
|
+
@apply bg-background text-foreground;
|
|
140
|
+
}
|
|
141
|
+
html {
|
|
142
|
+
@apply font-sans;
|
|
143
|
+
}
|
|
144
|
+
}
|
package/app/icon.svg
ADDED
|
@@ -0,0 +1,3 @@
|
|
|
1
|
+
<?xml version="1.0" encoding="utf-8"?>
|
|
2
|
+
<!-- Uploaded to: SVG Repo, www.svgrepo.com, Generator: SVG Repo Mixer Tools -->
|
|
3
|
+
<svg width="800px" height="800px" viewBox="0 -77.5 1179 1179" class="icon" version="1.1" xmlns="http://www.w3.org/2000/svg"><path d="M597.215632 994.574713h403.714943s43.549425-8.945287 43.549425-114.64092 94.16092-577.677241-459.976092-577.677241-457.151264 541.425287-457.151264 541.425287-25.423448 160.77977 54.848735 157.013333 415.014253-6.12046 415.014253-6.120459z" fill="#FFFFFF" /><path d="M1071.786667 712.798161h72.503908v136.297931h-72.503908zM36.016552 712.798161h72.503908v136.297931H36.016552z" fill="#EA5D5C" /><path d="M305.68366 559.40926l556.254412-1.165018 0.398364 190.20464-556.254412 1.165018-0.398364-190.20464Z" fill="#4C66AF" /><path d="M1129.931034 680.312644h-59.556781c-3.295632-152.069885-67.56046-258.942529-172.079081-324.384368l115.347127-238.462529a47.08046 47.08046 0 1 0-42.372414-20.48l-114.640919 236.57931a625.934713 625.934713 0 0 0-269.30023-53.200919 625.228506 625.228506 0 0 0-270.006437 54.848736l-115.817931-235.402299a47.08046 47.08046 0 1 0-42.372414 20.715402l117.701149 238.462529c-103.812414 65.441839-167.135632 173.02069-169.960459 324.61977H47.786667a47.08046 47.08046 0 0 0-47.08046 47.08046v117.701149a47.08046 47.08046 0 0 0 47.08046 47.08046h58.615172v57.908965a70.62069 70.62069 0 0 0 70.62069 70.62069l823.908046-1.647816a70.62069 70.62069 0 0 0 70.620689-70.62069v-57.908965h59.085977a47.08046 47.08046 0 0 0 47.08046-47.08046v-117.701149A47.08046 47.08046 0 0 0 1129.931034 680.312644zM94.16092 847.212874H47.08046v-117.70115h47.08046v117.70115z m929.83908 103.106206a23.54023 23.54023 0 0 1-23.54023 23.54023l-823.908046 1.647816a23.54023 23.54023 0 0 1-23.54023-23.540229v-258.942529c0-329.563218 303.668966-365.57977 434.788046-365.815173s435.494253 34.604138 436.20046 363.931954z m105.46023-105.224827h-47.08046v-117.70115h47.08046v117.70115z" fill="#3F4651" /><path d="M464.684138 135.827126l22.363218-19.53839 40.018391 62.381609a30.131494 30.131494 0 0 0 25.423448 13.888735h2.824828a30.131494 30.131494 0 0 0 25.188046-19.067586l20.715402-79.095172 21.186207 74.387126v2.118621a30.366897 30.366897 0 0 0 52.494713 6.826667l30.366896-57.202759 13.182529 12.947126a30.131494 30.131494 0 0 0 21.186207 8.709886h57.673563a23.54023 23.54023 0 0 0 23.54023-23.54023 23.54023 23.54023 0 0 0-23.54023-23.54023h-50.140689l-23.54023-23.54023a30.366897 30.366897 0 0 0-45.668046 3.766437l-21.42161 40.01839L629.465747 19.302989a30.131494 30.131494 0 0 0-28.012873-19.067587 30.131494 30.131494 0 0 0-28.012874 19.067587l-26.60046 101.693793-29.660689-47.08046a30.366897 30.366897 0 0 0-20.48-13.653333 30.837701 30.837701 0 0 0-23.54023 6.826666l-32.250115 28.248276h-60.027586a23.54023 23.54023 0 0 0-23.54023 23.54023 23.54023 23.54023 0 0 0 23.54023 23.54023h66.148046a31.308506 31.308506 0 0 0 17.655172-6.591265zM776.121379 532.950805H404.421149A121.232184 121.232184 0 0 0 282.482759 639.352644a117.701149 117.701149 0 0 0 117.701149 129.000459h371.70023a121.232184 121.232184 0 0 0 121.938391-106.401839 117.701149 117.701149 0 0 0-117.70115-129.000459z m0 188.321839H402.302529a72.503908 72.503908 0 0 1-72.268506-56.496552 70.62069 70.62069 0 0 1 68.972874-84.744828h373.81885a72.503908 72.503908 0 0 1 72.268506 56.496552 70.62069 70.62069 0 0 1-68.502069 84.744828z" fill="#3F4651" /></svg>
|
package/app/layout.tsx
ADDED
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
import { Geist, Geist_Mono, Inter } from "next/font/google"
|
|
2
|
+
|
|
3
|
+
import "./globals.css"
|
|
4
|
+
import { ThemeProvider } from "@/components/theme-provider"
|
|
5
|
+
import { cn } from "@/lib/utils";
|
|
6
|
+
|
|
7
|
+
const inter = Inter({subsets:['latin'],variable:'--font-sans'})
|
|
8
|
+
|
|
9
|
+
const fontMono = Geist_Mono({
|
|
10
|
+
subsets: ["latin"],
|
|
11
|
+
variable: "--font-mono",
|
|
12
|
+
})
|
|
13
|
+
|
|
14
|
+
export const metadata = {
|
|
15
|
+
title: 'AgentFit — Coding Agent Fitness Tracker',
|
|
16
|
+
description: 'Track your AI coding agent\'s fitness — usage, costs, personality, and behavioral patterns across Claude Code, Codex, and more.',
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
export default function RootLayout({
|
|
20
|
+
children,
|
|
21
|
+
}: Readonly<{
|
|
22
|
+
children: React.ReactNode
|
|
23
|
+
}>) {
|
|
24
|
+
return (
|
|
25
|
+
<html
|
|
26
|
+
lang="en"
|
|
27
|
+
suppressHydrationWarning
|
|
28
|
+
className={cn("antialiased", fontMono.variable, "font-sans", inter.variable)}
|
|
29
|
+
>
|
|
30
|
+
<body>
|
|
31
|
+
<ThemeProvider>{children}</ThemeProvider>
|
|
32
|
+
</body>
|
|
33
|
+
</html>
|
|
34
|
+
)
|
|
35
|
+
}
|
package/bin/agentfit.mjs
ADDED
|
@@ -0,0 +1,69 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
|
|
3
|
+
import { execSync, spawn } from 'child_process'
|
|
4
|
+
import { existsSync, writeFileSync } from 'fs'
|
|
5
|
+
import path from 'path'
|
|
6
|
+
import { fileURLToPath } from 'url'
|
|
7
|
+
|
|
8
|
+
const __dirname = path.dirname(fileURLToPath(import.meta.url))
|
|
9
|
+
const ROOT = path.resolve(__dirname, '..')
|
|
10
|
+
const PORT = process.env.AGENTFIT_PORT || process.env.PORT || '3000'
|
|
11
|
+
|
|
12
|
+
function info(msg) {
|
|
13
|
+
console.log(`\x1b[1;34m==>\x1b[0m ${msg}`)
|
|
14
|
+
}
|
|
15
|
+
function ok(msg) {
|
|
16
|
+
console.log(`\x1b[1;32m==>\x1b[0m ${msg}`)
|
|
17
|
+
}
|
|
18
|
+
function error(msg) {
|
|
19
|
+
console.error(`\x1b[1;31m==>\x1b[0m ${msg}`)
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
function run(cmd, opts = {}) {
|
|
23
|
+
execSync(cmd, { cwd: ROOT, stdio: 'inherit', ...opts })
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
// ─── Ensure .env exists ─────────────────────────────────────────────
|
|
27
|
+
const envPath = path.join(ROOT, '.env')
|
|
28
|
+
if (!existsSync(envPath)) {
|
|
29
|
+
writeFileSync(envPath, 'DATABASE_URL="file:./dev.db"\n')
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
// ─── First-run setup: prisma generate + migrate ─────────────────────
|
|
33
|
+
const generatedClient = path.join(ROOT, 'generated', 'prisma')
|
|
34
|
+
if (!existsSync(generatedClient)) {
|
|
35
|
+
info('First run detected — generating Prisma client...')
|
|
36
|
+
run('npx prisma generate')
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
const dbPath = path.join(ROOT, 'dev.db')
|
|
40
|
+
if (!existsSync(dbPath)) {
|
|
41
|
+
info('Creating database...')
|
|
42
|
+
run('npx prisma migrate deploy')
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
// ─── Build if .next doesn't exist ───────────────────────────────────
|
|
46
|
+
const nextDir = path.join(ROOT, '.next')
|
|
47
|
+
if (!existsSync(nextDir)) {
|
|
48
|
+
info('Building production bundle (first run)...')
|
|
49
|
+
run('npm run build')
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
// ─── Start server ───────────────────────────────────────────────────
|
|
53
|
+
ok(`Starting AgentFit on http://localhost:${PORT}`)
|
|
54
|
+
console.log(' Press Ctrl+C to stop.\n')
|
|
55
|
+
|
|
56
|
+
const server = spawn('npx', ['next', 'start', '-p', PORT], {
|
|
57
|
+
cwd: ROOT,
|
|
58
|
+
stdio: 'inherit',
|
|
59
|
+
env: { ...process.env, PORT },
|
|
60
|
+
})
|
|
61
|
+
|
|
62
|
+
server.on('close', (code) => process.exit(code ?? 0))
|
|
63
|
+
|
|
64
|
+
// Forward signals
|
|
65
|
+
for (const sig of ['SIGINT', 'SIGTERM']) {
|
|
66
|
+
process.on(sig, () => {
|
|
67
|
+
server.kill(sig)
|
|
68
|
+
})
|
|
69
|
+
}
|
|
File without changes
|