ccakashic 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/lib/parser.js ADDED
@@ -0,0 +1,348 @@
1
+ 'use strict';
2
+
3
+ const fs = require('fs');
4
+ const readline = require('readline');
5
+ const path = require('path');
6
+
7
+ async function parseSession(sessionPath) {
8
+ const lines = await readJsonlLines(sessionPath);
9
+ const messages = buildConversation(lines);
10
+
11
+ // Check for subagent conversations
12
+ const sessionId = path.basename(sessionPath, '.jsonl');
13
+ const subagentsDir = path.join(path.dirname(sessionPath), sessionId, 'subagents');
14
+ const subagents = {};
15
+
16
+ if (fs.existsSync(subagentsDir)) {
17
+ const agentFiles = fs.readdirSync(subagentsDir).filter(f => f.endsWith('.jsonl'));
18
+ for (const f of agentFiles) {
19
+ const agentLines = await readJsonlLines(path.join(subagentsDir, f));
20
+ const agentMessages = buildConversation(agentLines);
21
+ // Extract agent ID from filename: agent-a1234.jsonl → a1234
22
+ const agentId = f.replace(/^agent-/, '').replace(/\.jsonl$/, '');
23
+ subagents[agentId] = agentMessages;
24
+ }
25
+ }
26
+
27
+ // Inline subagent conversations into the main message list
28
+ inlineSubagents(messages, subagents);
29
+
30
+ // Aggregate token usage stats
31
+ const stats = aggregateUsage(lines);
32
+
33
+ return { messages, subagents: {}, sessionPath, stats };
34
+ }
35
+
36
+ function aggregateUsage(lines) {
37
+ let inputTokens = 0;
38
+ let outputTokens = 0;
39
+ let cacheCreation = 0;
40
+ let cacheRead = 0;
41
+ let turns = 0;
42
+ let firstTimestamp = null;
43
+ let lastTimestamp = null;
44
+
45
+ for (const line of lines) {
46
+ if (line.timestamp) {
47
+ if (!firstTimestamp) firstTimestamp = line.timestamp;
48
+ lastTimestamp = line.timestamp;
49
+ }
50
+
51
+ if (line.type === 'assistant') {
52
+ const usage = line.message?.usage;
53
+ if (usage) {
54
+ turns++;
55
+ inputTokens += usage.input_tokens || 0;
56
+ outputTokens += usage.output_tokens || 0;
57
+ cacheCreation += usage.cache_creation_input_tokens || 0;
58
+ cacheRead += usage.cache_read_input_tokens || 0;
59
+ }
60
+ }
61
+ }
62
+
63
+ const totalInput = inputTokens + cacheCreation + cacheRead;
64
+ const totalTokens = totalInput + outputTokens;
65
+ const cacheTotal = cacheCreation + cacheRead;
66
+ const cacheHitRate = cacheTotal > 0 ? cacheRead / cacheTotal : 0;
67
+
68
+ let durationMs = 0;
69
+ if (firstTimestamp && lastTimestamp) {
70
+ durationMs = new Date(lastTimestamp) - new Date(firstTimestamp);
71
+ }
72
+
73
+ return {
74
+ turns,
75
+ inputTokens,
76
+ outputTokens,
77
+ cacheCreation,
78
+ cacheRead,
79
+ totalTokens,
80
+ cacheHitRate,
81
+ durationMs,
82
+ };
83
+ }
84
+
85
+ function readJsonlLines(filePath) {
86
+ return new Promise((resolve) => {
87
+ const lines = [];
88
+ const rl = readline.createInterface({
89
+ input: fs.createReadStream(filePath, { encoding: 'utf-8' }),
90
+ crlfDelay: Infinity,
91
+ });
92
+
93
+ rl.on('line', (line) => {
94
+ try {
95
+ lines.push(JSON.parse(line));
96
+ } catch {
97
+ // skip
98
+ }
99
+ });
100
+
101
+ rl.on('close', () => resolve(lines));
102
+ rl.on('error', () => resolve(lines));
103
+ });
104
+ }
105
+
106
+ function buildConversation(lines) {
107
+ const messages = [];
108
+
109
+ // Deduplicate assistant messages by message.id (streaming: keep last)
110
+ const assistantById = new Map();
111
+ for (const line of lines) {
112
+ if (line.type === 'assistant' && line.message?.id) {
113
+ assistantById.set(line.message.id, line);
114
+ }
115
+ }
116
+ const seenAssistantIds = new Set();
117
+
118
+ for (const line of lines) {
119
+ // Skip metadata-only types
120
+ if (['file-history-snapshot', 'last-prompt', 'permission-mode'].includes(line.type)) {
121
+ continue;
122
+ }
123
+
124
+ if (line.type === 'user') {
125
+ processUserMessage(line, messages);
126
+ } else if (line.type === 'assistant') {
127
+ // Deduplicate: skip if we've already processed this message.id
128
+ const msgId = line.message?.id;
129
+ if (msgId) {
130
+ if (seenAssistantIds.has(msgId)) continue;
131
+ // Only process the final version
132
+ if (assistantById.get(msgId) !== line) continue;
133
+ seenAssistantIds.add(msgId);
134
+ }
135
+ processAssistantMessage(line, messages);
136
+ } else if (line.type === 'system') {
137
+ processSystemMessage(line, messages);
138
+ }
139
+ // Skip progress, queue-operation, etc.
140
+ }
141
+
142
+ // Pair tool_use with tool_result
143
+ pairToolMessages(messages);
144
+
145
+ return messages;
146
+ }
147
+
148
+ function processUserMessage(line, messages) {
149
+ const content = line.message?.content;
150
+ if (!content) return;
151
+
152
+ if (typeof content === 'string') {
153
+ if (content.trim()) {
154
+ messages.push({
155
+ type: 'user',
156
+ text: content,
157
+ timestamp: line.timestamp,
158
+ uuid: line.uuid,
159
+ });
160
+ }
161
+ return;
162
+ }
163
+
164
+ if (Array.isArray(content)) {
165
+ for (const block of content) {
166
+ if (block.type === 'text' && block.text?.trim()) {
167
+ messages.push({
168
+ type: 'user',
169
+ text: block.text,
170
+ timestamp: line.timestamp,
171
+ uuid: line.uuid,
172
+ });
173
+ } else if (block.type === 'tool_result') {
174
+ const toolResult = {
175
+ type: 'tool_result',
176
+ toolUseId: block.tool_use_id,
177
+ timestamp: line.timestamp,
178
+ uuid: line.uuid,
179
+ };
180
+
181
+ // Extract structured result from toolUseResult
182
+ if (line.toolUseResult) {
183
+ toolResult.richResult = line.toolUseResult;
184
+ }
185
+
186
+ // Raw content
187
+ if (typeof block.content === 'string') {
188
+ toolResult.content = block.content;
189
+ } else if (Array.isArray(block.content)) {
190
+ const texts = block.content
191
+ .filter(b => b.type === 'text')
192
+ .map(b => b.text);
193
+ toolResult.content = texts.join('\n');
194
+ }
195
+
196
+ // Try to read persisted output
197
+ if (toolResult.content && toolResult.content.includes('<persisted-output>')) {
198
+ const match = toolResult.content.match(/Full output saved to: ([^\n<]+)/);
199
+ if (match) {
200
+ const persistedPath = match[1].trim();
201
+ try {
202
+ if (fs.existsSync(persistedPath)) {
203
+ toolResult.fullContent = fs.readFileSync(persistedPath, 'utf-8');
204
+ }
205
+ } catch {
206
+ // ignore
207
+ }
208
+ }
209
+ }
210
+
211
+ messages.push(toolResult);
212
+ }
213
+ }
214
+ }
215
+ }
216
+
217
+ function processAssistantMessage(line, messages) {
218
+ const content = line.message?.content;
219
+ if (!content || !Array.isArray(content)) return;
220
+
221
+ for (const block of content) {
222
+ if (block.type === 'thinking') {
223
+ messages.push({
224
+ type: 'thinking',
225
+ timestamp: line.timestamp,
226
+ uuid: line.uuid,
227
+ });
228
+ } else if (block.type === 'text' && block.text?.trim()) {
229
+ messages.push({
230
+ type: 'assistant',
231
+ text: block.text,
232
+ model: line.message.model,
233
+ timestamp: line.timestamp,
234
+ uuid: line.uuid,
235
+ });
236
+ } else if (block.type === 'tool_use') {
237
+ messages.push({
238
+ type: 'tool_use',
239
+ toolName: block.name,
240
+ toolUseId: block.id,
241
+ input: block.input,
242
+ timestamp: line.timestamp,
243
+ uuid: line.uuid,
244
+ });
245
+ }
246
+ }
247
+ }
248
+
249
+ function processSystemMessage(line, messages) {
250
+ if (line.subtype === 'turn_duration') {
251
+ messages.push({
252
+ type: 'system',
253
+ subtype: 'turn_duration',
254
+ durationMs: line.durationMs,
255
+ timestamp: line.timestamp,
256
+ });
257
+ } else if (line.subtype === 'bridge_status') {
258
+ // skip
259
+ } else if (line.content) {
260
+ messages.push({
261
+ type: 'system',
262
+ subtype: line.subtype || 'info',
263
+ content: typeof line.content === 'string' ? line.content : JSON.stringify(line.content),
264
+ timestamp: line.timestamp,
265
+ });
266
+ }
267
+ }
268
+
269
+ function inlineSubagents(messages, subagents) {
270
+ if (!Object.keys(subagents).length) return;
271
+
272
+ // For each Agent tool_use that has a paired result, extract agentId from the result content
273
+ for (const msg of messages) {
274
+ if (msg.type !== 'tool_use' || msg.toolName !== 'Agent') continue;
275
+ if (!msg.result) continue;
276
+
277
+ const content = msg.result.content || msg.result.fullContent || '';
278
+ const match = content.match(/agentId:\s*([a-f0-9]+)/);
279
+ if (match) {
280
+ const agentId = match[1];
281
+ if (subagents[agentId]) {
282
+ msg.subagentMessages = subagents[agentId];
283
+ msg.subagentId = agentId;
284
+ }
285
+ }
286
+ }
287
+
288
+ // Fallback: match remaining subagents by description in meta files
289
+ const usedIds = new Set(
290
+ messages.filter(m => m.subagentId).map(m => m.subagentId)
291
+ );
292
+ const unmatchedAgents = Object.keys(subagents).filter(id => !usedIds.has(id));
293
+
294
+ if (unmatchedAgents.length) {
295
+ for (const msg of messages) {
296
+ if (msg.type !== 'tool_use' || msg.toolName !== 'Agent') continue;
297
+ if (msg.subagentMessages) continue;
298
+
299
+ const desc = (msg.input?.description || '').toLowerCase();
300
+ if (!desc) continue;
301
+
302
+ for (let i = unmatchedAgents.length - 1; i >= 0; i--) {
303
+ const agentId = unmatchedAgents[i];
304
+ // Check first user message of subagent for matching description
305
+ const firstMsg = subagents[agentId]?.[0];
306
+ if (firstMsg?.type === 'user') {
307
+ const agentText = (firstMsg.text || '').toLowerCase();
308
+ if (agentText.includes(desc) || desc.includes(agentText.slice(0, 30))) {
309
+ msg.subagentMessages = subagents[agentId];
310
+ msg.subagentId = agentId;
311
+ unmatchedAgents.splice(i, 1);
312
+ break;
313
+ }
314
+ }
315
+ }
316
+ }
317
+ }
318
+ }
319
+
320
+ function pairToolMessages(messages) {
321
+ // Build map of tool_use id → index
322
+ const toolUseMap = new Map();
323
+ for (let i = 0; i < messages.length; i++) {
324
+ if (messages[i].type === 'tool_use') {
325
+ toolUseMap.set(messages[i].toolUseId, i);
326
+ }
327
+ }
328
+
329
+ // Attach tool_result to corresponding tool_use
330
+ for (let i = 0; i < messages.length; i++) {
331
+ if (messages[i].type === 'tool_result' && messages[i].toolUseId) {
332
+ const useIdx = toolUseMap.get(messages[i].toolUseId);
333
+ if (useIdx !== undefined) {
334
+ messages[useIdx].result = messages[i];
335
+ messages[i]._paired = true;
336
+ }
337
+ }
338
+ }
339
+
340
+ // Remove paired tool_results (they're now nested in tool_use)
341
+ for (let i = messages.length - 1; i >= 0; i--) {
342
+ if (messages[i]._paired) {
343
+ messages.splice(i, 1);
344
+ }
345
+ }
346
+ }
347
+
348
+ module.exports = { parseSession };