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,71 @@
1
+ // ─── Tool Flow Analysis ──────────────────────────────────────────────
2
+ // Extracts tool call transition data for flow visualization.
3
+
4
+ import type { UsageData } from './parse-logs'
5
+
6
+ export interface ToolTransition {
7
+ source: string
8
+ target: string
9
+ count: number
10
+ }
11
+
12
+ export interface ToolFlowData {
13
+ nodes: { id: string; count: number }[]
14
+ edges: ToolTransition[]
15
+ totalTransitions: number
16
+ }
17
+
18
+ export function computeToolFlow(data: UsageData): ToolFlowData {
19
+ const toolUsage = data.toolUsage
20
+ const transitions = new Map<string, number>()
21
+
22
+ // We need per-session tool sequences. Since we store toolCallsJson as
23
+ // aggregate counts per session, we compute transitions from the global
24
+ // tool usage data and common patterns. For accurate transitions we'd
25
+ // need the raw sequence — but we can derive a meaningful flow from the
26
+ // session-level tool call co-occurrence.
27
+
28
+ // Build transitions from sessions that have tool call data
29
+ for (const session of data.sessions) {
30
+ const tools = Object.entries(session.toolCalls)
31
+ .sort((a, b) => b[1] - a[1])
32
+
33
+ // Create edges between co-occurring tools (weighted by min count)
34
+ for (let i = 0; i < tools.length; i++) {
35
+ for (let j = i + 1; j < tools.length; j++) {
36
+ const [toolA] = tools[i]
37
+ const [toolB] = tools[j]
38
+ // Direction: higher count tool → lower count tool
39
+ const key = `${toolA}→${toolB}`
40
+ const weight = Math.min(tools[i][1], tools[j][1])
41
+ transitions.set(key, (transitions.get(key) || 0) + weight)
42
+ }
43
+ }
44
+ }
45
+
46
+ // Build nodes from tool usage
47
+ const nodes = Object.entries(toolUsage)
48
+ .filter(([, count]) => count > 5)
49
+ .sort((a, b) => b[1] - a[1])
50
+ .slice(0, 15)
51
+ .map(([id, count]) => ({ id, count }))
52
+
53
+ const nodeSet = new Set(nodes.map(n => n.id))
54
+
55
+ // Build edges (filter to only include nodes we're showing)
56
+ const edges: ToolTransition[] = []
57
+ for (const [key, count] of transitions) {
58
+ const [source, target] = key.split('→')
59
+ if (nodeSet.has(source) && nodeSet.has(target) && count > 2) {
60
+ edges.push({ source, target, count })
61
+ }
62
+ }
63
+
64
+ edges.sort((a, b) => b.count - a.count)
65
+
66
+ return {
67
+ nodes,
68
+ edges: edges.slice(0, 30),
69
+ totalTransitions: edges.reduce((sum, e) => sum + e.count, 0),
70
+ }
71
+ }
package/next.config.mjs CHANGED
@@ -1,4 +1,9 @@
1
1
  /** @type {import('next').NextConfig} */
2
- const nextConfig = {}
2
+ const nextConfig = {
3
+ output: 'standalone',
4
+ outputFileTracingExcludes: {
5
+ '*': ['./dist-electron/**', './electron/server/**', './data/**'],
6
+ },
7
+ }
3
8
 
4
9
  export default nextConfig
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "agentfit",
3
- "version": "0.1.0",
3
+ "version": "0.1.1",
4
4
  "description": "Fitness tracker dashboard for AI coding agents (Claude Code, Codex). Visualize usage, cost, tokens, and productivity from local conversation logs.",
5
5
  "type": "module",
6
6
  "bin": {
@@ -20,6 +20,7 @@
20
20
  "type": "git",
21
21
  "url": "https://github.com/harrywang/agentfit.git"
22
22
  },
23
+ "main": "bin/agentfit.mjs",
23
24
  "scripts": {
24
25
  "postinstall": "prisma generate",
25
26
  "dev": "next dev --turbopack",
@@ -29,7 +30,13 @@
29
30
  "format": "prettier --write \"**/*.{ts,tsx}\"",
30
31
  "typecheck": "tsc --noEmit",
31
32
  "test": "vitest run",
32
- "test:watch": "vitest"
33
+ "test:watch": "vitest",
34
+ "electron:prepare": "node scripts/prepare-electron.mjs",
35
+ "electron:dev": "npm run build && npm run electron:prepare && electron .",
36
+ "electron:build": "rm -rf dist-electron electron/server && npm run build && npm run electron:prepare && electron-builder --config electron-builder.yml",
37
+ "electron:build:mac": "rm -rf dist-electron electron/server && npm run build && npm run electron:prepare && electron-builder --mac --config electron-builder.yml",
38
+ "electron:build:win": "rm -rf dist-electron electron/server && npm run build && npm run electron:prepare && electron-builder --win --config electron-builder.yml",
39
+ "electron:build:all": "rm -rf dist-electron electron/server && npm run build && npm run electron:prepare && electron-builder --mac --win --config electron-builder.yml"
33
40
  },
34
41
  "dependencies": {
35
42
  "@base-ui/react": "^1.3.0",
@@ -37,9 +44,13 @@
37
44
  "@prisma/adapter-libsql": "^7.6.0",
38
45
  "@prisma/client": "^7.6.0",
39
46
  "@tabler/icons-react": "^3.41.0",
47
+ "@types/dagre": "^0.7.54",
48
+ "@xyflow/react": "^12.10.2",
40
49
  "class-variance-authority": "^0.7.1",
41
50
  "clsx": "^2.1.1",
51
+ "dagre": "^0.8.5",
42
52
  "date-fns": "^4.1.0",
53
+ "embla-carousel-react": "^8.6.0",
43
54
  "lucide-react": "^1.7.0",
44
55
  "next": "16.1.7",
45
56
  "next-themes": "^0.4.6",
@@ -48,10 +59,13 @@
48
59
  "react-dom": "^19.2.4",
49
60
  "recharts": "^3.8.0",
50
61
  "shadcn": "^4.1.1",
62
+ "sonner": "^2.0.7",
51
63
  "tailwind-merge": "^3.5.0",
52
64
  "tw-animate-css": "^1.4.0"
53
65
  },
54
66
  "devDependencies": {
67
+ "electron": "^35.1.2",
68
+ "electron-builder": "^26.0.12",
55
69
  "@eslint/eslintrc": "^3",
56
70
  "@tailwindcss/postcss": "^4.2.1",
57
71
  "@testing-library/jest-dom": "^6.9.1",
@@ -16,12 +16,12 @@ function getMonthLabel(dateStr: string): string {
16
16
  return new Date(dateStr + 'T00:00:00').toLocaleString('en-US', { month: 'short' })
17
17
  }
18
18
 
19
- function intensityClass(ratio: number): string {
20
- if (ratio === 0) return 'bg-muted'
21
- if (ratio < 0.25) return 'bg-chart-3/30'
22
- if (ratio < 0.5) return 'bg-chart-3/50'
23
- if (ratio < 0.75) return 'bg-chart-3/70'
24
- return 'bg-chart-3'
19
+ function intensityStyle(ratio: number): React.CSSProperties {
20
+ if (ratio === 0)
21
+ return { backgroundColor: 'var(--muted)' }
22
+ // Use chart-2 (teal) with increasing opacity for a clean gradient
23
+ const alpha = 0.15 + ratio * 0.85
24
+ return { backgroundColor: `oklch(0.55 0.15 170 / ${alpha})` }
25
25
  }
26
26
 
27
27
  const WEEKDAYS = ['Sun', 'Mon', 'Tue', 'Wed', 'Thu', 'Fri', 'Sat']
@@ -29,7 +29,7 @@ const WEEKDAYS = ['Sun', 'Mon', 'Tue', 'Wed', 'Thu', 'Fri', 'Sat']
29
29
  // ─── Component ──────────────────────────────────────────────────────
30
30
 
31
31
  export default function CostHeatmap({ data }: PluginProps) {
32
- const { grid, maxCost, weeks, monthLabels, stats } = useMemo(() => {
32
+ const { grid, weeks, monthLabels, stats } = useMemo(() => {
33
33
  const dailyMap = new Map(data.daily.map((d) => [d.date, d.costUSD]))
34
34
  const dates = data.daily.map((d) => d.date).sort()
35
35
 
@@ -92,7 +92,8 @@ export default function CostHeatmap({ data }: PluginProps) {
92
92
  const costs = allDates.map((d) => dailyMap.get(d) || 0)
93
93
  const total = costs.reduce((a, b) => a + b, 0)
94
94
  const activeDays = costs.filter((c) => c > 0).length
95
- const peakDay = allDates[costs.indexOf(Math.max(...costs))]
95
+ const peakIdx = costs.indexOf(Math.max(...costs))
96
+ const peakDay = allDates[peakIdx]
96
97
 
97
98
  return {
98
99
  grid: g,
@@ -116,6 +117,9 @@ export default function CostHeatmap({ data }: PluginProps) {
116
117
  )
117
118
  }
118
119
 
120
+ const cellSize = 18
121
+ const cellGap = 3
122
+
119
123
  return (
120
124
  <div className="space-y-6">
121
125
  {/* Stats row */}
@@ -159,64 +163,82 @@ export default function CostHeatmap({ data }: PluginProps) {
159
163
  <CardContent>
160
164
  <div className="overflow-x-auto">
161
165
  {/* Month labels */}
162
- <div className="mb-1 flex" style={{ paddingLeft: '2rem' }}>
166
+ <div className="flex items-end" style={{ paddingLeft: 40, marginBottom: 6 }}>
163
167
  {monthLabels.map((m, i) => (
164
168
  <span
165
169
  key={i}
166
- className="text-xs text-muted-foreground"
170
+ className="text-xs font-medium text-muted-foreground"
167
171
  style={{
168
- position: 'relative',
169
- left: `${m.week * 14}px`,
170
- marginRight: i < monthLabels.length - 1 ? 0 : undefined,
172
+ position: 'absolute',
173
+ left: 40 + m.week * (cellSize + cellGap),
171
174
  }}
172
175
  >
173
176
  {m.label}
174
177
  </span>
175
178
  ))}
176
179
  </div>
180
+
177
181
  {/* Grid */}
178
- <div className="flex gap-0.5">
179
- {/* Weekday labels */}
180
- <div className="flex flex-col gap-0.5 pr-1">
181
- {WEEKDAYS.map((d, i) => (
182
- <span
183
- key={d}
184
- className="flex h-3 w-6 items-center text-[10px] text-muted-foreground"
185
- >
186
- {i % 2 === 1 ? d : ''}
187
- </span>
182
+ <div className="relative" style={{ paddingTop: 20 }}>
183
+ <div className="flex" style={{ gap: cellGap }}>
184
+ {/* Weekday labels */}
185
+ <div className="flex flex-col" style={{ gap: cellGap, width: 36 }}>
186
+ {WEEKDAYS.map((d, i) => (
187
+ <span
188
+ key={d}
189
+ className="flex items-center text-xs text-muted-foreground"
190
+ style={{ height: cellSize }}
191
+ >
192
+ {i % 2 === 1 ? d : ''}
193
+ </span>
194
+ ))}
195
+ </div>
196
+ {/* Weeks */}
197
+ {Array.from({ length: weeks }, (_, weekIdx) => (
198
+ <div key={weekIdx} className="flex flex-col" style={{ gap: cellGap }}>
199
+ {Array.from({ length: 7 }, (_, dayIdx) => {
200
+ const cell = grid[dayIdx]?.[weekIdx]
201
+ if (!cell) {
202
+ return (
203
+ <div
204
+ key={dayIdx}
205
+ style={{ width: cellSize, height: cellSize }}
206
+ className="rounded-sm"
207
+ />
208
+ )
209
+ }
210
+ return (
211
+ <Tooltip key={dayIdx}>
212
+ <TooltipTrigger
213
+ render={
214
+ <div
215
+ className="rounded-sm transition-all hover:ring-2 hover:ring-foreground/30 hover:scale-110 cursor-default"
216
+ style={{ width: cellSize, height: cellSize, ...intensityStyle(cell.ratio) }}
217
+ />
218
+ }
219
+ />
220
+ <TooltipContent side="top" className="text-center">
221
+ <p className="text-xs font-medium">{cell.date}</p>
222
+ <p className="text-sm font-bold">{formatCost(cell.cost)}</p>
223
+ </TooltipContent>
224
+ </Tooltip>
225
+ )
226
+ })}
227
+ </div>
188
228
  ))}
189
229
  </div>
190
- {/* Weeks */}
191
- {Array.from({ length: weeks }, (_, weekIdx) => (
192
- <div key={weekIdx} className="flex flex-col gap-0.5">
193
- {Array.from({ length: 7 }, (_, dayIdx) => {
194
- const cell = grid[dayIdx]?.[weekIdx]
195
- if (!cell) {
196
- return <div key={dayIdx} className="h-3 w-3 rounded-sm" />
197
- }
198
- return (
199
- <Tooltip key={dayIdx}>
200
- <TooltipTrigger render={<div className={`h-3 w-3 rounded-sm ${intensityClass(cell.ratio)} transition-colors hover:ring-1 hover:ring-foreground`} />}>
201
- </TooltipTrigger>
202
- <TooltipContent>
203
- <p className="text-xs font-medium">{cell.date}</p>
204
- <p className="text-xs text-muted-foreground">{formatCost(cell.cost)}</p>
205
- </TooltipContent>
206
- </Tooltip>
207
- )
208
- })}
209
- </div>
210
- ))}
211
230
  </div>
231
+
212
232
  {/* Legend */}
213
- <div className="mt-3 flex items-center gap-1 text-xs text-muted-foreground">
233
+ <div className="mt-4 flex items-center gap-1.5 text-xs text-muted-foreground">
214
234
  <span>Less</span>
215
- <div className="h-3 w-3 rounded-sm bg-muted" />
216
- <div className="h-3 w-3 rounded-sm bg-chart-3/30" />
217
- <div className="h-3 w-3 rounded-sm bg-chart-3/50" />
218
- <div className="h-3 w-3 rounded-sm bg-chart-3/70" />
219
- <div className="h-3 w-3 rounded-sm bg-chart-3" />
235
+ {[0, 0.2, 0.4, 0.6, 0.8, 1].map((ratio) => (
236
+ <div
237
+ key={ratio}
238
+ className="rounded-sm"
239
+ style={{ width: cellSize - 4, height: cellSize - 4, ...intensityStyle(ratio) }}
240
+ />
241
+ ))}
220
242
  <span>More</span>
221
243
  </div>
222
244
  </div>
@@ -27,6 +27,12 @@ model Session {
27
27
  model String
28
28
  toolCallsTotal Int
29
29
  toolCallsJson String // JSON string of Record<string, number>
30
+ skillCallsJson String @default("{}") // JSON string of Record<string, number>
31
+ messageTimestamps String @default("[]") // JSON array of ISO timestamp strings
32
+ apiErrors Int @default(0)
33
+ rateLimitErrors Int @default(0)
34
+ userInterruptions Int @default(0)
35
+ permissionModesJson String @default("{}") // JSON: {default:N, acceptEdits:N, bypassPermissions:N, plan:N}
30
36
  createdAt DateTime @default(now())
31
37
 
32
38
  @@index([project])
@@ -55,3 +61,14 @@ model SyncLog {
55
61
  sessionsAdded Int
56
62
  sessionsSkipped Int
57
63
  }
64
+
65
+ model Report {
66
+ id String @id @default(cuid())
67
+ title String
68
+ generatedAt DateTime @default(now())
69
+ contentJson String
70
+ sessionCount Int
71
+ createdAt DateTime @default(now())
72
+
73
+ @@index([generatedAt])
74
+ }
@@ -1,26 +0,0 @@
1
- {
2
- "permissions": {
3
- "allow": [
4
- "Bash(python3 -m json.tool)",
5
- "Bash(python3:*)",
6
- "Bash(head -5 /Users/harrywang/sandbox/bizpub-cc/logs/*.jsonl)",
7
- "Bash(wc -l /Users/harrywang/sandbox/bizpub-cc/logs/*.jsonl)",
8
- "Read(//private/tmp/**)",
9
- "Bash(rm -rf agentfit-scaffold)",
10
- "Bash(npx create-next-app@latest agentfit-scaffold --typescript --tailwind --eslint --app --src-dir --no-import-alias --use-npm --no-turbopack)",
11
- "Bash(curl -s https://api.github.com/repos/ryoppippi/ccusage/contents)",
12
- "Bash(curl -s \"https://api.github.com/search/code?q=repo:ryoppippi/ccusage+filename:model\")",
13
- "Bash(curl:*)",
14
- "WebFetch(domain:raw.githubusercontent.com)",
15
- "Bash(lsof -ti:3000,3001)",
16
- "Bash(npm install:*)",
17
- "Bash(npx prisma:*)",
18
- "Bash(ls -la /Users/harrywang/sandbox/agentfit/*.md)",
19
- "WebSearch",
20
- "WebFetch(domain:betelgeuse.work)",
21
- "WebFetch(domain:github.com)",
22
- "WebFetch(domain:ui.shadcn.com)",
23
- "Bash(npx shadcn@latest init -p blue --force)"
24
- ]
25
- }
26
- }
package/CONTRIBUTING.md DELETED
@@ -1,209 +0,0 @@
1
- # Contributing a Community Plugin
2
-
3
- AgentFit has a plugin system that lets anyone contribute new analysis views. Your plugin appears in the **Community** sidebar section and receives the same usage data as built-in pages.
4
-
5
- ## Quick Start
6
-
7
- ```bash
8
- # 1. Fork & clone
9
- git clone https://github.com/<you>/agentfit && cd agentfit
10
- npm install
11
-
12
- # 2. Scaffold your plugin
13
- mkdir -p plugins/my-analysis
14
-
15
- # 3. Create the two required files (see below)
16
- # 4. Register it in plugins/index.ts
17
- # 5. Run dev server & tests
18
- npm run dev
19
- npm test
20
- ```
21
-
22
- ## Plugin Structure
23
-
24
- Every plugin lives in its own folder under `plugins/` and has exactly **two files** plus a test:
25
-
26
- ```
27
- plugins/
28
- my-analysis/
29
- manifest.ts # metadata (name, slug, icon, etc.)
30
- component.tsx # React component that renders the analysis
31
- component.test.tsx # tests (required for PR acceptance)
32
- ```
33
-
34
- ### manifest.ts
35
-
36
- ```ts
37
- import type { PluginManifest } from '@/lib/plugins'
38
-
39
- const manifest: PluginManifest = {
40
- slug: 'my-analysis', // URL-safe, lowercase, hyphens only
41
- name: 'My Analysis', // shown in sidebar
42
- description: 'One-line summary of what this shows',
43
- author: 'your-github-handle',
44
- icon: 'ChartArea', // any lucide-react icon name
45
- version: '1.0.0',
46
- tags: ['cost', 'productivity'], // optional, for discoverability
47
- }
48
-
49
- export default manifest
50
- ```
51
-
52
- **Slug rules:** lowercase letters, numbers, hyphens only (e.g. `my-analysis`). Must be unique across all plugins.
53
-
54
- ### component.tsx
55
-
56
- ```tsx
57
- 'use client'
58
-
59
- import { useMemo } from 'react'
60
- import { Card, CardContent, CardHeader, CardTitle } from '@/components/ui/card'
61
- import type { PluginProps } from '@/lib/plugins'
62
-
63
- export default function MyAnalysis({ data }: PluginProps) {
64
- const stats = useMemo(() => {
65
- // Compute your analysis from data.sessions, data.daily, etc.
66
- return { total: data.sessions.length }
67
- }, [data])
68
-
69
- return (
70
- <Card>
71
- <CardHeader>
72
- <CardTitle>My Analysis</CardTitle>
73
- </CardHeader>
74
- <CardContent>
75
- <p>Found {stats.total} sessions</p>
76
- </CardContent>
77
- </Card>
78
- )
79
- }
80
- ```
81
-
82
- ### Registration
83
-
84
- Open `plugins/index.ts` and add your import + registration call:
85
-
86
- ```ts
87
- // ─── Community plugins ──────────────────────────────────────────────
88
- import myAnalysisManifest from './my-analysis/manifest'
89
- import MyAnalysis from './my-analysis/component'
90
- registerPlugin(myAnalysisManifest, MyAnalysis)
91
- ```
92
-
93
- That's it. Your plugin now appears in the sidebar at `/community/my-analysis`.
94
-
95
- ## Data Available to Plugins
96
-
97
- Your component receives `PluginProps`:
98
-
99
- ```ts
100
- interface PluginProps {
101
- data: UsageData
102
- }
103
- ```
104
-
105
- `UsageData` contains:
106
-
107
- | Field | Type | Description |
108
- |-------|------|-------------|
109
- | `overview` | `OverviewStats` | Aggregated metrics (totals for sessions, tokens, cost, etc.) |
110
- | `sessions` | `SessionSummary[]` | Every session with tokens, cost, duration, tool calls |
111
- | `projects` | `ProjectSummary[]` | Per-project aggregations |
112
- | `daily` | `DailyUsage[]` | Per-day aggregations |
113
- | `toolUsage` | `Record<string, number>` | Tool invocation counts |
114
-
115
- All data is already filtered by the user's selected time range and agent type. You don't need to filter again.
116
-
117
- See `lib/parse-logs.ts` for the full type definitions.
118
-
119
- ## UI Guidelines
120
-
121
- - Use **shadcn UI** components from `components/ui/` (Card, Badge, Table, etc.)
122
- - Use **Recharts** (already installed) for charts, wrapped in `ChartContainer`
123
- - Use `lib/format.ts` helpers: `formatCost()`, `formatTokens()`, `formatDuration()`
124
- - Use the existing CSS chart color variables: `--chart-1` through `--chart-10`
125
- - Mark your component as `'use client'` at the top
126
- - Wrap computed values in `useMemo` for performance
127
-
128
- ## Writing Tests
129
-
130
- Every plugin must include tests. Use the provided test helpers:
131
-
132
- ```tsx
133
- // plugins/my-analysis/component.test.tsx
134
- import { describe, it, expect } from 'vitest'
135
- import { screen } from '@testing-library/react'
136
- import { renderPlugin } from '@/tests/plugin-helpers'
137
- import { validateManifest } from '@/lib/plugins'
138
- import MyAnalysis from './component'
139
- import manifest from './manifest'
140
-
141
- describe('my-analysis plugin', () => {
142
- it('manifest passes validation', () => {
143
- expect(validateManifest(manifest)).toEqual([])
144
- })
145
-
146
- it('renders without crashing', () => {
147
- const { container } = renderPlugin(MyAnalysis)
148
- expect(container).toBeTruthy()
149
- })
150
-
151
- it('shows expected content', () => {
152
- renderPlugin(MyAnalysis)
153
- expect(screen.getByText('My Analysis')).toBeInTheDocument()
154
- })
155
-
156
- it('handles empty data', () => {
157
- renderPlugin(MyAnalysis, { sessions: [], daily: [], projects: [] })
158
- // Should not crash — show an empty state instead
159
- })
160
- })
161
- ```
162
-
163
- ### Test utilities
164
-
165
- | Helper | Import | Description |
166
- |--------|--------|-------------|
167
- | `renderPlugin(Component, dataOverrides?)` | `@/tests/plugin-helpers` | Renders your plugin with mock `UsageData` |
168
- | `createMockData(overrides?)` | `@/tests/fixtures` | Creates a realistic `UsageData` object |
169
- | `createMockSession(overrides?)` | `@/tests/fixtures` | Creates a single `SessionSummary` |
170
- | `createMockDaily(overrides?)` | `@/tests/fixtures` | Creates a single `DailyUsage` |
171
- | `validateManifest(manifest)` | `@/lib/plugins` | Returns array of validation errors (empty = valid) |
172
-
173
- ### Running tests
174
-
175
- ```bash
176
- npm test # run all tests once
177
- npm run test:watch # watch mode during development
178
- ```
179
-
180
- ## PR Checklist
181
-
182
- Before submitting your pull request:
183
-
184
- - [ ] Plugin folder is in `plugins/<your-slug>/`
185
- - [ ] `manifest.ts` passes `validateManifest()` with zero errors
186
- - [ ] `component.tsx` exports a default React component accepting `PluginProps`
187
- - [ ] Plugin is registered in `plugins/index.ts`
188
- - [ ] Tests exist in `component.test.tsx` and all pass (`npm test`)
189
- - [ ] Component handles empty data gracefully (no crashes)
190
- - [ ] No new dependencies added (use existing recharts, shadcn, lucide-react)
191
- - [ ] `npm run typecheck` passes
192
- - [ ] `npm run lint` passes
193
-
194
- ## Advanced: Custom Data Sources
195
-
196
- If your plugin fetches its own data (e.g., from a custom API route), set `customDataSource: true` in your manifest. This hides the time-range filter when your plugin is active.
197
-
198
- ```ts
199
- const manifest: PluginManifest = {
200
- // ...
201
- customDataSource: true,
202
- }
203
- ```
204
-
205
- You can still use the `data` prop as a fallback, but you're free to `fetch()` additional data in a `useEffect`.
206
-
207
- ## Example Plugin
208
-
209
- See `plugins/cost-heatmap/` for a complete working example with manifest, component, and tests.
@@ -1,41 +0,0 @@
1
- -- CreateTable
2
- CREATE TABLE "Session" (
3
- "id" TEXT NOT NULL PRIMARY KEY,
4
- "sessionId" TEXT NOT NULL,
5
- "project" TEXT NOT NULL,
6
- "projectPath" TEXT NOT NULL,
7
- "startTime" DATETIME NOT NULL,
8
- "endTime" DATETIME NOT NULL,
9
- "durationMinutes" REAL NOT NULL,
10
- "userMessages" INTEGER NOT NULL,
11
- "assistantMessages" INTEGER NOT NULL,
12
- "totalMessages" INTEGER NOT NULL,
13
- "inputTokens" INTEGER NOT NULL,
14
- "outputTokens" INTEGER NOT NULL,
15
- "cacheCreationTokens" INTEGER NOT NULL,
16
- "cacheReadTokens" INTEGER NOT NULL,
17
- "totalTokens" INTEGER NOT NULL,
18
- "costUSD" REAL NOT NULL,
19
- "model" TEXT NOT NULL,
20
- "toolCallsTotal" INTEGER NOT NULL,
21
- "toolCallsJson" TEXT NOT NULL,
22
- "createdAt" DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP
23
- );
24
-
25
- -- CreateTable
26
- CREATE TABLE "SyncLog" (
27
- "id" TEXT NOT NULL PRIMARY KEY,
28
- "syncedAt" DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP,
29
- "filesProcessed" INTEGER NOT NULL,
30
- "sessionsAdded" INTEGER NOT NULL,
31
- "sessionsSkipped" INTEGER NOT NULL
32
- );
33
-
34
- -- CreateIndex
35
- CREATE UNIQUE INDEX "Session_sessionId_key" ON "Session"("sessionId");
36
-
37
- -- CreateIndex
38
- CREATE INDEX "Session_project_idx" ON "Session"("project");
39
-
40
- -- CreateIndex
41
- CREATE INDEX "Session_startTime_idx" ON "Session"("startTime");
@@ -1,18 +0,0 @@
1
- -- CreateTable
2
- CREATE TABLE "Image" (
3
- "id" TEXT NOT NULL PRIMARY KEY,
4
- "sessionId" TEXT NOT NULL,
5
- "messageId" TEXT NOT NULL,
6
- "filename" TEXT NOT NULL,
7
- "mediaType" TEXT NOT NULL,
8
- "sizeBytes" INTEGER NOT NULL,
9
- "timestamp" DATETIME NOT NULL,
10
- "role" TEXT NOT NULL,
11
- "createdAt" DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP
12
- );
13
-
14
- -- CreateIndex
15
- CREATE INDEX "Image_sessionId_idx" ON "Image"("sessionId");
16
-
17
- -- CreateIndex
18
- CREATE UNIQUE INDEX "Image_sessionId_messageId_filename_key" ON "Image"("sessionId", "messageId", "filename");
@@ -1,3 +0,0 @@
1
- # Please do not edit this file manually
2
- # It should be added in your version-control system (e.g., Git)
3
- provider = "sqlite"
package/prisma.config.ts DELETED
@@ -1,14 +0,0 @@
1
- // This file was generated by Prisma, and assumes you have installed the following:
2
- // npm install --save-dev prisma dotenv
3
- import "dotenv/config";
4
- import { defineConfig } from "prisma/config";
5
-
6
- export default defineConfig({
7
- schema: "prisma/schema.prisma",
8
- migrations: {
9
- path: "prisma/migrations",
10
- },
11
- datasource: {
12
- url: process.env["DATABASE_URL"],
13
- },
14
- });