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
package/lib/commands.ts
ADDED
|
@@ -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
|
+
}
|