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/dist/bin/ccakashic.js +248 -0
- package/dist/discover.js +230 -0
- package/dist/html-generator.js +554 -0
- package/{lib → dist}/pages.js +65 -80
- package/dist/parser.js +465 -0
- package/{lib → dist}/template-assets.js +6 -7
- package/package.json +14 -5
- package/bin/ccakashic.js +0 -225
- package/lib/discover.js +0 -221
- package/lib/html-generator.js +0 -586
- package/lib/parser.js +0 -496
package/{lib → dist}/pages.js
RENAMED
|
@@ -1,41 +1,40 @@
|
|
|
1
|
-
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.generateIndex = generateIndex;
|
|
4
|
+
exports.generateSessionList = generateSessionList;
|
|
5
|
+
const template_assets_1 = require("./template-assets");
|
|
5
6
|
function escapeHtml(str) {
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
7
|
+
return String(str)
|
|
8
|
+
.replace(/&/g, '&')
|
|
9
|
+
.replace(/</g, '<')
|
|
10
|
+
.replace(/>/g, '>')
|
|
11
|
+
.replace(/"/g, '"');
|
|
11
12
|
}
|
|
12
|
-
|
|
13
13
|
function formatDate(ts) {
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
14
|
+
if (!ts)
|
|
15
|
+
return '';
|
|
16
|
+
const d = ts instanceof Date ? ts : new Date(ts);
|
|
17
|
+
return d.toLocaleDateString('en-CA') + ' ' + d.toLocaleTimeString('en-GB', { hour: '2-digit', minute: '2-digit' });
|
|
17
18
|
}
|
|
18
|
-
|
|
19
19
|
function formatTokens(n) {
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
20
|
+
if (n >= 1000000)
|
|
21
|
+
return (n / 1000000).toFixed(1) + 'M';
|
|
22
|
+
if (n >= 1000)
|
|
23
|
+
return (n / 1000).toFixed(1) + 'K';
|
|
24
|
+
return String(n);
|
|
23
25
|
}
|
|
24
|
-
|
|
25
26
|
const GITHUB_URL = 'https://github.com/ashimon83/ccakashic';
|
|
26
|
-
|
|
27
27
|
function githubCorner() {
|
|
28
|
-
|
|
28
|
+
return `<a href="${GITHUB_URL}" class="github-corner" aria-label="View source on GitHub" target="_blank" rel="noopener"><svg width="70" height="70" viewBox="0 0 250 250" aria-hidden="true"><path d="M0,0 L115,115 L130,115 L142,142 L250,250 L250,0 Z"></path><path d="M128.3,109.0 C113.8,99.7 119.0,89.6 119.0,89.6 C122.0,82.7 120.5,78.6 120.5,78.6 C119.2,72.0 123.4,76.3 123.4,76.3 C127.3,80.9 125.5,87.3 125.5,87.3 C122.9,97.6 130.6,101.9 134.4,103.2" fill="currentColor" style="transform-origin: 130px 106px;" class="octo-arm"></path><path d="M115.0,115.0 C114.9,115.1 118.7,116.5 119.8,115.4 L133.7,101.6 C136.9,99.2 139.9,98.4 142.2,98.6 C133.8,88.0 127.5,74.4 143.8,58.0 C148.5,53.4 154.0,51.2 159.7,51.0 C160.3,49.4 163.2,43.6 171.4,40.1 C171.4,40.1 176.1,42.5 178.8,56.2 C183.1,58.6 187.2,61.8 190.9,65.4 C194.5,69.0 197.7,73.2 200.1,77.6 C213.8,80.2 216.3,84.9 216.3,84.9 C212.7,93.1 206.9,96.0 205.4,96.6 C205.1,102.4 203.0,107.8 198.3,112.5 C181.9,128.9 168.3,122.5 157.7,114.1 C157.9,116.9 156.7,120.9 152.7,124.9 L141.0,136.5 C139.8,137.7 141.6,141.9 141.8,141.8 Z" fill="currentColor" class="octo-body"></path></svg></a>`;
|
|
29
29
|
}
|
|
30
|
-
|
|
31
30
|
function pageShell(title, bodyHtml) {
|
|
32
|
-
|
|
31
|
+
return `<!DOCTYPE html>
|
|
33
32
|
<html lang="en">
|
|
34
33
|
<head>
|
|
35
34
|
<meta charset="utf-8">
|
|
36
35
|
<meta name="viewport" content="width=device-width, initial-scale=1">
|
|
37
36
|
<title>${escapeHtml(title)}</title>
|
|
38
|
-
<style>${getCSS()}
|
|
37
|
+
<style>${(0, template_assets_1.getCSS)()}
|
|
39
38
|
${indexCSS()}
|
|
40
39
|
</style>
|
|
41
40
|
</head>
|
|
@@ -45,9 +44,8 @@ ${bodyHtml}
|
|
|
45
44
|
</body>
|
|
46
45
|
</html>`;
|
|
47
46
|
}
|
|
48
|
-
|
|
49
47
|
function indexCSS() {
|
|
50
|
-
|
|
48
|
+
return `
|
|
51
49
|
.page-header {
|
|
52
50
|
max-width: 900px;
|
|
53
51
|
margin: 0 auto;
|
|
@@ -238,20 +236,18 @@ function indexCSS() {
|
|
|
238
236
|
}
|
|
239
237
|
`;
|
|
240
238
|
}
|
|
241
|
-
|
|
242
239
|
function generateIndex(projects) {
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
240
|
+
const items = projects.map((p) => {
|
|
241
|
+
const href = `/project/${encodeURIComponent(p.rawName)}`;
|
|
242
|
+
return `<a class="list-item" href="${href}">
|
|
246
243
|
<div class="list-item-title">${escapeHtml(p.name)}</div>
|
|
247
244
|
<div class="list-item-meta">
|
|
248
245
|
<span>${p.sessionCount} sessions</span>
|
|
249
246
|
<span>Last: ${formatDate(p.lastModified)}</span>
|
|
250
247
|
</div>
|
|
251
248
|
</a>`;
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
return pageShell('ccakashic', `
|
|
249
|
+
}).join('\n');
|
|
250
|
+
return pageShell('ccakashic', `
|
|
255
251
|
<div class="page-header">
|
|
256
252
|
<h1>ccakashic</h1>
|
|
257
253
|
<div class="subtitle">Claude Code Session Logs</div>
|
|
@@ -269,65 +265,56 @@ document.querySelector('.search-box').addEventListener('input', function(e) {
|
|
|
269
265
|
});
|
|
270
266
|
</script>`);
|
|
271
267
|
}
|
|
272
|
-
|
|
273
268
|
function formatDateOnly(ts) {
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
|
|
269
|
+
if (!ts)
|
|
270
|
+
return '';
|
|
271
|
+
const d = ts instanceof Date ? ts : new Date(ts);
|
|
272
|
+
return d.toLocaleDateString('en-CA', { year: 'numeric', month: '2-digit', day: '2-digit' });
|
|
277
273
|
}
|
|
278
|
-
|
|
279
274
|
function formatTimeOnly(ts) {
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
|
|
275
|
+
if (!ts)
|
|
276
|
+
return '';
|
|
277
|
+
const d = ts instanceof Date ? ts : new Date(ts);
|
|
278
|
+
return d.toLocaleTimeString('en-GB', { hour: '2-digit', minute: '2-digit' });
|
|
283
279
|
}
|
|
284
|
-
|
|
285
280
|
function generateSessionList(project, sessions) {
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
|
|
289
|
-
|
|
290
|
-
|
|
291
|
-
|
|
292
|
-
|
|
293
|
-
|
|
281
|
+
const dateGroups = [];
|
|
282
|
+
let currentDate = null;
|
|
283
|
+
for (const s of sessions) {
|
|
284
|
+
const dateStr = formatDateOnly(new Date(s.lastModified));
|
|
285
|
+
if (dateStr !== currentDate) {
|
|
286
|
+
currentDate = dateStr;
|
|
287
|
+
dateGroups.push({ date: dateStr, sessions: [] });
|
|
288
|
+
}
|
|
289
|
+
dateGroups[dateGroups.length - 1].sessions.push(s);
|
|
294
290
|
}
|
|
295
|
-
|
|
296
|
-
|
|
297
|
-
|
|
298
|
-
|
|
299
|
-
|
|
300
|
-
|
|
301
|
-
|
|
302
|
-
|
|
303
|
-
|
|
304
|
-
|
|
305
|
-
|
|
306
|
-
|
|
307
|
-
|
|
308
|
-
|
|
309
|
-
|
|
310
|
-
|
|
311
|
-
const model = s.model ? `<span>${escapeHtml(s.model)}</span>` : '';
|
|
312
|
-
const branch = s.gitBranch && s.gitBranch !== 'HEAD' ? `<span>branch: ${escapeHtml(s.gitBranch)}</span>` : '';
|
|
313
|
-
const tokens = s.totalTokens ? `<span>${formatTokens(s.totalTokens)} tokens</span>` : '';
|
|
314
|
-
const outTok = s.outputTokens ? `<span>out: ${formatTokens(s.outputTokens)}</span>` : '';
|
|
315
|
-
const preview = s.preview ? `<div class="list-item-preview">${escapeHtml(s.preview)}</div>` : '';
|
|
316
|
-
|
|
317
|
-
return `<a class="list-item" href="${href}">
|
|
291
|
+
// Build side nav
|
|
292
|
+
const sideNavItems = dateGroups.map(g => `<a class="sidenav-item" href="#date-${g.date}" data-date="${escapeHtml(g.date)}">${escapeHtml(g.date)} <span class="sidenav-count">(${g.sessions.length})</span></a>`).join('\n');
|
|
293
|
+
// Build session items grouped by date
|
|
294
|
+
const groupsHtml = dateGroups.map(g => {
|
|
295
|
+
const items = g.sessions.map(s => {
|
|
296
|
+
const href = `/project/${encodeURIComponent(project.rawName)}/session/${encodeURIComponent(s.id)}`;
|
|
297
|
+
const lastModTime = formatTimeOnly(new Date(s.lastModified));
|
|
298
|
+
const startedTime = formatDate(s.timestamp);
|
|
299
|
+
const slug = s.slug ? `<span class="badge">${escapeHtml(s.slug)}</span>` : '';
|
|
300
|
+
const sub = s.hasSubagents ? '<span class="badge">subagents</span>' : '';
|
|
301
|
+
const model = s.model ? `<span>${escapeHtml(s.model)}</span>` : '';
|
|
302
|
+
const branch = s.gitBranch && s.gitBranch !== 'HEAD' ? `<span>branch: ${escapeHtml(s.gitBranch)}</span>` : '';
|
|
303
|
+
const tokens = s.totalTokens ? `<span>${formatTokens(s.totalTokens)} tokens</span>` : '';
|
|
304
|
+
const outTok = s.outputTokens ? `<span>out: ${formatTokens(s.outputTokens)}</span>` : '';
|
|
305
|
+
const preview = s.preview ? `<div class="list-item-preview">${escapeHtml(s.preview)}</div>` : '';
|
|
306
|
+
return `<a class="list-item" href="${href}">
|
|
318
307
|
<div class="list-item-title">${lastModTime} ${slug} ${sub}</div>
|
|
319
308
|
<div class="list-item-meta"><span>started: ${startedTime}</span>${model}${branch}${tokens}${outTok}</div>
|
|
320
309
|
${preview}
|
|
321
310
|
</a>`;
|
|
322
|
-
|
|
323
|
-
|
|
324
|
-
return `<div class="date-group" id="date-${g.date}" data-date="${escapeHtml(g.date)}">
|
|
311
|
+
}).join('\n');
|
|
312
|
+
return `<div class="date-group" id="date-${g.date}" data-date="${escapeHtml(g.date)}">
|
|
325
313
|
<div class="date-heading">${escapeHtml(g.date)}</div>
|
|
326
314
|
${items}
|
|
327
315
|
</div>`;
|
|
328
|
-
|
|
329
|
-
|
|
330
|
-
return pageShell(`${project.name} — ccakashic`, `
|
|
316
|
+
}).join('\n');
|
|
317
|
+
return pageShell(`${project.name} — ccakashic`, `
|
|
331
318
|
<div class="sticky-date-bar" id="stickyDateBar"></div>
|
|
332
319
|
<div class="page-header">
|
|
333
320
|
<div class="breadcrumb"><a href="/">ccakashic</a> / ${escapeHtml(project.name)}</div>
|
|
@@ -389,5 +376,3 @@ function generateSessionList(project, sessions) {
|
|
|
389
376
|
})();
|
|
390
377
|
</script>`);
|
|
391
378
|
}
|
|
392
|
-
|
|
393
|
-
module.exports = { generateIndex, generateSessionList };
|
package/dist/parser.js
ADDED
|
@@ -0,0 +1,465 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
|
|
3
|
+
if (k2 === undefined) k2 = k;
|
|
4
|
+
var desc = Object.getOwnPropertyDescriptor(m, k);
|
|
5
|
+
if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
|
|
6
|
+
desc = { enumerable: true, get: function() { return m[k]; } };
|
|
7
|
+
}
|
|
8
|
+
Object.defineProperty(o, k2, desc);
|
|
9
|
+
}) : (function(o, m, k, k2) {
|
|
10
|
+
if (k2 === undefined) k2 = k;
|
|
11
|
+
o[k2] = m[k];
|
|
12
|
+
}));
|
|
13
|
+
var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
|
|
14
|
+
Object.defineProperty(o, "default", { enumerable: true, value: v });
|
|
15
|
+
}) : function(o, v) {
|
|
16
|
+
o["default"] = v;
|
|
17
|
+
});
|
|
18
|
+
var __importStar = (this && this.__importStar) || (function () {
|
|
19
|
+
var ownKeys = function(o) {
|
|
20
|
+
ownKeys = Object.getOwnPropertyNames || function (o) {
|
|
21
|
+
var ar = [];
|
|
22
|
+
for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k;
|
|
23
|
+
return ar;
|
|
24
|
+
};
|
|
25
|
+
return ownKeys(o);
|
|
26
|
+
};
|
|
27
|
+
return function (mod) {
|
|
28
|
+
if (mod && mod.__esModule) return mod;
|
|
29
|
+
var result = {};
|
|
30
|
+
if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]);
|
|
31
|
+
__setModuleDefault(result, mod);
|
|
32
|
+
return result;
|
|
33
|
+
};
|
|
34
|
+
})();
|
|
35
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
36
|
+
exports.parseSession = parseSession;
|
|
37
|
+
const fs = __importStar(require("fs"));
|
|
38
|
+
const path = __importStar(require("path"));
|
|
39
|
+
const readline = __importStar(require("readline"));
|
|
40
|
+
async function parseSession(sessionPath) {
|
|
41
|
+
const lines = await readJsonlLines(sessionPath);
|
|
42
|
+
const messages = buildConversation(lines);
|
|
43
|
+
const sessionId = path.basename(sessionPath, '.jsonl');
|
|
44
|
+
const subagentsDir = path.join(path.dirname(sessionPath), sessionId, 'subagents');
|
|
45
|
+
const subagents = {};
|
|
46
|
+
if (fs.existsSync(subagentsDir)) {
|
|
47
|
+
const agentFiles = fs.readdirSync(subagentsDir).filter((f) => f.endsWith('.jsonl'));
|
|
48
|
+
for (const f of agentFiles) {
|
|
49
|
+
const agentLines = await readJsonlLines(path.join(subagentsDir, f));
|
|
50
|
+
const agentMessages = buildConversation(agentLines);
|
|
51
|
+
const agentId = f.replace(/^agent-/, '').replace(/\.jsonl$/, '');
|
|
52
|
+
subagents[agentId] = agentMessages;
|
|
53
|
+
}
|
|
54
|
+
}
|
|
55
|
+
inlineSubagents(messages, subagents);
|
|
56
|
+
const stats = aggregateUsage(lines);
|
|
57
|
+
return { messages, subagents: {}, sessionPath, stats };
|
|
58
|
+
}
|
|
59
|
+
function aggregateUsage(lines) {
|
|
60
|
+
let inputTokens = 0;
|
|
61
|
+
let outputTokens = 0;
|
|
62
|
+
let cacheCreation = 0;
|
|
63
|
+
let cacheRead = 0;
|
|
64
|
+
let turns = 0;
|
|
65
|
+
let firstTimestamp = null;
|
|
66
|
+
let lastTimestamp = null;
|
|
67
|
+
for (const line of lines) {
|
|
68
|
+
if (line.timestamp) {
|
|
69
|
+
if (!firstTimestamp)
|
|
70
|
+
firstTimestamp = line.timestamp;
|
|
71
|
+
lastTimestamp = line.timestamp;
|
|
72
|
+
}
|
|
73
|
+
if (line.type === 'assistant') {
|
|
74
|
+
const usage = line.message?.usage;
|
|
75
|
+
if (usage) {
|
|
76
|
+
turns++;
|
|
77
|
+
inputTokens += usage.input_tokens || 0;
|
|
78
|
+
outputTokens += usage.output_tokens || 0;
|
|
79
|
+
cacheCreation += usage.cache_creation_input_tokens || 0;
|
|
80
|
+
cacheRead += usage.cache_read_input_tokens || 0;
|
|
81
|
+
}
|
|
82
|
+
}
|
|
83
|
+
}
|
|
84
|
+
const totalInput = inputTokens + cacheCreation + cacheRead;
|
|
85
|
+
const totalTokens = totalInput + outputTokens;
|
|
86
|
+
const cacheTotal = cacheCreation + cacheRead;
|
|
87
|
+
const cacheHitRate = cacheTotal > 0 ? cacheRead / cacheTotal : 0;
|
|
88
|
+
let durationMs = 0;
|
|
89
|
+
if (firstTimestamp && lastTimestamp) {
|
|
90
|
+
durationMs = new Date(lastTimestamp).getTime() - new Date(firstTimestamp).getTime();
|
|
91
|
+
}
|
|
92
|
+
return {
|
|
93
|
+
turns,
|
|
94
|
+
inputTokens,
|
|
95
|
+
outputTokens,
|
|
96
|
+
cacheCreation,
|
|
97
|
+
cacheRead,
|
|
98
|
+
totalTokens,
|
|
99
|
+
cacheHitRate,
|
|
100
|
+
durationMs,
|
|
101
|
+
};
|
|
102
|
+
}
|
|
103
|
+
function readJsonlLines(filePath) {
|
|
104
|
+
return new Promise((resolve) => {
|
|
105
|
+
const lines = [];
|
|
106
|
+
const rl = readline.createInterface({
|
|
107
|
+
input: fs.createReadStream(filePath, { encoding: 'utf-8' }),
|
|
108
|
+
crlfDelay: Infinity,
|
|
109
|
+
});
|
|
110
|
+
rl.on('line', (line) => {
|
|
111
|
+
try {
|
|
112
|
+
lines.push(JSON.parse(line));
|
|
113
|
+
}
|
|
114
|
+
catch {
|
|
115
|
+
// skip
|
|
116
|
+
}
|
|
117
|
+
});
|
|
118
|
+
rl.on('close', () => resolve(lines));
|
|
119
|
+
rl.on('error', () => resolve(lines));
|
|
120
|
+
});
|
|
121
|
+
}
|
|
122
|
+
function buildConversation(lines) {
|
|
123
|
+
const messages = [];
|
|
124
|
+
const assistantById = new Map();
|
|
125
|
+
for (const line of lines) {
|
|
126
|
+
if (line.type === 'assistant' && line.message?.id) {
|
|
127
|
+
assistantById.set(line.message.id, line);
|
|
128
|
+
}
|
|
129
|
+
}
|
|
130
|
+
const seenAssistantIds = new Set();
|
|
131
|
+
for (const line of lines) {
|
|
132
|
+
if (['file-history-snapshot', 'last-prompt', 'permission-mode'].includes(line.type)) {
|
|
133
|
+
continue;
|
|
134
|
+
}
|
|
135
|
+
if (line.type === 'user') {
|
|
136
|
+
processUserMessage(line, messages);
|
|
137
|
+
}
|
|
138
|
+
else if (line.type === 'assistant') {
|
|
139
|
+
const msgId = line.message?.id;
|
|
140
|
+
if (msgId) {
|
|
141
|
+
if (seenAssistantIds.has(msgId))
|
|
142
|
+
continue;
|
|
143
|
+
if (assistantById.get(msgId) !== line)
|
|
144
|
+
continue;
|
|
145
|
+
seenAssistantIds.add(msgId);
|
|
146
|
+
}
|
|
147
|
+
processAssistantMessage(line, messages);
|
|
148
|
+
}
|
|
149
|
+
else if (line.type === 'system') {
|
|
150
|
+
processSystemMessage(line, messages);
|
|
151
|
+
}
|
|
152
|
+
}
|
|
153
|
+
pairToolMessages(messages);
|
|
154
|
+
calculateTurnUsage(messages);
|
|
155
|
+
calculateElapsed(messages);
|
|
156
|
+
return messages;
|
|
157
|
+
}
|
|
158
|
+
function parseLocalCommand(text) {
|
|
159
|
+
const inputMatch = text.match(/<bash-input>([\s\S]*?)<\/bash-input>/);
|
|
160
|
+
const stdoutMatch = text.match(/<bash-stdout>([\s\S]*?)<\/bash-stdout>/);
|
|
161
|
+
const stderrMatch = text.match(/<bash-stderr>([\s\S]*?)<\/bash-stderr>/);
|
|
162
|
+
if (inputMatch) {
|
|
163
|
+
return { type: 'local_command', subtype: 'input', command: inputMatch[1] };
|
|
164
|
+
}
|
|
165
|
+
if (stdoutMatch || stderrMatch) {
|
|
166
|
+
return {
|
|
167
|
+
type: 'local_command',
|
|
168
|
+
subtype: 'output',
|
|
169
|
+
stdout: stdoutMatch ? stdoutMatch[1] : '',
|
|
170
|
+
stderr: stderrMatch ? stderrMatch[1] : '',
|
|
171
|
+
};
|
|
172
|
+
}
|
|
173
|
+
return null;
|
|
174
|
+
}
|
|
175
|
+
function processUserText(text, line, messages) {
|
|
176
|
+
if (text.match(/^<local-command-caveat>/)) {
|
|
177
|
+
return;
|
|
178
|
+
}
|
|
179
|
+
const localCmd = parseLocalCommand(text);
|
|
180
|
+
if (localCmd) {
|
|
181
|
+
if (localCmd.subtype === 'input') {
|
|
182
|
+
messages.push({
|
|
183
|
+
type: 'local_command',
|
|
184
|
+
command: localCmd.command,
|
|
185
|
+
timestamp: line.timestamp,
|
|
186
|
+
uuid: line.uuid,
|
|
187
|
+
});
|
|
188
|
+
}
|
|
189
|
+
else if (localCmd.subtype === 'output') {
|
|
190
|
+
const prev = messages.length > 0 ? messages[messages.length - 1] : null;
|
|
191
|
+
if (prev && prev.type === 'local_command' && !prev.stdout) {
|
|
192
|
+
prev.stdout = localCmd.stdout;
|
|
193
|
+
prev.stderr = localCmd.stderr;
|
|
194
|
+
}
|
|
195
|
+
else {
|
|
196
|
+
messages.push({
|
|
197
|
+
type: 'local_command',
|
|
198
|
+
command: '',
|
|
199
|
+
stdout: localCmd.stdout,
|
|
200
|
+
stderr: localCmd.stderr,
|
|
201
|
+
timestamp: line.timestamp,
|
|
202
|
+
uuid: line.uuid,
|
|
203
|
+
});
|
|
204
|
+
}
|
|
205
|
+
}
|
|
206
|
+
return;
|
|
207
|
+
}
|
|
208
|
+
const stripped = text
|
|
209
|
+
.replace(/<system-reminder>[\s\S]*?<\/system-reminder>/g, '')
|
|
210
|
+
.replace(/<command-name>[\s\S]*?<\/command-name>/g, '')
|
|
211
|
+
.replace(/<command-message>[\s\S]*?<\/command-message>/g, '')
|
|
212
|
+
.replace(/<command-args>[\s\S]*?<\/command-args>/g, '')
|
|
213
|
+
.replace(/<local-command-stdout>[\s\S]*?<\/local-command-stdout>/g, '')
|
|
214
|
+
.trim();
|
|
215
|
+
if (stripped) {
|
|
216
|
+
messages.push({
|
|
217
|
+
type: 'user',
|
|
218
|
+
text: stripped,
|
|
219
|
+
timestamp: line.timestamp,
|
|
220
|
+
uuid: line.uuid,
|
|
221
|
+
});
|
|
222
|
+
}
|
|
223
|
+
}
|
|
224
|
+
function processUserMessage(line, messages) {
|
|
225
|
+
const content = line.message?.content;
|
|
226
|
+
if (!content)
|
|
227
|
+
return;
|
|
228
|
+
if (typeof content === 'string') {
|
|
229
|
+
if (content.trim()) {
|
|
230
|
+
processUserText(content, line, messages);
|
|
231
|
+
}
|
|
232
|
+
return;
|
|
233
|
+
}
|
|
234
|
+
if (Array.isArray(content)) {
|
|
235
|
+
for (const block of content) {
|
|
236
|
+
if (block.type === 'text' && block.text?.trim()) {
|
|
237
|
+
processUserText(block.text, line, messages);
|
|
238
|
+
}
|
|
239
|
+
else if (block.type === 'tool_result') {
|
|
240
|
+
const toolResult = {
|
|
241
|
+
type: 'tool_result',
|
|
242
|
+
toolUseId: block.tool_use_id,
|
|
243
|
+
timestamp: line.timestamp,
|
|
244
|
+
uuid: line.uuid,
|
|
245
|
+
};
|
|
246
|
+
if (line.toolUseResult) {
|
|
247
|
+
toolResult.richResult = line.toolUseResult;
|
|
248
|
+
}
|
|
249
|
+
if (typeof block.content === 'string') {
|
|
250
|
+
toolResult.content = block.content;
|
|
251
|
+
}
|
|
252
|
+
else if (Array.isArray(block.content)) {
|
|
253
|
+
const texts = block.content
|
|
254
|
+
.filter((b) => b.type === 'text')
|
|
255
|
+
.map((b) => b.text);
|
|
256
|
+
toolResult.content = texts.join('\n');
|
|
257
|
+
}
|
|
258
|
+
if (toolResult.content && toolResult.content.includes('<persisted-output>')) {
|
|
259
|
+
const match = toolResult.content.match(/Full output saved to: ([^\n<]+)/);
|
|
260
|
+
if (match) {
|
|
261
|
+
const persistedPath = match[1].trim();
|
|
262
|
+
try {
|
|
263
|
+
if (fs.existsSync(persistedPath)) {
|
|
264
|
+
toolResult.fullContent = fs.readFileSync(persistedPath, 'utf-8');
|
|
265
|
+
}
|
|
266
|
+
}
|
|
267
|
+
catch {
|
|
268
|
+
// ignore
|
|
269
|
+
}
|
|
270
|
+
}
|
|
271
|
+
}
|
|
272
|
+
messages.push(toolResult);
|
|
273
|
+
}
|
|
274
|
+
}
|
|
275
|
+
}
|
|
276
|
+
}
|
|
277
|
+
function processAssistantMessage(line, messages) {
|
|
278
|
+
const content = line.message?.content;
|
|
279
|
+
if (!content || !Array.isArray(content))
|
|
280
|
+
return;
|
|
281
|
+
const usage = line.message?.usage || null;
|
|
282
|
+
let usageAssigned = false;
|
|
283
|
+
for (const block of content) {
|
|
284
|
+
if (block.type === 'thinking') {
|
|
285
|
+
messages.push({
|
|
286
|
+
type: 'thinking',
|
|
287
|
+
timestamp: line.timestamp,
|
|
288
|
+
uuid: line.uuid,
|
|
289
|
+
});
|
|
290
|
+
}
|
|
291
|
+
else if (block.type === 'text' && block.text?.trim()) {
|
|
292
|
+
messages.push({
|
|
293
|
+
type: 'assistant',
|
|
294
|
+
text: block.text,
|
|
295
|
+
model: line.message.model,
|
|
296
|
+
timestamp: line.timestamp,
|
|
297
|
+
uuid: line.uuid,
|
|
298
|
+
usage: !usageAssigned ? usage : null,
|
|
299
|
+
});
|
|
300
|
+
usageAssigned = true;
|
|
301
|
+
}
|
|
302
|
+
else if (block.type === 'tool_use') {
|
|
303
|
+
messages.push({
|
|
304
|
+
type: 'tool_use',
|
|
305
|
+
toolName: block.name,
|
|
306
|
+
toolUseId: block.id,
|
|
307
|
+
input: block.input,
|
|
308
|
+
timestamp: line.timestamp,
|
|
309
|
+
uuid: line.uuid,
|
|
310
|
+
usage: !usageAssigned ? usage : null,
|
|
311
|
+
});
|
|
312
|
+
usageAssigned = true;
|
|
313
|
+
}
|
|
314
|
+
}
|
|
315
|
+
}
|
|
316
|
+
function calculateTurnUsage(messages) {
|
|
317
|
+
for (let i = 0; i < messages.length; i++) {
|
|
318
|
+
if (messages[i].type !== 'user' && messages[i].type !== 'local_command')
|
|
319
|
+
continue;
|
|
320
|
+
let totalInput = 0;
|
|
321
|
+
let totalOutput = 0;
|
|
322
|
+
let totalCacheRead = 0;
|
|
323
|
+
let totalCacheCreate = 0;
|
|
324
|
+
const seenUsageIds = new Set();
|
|
325
|
+
for (let j = i + 1; j < messages.length; j++) {
|
|
326
|
+
const m = messages[j];
|
|
327
|
+
if (m.type === 'user' || m.type === 'local_command')
|
|
328
|
+
break;
|
|
329
|
+
if (m.usage && !seenUsageIds.has(m.uuid + (m.usage.input_tokens || 0))) {
|
|
330
|
+
seenUsageIds.add(m.uuid + (m.usage.input_tokens || 0));
|
|
331
|
+
totalInput += m.usage.input_tokens || 0;
|
|
332
|
+
totalOutput += m.usage.output_tokens || 0;
|
|
333
|
+
totalCacheRead += m.usage.cache_read_input_tokens || 0;
|
|
334
|
+
totalCacheCreate += m.usage.cache_creation_input_tokens || 0;
|
|
335
|
+
}
|
|
336
|
+
}
|
|
337
|
+
let turnDurationMs = 0;
|
|
338
|
+
for (let j = i + 1; j < messages.length; j++) {
|
|
339
|
+
const m = messages[j];
|
|
340
|
+
if (m.type === 'user' || m.type === 'local_command')
|
|
341
|
+
break;
|
|
342
|
+
if (m.type === 'system' && m.subtype === 'turn_duration') {
|
|
343
|
+
turnDurationMs = m.durationMs || 0;
|
|
344
|
+
break;
|
|
345
|
+
}
|
|
346
|
+
}
|
|
347
|
+
const total = totalInput + totalOutput + totalCacheRead + totalCacheCreate;
|
|
348
|
+
if (total > 0 || turnDurationMs > 0) {
|
|
349
|
+
messages[i].turnUsage = {
|
|
350
|
+
input: totalInput,
|
|
351
|
+
output: totalOutput,
|
|
352
|
+
cacheRead: totalCacheRead,
|
|
353
|
+
cacheCreate: totalCacheCreate,
|
|
354
|
+
total,
|
|
355
|
+
durationMs: turnDurationMs,
|
|
356
|
+
};
|
|
357
|
+
}
|
|
358
|
+
}
|
|
359
|
+
}
|
|
360
|
+
function calculateElapsed(messages) {
|
|
361
|
+
for (let i = 1; i < messages.length; i++) {
|
|
362
|
+
const prev = messages[i - 1];
|
|
363
|
+
const curr = messages[i];
|
|
364
|
+
if (prev.timestamp && curr.timestamp) {
|
|
365
|
+
const elapsed = new Date(curr.timestamp).getTime() - new Date(prev.timestamp).getTime();
|
|
366
|
+
if (elapsed > 0) {
|
|
367
|
+
curr.elapsedMs = elapsed;
|
|
368
|
+
}
|
|
369
|
+
}
|
|
370
|
+
if (curr.type === 'tool_use' && curr.result?.timestamp && curr.timestamp) {
|
|
371
|
+
const execTime = new Date(curr.result.timestamp).getTime() - new Date(curr.timestamp).getTime();
|
|
372
|
+
if (execTime > 0) {
|
|
373
|
+
curr.execMs = execTime;
|
|
374
|
+
}
|
|
375
|
+
}
|
|
376
|
+
}
|
|
377
|
+
}
|
|
378
|
+
function processSystemMessage(line, messages) {
|
|
379
|
+
if (line.subtype === 'turn_duration') {
|
|
380
|
+
messages.push({
|
|
381
|
+
type: 'system',
|
|
382
|
+
subtype: 'turn_duration',
|
|
383
|
+
durationMs: line.durationMs,
|
|
384
|
+
timestamp: line.timestamp,
|
|
385
|
+
});
|
|
386
|
+
}
|
|
387
|
+
else if (line.subtype === 'bridge_status') {
|
|
388
|
+
// skip
|
|
389
|
+
}
|
|
390
|
+
else if (line.content) {
|
|
391
|
+
messages.push({
|
|
392
|
+
type: 'system',
|
|
393
|
+
subtype: line.subtype || 'info',
|
|
394
|
+
content: typeof line.content === 'string' ? line.content : JSON.stringify(line.content),
|
|
395
|
+
timestamp: line.timestamp,
|
|
396
|
+
});
|
|
397
|
+
}
|
|
398
|
+
}
|
|
399
|
+
function inlineSubagents(messages, subagents) {
|
|
400
|
+
if (!Object.keys(subagents).length)
|
|
401
|
+
return;
|
|
402
|
+
for (const msg of messages) {
|
|
403
|
+
if (msg.type !== 'tool_use' || msg.toolName !== 'Agent')
|
|
404
|
+
continue;
|
|
405
|
+
if (!msg.result)
|
|
406
|
+
continue;
|
|
407
|
+
const content = msg.result.content || msg.result.fullContent || '';
|
|
408
|
+
const match = content.match(/agentId:\s*([a-f0-9]+)/);
|
|
409
|
+
if (match) {
|
|
410
|
+
const agentId = match[1];
|
|
411
|
+
if (subagents[agentId]) {
|
|
412
|
+
msg.subagentMessages = subagents[agentId];
|
|
413
|
+
msg.subagentId = agentId;
|
|
414
|
+
}
|
|
415
|
+
}
|
|
416
|
+
}
|
|
417
|
+
const usedIds = new Set(messages.filter((m) => m.subagentId).map((m) => m.subagentId));
|
|
418
|
+
const unmatchedAgents = Object.keys(subagents).filter((id) => !usedIds.has(id));
|
|
419
|
+
if (unmatchedAgents.length) {
|
|
420
|
+
for (const msg of messages) {
|
|
421
|
+
if (msg.type !== 'tool_use' || msg.toolName !== 'Agent')
|
|
422
|
+
continue;
|
|
423
|
+
if (msg.subagentMessages)
|
|
424
|
+
continue;
|
|
425
|
+
const desc = (msg.input?.description || '').toLowerCase();
|
|
426
|
+
if (!desc)
|
|
427
|
+
continue;
|
|
428
|
+
for (let i = unmatchedAgents.length - 1; i >= 0; i--) {
|
|
429
|
+
const agentId = unmatchedAgents[i];
|
|
430
|
+
const firstMsg = subagents[agentId]?.[0];
|
|
431
|
+
if (firstMsg?.type === 'user') {
|
|
432
|
+
const agentText = (firstMsg.text || '').toLowerCase();
|
|
433
|
+
if (agentText.includes(desc) || desc.includes(agentText.slice(0, 30))) {
|
|
434
|
+
msg.subagentMessages = subagents[agentId];
|
|
435
|
+
msg.subagentId = agentId;
|
|
436
|
+
unmatchedAgents.splice(i, 1);
|
|
437
|
+
break;
|
|
438
|
+
}
|
|
439
|
+
}
|
|
440
|
+
}
|
|
441
|
+
}
|
|
442
|
+
}
|
|
443
|
+
}
|
|
444
|
+
function pairToolMessages(messages) {
|
|
445
|
+
const toolUseMap = new Map();
|
|
446
|
+
for (let i = 0; i < messages.length; i++) {
|
|
447
|
+
if (messages[i].type === 'tool_use') {
|
|
448
|
+
toolUseMap.set(messages[i].toolUseId, i);
|
|
449
|
+
}
|
|
450
|
+
}
|
|
451
|
+
for (let i = 0; i < messages.length; i++) {
|
|
452
|
+
if (messages[i].type === 'tool_result' && messages[i].toolUseId) {
|
|
453
|
+
const useIdx = toolUseMap.get(messages[i].toolUseId);
|
|
454
|
+
if (useIdx !== undefined) {
|
|
455
|
+
messages[useIdx].result = messages[i];
|
|
456
|
+
messages[i]._paired = true;
|
|
457
|
+
}
|
|
458
|
+
}
|
|
459
|
+
}
|
|
460
|
+
for (let i = messages.length - 1; i >= 0; i--) {
|
|
461
|
+
if (messages[i]._paired) {
|
|
462
|
+
messages.splice(i, 1);
|
|
463
|
+
}
|
|
464
|
+
}
|
|
465
|
+
}
|