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/README.md +51 -0
- package/bin/cctape.js +94 -0
- package/lib/discover.js +153 -0
- package/lib/html-generator.js +496 -0
- package/lib/pages.js +393 -0
- package/lib/parser.js +348 -0
- package/lib/template-assets.js +593 -0
- package/package.json +41 -0
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 };
|