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/pages.js
ADDED
|
@@ -0,0 +1,393 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
const { getCSS } = require('./template-assets');
|
|
4
|
+
|
|
5
|
+
function escapeHtml(str) {
|
|
6
|
+
return String(str)
|
|
7
|
+
.replace(/&/g, '&')
|
|
8
|
+
.replace(/</g, '<')
|
|
9
|
+
.replace(/>/g, '>')
|
|
10
|
+
.replace(/"/g, '"');
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
function formatDate(ts) {
|
|
14
|
+
if (!ts) return '';
|
|
15
|
+
const d = ts instanceof Date ? ts : new Date(ts);
|
|
16
|
+
return d.toLocaleDateString('en-CA') + ' ' + d.toLocaleTimeString('en-GB', { hour: '2-digit', minute: '2-digit' });
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
function formatTokens(n) {
|
|
20
|
+
if (n >= 1000000) return (n / 1000000).toFixed(1) + 'M';
|
|
21
|
+
if (n >= 1000) return (n / 1000).toFixed(1) + 'K';
|
|
22
|
+
return String(n);
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
const GITHUB_URL = 'https://github.com/ashimon83/cctape';
|
|
26
|
+
|
|
27
|
+
function githubCorner() {
|
|
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
|
+
}
|
|
30
|
+
|
|
31
|
+
function pageShell(title, bodyHtml) {
|
|
32
|
+
return `<!DOCTYPE html>
|
|
33
|
+
<html lang="en">
|
|
34
|
+
<head>
|
|
35
|
+
<meta charset="utf-8">
|
|
36
|
+
<meta name="viewport" content="width=device-width, initial-scale=1">
|
|
37
|
+
<title>${escapeHtml(title)}</title>
|
|
38
|
+
<style>${getCSS()}
|
|
39
|
+
${indexCSS()}
|
|
40
|
+
</style>
|
|
41
|
+
</head>
|
|
42
|
+
<body>
|
|
43
|
+
${githubCorner()}
|
|
44
|
+
${bodyHtml}
|
|
45
|
+
</body>
|
|
46
|
+
</html>`;
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
function indexCSS() {
|
|
50
|
+
return `
|
|
51
|
+
.page-header {
|
|
52
|
+
max-width: 900px;
|
|
53
|
+
margin: 0 auto;
|
|
54
|
+
padding: 24px 16px 16px;
|
|
55
|
+
border-bottom: 1px solid var(--border);
|
|
56
|
+
}
|
|
57
|
+
.page-header h1 {
|
|
58
|
+
font-size: 1.4rem;
|
|
59
|
+
font-weight: 700;
|
|
60
|
+
}
|
|
61
|
+
.page-header .subtitle {
|
|
62
|
+
color: var(--text-muted);
|
|
63
|
+
font-size: 0.85rem;
|
|
64
|
+
margin-top: 4px;
|
|
65
|
+
}
|
|
66
|
+
.breadcrumb {
|
|
67
|
+
font-size: 0.8rem;
|
|
68
|
+
color: var(--text-muted);
|
|
69
|
+
margin-bottom: 8px;
|
|
70
|
+
}
|
|
71
|
+
.breadcrumb a {
|
|
72
|
+
color: var(--link);
|
|
73
|
+
text-decoration: none;
|
|
74
|
+
}
|
|
75
|
+
.breadcrumb a:hover { text-decoration: underline; }
|
|
76
|
+
|
|
77
|
+
.list-container {
|
|
78
|
+
max-width: 900px;
|
|
79
|
+
margin: 0 auto;
|
|
80
|
+
padding: 16px;
|
|
81
|
+
}
|
|
82
|
+
.list-item {
|
|
83
|
+
display: block;
|
|
84
|
+
padding: 14px 16px;
|
|
85
|
+
border: 1px solid var(--border);
|
|
86
|
+
border-radius: 8px;
|
|
87
|
+
margin-bottom: 8px;
|
|
88
|
+
text-decoration: none;
|
|
89
|
+
color: var(--text);
|
|
90
|
+
transition: background 0.15s, border-color 0.15s;
|
|
91
|
+
cursor: pointer;
|
|
92
|
+
}
|
|
93
|
+
.list-item:hover {
|
|
94
|
+
background: var(--bg-secondary);
|
|
95
|
+
border-color: var(--link);
|
|
96
|
+
}
|
|
97
|
+
.list-item-title {
|
|
98
|
+
font-weight: 600;
|
|
99
|
+
font-size: 0.95rem;
|
|
100
|
+
margin-bottom: 4px;
|
|
101
|
+
}
|
|
102
|
+
.list-item-meta {
|
|
103
|
+
font-size: 0.8rem;
|
|
104
|
+
color: var(--text-muted);
|
|
105
|
+
display: flex;
|
|
106
|
+
flex-wrap: wrap;
|
|
107
|
+
gap: 12px;
|
|
108
|
+
}
|
|
109
|
+
.list-item-preview {
|
|
110
|
+
font-size: 0.85rem;
|
|
111
|
+
color: var(--text-muted);
|
|
112
|
+
margin-top: 6px;
|
|
113
|
+
overflow: hidden;
|
|
114
|
+
text-overflow: ellipsis;
|
|
115
|
+
white-space: nowrap;
|
|
116
|
+
}
|
|
117
|
+
.badge {
|
|
118
|
+
display: inline-block;
|
|
119
|
+
padding: 1px 6px;
|
|
120
|
+
border-radius: 4px;
|
|
121
|
+
font-size: 0.7rem;
|
|
122
|
+
font-weight: 600;
|
|
123
|
+
background: var(--tool-bg);
|
|
124
|
+
border: 1px solid var(--border);
|
|
125
|
+
}
|
|
126
|
+
.search-box {
|
|
127
|
+
width: 100%;
|
|
128
|
+
padding: 10px 14px;
|
|
129
|
+
border: 1px solid var(--border);
|
|
130
|
+
border-radius: 8px;
|
|
131
|
+
background: var(--bg);
|
|
132
|
+
color: var(--text);
|
|
133
|
+
font-size: 0.9rem;
|
|
134
|
+
margin-bottom: 16px;
|
|
135
|
+
outline: none;
|
|
136
|
+
}
|
|
137
|
+
.search-box:focus {
|
|
138
|
+
border-color: var(--link);
|
|
139
|
+
box-shadow: 0 0 0 2px rgba(37, 99, 235, 0.15);
|
|
140
|
+
}
|
|
141
|
+
.search-box::placeholder { color: var(--text-muted); }
|
|
142
|
+
.empty { text-align: center; color: var(--text-muted); padding: 40px; }
|
|
143
|
+
|
|
144
|
+
/* Session list layout */
|
|
145
|
+
.session-layout {
|
|
146
|
+
display: flex;
|
|
147
|
+
max-width: 1100px;
|
|
148
|
+
margin: 0 auto;
|
|
149
|
+
gap: 0;
|
|
150
|
+
}
|
|
151
|
+
.session-layout .list-container {
|
|
152
|
+
flex: 1;
|
|
153
|
+
min-width: 0;
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
/* Side nav */
|
|
157
|
+
.sidenav {
|
|
158
|
+
width: 160px;
|
|
159
|
+
flex-shrink: 0;
|
|
160
|
+
position: sticky;
|
|
161
|
+
top: 48px;
|
|
162
|
+
align-self: flex-start;
|
|
163
|
+
max-height: calc(100vh - 60px);
|
|
164
|
+
overflow-y: auto;
|
|
165
|
+
padding: 16px 8px 16px 16px;
|
|
166
|
+
border-right: 1px solid var(--border);
|
|
167
|
+
}
|
|
168
|
+
.sidenav-title {
|
|
169
|
+
font-size: 0.7rem;
|
|
170
|
+
text-transform: uppercase;
|
|
171
|
+
letter-spacing: 0.08em;
|
|
172
|
+
color: var(--text-muted);
|
|
173
|
+
margin-bottom: 8px;
|
|
174
|
+
font-weight: 600;
|
|
175
|
+
}
|
|
176
|
+
.sidenav-item {
|
|
177
|
+
display: block;
|
|
178
|
+
padding: 4px 8px;
|
|
179
|
+
font-size: 0.8rem;
|
|
180
|
+
color: var(--text-muted);
|
|
181
|
+
text-decoration: none;
|
|
182
|
+
border-radius: 4px;
|
|
183
|
+
margin-bottom: 2px;
|
|
184
|
+
transition: background 0.15s, color 0.15s;
|
|
185
|
+
}
|
|
186
|
+
.sidenav-item:hover {
|
|
187
|
+
background: var(--bg-secondary);
|
|
188
|
+
color: var(--text);
|
|
189
|
+
}
|
|
190
|
+
.sidenav-item.active {
|
|
191
|
+
background: var(--user-bg);
|
|
192
|
+
color: var(--text);
|
|
193
|
+
font-weight: 600;
|
|
194
|
+
}
|
|
195
|
+
.sidenav-count {
|
|
196
|
+
font-weight: 400;
|
|
197
|
+
opacity: 0.6;
|
|
198
|
+
}
|
|
199
|
+
|
|
200
|
+
/* Date group headings */
|
|
201
|
+
.date-heading {
|
|
202
|
+
font-size: 0.85rem;
|
|
203
|
+
font-weight: 700;
|
|
204
|
+
color: var(--text-muted);
|
|
205
|
+
padding: 12px 0 6px;
|
|
206
|
+
border-bottom: 1px solid var(--border);
|
|
207
|
+
margin-bottom: 8px;
|
|
208
|
+
position: sticky;
|
|
209
|
+
top: 44px;
|
|
210
|
+
background: var(--bg);
|
|
211
|
+
z-index: 5;
|
|
212
|
+
}
|
|
213
|
+
|
|
214
|
+
/* Sticky date bar */
|
|
215
|
+
.sticky-date-bar {
|
|
216
|
+
position: fixed;
|
|
217
|
+
top: 0;
|
|
218
|
+
left: 0;
|
|
219
|
+
right: 0;
|
|
220
|
+
z-index: 100;
|
|
221
|
+
background: var(--bg-secondary);
|
|
222
|
+
border-bottom: 1px solid var(--border);
|
|
223
|
+
padding: 8px 24px;
|
|
224
|
+
font-size: 0.85rem;
|
|
225
|
+
font-weight: 700;
|
|
226
|
+
color: var(--text);
|
|
227
|
+
transform: translateY(-100%);
|
|
228
|
+
transition: transform 0.2s;
|
|
229
|
+
}
|
|
230
|
+
.sticky-date-bar.visible {
|
|
231
|
+
transform: translateY(0);
|
|
232
|
+
}
|
|
233
|
+
|
|
234
|
+
/* Responsive: collapse sidenav on small screens */
|
|
235
|
+
@media (max-width: 768px) {
|
|
236
|
+
.sidenav { display: none; }
|
|
237
|
+
.session-layout { display: block; }
|
|
238
|
+
}
|
|
239
|
+
`;
|
|
240
|
+
}
|
|
241
|
+
|
|
242
|
+
function generateIndex(projects) {
|
|
243
|
+
const items = projects.map(p => {
|
|
244
|
+
const href = `/project/${encodeURIComponent(p.rawName)}`;
|
|
245
|
+
return `<a class="list-item" href="${href}">
|
|
246
|
+
<div class="list-item-title">${escapeHtml(p.name)}</div>
|
|
247
|
+
<div class="list-item-meta">
|
|
248
|
+
<span>${p.sessionCount} sessions</span>
|
|
249
|
+
<span>Last: ${formatDate(p.lastModified)}</span>
|
|
250
|
+
</div>
|
|
251
|
+
</a>`;
|
|
252
|
+
}).join('\n');
|
|
253
|
+
|
|
254
|
+
return pageShell('cctape', `
|
|
255
|
+
<div class="page-header">
|
|
256
|
+
<h1>cctape</h1>
|
|
257
|
+
<div class="subtitle">Claude Code Session Logs</div>
|
|
258
|
+
</div>
|
|
259
|
+
<div class="list-container">
|
|
260
|
+
<input class="search-box" type="text" placeholder="Filter projects..." autofocus>
|
|
261
|
+
${items || '<div class="empty">No projects found</div>'}
|
|
262
|
+
</div>
|
|
263
|
+
<script>
|
|
264
|
+
document.querySelector('.search-box').addEventListener('input', function(e) {
|
|
265
|
+
var q = e.target.value.toLowerCase();
|
|
266
|
+
document.querySelectorAll('.list-item').forEach(function(el) {
|
|
267
|
+
el.style.display = el.textContent.toLowerCase().includes(q) ? '' : 'none';
|
|
268
|
+
});
|
|
269
|
+
});
|
|
270
|
+
</script>`);
|
|
271
|
+
}
|
|
272
|
+
|
|
273
|
+
function formatDateOnly(ts) {
|
|
274
|
+
if (!ts) return '';
|
|
275
|
+
const d = ts instanceof Date ? ts : new Date(ts);
|
|
276
|
+
return d.toLocaleDateString('en-CA', { year: 'numeric', month: '2-digit', day: '2-digit' });
|
|
277
|
+
}
|
|
278
|
+
|
|
279
|
+
function formatTimeOnly(ts) {
|
|
280
|
+
if (!ts) return '';
|
|
281
|
+
const d = ts instanceof Date ? ts : new Date(ts);
|
|
282
|
+
return d.toLocaleTimeString('en-GB', { hour: '2-digit', minute: '2-digit' });
|
|
283
|
+
}
|
|
284
|
+
|
|
285
|
+
function generateSessionList(project, sessions) {
|
|
286
|
+
// Group sessions by date (based on lastModified)
|
|
287
|
+
const dateGroups = [];
|
|
288
|
+
let currentDate = null;
|
|
289
|
+
for (const s of sessions) {
|
|
290
|
+
const dateStr = formatDateOnly(new Date(s.lastModified));
|
|
291
|
+
if (dateStr !== currentDate) {
|
|
292
|
+
currentDate = dateStr;
|
|
293
|
+
dateGroups.push({ date: dateStr, sessions: [] });
|
|
294
|
+
}
|
|
295
|
+
dateGroups[dateGroups.length - 1].sessions.push(s);
|
|
296
|
+
}
|
|
297
|
+
|
|
298
|
+
// Build side nav
|
|
299
|
+
const sideNavItems = dateGroups.map(g =>
|
|
300
|
+
`<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>`
|
|
301
|
+
).join('\n');
|
|
302
|
+
|
|
303
|
+
// Build session items grouped by date
|
|
304
|
+
const groupsHtml = dateGroups.map(g => {
|
|
305
|
+
const items = g.sessions.map(s => {
|
|
306
|
+
const href = `/project/${encodeURIComponent(project.rawName)}/session/${encodeURIComponent(s.id)}`;
|
|
307
|
+
const lastModTime = formatTimeOnly(new Date(s.lastModified));
|
|
308
|
+
const startedTime = formatDate(s.timestamp);
|
|
309
|
+
const slug = s.slug ? `<span class="badge">${escapeHtml(s.slug)}</span>` : '';
|
|
310
|
+
const sub = s.hasSubagents ? '<span class="badge">subagents</span>' : '';
|
|
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}">
|
|
318
|
+
<div class="list-item-title">${lastModTime} ${slug} ${sub}</div>
|
|
319
|
+
<div class="list-item-meta"><span>started: ${startedTime}</span>${model}${branch}${tokens}${outTok}</div>
|
|
320
|
+
${preview}
|
|
321
|
+
</a>`;
|
|
322
|
+
}).join('\n');
|
|
323
|
+
|
|
324
|
+
return `<div class="date-group" id="date-${g.date}" data-date="${escapeHtml(g.date)}">
|
|
325
|
+
<div class="date-heading">${escapeHtml(g.date)}</div>
|
|
326
|
+
${items}
|
|
327
|
+
</div>`;
|
|
328
|
+
}).join('\n');
|
|
329
|
+
|
|
330
|
+
return pageShell(`${project.name} — cctape`, `
|
|
331
|
+
<div class="sticky-date-bar" id="stickyDateBar"></div>
|
|
332
|
+
<div class="page-header">
|
|
333
|
+
<div class="breadcrumb"><a href="/">cctape</a> / ${escapeHtml(project.name)}</div>
|
|
334
|
+
<h1>${escapeHtml(project.name)}</h1>
|
|
335
|
+
<div class="subtitle">${sessions.length} sessions</div>
|
|
336
|
+
</div>
|
|
337
|
+
<div class="session-layout">
|
|
338
|
+
<nav class="sidenav" id="sidenav">
|
|
339
|
+
<div class="sidenav-title">Dates</div>
|
|
340
|
+
${sideNavItems}
|
|
341
|
+
</nav>
|
|
342
|
+
<div class="list-container">
|
|
343
|
+
<input class="search-box" type="text" placeholder="Filter sessions..." autofocus>
|
|
344
|
+
${groupsHtml || '<div class="empty">No sessions found</div>'}
|
|
345
|
+
</div>
|
|
346
|
+
</div>
|
|
347
|
+
<script>
|
|
348
|
+
(function() {
|
|
349
|
+
// Filter
|
|
350
|
+
document.querySelector('.search-box').addEventListener('input', function(e) {
|
|
351
|
+
var q = e.target.value.toLowerCase();
|
|
352
|
+
document.querySelectorAll('.list-item').forEach(function(el) {
|
|
353
|
+
el.style.display = el.textContent.toLowerCase().includes(q) ? '' : 'none';
|
|
354
|
+
});
|
|
355
|
+
// Show/hide date groups if all items hidden
|
|
356
|
+
document.querySelectorAll('.date-group').forEach(function(g) {
|
|
357
|
+
var visible = g.querySelectorAll('.list-item:not([style*="display: none"])').length;
|
|
358
|
+
g.style.display = visible ? '' : 'none';
|
|
359
|
+
});
|
|
360
|
+
});
|
|
361
|
+
|
|
362
|
+
// Sticky date bar + sidenav active state on scroll
|
|
363
|
+
var groups = Array.from(document.querySelectorAll('.date-group'));
|
|
364
|
+
var bar = document.getElementById('stickyDateBar');
|
|
365
|
+
var navItems = Array.from(document.querySelectorAll('.sidenav-item'));
|
|
366
|
+
|
|
367
|
+
function updateCurrentDate() {
|
|
368
|
+
var scrollY = window.scrollY + 60;
|
|
369
|
+
var current = null;
|
|
370
|
+
for (var i = 0; i < groups.length; i++) {
|
|
371
|
+
if (groups[i].offsetTop <= scrollY) current = groups[i];
|
|
372
|
+
}
|
|
373
|
+
var date = current ? current.dataset.date : '';
|
|
374
|
+
|
|
375
|
+
if (date) {
|
|
376
|
+
bar.textContent = date;
|
|
377
|
+
bar.classList.add('visible');
|
|
378
|
+
} else {
|
|
379
|
+
bar.classList.remove('visible');
|
|
380
|
+
}
|
|
381
|
+
|
|
382
|
+
navItems.forEach(function(a) {
|
|
383
|
+
a.classList.toggle('active', a.dataset.date === date);
|
|
384
|
+
});
|
|
385
|
+
}
|
|
386
|
+
|
|
387
|
+
window.addEventListener('scroll', updateCurrentDate, { passive: true });
|
|
388
|
+
updateCurrentDate();
|
|
389
|
+
})();
|
|
390
|
+
</script>`);
|
|
391
|
+
}
|
|
392
|
+
|
|
393
|
+
module.exports = { generateIndex, generateSessionList };
|