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
@@ -0,0 +1,239 @@
1
+ // ─── Slash Command Usage Analysis ────────────────────────────────────
2
+ // Parses ~/.claude/history.jsonl to extract slash command usage patterns
3
+ // and compares against the full set of built-in Claude Code commands.
4
+
5
+ import fs from 'fs'
6
+ import path from 'path'
7
+ import os from 'os'
8
+
9
+ // ─── Types ───────────────────────────────────────────────────────────
10
+
11
+ export interface CommandInfo {
12
+ command: string
13
+ description: string
14
+ category: string
15
+ aliases?: string[]
16
+ }
17
+
18
+ export interface CommandUsage {
19
+ command: string
20
+ description: string
21
+ category: string
22
+ count: number
23
+ used: boolean
24
+ aliases?: string[]
25
+ }
26
+
27
+ export interface CommandAnalysis {
28
+ totalCommands: number
29
+ usedCommands: number
30
+ unusedCommands: number
31
+ usagePercentage: number
32
+ totalInvocations: number
33
+ commands: CommandUsage[]
34
+ categories: Record<string, { total: number; used: number; invocations: number }>
35
+ customCommands: { command: string; count: number }[]
36
+ }
37
+
38
+ // ─── Built-in Commands Registry ──────────────────────────────────────
39
+
40
+ // Source: https://code.claude.com/docs/en/commands (official documentation)
41
+ const BUILTIN_COMMANDS: CommandInfo[] = [
42
+ // Core — conversation lifecycle
43
+ { command: '/help', description: 'Show help and available commands', category: 'Core' },
44
+ { command: '/clear', description: 'Clear conversation history and free up context', category: 'Core', aliases: ['/reset', '/new'] },
45
+ { command: '/compact', description: 'Compact conversation with optional focus instructions', category: 'Core' },
46
+ { command: '/rewind', description: 'Rewind conversation and/or code to a previous point', category: 'Core', aliases: ['/checkpoint'] },
47
+ { command: '/rename', description: 'Rename the current session', category: 'Core' },
48
+ { command: '/exit', description: 'Exit the CLI', category: 'Core', aliases: ['/quit'] },
49
+
50
+ // Session & History
51
+ { command: '/branch', description: 'Create a branch of the current conversation at this point', category: 'Session', aliases: ['/fork'] },
52
+ { command: '/resume', description: 'Resume a conversation by ID or name, or open session picker', category: 'Session', aliases: ['/continue'] },
53
+ { command: '/export', description: 'Export the current conversation as plain text', category: 'Session' },
54
+
55
+ // Configuration & Preferences
56
+ { command: '/config', description: 'Open Settings interface for theme, model, output style, and preferences', category: 'Configuration', aliases: ['/settings'] },
57
+ { command: '/status', description: 'Show version, model, account, and connectivity info', category: 'Configuration' },
58
+ { command: '/theme', description: 'Change color theme (light, dark, colorblind-accessible, ANSI)', category: 'Configuration' },
59
+ { command: '/color', description: 'Set prompt bar color for the current session', category: 'Configuration' },
60
+ { command: '/model', description: 'Select or change the AI model', category: 'Configuration' },
61
+ { command: '/effort', description: 'Set model effort level (low/medium/high/max/auto)', category: 'Configuration' },
62
+ { command: '/fast', description: 'Toggle fast mode on or off', category: 'Configuration' },
63
+ { command: '/vim', description: 'Toggle between Vim and Normal editing modes', category: 'Configuration' },
64
+ { command: '/voice', description: 'Toggle push-to-talk voice dictation', category: 'Configuration' },
65
+ { command: '/statusline', description: 'Configure Claude Code status line display', category: 'Configuration' },
66
+ { command: '/terminal-setup', description: 'Configure terminal keybindings (Shift+Enter, etc.)', category: 'Configuration' },
67
+
68
+ // Account & Billing
69
+ { command: '/login', description: 'Sign in to your Anthropic account', category: 'Account' },
70
+ { command: '/logout', description: 'Sign out from your Anthropic account', category: 'Account' },
71
+ { command: '/usage', description: 'Show plan usage limits and rate limit status', category: 'Account' },
72
+ { command: '/cost', description: 'Show token usage statistics for current session', category: 'Account' },
73
+ { command: '/upgrade', description: 'Open upgrade page to switch to a higher plan tier', category: 'Account' },
74
+ { command: '/passes', description: 'Share a free week of Claude Code with friends', category: 'Account' },
75
+ { command: '/privacy-settings', description: 'View and update privacy settings (Pro/Max only)', category: 'Account' },
76
+ { command: '/extra-usage', description: 'Configure extra usage when rate limits are hit', category: 'Account' },
77
+
78
+ // Development Tools
79
+ { command: '/init', description: 'Initialize project with a CLAUDE.md guide', category: 'Dev Tools' },
80
+ { command: '/doctor', description: 'Diagnose and verify your Claude Code installation', category: 'Dev Tools' },
81
+ { command: '/diff', description: 'Interactive diff viewer for uncommitted changes and per-turn diffs', category: 'Dev Tools' },
82
+ { command: '/context', description: 'Visualize current context usage as a colored grid', category: 'Dev Tools' },
83
+ { command: '/copy', description: 'Copy last assistant response to clipboard (interactive picker for code blocks)', category: 'Dev Tools' },
84
+ { command: '/plan', description: 'Enter plan mode directly from the prompt', category: 'Dev Tools' },
85
+ { command: '/btw', description: 'Ask a quick side question without adding to conversation', category: 'Dev Tools' },
86
+ { command: '/sandbox', description: 'Toggle sandbox mode on supported platforms', category: 'Dev Tools' },
87
+
88
+ // Code Review & Security
89
+ { command: '/security-review', description: 'Analyze pending changes for security vulnerabilities', category: 'Code Review' },
90
+ { command: '/review', description: 'Deprecated — install code-review plugin instead', category: 'Code Review' },
91
+ { command: '/pr-comments', description: 'Fetch and display comments from a GitHub pull request', category: 'Code Review' },
92
+
93
+ // Skills & Extensions
94
+ { command: '/skills', description: 'List available skills', category: 'Extensions' },
95
+ { command: '/agents', description: 'Manage agent configurations', category: 'Extensions' },
96
+ { command: '/plugin', description: 'Manage Claude Code plugins', category: 'Extensions' },
97
+ { command: '/reload-plugins', description: 'Reload all active plugins to apply pending changes', category: 'Extensions' },
98
+
99
+ // Memory, Permissions & Config
100
+ { command: '/memory', description: 'Edit CLAUDE.md memory files, enable/disable auto-memory', category: 'Memory' },
101
+ { command: '/add-dir', description: 'Add a new working directory to the current session', category: 'Memory' },
102
+ { command: '/keybindings', description: 'Open or create your keybindings configuration file', category: 'Memory' },
103
+ { command: '/permissions', description: 'View or update tool permissions', category: 'Memory', aliases: ['/allowed-tools'] },
104
+ { command: '/hooks', description: 'View hook configurations for tool events', category: 'Memory' },
105
+
106
+ // Integrations
107
+ { command: '/mcp', description: 'Manage MCP server connections and OAuth authentication', category: 'Integrations' },
108
+ { command: '/ide', description: 'Manage IDE integrations and show status', category: 'Integrations' },
109
+ { command: '/install-slack-app', description: 'Install the Claude Slack app via OAuth flow', category: 'Integrations' },
110
+ { command: '/install-github-app', description: 'Set up Claude GitHub Actions app for a repository', category: 'Integrations' },
111
+ { command: '/chrome', description: 'Configure Claude in Chrome settings', category: 'Integrations' },
112
+ { command: '/remote-control', description: 'Make this session available for remote control from claude.ai', category: 'Integrations', aliases: ['/rc'] },
113
+
114
+ // Utilities & Info
115
+ { command: '/feedback', description: 'Submit feedback about Claude Code', category: 'Utilities', aliases: ['/bug'] },
116
+ { command: '/tasks', description: 'List and manage background tasks', category: 'Utilities' },
117
+ { command: '/stats', description: 'Visualize daily usage, session history, streaks, and model preferences', category: 'Utilities' },
118
+ { command: '/insights', description: 'Generate a report analyzing your Claude Code sessions', category: 'Utilities' },
119
+ { command: '/release-notes', description: 'View the full changelog', category: 'Utilities' },
120
+ { command: '/mobile', description: 'Show QR code to download the Claude mobile app', category: 'Utilities', aliases: ['/ios', '/android'] },
121
+ { command: '/desktop', description: 'Continue the current session in Claude Code Desktop app', category: 'Utilities', aliases: ['/app'] },
122
+ { command: '/stickers', description: 'Order Claude Code stickers', category: 'Utilities' },
123
+
124
+ // Cloud & Remote
125
+ { command: '/schedule', description: 'Create, update, list, or run Cloud scheduled tasks', category: 'Cloud' },
126
+ { command: '/remote-env', description: 'Configure default remote environment for web sessions', category: 'Cloud' },
127
+ ]
128
+
129
+ // ─── History Parsing ─────────────────────────────────────────────────
130
+
131
+ interface HistoryEntry {
132
+ display: string
133
+ timestamp: number
134
+ project?: string
135
+ sessionId?: string
136
+ }
137
+
138
+ function parseHistory(): Map<string, number> {
139
+ const historyPath = path.join(os.homedir(), '.claude', 'history.jsonl')
140
+ const counts = new Map<string, number>()
141
+
142
+ if (!fs.existsSync(historyPath)) return counts
143
+
144
+ const content = fs.readFileSync(historyPath, 'utf-8')
145
+ const lines = content.trim().split('\n')
146
+
147
+ for (const line of lines) {
148
+ if (!line.trim()) continue
149
+ try {
150
+ const entry: HistoryEntry = JSON.parse(line)
151
+ const text = entry.display?.trim() || ''
152
+ const match = text.match(/^\/([a-zA-Z_-]+)/)
153
+ if (match) {
154
+ const cmd = '/' + match[1]
155
+ counts.set(cmd, (counts.get(cmd) || 0) + 1)
156
+ }
157
+ } catch {
158
+ continue
159
+ }
160
+ }
161
+
162
+ return counts
163
+ }
164
+
165
+ // ─── Main Analysis ───────────────────────────────────────────────────
166
+
167
+ export function analyzeCommands(): CommandAnalysis {
168
+ const usageCounts = parseHistory()
169
+
170
+ // Build alias-to-primary map
171
+ const aliasMap = new Map<string, string>()
172
+ for (const cmd of BUILTIN_COMMANDS) {
173
+ if (cmd.aliases) {
174
+ for (const alias of cmd.aliases) {
175
+ aliasMap.set(alias, cmd.command)
176
+ }
177
+ }
178
+ }
179
+
180
+ // Merge alias counts into primary commands
181
+ const mergedCounts = new Map<string, number>()
182
+ for (const [cmd, count] of usageCounts) {
183
+ const primary = aliasMap.get(cmd) || cmd
184
+ mergedCounts.set(primary, (mergedCounts.get(primary) || 0) + count)
185
+ }
186
+
187
+ // Build command usage list
188
+ const builtinSet = new Set(BUILTIN_COMMANDS.map(c => c.command))
189
+ const commands: CommandUsage[] = BUILTIN_COMMANDS.map(cmd => ({
190
+ command: cmd.command,
191
+ description: cmd.description,
192
+ category: cmd.category,
193
+ count: mergedCounts.get(cmd.command) || 0,
194
+ used: (mergedCounts.get(cmd.command) || 0) > 0,
195
+ aliases: cmd.aliases,
196
+ }))
197
+
198
+ // Sort: used commands first (by count desc), then unused (alphabetical)
199
+ commands.sort((a, b) => {
200
+ if (a.used && !b.used) return -1
201
+ if (!a.used && b.used) return 1
202
+ if (a.used && b.used) return b.count - a.count
203
+ return a.command.localeCompare(b.command)
204
+ })
205
+
206
+ // Identify custom (non-built-in) commands
207
+ const customCommands: { command: string; count: number }[] = []
208
+ for (const [cmd, count] of mergedCounts) {
209
+ if (!builtinSet.has(cmd) && !aliasMap.has(cmd)) {
210
+ customCommands.push({ command: cmd, count })
211
+ }
212
+ }
213
+ customCommands.sort((a, b) => b.count - a.count)
214
+
215
+ // Category stats
216
+ const categories: Record<string, { total: number; used: number; invocations: number }> = {}
217
+ for (const cmd of commands) {
218
+ if (!categories[cmd.category]) {
219
+ categories[cmd.category] = { total: 0, used: 0, invocations: 0 }
220
+ }
221
+ categories[cmd.category].total++
222
+ if (cmd.used) categories[cmd.category].used++
223
+ categories[cmd.category].invocations += cmd.count
224
+ }
225
+
226
+ const usedCommands = commands.filter(c => c.used).length
227
+ const totalInvocations = commands.reduce((sum, c) => sum + c.count, 0)
228
+
229
+ return {
230
+ totalCommands: BUILTIN_COMMANDS.length,
231
+ usedCommands,
232
+ unusedCommands: BUILTIN_COMMANDS.length - usedCommands,
233
+ usagePercentage: Math.round((usedCommands / BUILTIN_COMMANDS.length) * 100),
234
+ totalInvocations,
235
+ commands,
236
+ categories,
237
+ customCommands,
238
+ }
239
+ }
package/lib/db.ts ADDED
@@ -0,0 +1,15 @@
1
+ import { PrismaClient } from '@/generated/prisma/client'
2
+ import { PrismaLibSql } from '@prisma/adapter-libsql'
3
+ import path from 'path'
4
+
5
+ function createPrisma() {
6
+ const dbPath = path.resolve(process.cwd(), 'dev.db')
7
+ const adapter = new PrismaLibSql({ url: `file:${dbPath}` })
8
+ return new PrismaClient({ adapter })
9
+ }
10
+
11
+ const globalForPrisma = globalThis as unknown as { prisma: PrismaClient }
12
+
13
+ export const prisma = globalForPrisma.prisma || createPrisma()
14
+
15
+ if (process.env.NODE_ENV !== 'production') globalForPrisma.prisma = prisma
package/lib/format.ts ADDED
@@ -0,0 +1,26 @@
1
+ export function formatCost(usd: number): string {
2
+ if (usd < 0.01) return `$${usd.toFixed(4)}`
3
+ if (usd < 1) return `$${usd.toFixed(3)}`
4
+ return `$${usd.toFixed(2)}`
5
+ }
6
+
7
+ export function formatTokens(n: number): string {
8
+ if (n >= 1_000_000_000) return `${(n / 1_000_000_000).toFixed(1)}B`
9
+ if (n >= 1_000_000) return `${(n / 1_000_000).toFixed(1)}M`
10
+ if (n >= 1_000) return `${(n / 1_000).toFixed(1)}K`
11
+ return n.toString()
12
+ }
13
+
14
+ export function formatDuration(minutes: number): string {
15
+ if (minutes < 60) return `${Math.round(minutes)}m`
16
+ const hours = Math.floor(minutes / 60)
17
+ const mins = Math.round(minutes % 60)
18
+ if (hours < 24) return `${hours}h ${mins}m`
19
+ const days = Math.floor(hours / 24)
20
+ const remainingHours = hours % 24
21
+ return `${days}d ${remainingHours}h`
22
+ }
23
+
24
+ export function formatNumber(n: number): string {
25
+ return n.toLocaleString()
26
+ }
@@ -0,0 +1,201 @@
1
+ // ─── Codex CLI Log Parser ────────────────────────────────────────────
2
+ // Parses OpenAI Codex session logs from ~/.codex/sessions/
3
+ // Rollout format: ~/.codex/sessions/YYYY/MM/DD/rollout-*.jsonl
4
+
5
+ import fs from 'fs'
6
+ import path from 'path'
7
+ import os from 'os'
8
+ import type { SessionSummary } from './parse-logs'
9
+
10
+ const CODEX_HOME = process.env.CODEX_HOME || path.join(os.homedir(), '.codex')
11
+ const SESSIONS_DIR = path.join(CODEX_HOME, 'sessions')
12
+
13
+ // ─── Codex Log Entry Types ──────────────────────────────────────────
14
+
15
+ interface CodexEntry {
16
+ timestamp: string
17
+ type: 'session_meta' | 'event_msg' | 'response_item' | 'turn_context'
18
+ payload: Record<string, unknown>
19
+ }
20
+
21
+ // ─── Session Parsing ─────────────────────────────────────────────────
22
+
23
+ function getProjectName(cwd: string): string {
24
+ const parts = cwd.split('/')
25
+ return parts[parts.length - 1] || parts[parts.length - 2] || cwd
26
+ }
27
+
28
+ export function parseCodexSession(filePath: string): SessionSummary | null {
29
+ try {
30
+ const content = fs.readFileSync(filePath, 'utf-8')
31
+ const lines = content.trim().split('\n')
32
+
33
+ let sessionId = ''
34
+ let project = ''
35
+ let projectPath = ''
36
+ let model = 'unknown'
37
+ let startTime = ''
38
+ let endTime = ''
39
+ let userMessages = 0
40
+ let assistantMessages = 0
41
+ const toolCalls: Record<string, number> = {}
42
+
43
+ // Codex doesn't expose per-message token counts in rollout files,
44
+ // so we estimate from message counts. Codex uses GPT-5 models which
45
+ // don't break down cache tokens the same way.
46
+ let inputTokens = 0
47
+ let outputTokens = 0
48
+
49
+ for (const line of lines) {
50
+ if (!line.trim()) continue
51
+ let entry: CodexEntry
52
+ try {
53
+ entry = JSON.parse(line)
54
+ } catch {
55
+ continue
56
+ }
57
+
58
+ // Track timestamps
59
+ if (entry.timestamp) {
60
+ if (!startTime) startTime = entry.timestamp
61
+ endTime = entry.timestamp
62
+ }
63
+
64
+ const payload = entry.payload || {}
65
+
66
+ switch (entry.type) {
67
+ case 'session_meta':
68
+ sessionId = (payload.id as string) || ''
69
+ projectPath = (payload.cwd as string) || ''
70
+ project = getProjectName(projectPath)
71
+ break
72
+
73
+ case 'turn_context':
74
+ model = (payload.model as string) || model
75
+ break
76
+
77
+ case 'response_item': {
78
+ const role = payload.role as string | undefined
79
+ const ptype = payload.type as string
80
+
81
+ if (role === 'user') {
82
+ userMessages++
83
+ // Estimate input tokens from user message content
84
+ const content = payload.content as Array<{ text?: string }> | undefined
85
+ if (content) {
86
+ for (const block of content) {
87
+ if (block.text) {
88
+ inputTokens += Math.round(block.text.length / 4) // rough estimate
89
+ }
90
+ }
91
+ }
92
+ } else if (role === 'assistant') {
93
+ assistantMessages++
94
+ // Estimate output tokens from assistant message content
95
+ const content = payload.content as Array<{ text?: string }> | undefined
96
+ if (content) {
97
+ for (const block of content) {
98
+ if (block.text) {
99
+ outputTokens += Math.round(block.text.length / 4)
100
+ }
101
+ }
102
+ }
103
+ }
104
+
105
+ if (ptype === 'function_call') {
106
+ const toolName = (payload.name as string) || 'unknown'
107
+ toolCalls[toolName] = (toolCalls[toolName] || 0) + 1
108
+ }
109
+ break
110
+ }
111
+
112
+ case 'event_msg': {
113
+ const eventType = payload.type as string
114
+ if (eventType === 'user_message') {
115
+ userMessages++
116
+ const text = (payload as Record<string, unknown>).text as string | undefined
117
+ if (text) {
118
+ inputTokens += Math.round(text.length / 4)
119
+ }
120
+ }
121
+ break
122
+ }
123
+ }
124
+ }
125
+
126
+ if (userMessages === 0 && assistantMessages === 0) return null
127
+
128
+ // Use filename-based session ID if not found in meta
129
+ if (!sessionId) {
130
+ const basename = path.basename(filePath, '.jsonl')
131
+ const match = basename.match(/rollout-[\dT-]+-(.+)/)
132
+ sessionId = match ? match[1] : basename
133
+ }
134
+
135
+ const durationMinutes =
136
+ startTime && endTime
137
+ ? (new Date(endTime).getTime() - new Date(startTime).getTime()) / 60000
138
+ : 0
139
+
140
+ const totalTokens = inputTokens + outputTokens
141
+
142
+ return {
143
+ sessionId: `codex-${sessionId}`,
144
+ project,
145
+ projectPath,
146
+ startTime,
147
+ endTime,
148
+ durationMinutes: Math.max(0, durationMinutes),
149
+ userMessages,
150
+ assistantMessages,
151
+ totalMessages: userMessages + assistantMessages,
152
+ inputTokens,
153
+ outputTokens,
154
+ cacheCreationTokens: 0,
155
+ cacheReadTokens: 0,
156
+ totalTokens,
157
+ costUSD: 0, // Codex doesn't expose per-session costs in logs
158
+ model: model || 'gpt-5',
159
+ toolCalls,
160
+ toolCallsTotal: Object.values(toolCalls).reduce((a, b) => a + b, 0),
161
+ }
162
+ } catch {
163
+ return null
164
+ }
165
+ }
166
+
167
+ // ─── Discover and Parse All Sessions ─────────────────────────────────
168
+
169
+ export function parseAllCodexSessions(): SessionSummary[] {
170
+ if (!fs.existsSync(SESSIONS_DIR)) return []
171
+
172
+ const sessions: SessionSummary[] = []
173
+ const files = findJsonlFiles(SESSIONS_DIR)
174
+
175
+ for (const file of files) {
176
+ const session = parseCodexSession(file)
177
+ if (session) sessions.push(session)
178
+ }
179
+
180
+ return sessions.sort((a, b) => (b.startTime || '').localeCompare(a.startTime || ''))
181
+ }
182
+
183
+ function findJsonlFiles(dir: string): string[] {
184
+ const results: string[] = []
185
+
186
+ try {
187
+ const entries = fs.readdirSync(dir, { withFileTypes: true })
188
+ for (const entry of entries) {
189
+ const fullPath = path.join(dir, entry.name)
190
+ if (entry.isDirectory()) {
191
+ results.push(...findJsonlFiles(fullPath))
192
+ } else if (entry.name.endsWith('.jsonl') && entry.name.startsWith('rollout-')) {
193
+ results.push(fullPath)
194
+ }
195
+ }
196
+ } catch {
197
+ // Permission errors, etc.
198
+ }
199
+
200
+ return results
201
+ }