agentfit 0.1.0 → 0.1.2

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 (74) hide show
  1. package/.github/workflows/release.yml +111 -0
  2. package/README.md +41 -38
  3. package/app/(dashboard)/daily/page.tsx +1 -1
  4. package/app/(dashboard)/data-management/page.tsx +180 -0
  5. package/app/(dashboard)/flow/page.tsx +17 -0
  6. package/app/(dashboard)/layout.tsx +2 -0
  7. package/app/(dashboard)/page.tsx +24 -5
  8. package/app/(dashboard)/reports/[id]/page.tsx +72 -0
  9. package/app/(dashboard)/reports/page.tsx +132 -0
  10. package/app/(dashboard)/sessions/[id]/page.tsx +167 -0
  11. package/app/api/backup/route.ts +215 -0
  12. package/app/api/check/route.ts +11 -1
  13. package/app/api/command-insights/route.ts +13 -0
  14. package/app/api/commands/route.ts +55 -1
  15. package/app/api/images-analysis/route.ts +3 -4
  16. package/app/api/reports/[id]/route.ts +23 -0
  17. package/app/api/reports/route.ts +50 -0
  18. package/app/api/reset/route.ts +21 -0
  19. package/app/api/session/route.ts +40 -0
  20. package/app/api/usage/route.ts +26 -1
  21. package/app/layout.tsx +1 -1
  22. package/bin/agentfit.mjs +2 -2
  23. package/components/agent-coach.tsx +256 -129
  24. package/components/app-sidebar.tsx +45 -10
  25. package/components/backup-section.tsx +236 -0
  26. package/components/daily-chart.tsx +447 -83
  27. package/components/dashboard-shell.tsx +29 -31
  28. package/components/data-provider.tsx +88 -8
  29. package/components/fitness-score.tsx +95 -54
  30. package/components/overview-cards.tsx +148 -41
  31. package/components/report-view.tsx +307 -0
  32. package/components/screenshots-analysis.tsx +51 -46
  33. package/components/session-chatlog.tsx +124 -0
  34. package/components/session-timeline.tsx +184 -0
  35. package/components/session-workflow.tsx +183 -0
  36. package/components/sessions-table.tsx +9 -1
  37. package/components/tool-flow-graph.tsx +144 -0
  38. package/components/ui/carousel.tsx +242 -0
  39. package/components/ui/sidebar.tsx +1 -1
  40. package/components/ui/sonner.tsx +51 -0
  41. package/electron/entitlements.mac.plist +16 -0
  42. package/electron/init-db.mjs +37 -0
  43. package/electron/main.mjs +203 -0
  44. package/generated/prisma/browser.ts +5 -0
  45. package/generated/prisma/client.ts +5 -0
  46. package/generated/prisma/internal/class.ts +14 -4
  47. package/generated/prisma/internal/prismaNamespace.ts +97 -2
  48. package/generated/prisma/internal/prismaNamespaceBrowser.ts +21 -1
  49. package/generated/prisma/models/Report.ts +1219 -0
  50. package/generated/prisma/models/Session.ts +221 -1
  51. package/generated/prisma/models.ts +1 -0
  52. package/lib/coach.ts +571 -211
  53. package/lib/command-insights.ts +231 -0
  54. package/lib/db.ts +2 -2
  55. package/lib/parse-codex.ts +6 -0
  56. package/lib/parse-logs.ts +80 -1
  57. package/lib/queries-codex.ts +24 -0
  58. package/lib/queries.ts +45 -0
  59. package/lib/report.ts +156 -0
  60. package/lib/session-detail.ts +382 -0
  61. package/lib/sync.ts +87 -0
  62. package/lib/tool-flow.ts +71 -0
  63. package/next.config.mjs +6 -1
  64. package/package.json +17 -2
  65. package/plugins/cost-heatmap/component.tsx +72 -50
  66. package/prisma/migrations/20260401144555_add_system_prompt_edits/migration.sql +80 -0
  67. package/prisma/schema.prisma +18 -0
  68. package/prisma/schema.sql +81 -0
  69. package/.claude/settings.local.json +0 -26
  70. package/CONTRIBUTING.md +0 -209
  71. package/prisma/migrations/20260328152517_init/migration.sql +0 -41
  72. package/prisma/migrations/20260328153801_add_image_model/migration.sql +0 -18
  73. package/prisma.config.ts +0 -14
  74. package/setup.sh +0 -73
package/lib/report.ts ADDED
@@ -0,0 +1,156 @@
1
+ // ─── Report Generation ───────────────────────────────────────────────
2
+ // Composes existing analyzers into a point-in-time snapshot report.
3
+
4
+ import { getUsageData } from './queries'
5
+ import { generateCoachInsights, type CoachInsight } from './coach'
6
+ import { analyzePersonality } from './personality'
7
+ import { generateCommandInsights, type CommandInsight } from './command-insights'
8
+ import { formatCost, formatDuration } from './format'
9
+
10
+ // ─── Report Content Schema ───────────────────────────────────────────
11
+
12
+ export interface ReportContent {
13
+ atAGlance: {
14
+ fitnessScore: number
15
+ scoreLabel: string
16
+ totalSessions: number
17
+ totalProjects: number
18
+ totalCostUSD: number
19
+ totalDurationMinutes: number
20
+ totalMessages: number
21
+ totalToolCalls: number
22
+ totalApiErrors: number
23
+ totalUserInterruptions: number
24
+ dateRange: { from: string; to: string }
25
+ currentStreak: number
26
+ longestStreak: number
27
+ }
28
+ projectAreas: {
29
+ name: string
30
+ sessions: number
31
+ totalCost: number
32
+ totalDurationMinutes: number
33
+ totalMessages: number
34
+ topTools: [string, number][]
35
+ }[]
36
+ interactionStyle: {
37
+ avgMessagesPerSession: number
38
+ avgDurationMinutes: number
39
+ avgCostPerSession: number
40
+ readEditRatio: number
41
+ bashRatio: number
42
+ agentCallsTotal: number
43
+ peakHour: number
44
+ mostActiveDay: string
45
+ mbtiType: string
46
+ mbtiDescription: string
47
+ }
48
+ whatWorks: CoachInsight[]
49
+ frictionAnalysis: CoachInsight[]
50
+ suggestions: {
51
+ coachTips: CoachInsight[]
52
+ commandTips: CommandInsight[]
53
+ claudeMdRules: string[]
54
+ }
55
+ }
56
+
57
+ // ─── Generation ──────────────────────────────────────────────────────
58
+
59
+ export async function generateReport(): Promise<{
60
+ title: string
61
+ contentJson: ReportContent
62
+ sessionCount: number
63
+ }> {
64
+ const data = await getUsageData()
65
+ const coach = generateCoachInsights(data)
66
+ const personality = analyzePersonality(data)
67
+ const cmdInsights = generateCommandInsights()
68
+
69
+ const { overview, sessions, projects, toolUsage } = data
70
+
71
+ // Date range
72
+ const sortedSessions = [...sessions].sort((a, b) => a.startTime.localeCompare(b.startTime))
73
+ const from = sortedSessions[0]?.startTime?.slice(0, 10) || 'N/A'
74
+ const to = sortedSessions[sortedSessions.length - 1]?.startTime?.slice(0, 10) || 'N/A'
75
+
76
+ // Tool ratios
77
+ const readCalls = (toolUsage['Read'] || 0) + (toolUsage['Grep'] || 0) + (toolUsage['Glob'] || 0)
78
+ const editCalls = (toolUsage['Edit'] || 0) + (toolUsage['Write'] || 0)
79
+ const bashCalls = toolUsage['Bash'] || 0
80
+ const agentCalls = toolUsage['Agent'] || 0
81
+ const totalTools = overview.totalToolCalls
82
+
83
+ // MBTI descriptions
84
+ const mbtiDescriptions: Record<string, string> = {
85
+ ISTJ: 'The Inspector — Methodical, detail-oriented, follows established procedures',
86
+ ISFJ: 'The Protector — Supportive, reliable, focused on preserving working code',
87
+ INFJ: 'The Counselor — Insightful, sees patterns, anticipates architectural needs',
88
+ INTJ: 'The Architect — Strategic, independent, designs for long-term quality',
89
+ ISTP: 'The Craftsman — Practical, efficient, excels at targeted fixes',
90
+ ISFP: 'The Composer — Adaptable, aesthetic sense, good at UI refinement',
91
+ INFP: 'The Healer — Idealistic, explores creative solutions, values code clarity',
92
+ INTP: 'The Thinker — Analytical, explores edge cases, values logical consistency',
93
+ ESTP: 'The Dynamo — Action-oriented, quick iterations, bias toward execution',
94
+ ESFP: 'The Performer — Energetic, responsive, generates many alternatives',
95
+ ENFP: 'The Champion — Enthusiastic explorer, broad tool usage, creative approaches',
96
+ ENTP: 'The Visionary — Innovative, questions assumptions, proposes novel solutions',
97
+ ESTJ: 'The Supervisor — Organized, efficient, follows project conventions strictly',
98
+ ESFJ: 'The Provider — Cooperative, communicative, adapts to user preferences',
99
+ ENFJ: 'The Teacher — Explains reasoning, mentors through code, proactive guidance',
100
+ ENTJ: 'The Commander — Decisive, takes charge, designs comprehensive solutions',
101
+ }
102
+
103
+ const content: ReportContent = {
104
+ atAGlance: {
105
+ fitnessScore: coach.score,
106
+ scoreLabel: coach.scoreLabel,
107
+ totalSessions: overview.totalSessions,
108
+ totalProjects: overview.totalProjects,
109
+ totalCostUSD: overview.totalCostUSD,
110
+ totalDurationMinutes: overview.totalDurationMinutes,
111
+ totalMessages: overview.totalMessages,
112
+ totalToolCalls: overview.totalToolCalls,
113
+ totalApiErrors: overview.totalApiErrors,
114
+ totalUserInterruptions: overview.totalUserInterruptions,
115
+ dateRange: { from, to },
116
+ currentStreak: coach.stats.currentStreak,
117
+ longestStreak: coach.stats.longestStreak,
118
+ },
119
+ projectAreas: projects.slice(0, 10).map(p => ({
120
+ name: p.name,
121
+ sessions: p.sessions,
122
+ totalCost: p.totalCost,
123
+ totalDurationMinutes: p.totalDurationMinutes,
124
+ totalMessages: p.totalMessages,
125
+ topTools: Object.entries(p.toolCalls)
126
+ .sort((a, b) => b[1] - a[1])
127
+ .slice(0, 5),
128
+ })),
129
+ interactionStyle: {
130
+ avgMessagesPerSession: coach.stats.avgMessagesPerSession,
131
+ avgDurationMinutes: coach.stats.avgDurationMinutes,
132
+ avgCostPerSession: coach.stats.avgCostPerSession,
133
+ readEditRatio: editCalls > 0 ? readCalls / editCalls : 0,
134
+ bashRatio: totalTools > 0 ? bashCalls / totalTools : 0,
135
+ agentCallsTotal: agentCalls,
136
+ peakHour: coach.stats.peakHour,
137
+ mostActiveDay: coach.stats.mostActiveDay,
138
+ mbtiType: personality.mbtiType,
139
+ mbtiDescription: mbtiDescriptions[personality.mbtiType] || 'Unique profile',
140
+ },
141
+ whatWorks: coach.insights.filter(i => i.severity === 'achievement'),
142
+ frictionAnalysis: coach.insights.filter(i => i.severity === 'warning'),
143
+ suggestions: {
144
+ coachTips: coach.insights.filter(i => i.severity === 'tip'),
145
+ commandTips: cmdInsights,
146
+ claudeMdRules: coach.insights
147
+ .filter(i => i.severity === 'warning' && i.recommendation)
148
+ .map(i => i.recommendation!),
149
+ },
150
+ }
151
+
152
+ const now = new Date()
153
+ const title = `Report — ${now.toLocaleDateString('en-US', { month: 'short', day: 'numeric', year: 'numeric' })} ${now.toLocaleTimeString('en-US', { hour: 'numeric', minute: '2-digit' })}`
154
+
155
+ return { title, contentJson: content, sessionCount: overview.totalSessions }
156
+ }
@@ -0,0 +1,382 @@
1
+ // ─── Session Detail Parser ───────────────────────────────────────────
2
+ // Parses a single Claude Code JSONL session file into a workflow DAG
3
+ // and chat log for visualization (inspired by CommuGraph).
4
+
5
+ import fs from 'fs'
6
+
7
+ // ─── Types ───────────────────────────────────────────────────────────
8
+
9
+ export type WorkflowNodeType =
10
+ | 'user_input'
11
+ | 'thinking'
12
+ | 'text_response'
13
+ | 'tool_call'
14
+ | 'tool_result'
15
+ | 'system'
16
+
17
+ export interface WorkflowNode {
18
+ id: string
19
+ stepIndex: number
20
+ timestamp: string
21
+ nodeType: WorkflowNodeType
22
+ label: string
23
+ content: string
24
+ contentPreview: string
25
+ toolName?: string
26
+ toolInput?: string
27
+ parentId: string | null
28
+ durationMs?: number
29
+ isError?: boolean
30
+ isSidechain?: boolean
31
+ agentId?: string
32
+ }
33
+
34
+ export interface WorkflowEdge {
35
+ id: string
36
+ source: string
37
+ target: string
38
+ durationMs?: number
39
+ }
40
+
41
+ export interface ChatMessage {
42
+ id: string
43
+ stepIndex: number
44
+ timestamp: string
45
+ role: 'user' | 'assistant' | 'system' | 'tool_result'
46
+ content: string
47
+ toolName?: string
48
+ isThinking?: boolean
49
+ isSidechain?: boolean
50
+ agentId?: string
51
+ images?: number
52
+ }
53
+
54
+ export interface SessionDetail {
55
+ sessionId: string
56
+ workflowNodes: WorkflowNode[]
57
+ workflowEdges: WorkflowEdge[]
58
+ chatLog: ChatMessage[]
59
+ stats: {
60
+ totalMessages: number
61
+ userTurns: number
62
+ assistantTurns: number
63
+ toolCalls: number
64
+ successCount: number
65
+ failureCount: number
66
+ duration: string
67
+ tokens: number
68
+ }
69
+ }
70
+
71
+ // ─── Parser ──────────────────────────────────────────────────────────
72
+
73
+ interface RawEntry {
74
+ uuid?: string
75
+ parentUuid?: string
76
+ type?: string
77
+ timestamp?: string
78
+ isSidechain?: boolean
79
+ message?: {
80
+ role?: string
81
+ content?: unknown[]
82
+ model?: string
83
+ usage?: {
84
+ input_tokens?: number
85
+ output_tokens?: number
86
+ }
87
+ }
88
+ }
89
+
90
+ export function parseSessionDetail(filePath: string, sessionId: string): SessionDetail {
91
+ const content = fs.readFileSync(filePath, 'utf-8')
92
+ const lines = content.trim().split('\n')
93
+
94
+ const nodes: WorkflowNode[] = []
95
+ const edges: WorkflowEdge[] = []
96
+ const chatLog: ChatMessage[] = []
97
+
98
+ let stepIndex = 0
99
+ let lastNodeId: string | null = null
100
+ let totalTokens = 0
101
+ let toolCallCount = 0
102
+ let successCount = 0
103
+ let failureCount = 0
104
+ let userTurns = 0
105
+ let assistantTurns = 0
106
+ let firstTimestamp = ''
107
+ let lastTimestamp = ''
108
+
109
+ for (const line of lines) {
110
+ if (!line.trim()) continue
111
+ let entry: RawEntry
112
+ try {
113
+ entry = JSON.parse(line)
114
+ } catch {
115
+ continue
116
+ }
117
+
118
+ const ts = entry.timestamp || ''
119
+ if (ts && !firstTimestamp) firstTimestamp = ts
120
+ if (ts) lastTimestamp = ts
121
+
122
+ const uuid = entry.uuid || `step-${stepIndex}`
123
+ const parentUuid = entry.parentUuid || null
124
+ const isSidechain = entry.isSidechain || false
125
+
126
+ if (entry.type === 'user') {
127
+ stepIndex++
128
+ userTurns++
129
+
130
+ // Extract user text
131
+ let text = ''
132
+ let imageCount = 0
133
+ const msg = entry.message
134
+ if (msg?.content && Array.isArray(msg.content)) {
135
+ for (const block of msg.content) {
136
+ if (typeof block === 'object' && block !== null) {
137
+ const b = block as Record<string, unknown>
138
+ if (b.type === 'text') text += (b.text as string) || ''
139
+ else if (b.type === 'tool_result') {
140
+ const resultContent = b.content
141
+ if (typeof resultContent === 'string') text += resultContent
142
+ else if (Array.isArray(resultContent)) {
143
+ for (const rc of resultContent) {
144
+ if (typeof rc === 'object' && rc !== null) {
145
+ const r = rc as Record<string, unknown>
146
+ if (r.type === 'text') text += (r.text as string) || ''
147
+ }
148
+ }
149
+ }
150
+ // This is actually a tool result
151
+ const isError = b.is_error === true
152
+ const toolUseId = b.tool_use_id as string | undefined
153
+
154
+ if (isError) failureCount++
155
+ else successCount++
156
+
157
+ const resultNode: WorkflowNode = {
158
+ id: `${uuid}-result`,
159
+ stepIndex,
160
+ timestamp: ts,
161
+ nodeType: 'tool_result',
162
+ label: isError ? 'Error' : 'Result',
163
+ content: text.slice(0, 500),
164
+ contentPreview: text.slice(0, 100),
165
+ parentId: toolUseId ? `tool-${toolUseId}` : lastNodeId,
166
+ isError,
167
+ isSidechain,
168
+ }
169
+ nodes.push(resultNode)
170
+
171
+ if (resultNode.parentId) {
172
+ edges.push({
173
+ id: `e-${resultNode.parentId}-${resultNode.id}`,
174
+ source: resultNode.parentId,
175
+ target: resultNode.id,
176
+ })
177
+ }
178
+
179
+ chatLog.push({
180
+ id: `${uuid}-result`,
181
+ stepIndex,
182
+ timestamp: ts,
183
+ role: 'tool_result',
184
+ content: text.slice(0, 300),
185
+ isSidechain,
186
+ })
187
+
188
+ lastNodeId = resultNode.id
189
+ text = ''
190
+ continue
191
+ } else if (b.type === 'image') {
192
+ imageCount++
193
+ }
194
+ } else if (typeof block === 'string') {
195
+ text += block
196
+ }
197
+ }
198
+ } else if (typeof msg?.content === 'string') {
199
+ text = msg.content
200
+ }
201
+
202
+ if (!text && imageCount === 0) continue
203
+
204
+ const nodeId = uuid
205
+ const node: WorkflowNode = {
206
+ id: nodeId,
207
+ stepIndex,
208
+ timestamp: ts,
209
+ nodeType: 'user_input',
210
+ label: 'User Input',
211
+ content: text,
212
+ contentPreview: text.slice(0, 100) || `[${imageCount} image(s)]`,
213
+ parentId: lastNodeId,
214
+ isSidechain,
215
+ }
216
+ nodes.push(node)
217
+
218
+ if (lastNodeId) {
219
+ edges.push({ id: `e-${lastNodeId}-${nodeId}`, source: lastNodeId, target: nodeId })
220
+ }
221
+
222
+ chatLog.push({
223
+ id: nodeId,
224
+ stepIndex,
225
+ timestamp: ts,
226
+ role: 'user',
227
+ content: text || `[${imageCount} image(s)]`,
228
+ isSidechain,
229
+ images: imageCount,
230
+ })
231
+
232
+ lastNodeId = nodeId
233
+
234
+ } else if (entry.type === 'assistant' && entry.message) {
235
+ const msg = entry.message
236
+ assistantTurns++
237
+
238
+ if (msg.usage) {
239
+ totalTokens += (msg.usage.input_tokens || 0) + (msg.usage.output_tokens || 0)
240
+ }
241
+
242
+ if (!Array.isArray(msg.content)) continue
243
+
244
+ for (const block of msg.content) {
245
+ if (typeof block !== 'object' || block === null) continue
246
+ const b = block as Record<string, unknown>
247
+
248
+ if (b.type === 'thinking') {
249
+ stepIndex++
250
+ const text = (b.thinking as string) || ''
251
+ const nodeId = `${uuid}-thinking-${stepIndex}`
252
+
253
+ nodes.push({
254
+ id: nodeId,
255
+ stepIndex,
256
+ timestamp: ts,
257
+ nodeType: 'thinking',
258
+ label: 'Thinking',
259
+ content: text,
260
+ contentPreview: text.slice(0, 100),
261
+ parentId: lastNodeId,
262
+ isSidechain,
263
+ })
264
+
265
+ if (lastNodeId) {
266
+ edges.push({ id: `e-${lastNodeId}-${nodeId}`, source: lastNodeId, target: nodeId })
267
+ }
268
+
269
+ chatLog.push({
270
+ id: nodeId,
271
+ stepIndex,
272
+ timestamp: ts,
273
+ role: 'assistant',
274
+ content: text.slice(0, 300),
275
+ isThinking: true,
276
+ isSidechain,
277
+ })
278
+
279
+ lastNodeId = nodeId
280
+
281
+ } else if (b.type === 'text') {
282
+ stepIndex++
283
+ const text = (b.text as string) || ''
284
+ if (!text.trim()) continue
285
+ const nodeId = `${uuid}-text-${stepIndex}`
286
+
287
+ nodes.push({
288
+ id: nodeId,
289
+ stepIndex,
290
+ timestamp: ts,
291
+ nodeType: 'text_response',
292
+ label: 'Response',
293
+ content: text,
294
+ contentPreview: text.slice(0, 100),
295
+ parentId: lastNodeId,
296
+ isSidechain,
297
+ })
298
+
299
+ if (lastNodeId) {
300
+ edges.push({ id: `e-${lastNodeId}-${nodeId}`, source: lastNodeId, target: nodeId })
301
+ }
302
+
303
+ chatLog.push({
304
+ id: nodeId,
305
+ stepIndex,
306
+ timestamp: ts,
307
+ role: 'assistant',
308
+ content: text.slice(0, 500),
309
+ isSidechain,
310
+ })
311
+
312
+ lastNodeId = nodeId
313
+
314
+ } else if (b.type === 'tool_use') {
315
+ stepIndex++
316
+ toolCallCount++
317
+ const toolName = (b.name as string) || 'unknown'
318
+ const toolInput = b.input ? JSON.stringify(b.input).slice(0, 200) : ''
319
+ const toolUseId = (b.id as string) || `tool-${stepIndex}`
320
+ const nodeId = `tool-${toolUseId}`
321
+
322
+ nodes.push({
323
+ id: nodeId,
324
+ stepIndex,
325
+ timestamp: ts,
326
+ nodeType: 'tool_call',
327
+ label: toolName,
328
+ content: toolInput,
329
+ contentPreview: toolInput.slice(0, 80),
330
+ toolName,
331
+ toolInput,
332
+ parentId: lastNodeId,
333
+ isSidechain,
334
+ })
335
+
336
+ if (lastNodeId) {
337
+ edges.push({ id: `e-${lastNodeId}-${nodeId}`, source: lastNodeId, target: nodeId })
338
+ }
339
+
340
+ chatLog.push({
341
+ id: nodeId,
342
+ stepIndex,
343
+ timestamp: ts,
344
+ role: 'assistant',
345
+ content: `${toolName}: ${toolInput.slice(0, 200)}`,
346
+ toolName,
347
+ isSidechain,
348
+ })
349
+
350
+ lastNodeId = nodeId
351
+ }
352
+ }
353
+ }
354
+ }
355
+
356
+ // Compute duration
357
+ let durationStr = '0s'
358
+ if (firstTimestamp && lastTimestamp) {
359
+ const ms = new Date(lastTimestamp).getTime() - new Date(firstTimestamp).getTime()
360
+ const mins = ms / 60000
361
+ if (mins < 1) durationStr = `${Math.round(ms / 1000)}s`
362
+ else if (mins < 60) durationStr = `${Math.round(mins)}m`
363
+ else durationStr = `${Math.floor(mins / 60)}h ${Math.round(mins % 60)}m`
364
+ }
365
+
366
+ return {
367
+ sessionId,
368
+ workflowNodes: nodes,
369
+ workflowEdges: edges,
370
+ chatLog,
371
+ stats: {
372
+ totalMessages: userTurns + assistantTurns,
373
+ userTurns,
374
+ assistantTurns,
375
+ toolCalls: toolCallCount,
376
+ successCount,
377
+ failureCount,
378
+ duration: durationStr,
379
+ tokens: totalTokens,
380
+ },
381
+ }
382
+ }
package/lib/sync.ts CHANGED
@@ -38,7 +38,23 @@ function decodeProjectPath(dirName: string): string {
38
38
  }
39
39
 
40
40
  function getProjectName(projectPath: string): string {
41
+ // The decoded path may be wrong if the actual folder name contains dashes,
42
+ // since decodeProjectPath replaces ALL dashes with slashes.
43
+ // Try merging trailing segments to find the real directory on disk.
44
+ if (fs.existsSync(projectPath)) {
45
+ return path.basename(projectPath)
46
+ }
41
47
  const parts = projectPath.split('/')
48
+ for (let merge = 2; merge <= Math.min(parts.length, 6); merge++) {
49
+ const parentParts = parts.slice(0, -merge)
50
+ const nameParts = parts.slice(-merge)
51
+ const candidateName = nameParts.join('-')
52
+ const candidatePath = [...parentParts, candidateName].join('/')
53
+ if (fs.existsSync(candidatePath)) {
54
+ return candidateName
55
+ }
56
+ }
57
+ // Fallback: last segment
42
58
  return parts[parts.length - 1] || parts[parts.length - 2] || projectPath
43
59
  }
44
60
 
@@ -62,6 +78,13 @@ function parseSessionFile(
62
78
  let endTime = ''
63
79
  const toolCalls: Record<string, number> = {}
64
80
  const images: ImageInfo[] = []
81
+ const messageTimestamps: string[] = []
82
+ let apiErrors = 0
83
+ let rateLimitErrors = 0
84
+ let userInterruptions = 0
85
+ const skillCalls: Record<string, number> = {}
86
+ const permissionModes: Record<string, number> = {}
87
+ let systemPromptEdits = 0
65
88
 
66
89
  for (const line of lines) {
67
90
  if (!line.trim()) continue
@@ -75,6 +98,7 @@ function parseSessionFile(
75
98
  if (entry.timestamp) {
76
99
  if (!startTime) startTime = entry.timestamp
77
100
  endTime = entry.timestamp
101
+ messageTimestamps.push(entry.timestamp)
78
102
  }
79
103
 
80
104
  const entryType = entry.type
@@ -82,9 +106,44 @@ function parseSessionFile(
82
106
 
83
107
  if (entryType === 'user') {
84
108
  userMessages++
109
+ // Track permission mode
110
+ const pm = (entry as Record<string, unknown>).permissionMode as string | undefined
111
+ if (pm) {
112
+ permissionModes[pm] = (permissionModes[pm] || 0) + 1
113
+ }
114
+ // Detect user interruptions from tool_result content
115
+ if (msg && Array.isArray(msg.content)) {
116
+ for (const block of msg.content) {
117
+ if (block && typeof block === 'object' && 'type' in block) {
118
+ const b = block as Record<string, unknown>
119
+ if (b.type === 'tool_result') {
120
+ const c = String(b.content || '')
121
+ if (c.includes("doesn't want to proceed") || c.includes('was rejected')) {
122
+ userInterruptions++
123
+ }
124
+ }
125
+ }
126
+ }
127
+ }
85
128
  } else if (entryType === 'assistant' && msg) {
86
129
  assistantMessages++
87
130
 
131
+ // Detect API errors from synthetic messages
132
+ if (msg.model === '<synthetic>' && Array.isArray(msg.content)) {
133
+ for (const block of msg.content) {
134
+ if (block && typeof block === 'object' && 'type' in block) {
135
+ const b = block as Record<string, unknown>
136
+ if (b.type === 'text' && String(b.text || '').startsWith('API Error')) {
137
+ apiErrors++
138
+ const errText = String(b.text || '').toLowerCase()
139
+ if (errText.includes('rate limit') || errText.includes('rate_limit') || errText.includes('429') || errText.includes('529') || errText.includes('overloaded')) {
140
+ rateLimitErrors++
141
+ }
142
+ }
143
+ }
144
+ }
145
+ }
146
+
88
147
  if (msg.model && msg.model !== '<synthetic>') {
89
148
  model = msg.model
90
149
  }
@@ -112,6 +171,20 @@ function parseSessionFile(
112
171
  const toolName = b.name as string
113
172
  if (toolName) {
114
173
  toolCalls[toolName] = (toolCalls[toolName] || 0) + 1
174
+ // Track individual skill names
175
+ if (toolName === 'Skill' && b.input && typeof b.input === 'object') {
176
+ const skillName = (b.input as Record<string, unknown>).skill as string
177
+ if (skillName) {
178
+ skillCalls[skillName] = (skillCalls[skillName] || 0) + 1
179
+ }
180
+ }
181
+ // Detect system prompt file edits (CLAUDE.md, AGENTS.md, agent.md)
182
+ if ((toolName === 'Edit' || toolName === 'Write') && b.input && typeof b.input === 'object') {
183
+ const fp = (b.input as Record<string, unknown>).file_path as string
184
+ if (fp && /\/(CLAUDE|AGENTS|agent)\.md$/i.test(fp)) {
185
+ systemPromptEdits++
186
+ }
187
+ }
115
188
  }
116
189
  }
117
190
 
@@ -172,6 +245,13 @@ function parseSessionFile(
172
245
  model: model || 'unknown',
173
246
  toolCalls,
174
247
  toolCallsTotal: Object.values(toolCalls).reduce((a, b) => a + b, 0),
248
+ messageTimestamps,
249
+ apiErrors,
250
+ rateLimitErrors,
251
+ userInterruptions,
252
+ skillCalls,
253
+ permissionModes,
254
+ systemPromptEdits,
175
255
  images,
176
256
  }
177
257
  }
@@ -263,6 +343,13 @@ export async function syncLogs(): Promise<SyncResult> {
263
343
  model: parsed.model,
264
344
  toolCallsTotal: parsed.toolCallsTotal,
265
345
  toolCallsJson: JSON.stringify(parsed.toolCalls),
346
+ skillCallsJson: JSON.stringify(parsed.skillCalls),
347
+ messageTimestamps: JSON.stringify(parsed.messageTimestamps),
348
+ apiErrors: parsed.apiErrors,
349
+ rateLimitErrors: parsed.rateLimitErrors,
350
+ userInterruptions: parsed.userInterruptions,
351
+ permissionModesJson: JSON.stringify(parsed.permissionModes),
352
+ systemPromptEdits: parsed.systemPromptEdits,
266
353
  },
267
354
  })
268
355