ccakashic 0.2.4 → 0.2.5

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 DELETED
@@ -1,496 +0,0 @@
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
- // Calculate per-turn token usage (tokens between each user input)
146
- calculateTurnUsage(messages);
147
-
148
- // Calculate elapsed time for each message (time since previous message)
149
- calculateElapsed(messages);
150
-
151
- return messages;
152
- }
153
-
154
- function parseLocalCommand(text) {
155
- // Parse <bash-input>, <bash-stdout>, <bash-stderr> tags
156
- const inputMatch = text.match(/<bash-input>([\s\S]*?)<\/bash-input>/);
157
- const stdoutMatch = text.match(/<bash-stdout>([\s\S]*?)<\/bash-stdout>/);
158
- const stderrMatch = text.match(/<bash-stderr>([\s\S]*?)<\/bash-stderr>/);
159
-
160
- if (inputMatch) {
161
- return { type: 'local_command', subtype: 'input', command: inputMatch[1] };
162
- }
163
- if (stdoutMatch || stderrMatch) {
164
- return {
165
- type: 'local_command',
166
- subtype: 'output',
167
- stdout: stdoutMatch ? stdoutMatch[1] : '',
168
- stderr: stderrMatch ? stderrMatch[1] : '',
169
- };
170
- }
171
- return null;
172
- }
173
-
174
- function processUserText(text, line, messages) {
175
- // Skip local-command-caveat (system instruction, not user input)
176
- if (text.match(/^<local-command-caveat>/)) {
177
- return;
178
- }
179
-
180
- // Parse local command input/output
181
- const localCmd = parseLocalCommand(text);
182
- if (localCmd) {
183
- if (localCmd.subtype === 'input') {
184
- messages.push({
185
- type: 'local_command',
186
- command: localCmd.command,
187
- timestamp: line.timestamp,
188
- uuid: line.uuid,
189
- });
190
- } else if (localCmd.subtype === 'output') {
191
- // Attach to previous local_command if exists
192
- const prev = messages.length > 0 ? messages[messages.length - 1] : null;
193
- if (prev && prev.type === 'local_command' && !prev.stdout) {
194
- prev.stdout = localCmd.stdout;
195
- prev.stderr = localCmd.stderr;
196
- } else {
197
- messages.push({
198
- type: 'local_command',
199
- command: '',
200
- stdout: localCmd.stdout,
201
- stderr: localCmd.stderr,
202
- timestamp: line.timestamp,
203
- uuid: line.uuid,
204
- });
205
- }
206
- }
207
- return;
208
- }
209
-
210
- // Strip system-reminder tags but keep surrounding user text
211
- const stripped = text
212
- .replace(/<system-reminder>[\s\S]*?<\/system-reminder>/g, '')
213
- .replace(/<command-name>[\s\S]*?<\/command-name>/g, '')
214
- .replace(/<command-message>[\s\S]*?<\/command-message>/g, '')
215
- .replace(/<command-args>[\s\S]*?<\/command-args>/g, '')
216
- .replace(/<local-command-stdout>[\s\S]*?<\/local-command-stdout>/g, '')
217
- .trim();
218
-
219
- if (stripped) {
220
- messages.push({
221
- type: 'user',
222
- text: stripped,
223
- timestamp: line.timestamp,
224
- uuid: line.uuid,
225
- });
226
- }
227
- }
228
-
229
- function processUserMessage(line, messages) {
230
- const content = line.message?.content;
231
- if (!content) return;
232
-
233
- if (typeof content === 'string') {
234
- if (content.trim()) {
235
- processUserText(content, line, messages);
236
- }
237
- return;
238
- }
239
-
240
- if (Array.isArray(content)) {
241
- for (const block of content) {
242
- if (block.type === 'text' && block.text?.trim()) {
243
- processUserText(block.text, line, messages);
244
- } else if (block.type === 'tool_result') {
245
- const toolResult = {
246
- type: 'tool_result',
247
- toolUseId: block.tool_use_id,
248
- timestamp: line.timestamp,
249
- uuid: line.uuid,
250
- };
251
-
252
- // Extract structured result from toolUseResult
253
- if (line.toolUseResult) {
254
- toolResult.richResult = line.toolUseResult;
255
- }
256
-
257
- // Raw content
258
- if (typeof block.content === 'string') {
259
- toolResult.content = block.content;
260
- } else if (Array.isArray(block.content)) {
261
- const texts = block.content
262
- .filter(b => b.type === 'text')
263
- .map(b => b.text);
264
- toolResult.content = texts.join('\n');
265
- }
266
-
267
- // Try to read persisted output
268
- if (toolResult.content && toolResult.content.includes('<persisted-output>')) {
269
- const match = toolResult.content.match(/Full output saved to: ([^\n<]+)/);
270
- if (match) {
271
- const persistedPath = match[1].trim();
272
- try {
273
- if (fs.existsSync(persistedPath)) {
274
- toolResult.fullContent = fs.readFileSync(persistedPath, 'utf-8');
275
- }
276
- } catch {
277
- // ignore
278
- }
279
- }
280
- }
281
-
282
- messages.push(toolResult);
283
- }
284
- }
285
- }
286
- }
287
-
288
- function processAssistantMessage(line, messages) {
289
- const content = line.message?.content;
290
- if (!content || !Array.isArray(content)) return;
291
-
292
- const usage = line.message?.usage || null;
293
- let usageAssigned = false;
294
-
295
- for (const block of content) {
296
- if (block.type === 'thinking') {
297
- messages.push({
298
- type: 'thinking',
299
- timestamp: line.timestamp,
300
- uuid: line.uuid,
301
- });
302
- } else if (block.type === 'text' && block.text?.trim()) {
303
- messages.push({
304
- type: 'assistant',
305
- text: block.text,
306
- model: line.message.model,
307
- timestamp: line.timestamp,
308
- uuid: line.uuid,
309
- usage: !usageAssigned ? usage : null,
310
- });
311
- usageAssigned = true;
312
- } else if (block.type === 'tool_use') {
313
- messages.push({
314
- type: 'tool_use',
315
- toolName: block.name,
316
- toolUseId: block.id,
317
- input: block.input,
318
- timestamp: line.timestamp,
319
- uuid: line.uuid,
320
- usage: !usageAssigned ? usage : null,
321
- });
322
- usageAssigned = true;
323
- }
324
- }
325
- }
326
-
327
- function calculateTurnUsage(messages) {
328
- // Walk backwards from each user message, summing usage of preceding assistant messages
329
- // A "turn" = user input → all assistant responses until next user input
330
- for (let i = 0; i < messages.length; i++) {
331
- if (messages[i].type !== 'user' && messages[i].type !== 'local_command') continue;
332
-
333
- // Sum usage of all following assistant/tool messages until next user message
334
- let totalInput = 0;
335
- let totalOutput = 0;
336
- let totalCacheRead = 0;
337
- let totalCacheCreate = 0;
338
- const seenUsageIds = new Set(); // Dedupe (multiple blocks from same API response)
339
-
340
- for (let j = i + 1; j < messages.length; j++) {
341
- const m = messages[j];
342
- if (m.type === 'user' || m.type === 'local_command') break;
343
- if (m.usage && !seenUsageIds.has(m.uuid + (m.usage.input_tokens || 0))) {
344
- seenUsageIds.add(m.uuid + (m.usage.input_tokens || 0));
345
- totalInput += m.usage.input_tokens || 0;
346
- totalOutput += m.usage.output_tokens || 0;
347
- totalCacheRead += m.usage.cache_read_input_tokens || 0;
348
- totalCacheCreate += m.usage.cache_creation_input_tokens || 0;
349
- }
350
- }
351
-
352
- // Find turn_duration in the following messages
353
- let turnDurationMs = 0;
354
- for (let j = i + 1; j < messages.length; j++) {
355
- const m = messages[j];
356
- if (m.type === 'user' || m.type === 'local_command') break;
357
- if (m.type === 'system' && m.subtype === 'turn_duration') {
358
- turnDurationMs = m.durationMs || 0;
359
- break;
360
- }
361
- }
362
-
363
- const total = totalInput + totalOutput + totalCacheRead + totalCacheCreate;
364
- if (total > 0 || turnDurationMs > 0) {
365
- messages[i].turnUsage = {
366
- input: totalInput,
367
- output: totalOutput,
368
- cacheRead: totalCacheRead,
369
- cacheCreate: totalCacheCreate,
370
- total,
371
- durationMs: turnDurationMs,
372
- };
373
- }
374
- }
375
- }
376
-
377
- function calculateElapsed(messages) {
378
- for (let i = 1; i < messages.length; i++) {
379
- const prev = messages[i - 1];
380
- const curr = messages[i];
381
- if (prev.timestamp && curr.timestamp) {
382
- const elapsed = new Date(curr.timestamp) - new Date(prev.timestamp);
383
- if (elapsed > 0) {
384
- curr.elapsedMs = elapsed;
385
- }
386
- }
387
- // For tool_use with paired result, calculate execution time
388
- if (curr.type === 'tool_use' && curr.result?.timestamp && curr.timestamp) {
389
- const execTime = new Date(curr.result.timestamp) - new Date(curr.timestamp);
390
- if (execTime > 0) {
391
- curr.execMs = execTime;
392
- }
393
- }
394
- }
395
- }
396
-
397
- function processSystemMessage(line, messages) {
398
- if (line.subtype === 'turn_duration') {
399
- messages.push({
400
- type: 'system',
401
- subtype: 'turn_duration',
402
- durationMs: line.durationMs,
403
- timestamp: line.timestamp,
404
- });
405
- } else if (line.subtype === 'bridge_status') {
406
- // skip
407
- } else if (line.content) {
408
- messages.push({
409
- type: 'system',
410
- subtype: line.subtype || 'info',
411
- content: typeof line.content === 'string' ? line.content : JSON.stringify(line.content),
412
- timestamp: line.timestamp,
413
- });
414
- }
415
- }
416
-
417
- function inlineSubagents(messages, subagents) {
418
- if (!Object.keys(subagents).length) return;
419
-
420
- // For each Agent tool_use that has a paired result, extract agentId from the result content
421
- for (const msg of messages) {
422
- if (msg.type !== 'tool_use' || msg.toolName !== 'Agent') continue;
423
- if (!msg.result) continue;
424
-
425
- const content = msg.result.content || msg.result.fullContent || '';
426
- const match = content.match(/agentId:\s*([a-f0-9]+)/);
427
- if (match) {
428
- const agentId = match[1];
429
- if (subagents[agentId]) {
430
- msg.subagentMessages = subagents[agentId];
431
- msg.subagentId = agentId;
432
- }
433
- }
434
- }
435
-
436
- // Fallback: match remaining subagents by description in meta files
437
- const usedIds = new Set(
438
- messages.filter(m => m.subagentId).map(m => m.subagentId)
439
- );
440
- const unmatchedAgents = Object.keys(subagents).filter(id => !usedIds.has(id));
441
-
442
- if (unmatchedAgents.length) {
443
- for (const msg of messages) {
444
- if (msg.type !== 'tool_use' || msg.toolName !== 'Agent') continue;
445
- if (msg.subagentMessages) continue;
446
-
447
- const desc = (msg.input?.description || '').toLowerCase();
448
- if (!desc) continue;
449
-
450
- for (let i = unmatchedAgents.length - 1; i >= 0; i--) {
451
- const agentId = unmatchedAgents[i];
452
- // Check first user message of subagent for matching description
453
- const firstMsg = subagents[agentId]?.[0];
454
- if (firstMsg?.type === 'user') {
455
- const agentText = (firstMsg.text || '').toLowerCase();
456
- if (agentText.includes(desc) || desc.includes(agentText.slice(0, 30))) {
457
- msg.subagentMessages = subagents[agentId];
458
- msg.subagentId = agentId;
459
- unmatchedAgents.splice(i, 1);
460
- break;
461
- }
462
- }
463
- }
464
- }
465
- }
466
- }
467
-
468
- function pairToolMessages(messages) {
469
- // Build map of tool_use id → index
470
- const toolUseMap = new Map();
471
- for (let i = 0; i < messages.length; i++) {
472
- if (messages[i].type === 'tool_use') {
473
- toolUseMap.set(messages[i].toolUseId, i);
474
- }
475
- }
476
-
477
- // Attach tool_result to corresponding tool_use
478
- for (let i = 0; i < messages.length; i++) {
479
- if (messages[i].type === 'tool_result' && messages[i].toolUseId) {
480
- const useIdx = toolUseMap.get(messages[i].toolUseId);
481
- if (useIdx !== undefined) {
482
- messages[useIdx].result = messages[i];
483
- messages[i]._paired = true;
484
- }
485
- }
486
- }
487
-
488
- // Remove paired tool_results (they're now nested in tool_use)
489
- for (let i = messages.length - 1; i >= 0; i--) {
490
- if (messages[i]._paired) {
491
- messages.splice(i, 1);
492
- }
493
- }
494
- }
495
-
496
- module.exports = { parseSession };