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.
- package/.github/workflows/release.yml +111 -0
- package/README.md +41 -38
- package/app/(dashboard)/daily/page.tsx +1 -1
- package/app/(dashboard)/data-management/page.tsx +180 -0
- package/app/(dashboard)/flow/page.tsx +17 -0
- package/app/(dashboard)/layout.tsx +2 -0
- package/app/(dashboard)/page.tsx +24 -5
- package/app/(dashboard)/reports/[id]/page.tsx +72 -0
- package/app/(dashboard)/reports/page.tsx +132 -0
- package/app/(dashboard)/sessions/[id]/page.tsx +167 -0
- package/app/api/backup/route.ts +215 -0
- package/app/api/check/route.ts +11 -1
- package/app/api/command-insights/route.ts +13 -0
- package/app/api/commands/route.ts +55 -1
- package/app/api/images-analysis/route.ts +3 -4
- package/app/api/reports/[id]/route.ts +23 -0
- package/app/api/reports/route.ts +50 -0
- package/app/api/reset/route.ts +21 -0
- package/app/api/session/route.ts +40 -0
- package/app/api/usage/route.ts +26 -1
- package/app/layout.tsx +1 -1
- package/bin/agentfit.mjs +2 -2
- package/components/agent-coach.tsx +256 -129
- package/components/app-sidebar.tsx +45 -10
- package/components/backup-section.tsx +236 -0
- package/components/daily-chart.tsx +447 -83
- package/components/dashboard-shell.tsx +29 -31
- package/components/data-provider.tsx +88 -8
- package/components/fitness-score.tsx +95 -54
- package/components/overview-cards.tsx +148 -41
- package/components/report-view.tsx +307 -0
- package/components/screenshots-analysis.tsx +51 -46
- package/components/session-chatlog.tsx +124 -0
- package/components/session-timeline.tsx +184 -0
- package/components/session-workflow.tsx +183 -0
- package/components/sessions-table.tsx +9 -1
- package/components/tool-flow-graph.tsx +144 -0
- package/components/ui/carousel.tsx +242 -0
- package/components/ui/sidebar.tsx +1 -1
- package/components/ui/sonner.tsx +51 -0
- package/electron/entitlements.mac.plist +16 -0
- package/electron/init-db.mjs +37 -0
- package/electron/main.mjs +203 -0
- package/generated/prisma/browser.ts +5 -0
- package/generated/prisma/client.ts +5 -0
- package/generated/prisma/internal/class.ts +14 -4
- package/generated/prisma/internal/prismaNamespace.ts +97 -2
- package/generated/prisma/internal/prismaNamespaceBrowser.ts +21 -1
- package/generated/prisma/models/Report.ts +1219 -0
- package/generated/prisma/models/Session.ts +221 -1
- package/generated/prisma/models.ts +1 -0
- package/lib/coach.ts +571 -211
- package/lib/command-insights.ts +231 -0
- package/lib/db.ts +2 -2
- package/lib/parse-codex.ts +6 -0
- package/lib/parse-logs.ts +80 -1
- package/lib/queries-codex.ts +24 -0
- package/lib/queries.ts +45 -0
- package/lib/report.ts +156 -0
- package/lib/session-detail.ts +382 -0
- package/lib/sync.ts +87 -0
- package/lib/tool-flow.ts +71 -0
- package/next.config.mjs +6 -1
- package/package.json +17 -2
- package/plugins/cost-heatmap/component.tsx +72 -50
- package/prisma/migrations/20260401144555_add_system_prompt_edits/migration.sql +80 -0
- package/prisma/schema.prisma +18 -0
- package/prisma/schema.sql +81 -0
- package/.claude/settings.local.json +0 -26
- package/CONTRIBUTING.md +0 -209
- package/prisma/migrations/20260328152517_init/migration.sql +0 -41
- package/prisma/migrations/20260328153801_add_image_model/migration.sql +0 -18
- package/prisma.config.ts +0 -14
- 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
|
|