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.
Files changed (107) hide show
  1. package/.claude/settings.local.json +26 -0
  2. package/.prettierignore +7 -0
  3. package/.prettierrc +11 -0
  4. package/CONTRIBUTING.md +209 -0
  5. package/LICENSE +21 -0
  6. package/README.md +109 -0
  7. package/app/(dashboard)/coach/page.tsx +11 -0
  8. package/app/(dashboard)/commands/page.tsx +7 -0
  9. package/app/(dashboard)/community/[slug]/page.tsx +23 -0
  10. package/app/(dashboard)/community/page.tsx +71 -0
  11. package/app/(dashboard)/daily/page.tsx +19 -0
  12. package/app/(dashboard)/images/page.tsx +5 -0
  13. package/app/(dashboard)/layout.tsx +12 -0
  14. package/app/(dashboard)/page.tsx +23 -0
  15. package/app/(dashboard)/personality/page.tsx +11 -0
  16. package/app/(dashboard)/projects/page.tsx +11 -0
  17. package/app/(dashboard)/sessions/page.tsx +11 -0
  18. package/app/(dashboard)/tokens/page.tsx +11 -0
  19. package/app/(dashboard)/tools/page.tsx +11 -0
  20. package/app/api/check/route.ts +13 -0
  21. package/app/api/commands/route.ts +16 -0
  22. package/app/api/images/[...path]/route.ts +33 -0
  23. package/app/api/images-analysis/route.ts +177 -0
  24. package/app/api/sync/route.ts +14 -0
  25. package/app/api/usage/route.ts +117 -0
  26. package/app/favicon.ico +0 -0
  27. package/app/globals.css +144 -0
  28. package/app/icon.svg +3 -0
  29. package/app/layout.tsx +35 -0
  30. package/bin/agentfit.mjs +69 -0
  31. package/components/.gitkeep +0 -0
  32. package/components/agent-coach.tsx +248 -0
  33. package/components/app-sidebar.tsx +161 -0
  34. package/components/command-usage.tsx +294 -0
  35. package/components/daily-chart.tsx +118 -0
  36. package/components/daily-table.tsx +115 -0
  37. package/components/dashboard-shell.tsx +149 -0
  38. package/components/data-provider.tsx +213 -0
  39. package/components/fitness-score.tsx +95 -0
  40. package/components/overview-cards.tsx +198 -0
  41. package/components/pagination-controls.tsx +104 -0
  42. package/components/personality-fit.tsx +446 -0
  43. package/components/projects-table.tsx +70 -0
  44. package/components/screenshots-analysis.tsx +359 -0
  45. package/components/sessions-table.tsx +97 -0
  46. package/components/theme-provider.tsx +71 -0
  47. package/components/token-breakdown.tsx +179 -0
  48. package/components/tool-usage-chart.tsx +63 -0
  49. package/components/ui/badge.tsx +52 -0
  50. package/components/ui/button.tsx +60 -0
  51. package/components/ui/card.tsx +103 -0
  52. package/components/ui/chart.tsx +373 -0
  53. package/components/ui/dialog.tsx +160 -0
  54. package/components/ui/input.tsx +20 -0
  55. package/components/ui/scroll-area.tsx +55 -0
  56. package/components/ui/select.tsx +201 -0
  57. package/components/ui/separator.tsx +25 -0
  58. package/components/ui/sheet.tsx +138 -0
  59. package/components/ui/sidebar.tsx +723 -0
  60. package/components/ui/skeleton.tsx +13 -0
  61. package/components/ui/table.tsx +116 -0
  62. package/components/ui/tabs.tsx +82 -0
  63. package/components/ui/tooltip.tsx +66 -0
  64. package/components.json +25 -0
  65. package/generated/prisma/browser.ts +34 -0
  66. package/generated/prisma/client.ts +58 -0
  67. package/generated/prisma/commonInputTypes.ts +237 -0
  68. package/generated/prisma/enums.ts +15 -0
  69. package/generated/prisma/internal/class.ts +224 -0
  70. package/generated/prisma/internal/prismaNamespace.ts +920 -0
  71. package/generated/prisma/internal/prismaNamespaceBrowser.ts +130 -0
  72. package/generated/prisma/models/Image.ts +1310 -0
  73. package/generated/prisma/models/Session.ts +1695 -0
  74. package/generated/prisma/models/SyncLog.ts +1203 -0
  75. package/generated/prisma/models.ts +14 -0
  76. package/hooks/.gitkeep +0 -0
  77. package/hooks/use-mobile.ts +19 -0
  78. package/hooks/use-pagination.ts +60 -0
  79. package/lib/.gitkeep +0 -0
  80. package/lib/coach.ts +425 -0
  81. package/lib/commands.ts +239 -0
  82. package/lib/db.ts +15 -0
  83. package/lib/format.ts +26 -0
  84. package/lib/parse-codex.ts +201 -0
  85. package/lib/parse-logs.ts +369 -0
  86. package/lib/personality.ts +481 -0
  87. package/lib/plugins.ts +107 -0
  88. package/lib/pricing.ts +112 -0
  89. package/lib/queries-codex.ts +130 -0
  90. package/lib/queries.ts +154 -0
  91. package/lib/resolve-icon.ts +12 -0
  92. package/lib/sync.ts +335 -0
  93. package/lib/utils.ts +6 -0
  94. package/next.config.mjs +4 -0
  95. package/package.json +73 -0
  96. package/plugins/cost-heatmap/component.test.tsx +52 -0
  97. package/plugins/cost-heatmap/component.tsx +227 -0
  98. package/plugins/cost-heatmap/manifest.ts +13 -0
  99. package/plugins/index.ts +18 -0
  100. package/prisma/migrations/20260328152517_init/migration.sql +41 -0
  101. package/prisma/migrations/20260328153801_add_image_model/migration.sql +18 -0
  102. package/prisma/migrations/migration_lock.toml +3 -0
  103. package/prisma/schema.prisma +57 -0
  104. package/prisma.config.ts +14 -0
  105. package/public/.gitkeep +0 -0
  106. package/public/logo.svg +3 -0
  107. package/setup.sh +73 -0
package/lib/pricing.ts ADDED
@@ -0,0 +1,112 @@
1
+ // Fetches pricing from LiteLLM's model pricing database (same source as ccusage)
2
+
3
+ const LITELLM_PRICING_URL =
4
+ 'https://raw.githubusercontent.com/BerriAI/litellm/main/model_prices_and_context_window.json'
5
+
6
+ export interface ModelPricing {
7
+ input_cost_per_token?: number
8
+ output_cost_per_token?: number
9
+ cache_creation_input_token_cost?: number
10
+ cache_read_input_token_cost?: number
11
+ }
12
+
13
+ let pricingCache: Record<string, ModelPricing> | null = null
14
+
15
+ export async function loadPricing(): Promise<Record<string, ModelPricing>> {
16
+ if (pricingCache) return pricingCache
17
+
18
+ try {
19
+ const res = await fetch(LITELLM_PRICING_URL, { next: { revalidate: 86400 } })
20
+ const data = await res.json()
21
+ // Filter to Claude/Anthropic models only
22
+ const filtered: Record<string, ModelPricing> = {}
23
+ for (const [key, value] of Object.entries(data)) {
24
+ if (
25
+ key.includes('claude') ||
26
+ key.includes('anthropic')
27
+ ) {
28
+ const v = value as Record<string, unknown>
29
+ filtered[key] = {
30
+ input_cost_per_token: v.input_cost_per_token as number | undefined,
31
+ output_cost_per_token: v.output_cost_per_token as number | undefined,
32
+ cache_creation_input_token_cost: v.cache_creation_input_token_cost as number | undefined,
33
+ cache_read_input_token_cost: v.cache_read_input_token_cost as number | undefined,
34
+ }
35
+ }
36
+ }
37
+ pricingCache = filtered
38
+ return filtered
39
+ } catch {
40
+ // Fallback pricing if fetch fails
41
+ return getFallbackPricing()
42
+ }
43
+ }
44
+
45
+ function getFallbackPricing(): Record<string, ModelPricing> {
46
+ return {
47
+ 'claude-opus-4-6': {
48
+ input_cost_per_token: 15e-6,
49
+ output_cost_per_token: 75e-6,
50
+ cache_creation_input_token_cost: 18.75e-6,
51
+ cache_read_input_token_cost: 1.5e-6,
52
+ },
53
+ 'claude-sonnet-4-6': {
54
+ input_cost_per_token: 3e-6,
55
+ output_cost_per_token: 15e-6,
56
+ cache_creation_input_token_cost: 3.75e-6,
57
+ cache_read_input_token_cost: 0.3e-6,
58
+ },
59
+ 'claude-haiku-4-5-20251001': {
60
+ input_cost_per_token: 0.8e-6,
61
+ output_cost_per_token: 4e-6,
62
+ cache_creation_input_token_cost: 1e-6,
63
+ cache_read_input_token_cost: 0.08e-6,
64
+ },
65
+ }
66
+ }
67
+
68
+ export function findPricing(
69
+ model: string,
70
+ allPricing: Record<string, ModelPricing>
71
+ ): ModelPricing {
72
+ // Try exact match
73
+ if (allPricing[model]) return allPricing[model]
74
+
75
+ // Try with anthropic/ prefix
76
+ if (allPricing[`anthropic/${model}`]) return allPricing[`anthropic/${model}`]
77
+
78
+ // Try partial match
79
+ for (const [key, pricing] of Object.entries(allPricing)) {
80
+ const normalizedKey = key.replace('anthropic/', '').replace('anthropic.', '')
81
+ if (normalizedKey === model || normalizedKey.startsWith(model) || model.startsWith(normalizedKey)) {
82
+ return pricing
83
+ }
84
+ }
85
+
86
+ // Default to Sonnet pricing
87
+ return allPricing['claude-sonnet-4-6'] || allPricing['anthropic/claude-sonnet-4-6'] || {
88
+ input_cost_per_token: 3e-6,
89
+ output_cost_per_token: 15e-6,
90
+ cache_creation_input_token_cost: 3.75e-6,
91
+ cache_read_input_token_cost: 0.3e-6,
92
+ }
93
+ }
94
+
95
+ export function calculateCost(
96
+ model: string,
97
+ usage: {
98
+ input_tokens?: number
99
+ output_tokens?: number
100
+ cache_creation_input_tokens?: number
101
+ cache_read_input_tokens?: number
102
+ },
103
+ allPricing: Record<string, ModelPricing>
104
+ ): number {
105
+ const pricing = findPricing(model, allPricing)
106
+ return (
107
+ (usage.input_tokens || 0) * (pricing.input_cost_per_token || 0) +
108
+ (usage.output_tokens || 0) * (pricing.output_cost_per_token || 0) +
109
+ (usage.cache_creation_input_tokens || 0) * (pricing.cache_creation_input_token_cost || 0) +
110
+ (usage.cache_read_input_tokens || 0) * (pricing.cache_read_input_token_cost || 0)
111
+ )
112
+ }
@@ -0,0 +1,130 @@
1
+ // ─── Codex Usage Data Queries ────────────────────────────────────────
2
+ // Produces UsageData from Codex session logs (no database, parsed live)
3
+
4
+ import { parseAllCodexSessions } from './parse-codex'
5
+ import type {
6
+ UsageData,
7
+ ProjectSummary,
8
+ DailyUsage,
9
+ OverviewStats,
10
+ } from './parse-logs'
11
+
12
+ export function getCodexUsageData(): UsageData {
13
+ const sessions = parseAllCodexSessions()
14
+
15
+ if (sessions.length === 0) {
16
+ return emptyUsageData()
17
+ }
18
+
19
+ // Aggregate projects
20
+ const projectMap = new Map<string, ProjectSummary>()
21
+ const dailyMap = new Map<string, DailyUsage>()
22
+ const toolUsage: Record<string, number> = {}
23
+
24
+ for (const s of sessions) {
25
+ // Projects
26
+ if (!projectMap.has(s.project)) {
27
+ projectMap.set(s.project, {
28
+ name: s.project,
29
+ path: s.projectPath,
30
+ sessions: 0,
31
+ totalMessages: 0,
32
+ totalTokens: 0,
33
+ totalCost: 0,
34
+ totalDurationMinutes: 0,
35
+ toolCalls: {},
36
+ })
37
+ }
38
+ const proj = projectMap.get(s.project)!
39
+ proj.sessions++
40
+ proj.totalMessages += s.totalMessages
41
+ proj.totalTokens += s.totalTokens
42
+ proj.totalCost += s.costUSD
43
+ proj.totalDurationMinutes += s.durationMinutes
44
+ for (const [tool, count] of Object.entries(s.toolCalls)) {
45
+ proj.toolCalls[tool] = (proj.toolCalls[tool] || 0) + count
46
+ }
47
+
48
+ // Daily
49
+ const date = s.startTime.slice(0, 10)
50
+ if (!dailyMap.has(date)) {
51
+ dailyMap.set(date, {
52
+ date,
53
+ sessions: 0,
54
+ messages: 0,
55
+ inputTokens: 0,
56
+ outputTokens: 0,
57
+ cacheCreationTokens: 0,
58
+ cacheReadTokens: 0,
59
+ totalTokens: 0,
60
+ costUSD: 0,
61
+ toolCalls: 0,
62
+ })
63
+ }
64
+ const daily = dailyMap.get(date)!
65
+ daily.sessions++
66
+ daily.messages += s.totalMessages
67
+ daily.inputTokens += s.inputTokens
68
+ daily.outputTokens += s.outputTokens
69
+ daily.totalTokens += s.totalTokens
70
+ daily.costUSD += s.costUSD
71
+ daily.toolCalls += s.toolCallsTotal
72
+
73
+ // Tool usage
74
+ for (const [tool, count] of Object.entries(s.toolCalls)) {
75
+ toolUsage[tool] = (toolUsage[tool] || 0) + count
76
+ }
77
+ }
78
+
79
+ const models: Record<string, number> = {}
80
+ for (const s of sessions) {
81
+ models[s.model] = (models[s.model] || 0) + 1
82
+ }
83
+
84
+ const projects = Array.from(projectMap.values()).sort((a, b) => b.totalCost - a.totalCost)
85
+ const daily = Array.from(dailyMap.values()).sort((a, b) => a.date.localeCompare(b.date))
86
+
87
+ const overview: OverviewStats = {
88
+ totalSessions: sessions.length,
89
+ totalProjects: projects.length,
90
+ totalMessages: sessions.reduce((a, s) => a + s.totalMessages, 0),
91
+ totalUserMessages: sessions.reduce((a, s) => a + s.userMessages, 0),
92
+ totalAssistantMessages: sessions.reduce((a, s) => a + s.assistantMessages, 0),
93
+ totalInputTokens: sessions.reduce((a, s) => a + s.inputTokens, 0),
94
+ totalOutputTokens: sessions.reduce((a, s) => a + s.outputTokens, 0),
95
+ totalCacheCreationTokens: 0,
96
+ totalCacheReadTokens: 0,
97
+ totalTokens: sessions.reduce((a, s) => a + s.totalTokens, 0),
98
+ totalCostUSD: sessions.reduce((a, s) => a + s.costUSD, 0),
99
+ totalDurationMinutes: sessions.reduce((a, s) => a + s.durationMinutes, 0),
100
+ totalToolCalls: sessions.reduce((a, s) => a + s.toolCallsTotal, 0),
101
+ models,
102
+ }
103
+
104
+ return { overview, sessions, projects, daily, toolUsage }
105
+ }
106
+
107
+ function emptyUsageData(): UsageData {
108
+ return {
109
+ overview: {
110
+ totalSessions: 0,
111
+ totalProjects: 0,
112
+ totalMessages: 0,
113
+ totalUserMessages: 0,
114
+ totalAssistantMessages: 0,
115
+ totalInputTokens: 0,
116
+ totalOutputTokens: 0,
117
+ totalCacheCreationTokens: 0,
118
+ totalCacheReadTokens: 0,
119
+ totalTokens: 0,
120
+ totalCostUSD: 0,
121
+ totalDurationMinutes: 0,
122
+ totalToolCalls: 0,
123
+ models: {},
124
+ },
125
+ sessions: [],
126
+ projects: [],
127
+ daily: [],
128
+ toolUsage: {},
129
+ }
130
+ }
package/lib/queries.ts ADDED
@@ -0,0 +1,154 @@
1
+ import { prisma } from './db'
2
+ import type {
3
+ UsageData,
4
+ SessionSummary,
5
+ ProjectSummary,
6
+ DailyUsage,
7
+ OverviewStats,
8
+ } from './parse-logs'
9
+
10
+ export async function getUsageData(): Promise<UsageData> {
11
+ const dbSessions = await prisma.session.findMany({
12
+ orderBy: { startTime: 'desc' },
13
+ })
14
+
15
+ if (dbSessions.length === 0) {
16
+ return emptyUsageData()
17
+ }
18
+
19
+ // Map DB rows to SessionSummary
20
+ const sessions: SessionSummary[] = dbSessions.map((s) => ({
21
+ sessionId: s.sessionId,
22
+ project: s.project,
23
+ projectPath: s.projectPath,
24
+ startTime: s.startTime.toISOString(),
25
+ endTime: s.endTime.toISOString(),
26
+ durationMinutes: s.durationMinutes,
27
+ userMessages: s.userMessages,
28
+ assistantMessages: s.assistantMessages,
29
+ totalMessages: s.totalMessages,
30
+ inputTokens: s.inputTokens,
31
+ outputTokens: s.outputTokens,
32
+ cacheCreationTokens: s.cacheCreationTokens,
33
+ cacheReadTokens: s.cacheReadTokens,
34
+ totalTokens: s.totalTokens,
35
+ costUSD: s.costUSD,
36
+ model: s.model,
37
+ toolCalls: JSON.parse(s.toolCallsJson) as Record<string, number>,
38
+ toolCallsTotal: s.toolCallsTotal,
39
+ }))
40
+
41
+ // Aggregate projects
42
+ const projectMap = new Map<string, ProjectSummary>()
43
+ const dailyMap = new Map<string, DailyUsage>()
44
+ const toolUsage: Record<string, number> = {}
45
+
46
+ for (const s of sessions) {
47
+ // Projects
48
+ if (!projectMap.has(s.project)) {
49
+ projectMap.set(s.project, {
50
+ name: s.project,
51
+ path: s.projectPath,
52
+ sessions: 0,
53
+ totalMessages: 0,
54
+ totalTokens: 0,
55
+ totalCost: 0,
56
+ totalDurationMinutes: 0,
57
+ toolCalls: {},
58
+ })
59
+ }
60
+ const proj = projectMap.get(s.project)!
61
+ proj.sessions++
62
+ proj.totalMessages += s.totalMessages
63
+ proj.totalTokens += s.totalTokens
64
+ proj.totalCost += s.costUSD
65
+ proj.totalDurationMinutes += s.durationMinutes
66
+ for (const [tool, count] of Object.entries(s.toolCalls)) {
67
+ proj.toolCalls[tool] = (proj.toolCalls[tool] || 0) + count
68
+ }
69
+
70
+ // Daily
71
+ const date = s.startTime.slice(0, 10)
72
+ if (!dailyMap.has(date)) {
73
+ dailyMap.set(date, {
74
+ date,
75
+ sessions: 0,
76
+ messages: 0,
77
+ inputTokens: 0,
78
+ outputTokens: 0,
79
+ cacheCreationTokens: 0,
80
+ cacheReadTokens: 0,
81
+ totalTokens: 0,
82
+ costUSD: 0,
83
+ toolCalls: 0,
84
+ })
85
+ }
86
+ const daily = dailyMap.get(date)!
87
+ daily.sessions++
88
+ daily.messages += s.totalMessages
89
+ daily.inputTokens += s.inputTokens
90
+ daily.outputTokens += s.outputTokens
91
+ daily.cacheCreationTokens += s.cacheCreationTokens
92
+ daily.cacheReadTokens += s.cacheReadTokens
93
+ daily.totalTokens += s.totalTokens
94
+ daily.costUSD += s.costUSD
95
+ daily.toolCalls += s.toolCallsTotal
96
+
97
+ // Tool usage
98
+ for (const [tool, count] of Object.entries(s.toolCalls)) {
99
+ toolUsage[tool] = (toolUsage[tool] || 0) + count
100
+ }
101
+ }
102
+
103
+ const dailyArr = Array.from(dailyMap.values()).sort((a, b) => a.date.localeCompare(b.date))
104
+ const projects = Array.from(projectMap.values()).sort((a, b) => b.totalCost - a.totalCost)
105
+
106
+ const models: Record<string, number> = {}
107
+ for (const s of sessions) {
108
+ models[s.model] = (models[s.model] || 0) + 1
109
+ }
110
+
111
+ const overview: OverviewStats = {
112
+ totalSessions: sessions.length,
113
+ totalProjects: projects.length,
114
+ totalMessages: sessions.reduce((a, s) => a + s.totalMessages, 0),
115
+ totalUserMessages: sessions.reduce((a, s) => a + s.userMessages, 0),
116
+ totalAssistantMessages: sessions.reduce((a, s) => a + s.assistantMessages, 0),
117
+ totalInputTokens: sessions.reduce((a, s) => a + s.inputTokens, 0),
118
+ totalOutputTokens: sessions.reduce((a, s) => a + s.outputTokens, 0),
119
+ totalCacheCreationTokens: sessions.reduce((a, s) => a + s.cacheCreationTokens, 0),
120
+ totalCacheReadTokens: sessions.reduce((a, s) => a + s.cacheReadTokens, 0),
121
+ totalTokens: sessions.reduce((a, s) => a + s.totalTokens, 0),
122
+ totalCostUSD: sessions.reduce((a, s) => a + s.costUSD, 0),
123
+ totalDurationMinutes: sessions.reduce((a, s) => a + s.durationMinutes, 0),
124
+ totalToolCalls: sessions.reduce((a, s) => a + s.toolCallsTotal, 0),
125
+ models,
126
+ }
127
+
128
+ return { overview, sessions, projects, daily: dailyArr, toolUsage }
129
+ }
130
+
131
+ function emptyUsageData(): UsageData {
132
+ return {
133
+ overview: {
134
+ totalSessions: 0,
135
+ totalProjects: 0,
136
+ totalMessages: 0,
137
+ totalUserMessages: 0,
138
+ totalAssistantMessages: 0,
139
+ totalInputTokens: 0,
140
+ totalOutputTokens: 0,
141
+ totalCacheCreationTokens: 0,
142
+ totalCacheReadTokens: 0,
143
+ totalTokens: 0,
144
+ totalCostUSD: 0,
145
+ totalDurationMinutes: 0,
146
+ totalToolCalls: 0,
147
+ models: {},
148
+ },
149
+ sessions: [],
150
+ projects: [],
151
+ daily: [],
152
+ toolUsage: {},
153
+ }
154
+ }
@@ -0,0 +1,12 @@
1
+ import * as icons from 'lucide-react'
2
+ import { Puzzle } from 'lucide-react'
3
+ import type { ComponentType } from 'react'
4
+
5
+ /**
6
+ * Resolve a lucide-react icon by name string.
7
+ * Falls back to the Puzzle icon if the name is not found.
8
+ */
9
+ export function resolveLucideIcon(name: string): ComponentType<{ className?: string }> {
10
+ const Icon = (icons as unknown as Record<string, ComponentType<{ className?: string }>>)[name]
11
+ return Icon || Puzzle
12
+ }