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.
@@ -0,0 +1,496 @@
1
+ 'use strict';
2
+
3
+ const { getCSS, getAppJS } = require('./template-assets');
4
+
5
+ function escapeHtml(str) {
6
+ return str
7
+ .replace(/&/g, '&')
8
+ .replace(/</g, '&lt;')
9
+ .replace(/>/g, '&gt;')
10
+ .replace(/"/g, '&quot;');
11
+ }
12
+
13
+ function formatTime(ts) {
14
+ if (!ts) return '';
15
+ const d = new Date(ts);
16
+ return d.toLocaleTimeString('en-GB', { hour: '2-digit', minute: '2-digit', second: '2-digit' });
17
+ }
18
+
19
+ function formatDuration(ms) {
20
+ if (!ms) return '';
21
+ if (ms < 1000) return `${ms}ms`;
22
+ const s = Math.round(ms / 1000);
23
+ if (s < 60) return `${s}s`;
24
+ const m = Math.floor(s / 60);
25
+ const rs = s % 60;
26
+ return `${m}m ${rs}s`;
27
+ }
28
+
29
+ function toolUseSummary(msg) {
30
+ const name = msg.toolName;
31
+ const input = msg.input || {};
32
+
33
+ switch (name) {
34
+ case 'Bash':
35
+ return `Bash: <code>${escapeHtml((input.command || '').slice(0, 120))}</code>`;
36
+ case 'Read':
37
+ return `Read: <code>${escapeHtml(input.file_path || '')}</code>`;
38
+ case 'Write':
39
+ return `Write: <code>${escapeHtml(input.file_path || '')}</code>`;
40
+ case 'Edit':
41
+ return `Edit: <code>${escapeHtml(input.file_path || '')}</code>`;
42
+ case 'Grep':
43
+ return `Grep: <code>${escapeHtml(input.pattern || '')}</code>`;
44
+ case 'Glob':
45
+ return `Glob: <code>${escapeHtml(input.pattern || '')}</code>`;
46
+ case 'Agent':
47
+ return `Agent: ${escapeHtml(input.description || input.subagent_type || '')}`;
48
+ default:
49
+ return escapeHtml(name);
50
+ }
51
+ }
52
+
53
+ function renderToolResult(msg) {
54
+ if (!msg.result) return '';
55
+
56
+ const result = msg.result;
57
+ const rich = result.richResult;
58
+ const parts = [];
59
+
60
+ if (rich) {
61
+ // Bash result
62
+ if (rich.stdout !== undefined) {
63
+ if (rich.stdout) {
64
+ parts.push(`<div class="tool-output"><pre><code>${escapeHtml(rich.stdout)}</code></pre></div>`);
65
+ }
66
+ if (rich.stderr) {
67
+ parts.push(`<div class="tool-output stderr"><pre><code>${escapeHtml(rich.stderr)}</code></pre></div>`);
68
+ }
69
+ return parts.join('');
70
+ }
71
+
72
+ // File read result
73
+ if (rich.type === 'text' && rich.file) {
74
+ const f = rich.file;
75
+ parts.push(`<div class="tool-meta">${escapeHtml(f.filePath)} (lines ${f.startLine}-${f.startLine + f.numLines - 1} of ${f.totalLines})</div>`);
76
+ if (f.content) {
77
+ parts.push(`<div class="tool-output"><pre><code>${escapeHtml(f.content.slice(0, 3000))}</code></pre></div>`);
78
+ }
79
+ return parts.join('');
80
+ }
81
+
82
+ // File edit/create with structuredPatch
83
+ if ((rich.type === 'update' || rich.type === 'create') && rich.filePath) {
84
+ parts.push(`<div class="tool-meta">${escapeHtml(rich.filePath)}</div>`);
85
+ if (rich.structuredPatch && rich.structuredPatch.length > 0) {
86
+ parts.push(renderDiff(rich.structuredPatch));
87
+ } else if (rich.content) {
88
+ parts.push(`<div class="tool-output"><pre><code>${escapeHtml(rich.content.slice(0, 3000))}</code></pre></div>`);
89
+ }
90
+ return parts.join('');
91
+ }
92
+ }
93
+
94
+ // Fallback: raw content
95
+ const content = result.fullContent || result.content;
96
+ if (content) {
97
+ parts.push(`<div class="tool-output"><pre><code>${escapeHtml(content.slice(0, 5000))}</code></pre></div>`);
98
+ }
99
+
100
+ return parts.join('');
101
+ }
102
+
103
+ function renderDiff(patches) {
104
+ const lines = [];
105
+ lines.push('<div class="diff">');
106
+ for (const patch of patches) {
107
+ lines.push(`<div class="diff-hunk">@@ -${patch.oldStart},${patch.oldLines} +${patch.newStart},${patch.newLines} @@</div>`);
108
+ for (const line of (patch.lines || [])) {
109
+ const ch = line[0];
110
+ const text = line.slice(1);
111
+ if (ch === '+') {
112
+ lines.push(`<div class="diff-add">+${escapeHtml(text)}</div>`);
113
+ } else if (ch === '-') {
114
+ lines.push(`<div class="diff-del">-${escapeHtml(text)}</div>`);
115
+ } else {
116
+ lines.push(`<div class="diff-ctx"> ${escapeHtml(text)}</div>`);
117
+ }
118
+ }
119
+ }
120
+ lines.push('</div>');
121
+ return lines.join('\n');
122
+ }
123
+
124
+ function msgId(ts) {
125
+ if (!ts) return `msg-${Date.now()}${Math.random().toString(36).slice(2, 5)}`;
126
+ const d = new Date(ts);
127
+ const pad = (n, len = 2) => String(n).padStart(len, '0');
128
+ return `t${d.getFullYear()}${pad(d.getMonth() + 1)}${pad(d.getDate())}${pad(d.getHours())}${pad(d.getMinutes())}${pad(d.getSeconds())}`;
129
+ }
130
+
131
+ function renderMessage(msg) {
132
+ const id = msgId(msg.timestamp);
133
+ const time = `<a class="timestamp" href="#${id}">${formatTime(msg.timestamp)}</a>`;
134
+
135
+ switch (msg.type) {
136
+ case 'user':
137
+ return `<div class="msg msg-user" id="${id}">${time}<div class="msg-content" data-markdown>${escapeHtml(msg.text)}</div></div>`;
138
+
139
+ case 'assistant':
140
+ return `<div class="msg msg-assistant" id="${id}">${time}<div class="msg-content" data-markdown>${escapeHtml(msg.text)}</div></div>`;
141
+
142
+ case 'thinking':
143
+ return `<div class="msg msg-thinking" id="${id}">${time}<span class="thinking-indicator">Thinking...</span></div>`;
144
+
145
+ case 'tool_use': {
146
+ const summary = toolUseSummary(msg);
147
+ const resultHtml = renderToolResult(msg);
148
+ const subagentHtml = msg.subagentMessages
149
+ ? `<div class="subagent-inline"><details><summary><span class="tool-summary">Subagent conversation</span></summary><div class="subagent-content">${msg.subagentMessages.map(renderMessage).join('\n')}</div></details></div>`
150
+ : '';
151
+ return `<div class="msg msg-tool" id="${id}"><details><summary>${time}<span class="tool-summary">${summary}</span></summary><div class="tool-details">${renderToolInput(msg)}${resultHtml}${subagentHtml}</div></details></div>`;
152
+ }
153
+
154
+ case 'tool_result':
155
+ // Unpaired tool result (shouldn't happen often)
156
+ return `<div class="msg msg-tool" id="${id}"><div class="tool-output"><pre><code>${escapeHtml((msg.content || '').slice(0, 2000))}</code></pre></div></div>`;
157
+
158
+ case 'system':
159
+ if (msg.subtype === 'turn_duration') {
160
+ return `<div class="msg msg-system" id="${id}"><span class="duration">Turn: ${formatDuration(msg.durationMs)}</span></div>`;
161
+ }
162
+ return `<div class="msg msg-system" id="${id}">${escapeHtml(msg.content || '')}</div>`;
163
+
164
+ default:
165
+ return '';
166
+ }
167
+ }
168
+
169
+ function renderToolInput(msg) {
170
+ const input = msg.input || {};
171
+ const name = msg.toolName;
172
+
173
+ // Show command for Bash
174
+ if (name === 'Bash' && input.command) {
175
+ return `<div class="tool-input"><div class="tool-input-label">Command:</div><pre><code>${escapeHtml(input.command)}</code></pre></div>`;
176
+ }
177
+
178
+ // Show old_string/new_string for Edit
179
+ if (name === 'Edit' && input.old_string) {
180
+ return `<div class="tool-input"><div class="tool-input-label">Edit:</div><div class="diff"><div class="diff-del">${escapeHtml(input.old_string)}</div><div class="diff-add">${escapeHtml(input.new_string || '')}</div></div></div>`;
181
+ }
182
+
183
+ // For other tools, show input as JSON if small enough
184
+ const json = JSON.stringify(input, null, 2);
185
+ if (json.length > 500) return '';
186
+ return `<div class="tool-input"><pre><code>${escapeHtml(json)}</code></pre></div>`;
187
+ }
188
+
189
+ function renderSubagent(agentId, messages) {
190
+ const agentHtml = messages.map(renderMessage).join('\n');
191
+ return `<div class="msg msg-subagent"><details><summary><span class="tool-summary">Subagent: ${escapeHtml(agentId)}</span></summary><div class="subagent-content">${agentHtml}</div></details></div>`;
192
+ }
193
+
194
+ function formatDateOnly(ts) {
195
+ if (!ts) return '';
196
+ const d = new Date(ts);
197
+ return d.toLocaleDateString('en-CA', { year: 'numeric', month: '2-digit', day: '2-digit' });
198
+ }
199
+
200
+ function groupMessagesByDate(messages) {
201
+ const groups = [];
202
+ let currentDate = null;
203
+ for (const msg of messages) {
204
+ const dateStr = formatDateOnly(msg.timestamp);
205
+ if (dateStr && dateStr !== currentDate) {
206
+ currentDate = dateStr;
207
+ groups.push({ date: dateStr, messages: [] });
208
+ }
209
+ if (groups.length === 0) {
210
+ groups.push({ date: 'unknown', messages: [] });
211
+ }
212
+ groups[groups.length - 1].messages.push(msg);
213
+ }
214
+ return groups;
215
+ }
216
+
217
+ function formatTokens(n) {
218
+ if (n >= 1000000) return (n / 1000000).toFixed(1) + 'M';
219
+ if (n >= 1000) return (n / 1000).toFixed(1) + 'K';
220
+ return String(n);
221
+ }
222
+
223
+ function formatDurationLong(ms) {
224
+ if (!ms) return '';
225
+ const s = Math.floor(ms / 1000);
226
+ if (s < 60) return `${s}s`;
227
+ const m = Math.floor(s / 60);
228
+ const h = Math.floor(m / 60);
229
+ if (h > 0) return `${h}h ${m % 60}m`;
230
+ return `${m}m`;
231
+ }
232
+
233
+ function renderStats(stats) {
234
+ if (!stats || !stats.turns) return '';
235
+
236
+ const items = [];
237
+ items.push(`<span class="stat-item"><span class="stat-label">Turns</span><span class="stat-value">${stats.turns}</span></span>`);
238
+ items.push(`<span class="stat-item"><span class="stat-label">Input</span><span class="stat-value">${formatTokens(stats.inputTokens)}</span></span>`);
239
+ items.push(`<span class="stat-item"><span class="stat-label">Output</span><span class="stat-value">${formatTokens(stats.outputTokens)}</span></span>`);
240
+ items.push(`<span class="stat-item"><span class="stat-label">Cache Create</span><span class="stat-value">${formatTokens(stats.cacheCreation)}</span></span>`);
241
+ items.push(`<span class="stat-item"><span class="stat-label">Cache Read</span><span class="stat-value">${formatTokens(stats.cacheRead)}</span></span>`);
242
+ items.push(`<span class="stat-item"><span class="stat-label">Total</span><span class="stat-value">${formatTokens(stats.totalTokens)}</span></span>`);
243
+ items.push(`<span class="stat-item"><span class="stat-label">Cache Hit</span><span class="stat-value">${(stats.cacheHitRate * 100).toFixed(0)}%</span></span>`);
244
+ if (stats.durationMs) {
245
+ items.push(`<span class="stat-item"><span class="stat-label">Duration</span><span class="stat-value">${formatDurationLong(stats.durationMs)}</span></span>`);
246
+ if (stats.outputTokens && stats.durationMs > 0) {
247
+ const tokPerMin = Math.round(stats.outputTokens / (stats.durationMs / 60000));
248
+ items.push(`<span class="stat-item"><span class="stat-label">Output/min</span><span class="stat-value">${formatTokens(tokPerMin)}</span></span>`);
249
+ }
250
+ }
251
+
252
+ return `<div class="stats-bar">${items.join('')}</div>`;
253
+ }
254
+
255
+ function generate(parsed, options = {}) {
256
+ const { projectName, session, backUrl } = options;
257
+ const title = session?.slug || session?.id || 'Session';
258
+ const date = session?.timestamp
259
+ ? new Date(session.timestamp).toLocaleDateString('en-CA')
260
+ : '';
261
+
262
+ // Group messages by date
263
+ const dateGroups = groupMessagesByDate(parsed.messages);
264
+
265
+ // Build grouped HTML with date anchors
266
+ const groupsHtml = dateGroups.map(g => {
267
+ const msgsHtml = g.messages.map(renderMessage).join('\n');
268
+ return `<div class="detail-date-group" id="date-${g.date}" data-date="${escapeHtml(g.date)}">
269
+ <div class="detail-date-heading">${escapeHtml(g.date)}</div>
270
+ ${msgsHtml}
271
+ </div>`;
272
+ }).join('\n');
273
+
274
+ // Side nav for dates
275
+ const sideNavItems = dateGroups
276
+ .filter(g => g.date !== 'unknown')
277
+ .map(g =>
278
+ `<a class="detail-sidenav-item" href="#date-${g.date}" data-date="${escapeHtml(g.date)}">${escapeHtml(g.date)}</a>`
279
+ ).join('\n');
280
+
281
+ const backLink = backUrl
282
+ ? `<div style="font-size:0.8rem;margin-bottom:8px"><a href="${escapeHtml(backUrl)}" style="color:var(--link);text-decoration:none">&larr; Back to sessions</a> &nbsp;|&nbsp; <a href="/" style="color:var(--link);text-decoration:none">All projects</a></div>`
283
+ : '';
284
+
285
+ return `<!DOCTYPE html>
286
+ <html lang="en">
287
+ <head>
288
+ <meta charset="utf-8">
289
+ <meta name="viewport" content="width=device-width, initial-scale=1">
290
+ <title>${escapeHtml(title)} — ${escapeHtml(date)}</title>
291
+ <style>${getCSS()}
292
+ ${detailLayoutCSS()}
293
+ </style>
294
+ </head>
295
+ <body>
296
+ <a href="https://github.com/ashimon83/cctape" 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>
297
+ <div class="detail-sticky-bar" id="detailStickyBar"></div>
298
+ <header class="session-header">
299
+ ${backLink}
300
+ <h1>${escapeHtml(title)}</h1>
301
+ <div class="session-meta">
302
+ ${projectName ? `<span class="meta-item">Project: ${escapeHtml(projectName)}</span>` : ''}
303
+ ${date ? `<span class="meta-item">Date: ${escapeHtml(date)}</span>` : ''}
304
+ ${session?.gitBranch ? `<span class="meta-item">Branch: ${escapeHtml(session.gitBranch)}</span>` : ''}
305
+ ${session?.model ? `<span class="meta-item">Model: ${escapeHtml(session.model)}</span>` : ''}
306
+ </div>
307
+ ${renderStats(parsed.stats)}
308
+ </header>
309
+ <div class="detail-layout">
310
+ <nav class="detail-sidenav" id="detailSidenav">
311
+ <div class="detail-sidenav-title">Dates</div>
312
+ ${sideNavItems}
313
+ <div class="detail-sidenav-jump">
314
+ <a class="detail-sidenav-item" href="#session-bottom">&#8595; Bottom</a>
315
+ <a class="detail-sidenav-item" href="#session-top">&#8593; Top</a>
316
+ </div>
317
+ </nav>
318
+ <main class="chat-container" id="session-top">
319
+ ${groupsHtml}
320
+ <div id="session-bottom"></div>
321
+ </main>
322
+ </div>
323
+ <script>${getAppJS()}
324
+ ${detailNavJS()}
325
+ </script>
326
+ </body>
327
+ </html>`;
328
+ }
329
+
330
+ function detailLayoutCSS() {
331
+ return `
332
+ .detail-layout {
333
+ display: flex;
334
+ max-width: 1100px;
335
+ margin: 0 auto;
336
+ gap: 0;
337
+ }
338
+ .detail-layout .chat-container {
339
+ flex: 1;
340
+ min-width: 0;
341
+ }
342
+
343
+ /* Stats bar */
344
+ .stats-bar {
345
+ display: flex;
346
+ flex-wrap: wrap;
347
+ gap: 4px 12px;
348
+ margin-top: 12px;
349
+ padding: 10px 14px;
350
+ background: var(--bg-secondary);
351
+ border: 1px solid var(--border);
352
+ border-radius: 8px;
353
+ }
354
+ .stat-item {
355
+ display: flex;
356
+ flex-direction: column;
357
+ align-items: center;
358
+ min-width: 60px;
359
+ }
360
+ .stat-label {
361
+ font-size: 0.65rem;
362
+ color: var(--text-muted);
363
+ text-transform: uppercase;
364
+ letter-spacing: 0.04em;
365
+ }
366
+ .stat-value {
367
+ font-size: 0.9rem;
368
+ font-weight: 700;
369
+ font-variant-numeric: tabular-nums;
370
+ }
371
+
372
+ /* Detail side nav */
373
+ .detail-sidenav {
374
+ width: 140px;
375
+ flex-shrink: 0;
376
+ position: sticky;
377
+ top: 48px;
378
+ align-self: flex-start;
379
+ max-height: calc(100vh - 60px);
380
+ overflow-y: auto;
381
+ padding: 16px 8px 16px 16px;
382
+ border-right: 1px solid var(--border);
383
+ }
384
+ .detail-sidenav-title {
385
+ font-size: 0.7rem;
386
+ text-transform: uppercase;
387
+ letter-spacing: 0.08em;
388
+ color: var(--text-muted);
389
+ margin-bottom: 8px;
390
+ font-weight: 600;
391
+ }
392
+ .detail-sidenav-jump {
393
+ margin-top: 12px;
394
+ padding-top: 8px;
395
+ border-top: 1px solid var(--border);
396
+ }
397
+ .detail-sidenav-item {
398
+ display: block;
399
+ padding: 4px 8px;
400
+ font-size: 0.78rem;
401
+ color: var(--text-muted);
402
+ text-decoration: none;
403
+ border-radius: 4px;
404
+ margin-bottom: 2px;
405
+ transition: background 0.15s, color 0.15s;
406
+ }
407
+ .detail-sidenav-item:hover {
408
+ background: var(--bg-secondary);
409
+ color: var(--text);
410
+ }
411
+ .detail-sidenav-item.active {
412
+ background: var(--user-bg);
413
+ color: var(--text);
414
+ font-weight: 600;
415
+ }
416
+
417
+ /* Date group headings in detail */
418
+ .detail-date-heading {
419
+ font-size: 0.85rem;
420
+ font-weight: 700;
421
+ color: var(--text-muted);
422
+ padding: 14px 0 6px;
423
+ border-bottom: 2px solid var(--border);
424
+ margin-bottom: 12px;
425
+ position: sticky;
426
+ top: 44px;
427
+ background: var(--bg);
428
+ z-index: 5;
429
+ }
430
+ .detail-date-group {
431
+ margin-bottom: 8px;
432
+ display: flex;
433
+ flex-direction: column;
434
+ gap: 28px;
435
+ }
436
+
437
+ /* Sticky date bar */
438
+ .detail-sticky-bar {
439
+ position: fixed;
440
+ top: 0;
441
+ left: 0;
442
+ right: 0;
443
+ z-index: 100;
444
+ background: var(--bg-secondary);
445
+ border-bottom: 1px solid var(--border);
446
+ padding: 8px 24px;
447
+ font-size: 0.85rem;
448
+ font-weight: 700;
449
+ color: var(--text);
450
+ transform: translateY(-100%);
451
+ transition: transform 0.2s;
452
+ }
453
+ .detail-sticky-bar.visible {
454
+ transform: translateY(0);
455
+ }
456
+
457
+ @media (max-width: 768px) {
458
+ .detail-sidenav { display: none; }
459
+ .detail-layout { display: block; }
460
+ }
461
+ `;
462
+ }
463
+
464
+ function detailNavJS() {
465
+ return `
466
+ (function() {
467
+ var groups = Array.from(document.querySelectorAll('.detail-date-group'));
468
+ var bar = document.getElementById('detailStickyBar');
469
+ var navItems = Array.from(document.querySelectorAll('.detail-sidenav-item'));
470
+ if (!groups.length) return;
471
+
472
+ function update() {
473
+ var scrollY = window.scrollY + 60;
474
+ var current = null;
475
+ for (var i = 0; i < groups.length; i++) {
476
+ if (groups[i].offsetTop <= scrollY) current = groups[i];
477
+ }
478
+ var date = current ? current.dataset.date : '';
479
+ if (date) {
480
+ bar.textContent = date;
481
+ bar.classList.add('visible');
482
+ } else {
483
+ bar.classList.remove('visible');
484
+ }
485
+ navItems.forEach(function(a) {
486
+ a.classList.toggle('active', a.dataset.date === date);
487
+ });
488
+ }
489
+
490
+ window.addEventListener('scroll', update, { passive: true });
491
+ update();
492
+ })();
493
+ `;
494
+ }
495
+
496
+ module.exports = { generate };