agentfit 0.1.0 → 0.1.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (68) hide show
  1. package/README.md +30 -34
  2. package/app/(dashboard)/daily/page.tsx +1 -1
  3. package/app/(dashboard)/flow/page.tsx +17 -0
  4. package/app/(dashboard)/layout.tsx +2 -0
  5. package/app/(dashboard)/page.tsx +24 -5
  6. package/app/(dashboard)/reports/[id]/page.tsx +72 -0
  7. package/app/(dashboard)/reports/page.tsx +132 -0
  8. package/app/(dashboard)/sessions/[id]/page.tsx +167 -0
  9. package/app/(dashboard)/settings/page.tsx +180 -0
  10. package/app/api/backup/route.ts +215 -0
  11. package/app/api/check/route.ts +11 -1
  12. package/app/api/command-insights/route.ts +13 -0
  13. package/app/api/images-analysis/route.ts +3 -4
  14. package/app/api/reports/[id]/route.ts +23 -0
  15. package/app/api/reports/route.ts +50 -0
  16. package/app/api/reset/route.ts +21 -0
  17. package/app/api/session/route.ts +40 -0
  18. package/app/api/usage/route.ts +25 -1
  19. package/app/layout.tsx +1 -1
  20. package/bin/agentfit.mjs +2 -2
  21. package/components/agent-coach.tsx +256 -129
  22. package/components/app-sidebar.tsx +258 -8
  23. package/components/backup-section.tsx +236 -0
  24. package/components/daily-chart.tsx +404 -83
  25. package/components/dashboard-shell.tsx +9 -24
  26. package/components/data-provider.tsx +66 -2
  27. package/components/fitness-score.tsx +95 -54
  28. package/components/overview-cards.tsx +148 -41
  29. package/components/report-view.tsx +307 -0
  30. package/components/screenshots-analysis.tsx +51 -46
  31. package/components/session-chatlog.tsx +124 -0
  32. package/components/session-timeline.tsx +184 -0
  33. package/components/session-workflow.tsx +183 -0
  34. package/components/sessions-table.tsx +9 -1
  35. package/components/tool-flow-graph.tsx +144 -0
  36. package/components/ui/carousel.tsx +242 -0
  37. package/components/ui/sidebar.tsx +1 -1
  38. package/components/ui/sonner.tsx +51 -0
  39. package/generated/prisma/browser.ts +5 -0
  40. package/generated/prisma/client.ts +5 -0
  41. package/generated/prisma/internal/class.ts +14 -4
  42. package/generated/prisma/internal/prismaNamespace.ts +96 -2
  43. package/generated/prisma/internal/prismaNamespaceBrowser.ts +20 -1
  44. package/generated/prisma/models/Report.ts +1219 -0
  45. package/generated/prisma/models/Session.ts +187 -1
  46. package/generated/prisma/models.ts +1 -0
  47. package/lib/coach.ts +530 -211
  48. package/lib/command-insights.ts +231 -0
  49. package/lib/db.ts +1 -1
  50. package/lib/parse-codex.ts +5 -0
  51. package/lib/parse-logs.ts +65 -0
  52. package/lib/queries-codex.ts +22 -0
  53. package/lib/queries.ts +42 -0
  54. package/lib/report.ts +156 -0
  55. package/lib/session-detail.ts +382 -0
  56. package/lib/sync.ts +77 -0
  57. package/lib/tool-flow.ts +71 -0
  58. package/next.config.mjs +6 -1
  59. package/package.json +16 -2
  60. package/plugins/cost-heatmap/component.tsx +72 -50
  61. package/prisma/schema.prisma +17 -0
  62. package/.claude/settings.local.json +0 -26
  63. package/CONTRIBUTING.md +0 -209
  64. package/prisma/migrations/20260328152517_init/migration.sql +0 -41
  65. package/prisma/migrations/20260328153801_add_image_model/migration.sql +0 -18
  66. package/prisma/migrations/migration_lock.toml +0 -3
  67. package/prisma.config.ts +0 -14
  68. package/setup.sh +0 -73
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,12 @@ 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> = {}
65
87
 
66
88
  for (const line of lines) {
67
89
  if (!line.trim()) continue
@@ -75,6 +97,7 @@ function parseSessionFile(
75
97
  if (entry.timestamp) {
76
98
  if (!startTime) startTime = entry.timestamp
77
99
  endTime = entry.timestamp
100
+ messageTimestamps.push(entry.timestamp)
78
101
  }
79
102
 
80
103
  const entryType = entry.type
@@ -82,9 +105,44 @@ function parseSessionFile(
82
105
 
83
106
  if (entryType === 'user') {
84
107
  userMessages++
108
+ // Track permission mode
109
+ const pm = (entry as Record<string, unknown>).permissionMode as string | undefined
110
+ if (pm) {
111
+ permissionModes[pm] = (permissionModes[pm] || 0) + 1
112
+ }
113
+ // Detect user interruptions from tool_result content
114
+ if (msg && Array.isArray(msg.content)) {
115
+ for (const block of msg.content) {
116
+ if (block && typeof block === 'object' && 'type' in block) {
117
+ const b = block as Record<string, unknown>
118
+ if (b.type === 'tool_result') {
119
+ const c = String(b.content || '')
120
+ if (c.includes("doesn't want to proceed") || c.includes('was rejected')) {
121
+ userInterruptions++
122
+ }
123
+ }
124
+ }
125
+ }
126
+ }
85
127
  } else if (entryType === 'assistant' && msg) {
86
128
  assistantMessages++
87
129
 
130
+ // Detect API errors from synthetic messages
131
+ if (msg.model === '<synthetic>' && Array.isArray(msg.content)) {
132
+ for (const block of msg.content) {
133
+ if (block && typeof block === 'object' && 'type' in block) {
134
+ const b = block as Record<string, unknown>
135
+ if (b.type === 'text' && String(b.text || '').startsWith('API Error')) {
136
+ apiErrors++
137
+ const errText = String(b.text || '').toLowerCase()
138
+ if (errText.includes('rate limit') || errText.includes('rate_limit') || errText.includes('429') || errText.includes('529') || errText.includes('overloaded')) {
139
+ rateLimitErrors++
140
+ }
141
+ }
142
+ }
143
+ }
144
+ }
145
+
88
146
  if (msg.model && msg.model !== '<synthetic>') {
89
147
  model = msg.model
90
148
  }
@@ -112,6 +170,13 @@ function parseSessionFile(
112
170
  const toolName = b.name as string
113
171
  if (toolName) {
114
172
  toolCalls[toolName] = (toolCalls[toolName] || 0) + 1
173
+ // Track individual skill names
174
+ if (toolName === 'Skill' && b.input && typeof b.input === 'object') {
175
+ const skillName = (b.input as Record<string, unknown>).skill as string
176
+ if (skillName) {
177
+ skillCalls[skillName] = (skillCalls[skillName] || 0) + 1
178
+ }
179
+ }
115
180
  }
116
181
  }
117
182
 
@@ -172,6 +237,12 @@ function parseSessionFile(
172
237
  model: model || 'unknown',
173
238
  toolCalls,
174
239
  toolCallsTotal: Object.values(toolCalls).reduce((a, b) => a + b, 0),
240
+ messageTimestamps,
241
+ apiErrors,
242
+ rateLimitErrors,
243
+ userInterruptions,
244
+ skillCalls,
245
+ permissionModes,
175
246
  images,
176
247
  }
177
248
  }
@@ -263,6 +334,12 @@ export async function syncLogs(): Promise<SyncResult> {
263
334
  model: parsed.model,
264
335
  toolCallsTotal: parsed.toolCallsTotal,
265
336
  toolCallsJson: JSON.stringify(parsed.toolCalls),
337
+ skillCallsJson: JSON.stringify(parsed.skillCalls),
338
+ messageTimestamps: JSON.stringify(parsed.messageTimestamps),
339
+ apiErrors: parsed.apiErrors,
340
+ rateLimitErrors: parsed.rateLimitErrors,
341
+ userInterruptions: parsed.userInterruptions,
342
+ permissionModesJson: JSON.stringify(parsed.permissionModes),
266
343
  },
267
344
  })
268
345