@openmnemo/report 0.2.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/dist/index.js ADDED
@@ -0,0 +1,3235 @@
1
+ import {
2
+ getLogger
3
+ } from "./chunk-RCUFIYQZ.js";
4
+
5
+ // src/build.ts
6
+ import {
7
+ existsSync as existsSync4,
8
+ mkdirSync as mkdirSync3,
9
+ readdirSync,
10
+ readFileSync as readFileSync4,
11
+ writeFileSync as writeFileSync3,
12
+ appendFileSync,
13
+ rmSync
14
+ } from "fs";
15
+ import { basename as basename3, dirname, join as join4 } from "path";
16
+
17
+ // src/stats.ts
18
+ function computeStats(manifests, toolCounts = {}) {
19
+ const totalSessions = manifests.length;
20
+ let totalMessages = 0;
21
+ let totalToolEvents = 0;
22
+ const clientCounts = {};
23
+ const daySet = /* @__PURE__ */ new Set();
24
+ for (const m of manifests) {
25
+ totalMessages += m.message_count;
26
+ totalToolEvents += m.tool_event_count;
27
+ const client = m.client || "unknown";
28
+ clientCounts[client] = (clientCounts[client] ?? 0) + 1;
29
+ const day = isoDay(m.started_at);
30
+ if (day) daySet.add(day);
31
+ }
32
+ const dayBuckets = buildDayBuckets(manifests);
33
+ const weekBuckets = buildWeekBuckets(manifests);
34
+ const dates = manifests.map((m) => m.started_at).filter(Boolean).sort();
35
+ const from = dates[0] ?? "";
36
+ const to = dates[dates.length - 1] ?? "";
37
+ return {
38
+ totalSessions,
39
+ totalMessages,
40
+ totalToolEvents,
41
+ activeDays: daySet.size,
42
+ dateRange: { from, to },
43
+ clientCounts,
44
+ dayBuckets,
45
+ weekBuckets,
46
+ toolCounts
47
+ };
48
+ }
49
+ function buildDayBuckets(manifests) {
50
+ const buckets = {};
51
+ for (const m of manifests) {
52
+ const day = isoDay(m.started_at);
53
+ if (day) {
54
+ buckets[day] = (buckets[day] ?? 0) + 1;
55
+ }
56
+ }
57
+ return buckets;
58
+ }
59
+ function buildWeekBuckets(manifests) {
60
+ const buckets = {};
61
+ for (const m of manifests) {
62
+ const week = isoWeek(m.started_at);
63
+ if (week) {
64
+ buckets[week] = (buckets[week] ?? 0) + m.message_count;
65
+ }
66
+ }
67
+ return buckets;
68
+ }
69
+ function extractToolNames(summaries) {
70
+ return summaries.map((s) => {
71
+ const trimmed = s.trim();
72
+ const match = trimmed.match(/^([A-Za-z_][A-Za-z0-9_:-]*)/);
73
+ if (match?.[1]) {
74
+ return match[1];
75
+ }
76
+ return "unknown";
77
+ });
78
+ }
79
+ function accumulateToolCounts(toolCounts, names) {
80
+ const result = { ...toolCounts };
81
+ for (const name of names) {
82
+ result[name] = (result[name] ?? 0) + 1;
83
+ }
84
+ return result;
85
+ }
86
+ function isoDay(ts) {
87
+ if (!ts) return "";
88
+ const match = ts.match(/^(\d{4}-\d{2}-\d{2})/);
89
+ return match?.[1] ?? "";
90
+ }
91
+ function isoWeek(ts) {
92
+ if (!ts) return "";
93
+ try {
94
+ const d = new Date(ts);
95
+ if (isNaN(d.getTime())) return "";
96
+ return isoWeekKey(d);
97
+ } catch {
98
+ return "";
99
+ }
100
+ }
101
+ function isoWeekKey(d) {
102
+ const thursday = new Date(d);
103
+ thursday.setDate(d.getDate() + (4 - ((d.getDay() + 6) % 7 + 1)));
104
+ const year = thursday.getFullYear();
105
+ const jan4 = new Date(year, 0, 4);
106
+ const week = Math.ceil(
107
+ ((thursday.getTime() - jan4.getTime()) / 864e5 + jan4.getDay() + 1) / 7
108
+ );
109
+ return `${year}-W${String(week).padStart(2, "0")}`;
110
+ }
111
+
112
+ // src/render/layout.ts
113
+ import { basename } from "path";
114
+
115
+ // src/render/css.ts
116
+ var REPORT_CSS = `
117
+ :root {
118
+ --bg: #0d1117;
119
+ --bg-secondary: #161b22;
120
+ --bg-card: #21262d;
121
+ --border: #30363d;
122
+ --text: #e6edf3;
123
+ --text-muted: #8b949e;
124
+ --accent: #58a6ff;
125
+ --accent-hover: #79b8ff;
126
+ --green-0: #161b22;
127
+ --green-1: #0e4429;
128
+ --green-2: #006d32;
129
+ --green-3: #26a641;
130
+ --green-4: #39d353;
131
+ --user-bg: #1c2d3e;
132
+ --user-border: #388bfd44;
133
+ --assistant-bg: #1c1c1c;
134
+ --assistant-border: #30363d;
135
+ --code-bg: #161b22;
136
+ --badge-codex: #1f6feb;
137
+ --badge-claude: #6e40c9;
138
+ --badge-gemini: #1a7f37;
139
+ --danger: #f85149;
140
+ --warning: #d29922;
141
+ --success: #3fb950;
142
+ --mark-bg: #3d3000;
143
+ --sidebar-width: 240px;
144
+ --font-mono: 'SF Mono', 'Consolas', 'Liberation Mono', 'Menlo', monospace;
145
+ }
146
+
147
+ [data-theme="light"] {
148
+ --bg: #ffffff;
149
+ --bg-secondary: #f6f8fa;
150
+ --bg-card: #ffffff;
151
+ --border: #d0d7de;
152
+ --text: #1f2328;
153
+ --text-muted: #636c76;
154
+ --accent: #0969da;
155
+ --accent-hover: #0550ae;
156
+ --user-bg: #ddf4ff;
157
+ --user-border: #54aeff44;
158
+ --assistant-bg: #f6f8fa;
159
+ --assistant-border: #d0d7de;
160
+ --code-bg: #f6f8fa;
161
+ --mark-bg: #fff8c5;
162
+ --green-0: #f6f8fa;
163
+ --green-1: #acf2bd;
164
+ --green-2: #40c463;
165
+ --green-3: #30a14e;
166
+ --green-4: #216e39;
167
+ }
168
+
169
+ *, *::before, *::after { box-sizing: border-box; margin: 0; padding: 0; }
170
+
171
+ html { font-size: 16px; scroll-behavior: smooth; }
172
+
173
+ body {
174
+ background: var(--bg);
175
+ color: var(--text);
176
+ font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', 'Helvetica Neue', sans-serif;
177
+ line-height: 1.6;
178
+ min-height: 100vh;
179
+ display: flex;
180
+ flex-direction: column;
181
+ }
182
+
183
+ a { color: var(--accent); text-decoration: none; }
184
+ a:hover { color: var(--accent-hover); text-decoration: underline; }
185
+
186
+ code {
187
+ font-family: var(--font-mono);
188
+ font-size: 0.875em;
189
+ background: var(--code-bg);
190
+ padding: 0.15em 0.4em;
191
+ border-radius: 4px;
192
+ border: 1px solid var(--border);
193
+ }
194
+
195
+ pre {
196
+ background: var(--code-bg);
197
+ border: 1px solid var(--border);
198
+ border-radius: 8px;
199
+ padding: 1rem;
200
+ overflow-x: auto;
201
+ margin: 0.75rem 0;
202
+ }
203
+
204
+ pre code {
205
+ background: none;
206
+ border: none;
207
+ padding: 0;
208
+ font-size: 0.875rem;
209
+ }
210
+
211
+ h1, h2, h3, h4, h5, h6 {
212
+ color: var(--text);
213
+ line-height: 1.3;
214
+ margin-bottom: 0.5rem;
215
+ }
216
+
217
+ h1 { font-size: 1.75rem; }
218
+ h2 { font-size: 1.375rem; border-bottom: 1px solid var(--border); padding-bottom: 0.4rem; margin-top: 1.5rem; margin-bottom: 0.75rem; }
219
+ h3 { font-size: 1.125rem; }
220
+
221
+ p { margin-bottom: 0.75rem; }
222
+
223
+ ul, ol { padding-left: 1.5rem; margin-bottom: 0.75rem; }
224
+ li { margin-bottom: 0.25rem; }
225
+
226
+ table { width: 100%; border-collapse: collapse; font-size: 0.9rem; }
227
+ th, td { padding: 0.5rem 0.75rem; text-align: left; border-bottom: 1px solid var(--border); }
228
+ th { color: var(--text-muted); font-weight: 600; font-size: 0.8rem; text-transform: uppercase; letter-spacing: 0.05em; background: var(--bg-secondary); }
229
+ tr:hover td { background: var(--bg-secondary); }
230
+
231
+ /* \u2500\u2500 Mobile topbar \u2500\u2500 */
232
+ .topbar {
233
+ display: none;
234
+ background: var(--bg-secondary);
235
+ border-bottom: 1px solid var(--border);
236
+ padding: 0 1rem;
237
+ align-items: center;
238
+ gap: 0.75rem;
239
+ height: 48px;
240
+ position: sticky;
241
+ top: 0;
242
+ z-index: 200;
243
+ }
244
+
245
+ .topbar-brand {
246
+ font-weight: 700;
247
+ font-size: 0.95rem;
248
+ color: var(--text);
249
+ flex: 1;
250
+ }
251
+
252
+ .topbar-actions {
253
+ display: flex;
254
+ align-items: center;
255
+ gap: 0.5rem;
256
+ }
257
+
258
+ .hamburger, .theme-toggle-btn {
259
+ background: none;
260
+ border: 1px solid var(--border);
261
+ color: var(--text);
262
+ border-radius: 6px;
263
+ padding: 0.3rem 0.5rem;
264
+ cursor: pointer;
265
+ font-size: 1rem;
266
+ line-height: 1;
267
+ display: flex;
268
+ align-items: center;
269
+ justify-content: center;
270
+ }
271
+
272
+ .hamburger:hover, .theme-toggle-btn:hover {
273
+ background: var(--bg-card);
274
+ }
275
+
276
+ /* \u2500\u2500 Sidebar \u2500\u2500 */
277
+ .sidebar {
278
+ width: var(--sidebar-width);
279
+ background: var(--bg-secondary);
280
+ border-right: 1px solid var(--border);
281
+ position: fixed;
282
+ top: 0;
283
+ left: 0;
284
+ bottom: 0;
285
+ overflow-y: auto;
286
+ z-index: 150;
287
+ display: flex;
288
+ flex-direction: column;
289
+ transition: transform 0.2s ease;
290
+ }
291
+
292
+ .sidebar-header {
293
+ padding: 1rem 1.25rem;
294
+ border-bottom: 1px solid var(--border);
295
+ display: flex;
296
+ align-items: center;
297
+ justify-content: space-between;
298
+ flex-shrink: 0;
299
+ }
300
+
301
+ .sidebar-brand {
302
+ font-weight: 700;
303
+ font-size: 1rem;
304
+ color: var(--text);
305
+ }
306
+
307
+ .sidebar-nav {
308
+ flex: 1;
309
+ padding: 0.5rem 0;
310
+ overflow-y: auto;
311
+ }
312
+
313
+ .nav-section-label {
314
+ padding: 0.75rem 1.25rem 0.25rem;
315
+ font-size: 0.7rem;
316
+ font-weight: 600;
317
+ text-transform: uppercase;
318
+ letter-spacing: 0.08em;
319
+ color: var(--text-muted);
320
+ }
321
+
322
+ .nav-link {
323
+ display: flex;
324
+ align-items: center;
325
+ gap: 0.5rem;
326
+ padding: 0.45rem 1.25rem;
327
+ color: var(--text-muted);
328
+ font-size: 0.9rem;
329
+ transition: color 0.15s, background 0.15s;
330
+ border-radius: 0;
331
+ }
332
+
333
+ .nav-link:hover {
334
+ color: var(--text);
335
+ background: var(--bg-card);
336
+ text-decoration: none;
337
+ }
338
+
339
+ .nav-link.active {
340
+ color: var(--accent);
341
+ background: var(--bg-card);
342
+ border-left: 3px solid var(--accent);
343
+ padding-left: calc(1.25rem - 3px);
344
+ }
345
+
346
+ .sidebar-footer {
347
+ padding: 0.75rem 1.25rem;
348
+ border-top: 1px solid var(--border);
349
+ display: flex;
350
+ align-items: center;
351
+ gap: 0.5rem;
352
+ flex-shrink: 0;
353
+ }
354
+
355
+ .sidebar-footer .theme-toggle-btn {
356
+ font-size: 0.85rem;
357
+ padding: 0.25rem 0.6rem;
358
+ gap: 0.3rem;
359
+ }
360
+
361
+ /* \u2500\u2500 Overlay (mobile) \u2500\u2500 */
362
+ .sidebar-overlay {
363
+ display: none;
364
+ position: fixed;
365
+ inset: 0;
366
+ background: rgba(0,0,0,0.5);
367
+ z-index: 140;
368
+ }
369
+
370
+ /* \u2500\u2500 Main content \u2500\u2500 */
371
+ .main-content {
372
+ margin-left: var(--sidebar-width);
373
+ flex: 1;
374
+ min-height: 100vh;
375
+ }
376
+
377
+ .container {
378
+ max-width: 960px;
379
+ margin: 0 auto;
380
+ padding: 1.5rem;
381
+ }
382
+
383
+ .page-header { margin-bottom: 2rem; }
384
+ .page-header h1 { font-size: 1.5rem; }
385
+ .page-header .subtitle { color: var(--text-muted); font-size: 0.9rem; margin-top: 0.25rem; }
386
+
387
+ /* \u2500\u2500 Cards \u2500\u2500 */
388
+ .card {
389
+ background: var(--bg-card);
390
+ border: 1px solid var(--border);
391
+ border-radius: 8px;
392
+ padding: 1.25rem;
393
+ margin-bottom: 1rem;
394
+ }
395
+
396
+ .card-title {
397
+ font-size: 0.8rem;
398
+ font-weight: 600;
399
+ text-transform: uppercase;
400
+ letter-spacing: 0.05em;
401
+ color: var(--text-muted);
402
+ margin-bottom: 0.5rem;
403
+ }
404
+
405
+ .stat-value {
406
+ font-size: 2rem;
407
+ font-weight: 700;
408
+ color: var(--text);
409
+ line-height: 1;
410
+ }
411
+
412
+ .stat-label { font-size: 0.85rem; color: var(--text-muted); margin-top: 0.25rem; }
413
+
414
+ .stats-grid {
415
+ display: grid;
416
+ grid-template-columns: repeat(auto-fill, minmax(160px, 1fr));
417
+ gap: 1rem;
418
+ margin-bottom: 1.5rem;
419
+ }
420
+
421
+ /* \u2500\u2500 Charts \u2500\u2500 */
422
+ .chart-grid {
423
+ display: grid;
424
+ grid-template-columns: 1fr 1fr;
425
+ gap: 1rem;
426
+ margin-bottom: 1.5rem;
427
+ }
428
+
429
+ @media (max-width: 900px) {
430
+ .chart-grid { grid-template-columns: 1fr; }
431
+ }
432
+
433
+ .chart-card {
434
+ background: var(--bg-card);
435
+ border: 1px solid var(--border);
436
+ border-radius: 8px;
437
+ padding: 1.25rem;
438
+ }
439
+
440
+ .chart-title {
441
+ font-size: 0.85rem;
442
+ font-weight: 600;
443
+ color: var(--text-muted);
444
+ margin-bottom: 1rem;
445
+ }
446
+
447
+ .chart-card svg { display: block; width: 100%; overflow: visible; }
448
+ .chart-card.full-width { grid-column: 1 / -1; }
449
+
450
+ /* \u2500\u2500 Badge \u2500\u2500 */
451
+ .badge {
452
+ display: inline-block;
453
+ padding: 0.2em 0.6em;
454
+ border-radius: 12px;
455
+ font-size: 0.75rem;
456
+ font-weight: 600;
457
+ text-transform: uppercase;
458
+ letter-spacing: 0.04em;
459
+ }
460
+
461
+ .badge-codex { background: #1f6feb33; color: #58a6ff; border: 1px solid #1f6feb66; }
462
+ .badge-claude { background: #6e40c933; color: #bc8cff; border: 1px solid #6e40c966; }
463
+ .badge-gemini { background: #1a7f3733; color: #3fb950; border: 1px solid #1a7f3766; }
464
+ .badge-unknown { background: #30363d33; color: #8b949e; border: 1px solid #30363d66; }
465
+
466
+ /* \u2500\u2500 Tab bar \u2500\u2500 */
467
+ .tab-bar {
468
+ display: flex;
469
+ gap: 0.25rem;
470
+ margin-bottom: 1.25rem;
471
+ flex-wrap: wrap;
472
+ }
473
+
474
+ .tab-btn {
475
+ background: var(--bg-card);
476
+ border: 1px solid var(--border);
477
+ color: var(--text-muted);
478
+ padding: 0.35rem 0.9rem;
479
+ border-radius: 6px;
480
+ font-size: 0.85rem;
481
+ cursor: pointer;
482
+ transition: all 0.15s;
483
+ }
484
+
485
+ .tab-btn:hover { color: var(--text); border-color: var(--accent); }
486
+ .tab-btn.active { color: var(--accent); border-color: var(--accent); background: var(--bg-secondary); }
487
+
488
+ /* \u2500\u2500 Messages \u2500\u2500 */
489
+ .messages { display: flex; flex-direction: column; gap: 0.75rem; margin-top: 1rem; }
490
+
491
+ .message {
492
+ border-radius: 8px;
493
+ padding: 1rem;
494
+ border: 1px solid;
495
+ max-width: 100%;
496
+ }
497
+
498
+ .message-user {
499
+ background: var(--user-bg);
500
+ border-color: var(--user-border);
501
+ align-self: flex-start;
502
+ }
503
+
504
+ .message-assistant {
505
+ background: var(--assistant-bg);
506
+ border-color: var(--assistant-border);
507
+ }
508
+
509
+ .message-header {
510
+ display: flex;
511
+ align-items: center;
512
+ gap: 0.5rem;
513
+ margin-bottom: 0.5rem;
514
+ font-size: 0.8rem;
515
+ color: var(--text-muted);
516
+ }
517
+
518
+ .message-role {
519
+ font-weight: 600;
520
+ font-size: 0.8rem;
521
+ text-transform: capitalize;
522
+ }
523
+
524
+ .message-user .message-role { color: var(--accent); }
525
+ .message-assistant .message-role { color: var(--text-muted); }
526
+
527
+ .message-body { font-size: 0.9rem; line-height: 1.7; word-break: break-word; }
528
+ .message-body p { margin-bottom: 0.5rem; }
529
+ .message-body p:last-child { margin-bottom: 0; }
530
+
531
+ /* \u2500\u2500 Summary card \u2500\u2500 */
532
+ .summary-card {
533
+ background: var(--bg-secondary);
534
+ border: 1px solid var(--border);
535
+ border-left: 3px solid var(--accent);
536
+ border-radius: 6px;
537
+ padding: 1rem 1.25rem;
538
+ margin-bottom: 1.25rem;
539
+ font-size: 0.9rem;
540
+ color: var(--text-muted);
541
+ }
542
+
543
+ .summary-card .summary-label {
544
+ font-size: 0.75rem;
545
+ font-weight: 600;
546
+ text-transform: uppercase;
547
+ letter-spacing: 0.05em;
548
+ color: var(--accent);
549
+ margin-bottom: 0.4rem;
550
+ }
551
+
552
+ /* \u2500\u2500 Metadata table \u2500\u2500 */
553
+ .meta-table { font-size: 0.85rem; margin-bottom: 1.25rem; }
554
+ .meta-table td:first-child { color: var(--text-muted); width: 140px; font-weight: 500; }
555
+
556
+ /* \u2500\u2500 Backlinks \u2500\u2500 */
557
+ .backlinks {
558
+ background: var(--bg-secondary);
559
+ border: 1px solid var(--border);
560
+ border-radius: 6px;
561
+ padding: 0.75rem 1rem;
562
+ margin-bottom: 1.25rem;
563
+ font-size: 0.85rem;
564
+ }
565
+
566
+ .backlinks-title { font-weight: 600; color: var(--text-muted); margin-bottom: 0.4rem; font-size: 0.8rem; text-transform: uppercase; }
567
+ .backlinks ul { padding-left: 1rem; }
568
+
569
+ /* \u2500\u2500 Search \u2500\u2500 */
570
+ .search-box {
571
+ width: 100%;
572
+ background: var(--bg-card);
573
+ border: 1px solid var(--border);
574
+ border-radius: 8px;
575
+ padding: 0.75rem 1rem;
576
+ color: var(--text);
577
+ font-size: 1rem;
578
+ font-family: inherit;
579
+ margin-bottom: 1rem;
580
+ outline: none;
581
+ transition: border-color 0.15s;
582
+ }
583
+
584
+ .search-box:focus { border-color: var(--accent); }
585
+
586
+ .search-results { display: flex; flex-direction: column; gap: 0.75rem; }
587
+
588
+ .search-result {
589
+ background: var(--bg-card);
590
+ border: 1px solid var(--border);
591
+ border-radius: 8px;
592
+ padding: 1rem;
593
+ transition: border-color 0.15s;
594
+ }
595
+
596
+ .search-result:hover { border-color: var(--accent); }
597
+ .search-result-title { font-weight: 600; margin-bottom: 0.25rem; }
598
+ .search-result-meta { font-size: 0.8rem; color: var(--text-muted); margin-bottom: 0.5rem; }
599
+ .search-result-snippet { font-size: 0.85rem; color: var(--text-muted); }
600
+ .search-result mark { background: var(--mark-bg); color: var(--warning); border-radius: 2px; padding: 0 0.1em; }
601
+ #search-count { font-size: 0.85rem; color: var(--text-muted); margin-bottom: 0.75rem; }
602
+
603
+ /* \u2500\u2500 Heatmap legend \u2500\u2500 */
604
+ .heatmap-legend {
605
+ display: flex;
606
+ align-items: center;
607
+ gap: 0.4rem;
608
+ font-size: 0.75rem;
609
+ color: var(--text-muted);
610
+ margin-top: 0.5rem;
611
+ justify-content: flex-end;
612
+ }
613
+
614
+ /* \u2500\u2500 Popover \u2500\u2500 */
615
+ .popover {
616
+ position: fixed;
617
+ z-index: 500;
618
+ background: var(--bg-card);
619
+ border: 1px solid var(--border);
620
+ border-radius: 8px;
621
+ padding: 0.75rem 1rem;
622
+ max-width: 320px;
623
+ font-size: 0.85rem;
624
+ box-shadow: 0 8px 24px rgba(0,0,0,0.4);
625
+ pointer-events: none;
626
+ }
627
+
628
+ .popover-meta { color: var(--text-muted); font-size: 0.78rem; margin-bottom: 0.4rem; }
629
+ .popover-summary { color: var(--text); line-height: 1.5; }
630
+
631
+ /* \u2500\u2500 Graph canvas \u2500\u2500 */
632
+ #graph-canvas {
633
+ display: block;
634
+ width: 100%;
635
+ height: 600px;
636
+ border: 1px solid var(--border);
637
+ border-radius: 8px;
638
+ background: var(--bg-secondary);
639
+ cursor: grab;
640
+ }
641
+
642
+ #graph-canvas:active { cursor: grabbing; }
643
+
644
+ .graph-legend {
645
+ display: flex;
646
+ gap: 1.25rem;
647
+ flex-wrap: wrap;
648
+ margin-top: 0.75rem;
649
+ font-size: 0.8rem;
650
+ color: var(--text-muted);
651
+ }
652
+
653
+ .graph-legend-item {
654
+ display: flex;
655
+ align-items: center;
656
+ gap: 0.4rem;
657
+ }
658
+
659
+ .graph-legend-dot {
660
+ width: 10px;
661
+ height: 10px;
662
+ border-radius: 50%;
663
+ flex-shrink: 0;
664
+ }
665
+
666
+ /* \u2500\u2500 Project cards \u2500\u2500 */
667
+ .project-grid {
668
+ display: grid;
669
+ grid-template-columns: repeat(auto-fill, minmax(260px, 1fr));
670
+ gap: 1rem;
671
+ margin-bottom: 1.5rem;
672
+ }
673
+
674
+ .project-card {
675
+ background: var(--bg-card);
676
+ border: 1px solid var(--border);
677
+ border-radius: 8px;
678
+ padding: 1.25rem;
679
+ transition: border-color 0.15s;
680
+ }
681
+
682
+ .project-card:hover { border-color: var(--accent); }
683
+ .project-card-name { font-weight: 700; font-size: 1.05rem; margin-bottom: 0.5rem; }
684
+ .project-card-meta { font-size: 0.82rem; color: var(--text-muted); }
685
+ .project-card-clients { margin-top: 0.6rem; display: flex; gap: 0.35rem; flex-wrap: wrap; }
686
+
687
+ /* \u2500\u2500 Responsive: tablet icon mode (768\u20131024px) \u2500\u2500 */
688
+ @media (max-width: 1024px) and (min-width: 769px) {
689
+ :root { --sidebar-width: 52px; }
690
+ .nav-link .nav-label { display: none; }
691
+ .nav-link { justify-content: center; padding: 0.55rem; gap: 0; }
692
+ .nav-section-label { display: none; }
693
+ .sidebar-brand { display: none; }
694
+ .sidebar-footer .theme-label { display: none; }
695
+ .sidebar-footer .theme-toggle-btn { padding: 0.35rem 0.5rem; }
696
+ }
697
+
698
+ /* \u2500\u2500 Responsive: mobile (<768px) \u2500\u2500 */
699
+ @media (max-width: 768px) {
700
+ .topbar { display: flex; }
701
+ .sidebar {
702
+ transform: translateX(-100%);
703
+ top: 48px;
704
+ }
705
+ .sidebar.open { transform: translateX(0); }
706
+ .sidebar-overlay.open { display: block; }
707
+ .main-content { margin-left: 0; }
708
+ .container { padding: 1rem 0.75rem; }
709
+ .stats-grid { grid-template-columns: repeat(2, 1fr); }
710
+ .project-grid { grid-template-columns: 1fr; }
711
+ }
712
+
713
+ /* \u2500\u2500 Sidebar collapse \u2500\u2500 */
714
+ body.sidebar-collapsed .sidebar { width: 52px; }
715
+ body.sidebar-collapsed .nav-label { display: none; }
716
+ body.sidebar-collapsed .nav-link { justify-content: center; padding: 0.55rem; gap: 0; }
717
+ body.sidebar-collapsed .nav-section-label { display: none; }
718
+ body.sidebar-collapsed .sidebar-brand { display: none; }
719
+ body.sidebar-collapsed .theme-label { display: none; }
720
+ body.sidebar-collapsed .main-content { margin-left: 52px; }
721
+ .sidebar-collapse-btn {
722
+ background: none;
723
+ border: 1px solid var(--border);
724
+ color: var(--text-muted);
725
+ border-radius: 6px;
726
+ padding: 0.25rem 0.5rem;
727
+ cursor: pointer;
728
+ font-size: 0.75rem;
729
+ line-height: 1;
730
+ }
731
+ .sidebar-collapse-btn:hover { color: var(--text); background: var(--bg-card); }
732
+
733
+ /* \u2500\u2500 Breadcrumb \u2500\u2500 */
734
+ .breadcrumb {
735
+ display: flex;
736
+ align-items: center;
737
+ flex-wrap: wrap;
738
+ gap: 0.25rem;
739
+ font-size: 0.82rem;
740
+ color: var(--text-muted);
741
+ margin-bottom: 1rem;
742
+ }
743
+ .breadcrumb-item { color: var(--text-muted); }
744
+ a.breadcrumb-item { color: var(--accent); text-decoration: none; }
745
+ a.breadcrumb-item:hover { text-decoration: underline; }
746
+ .breadcrumb-sep { color: var(--border); user-select: none; }
747
+
748
+ /* \u2500\u2500 TOC \u2500\u2500 */
749
+ .toc {
750
+ background: var(--bg-secondary);
751
+ border: 1px solid var(--border);
752
+ border-left: 3px solid var(--accent);
753
+ border-radius: 6px;
754
+ padding: 0.75rem 1rem;
755
+ margin-bottom: 1.25rem;
756
+ font-size: 0.85rem;
757
+ }
758
+ .toc-toggle {
759
+ background: none;
760
+ border: none;
761
+ color: var(--text-muted);
762
+ font-size: 0.8rem;
763
+ font-weight: 600;
764
+ text-transform: uppercase;
765
+ letter-spacing: 0.05em;
766
+ cursor: pointer;
767
+ padding: 0;
768
+ display: flex;
769
+ align-items: center;
770
+ gap: 0.35rem;
771
+ }
772
+ .toc-toggle:hover { color: var(--text); }
773
+ .toc-list {
774
+ list-style: none;
775
+ padding: 0;
776
+ margin: 0.5rem 0 0;
777
+ }
778
+ .toc-list li { margin-bottom: 0.2rem; }
779
+ .toc-list a { color: var(--accent); }
780
+ .toc-list a:hover { text-decoration: underline; }
781
+ .toc-list[hidden] { display: none; }
782
+ .toc-arrow { transition: transform 0.15s; }
783
+ .toc-toggle[aria-expanded="false"] .toc-arrow { transform: rotate(-90deg); }
784
+
785
+ /* \u2500\u2500 Callouts \u2500\u2500 */
786
+ .callout {
787
+ border-left: 4px solid;
788
+ border-radius: 6px;
789
+ padding: 0.85rem 1rem;
790
+ margin: 0.75rem 0;
791
+ font-size: 0.9rem;
792
+ }
793
+ .callout-title {
794
+ font-weight: 600;
795
+ margin-bottom: 0.4rem;
796
+ font-size: 0.88rem;
797
+ text-transform: capitalize;
798
+ }
799
+ .callout-body p { margin-bottom: 0.35rem; }
800
+ .callout-body p:last-child { margin-bottom: 0; }
801
+ .callout-note { background: #1f6feb22; border-color: #58a6ff; }
802
+ .callout-tip { background: #1a7f3722; border-color: #3fb950; }
803
+ .callout-info { background: #1f6feb22; border-color: #58a6ff; }
804
+ .callout-success { background: #1a7f3722; border-color: #3fb950; }
805
+ .callout-important { background: #6e40c922; border-color: #bc8cff; }
806
+ .callout-warning { background: #d2992222; border-color: #d29922; }
807
+ .callout-caution { background: #f8514922; border-color: #f85149; }
808
+ .callout-error { background: #f8514922; border-color: #f85149; }
809
+ [data-theme="light"] .callout-note,
810
+ [data-theme="light"] .callout-info { background: #ddf4ff; border-color: #0969da; }
811
+ [data-theme="light"] .callout-tip,
812
+ [data-theme="light"] .callout-success { background: #dafbe1; border-color: #1a7f37; }
813
+ [data-theme="light"] .callout-important { background: #fbefff; border-color: #8250df; }
814
+ [data-theme="light"] .callout-warning { background: #fff8c5; border-color: #9a6700; }
815
+ [data-theme="light"] .callout-caution,
816
+ [data-theme="light"] .callout-error { background: #ffebe9; border-color: #cf222e; }
817
+
818
+ /* \u2500\u2500 Mermaid diagrams \u2500\u2500 */
819
+ pre.mermaid {
820
+ background: var(--bg-secondary);
821
+ border: 1px solid var(--border);
822
+ border-radius: 8px;
823
+ padding: 1rem;
824
+ text-align: center;
825
+ overflow-x: auto;
826
+ }
827
+
828
+ /* \u2500\u2500 Reader Mode \u2500\u2500 */
829
+ .reader-mode .sidebar,
830
+ .reader-mode .topbar,
831
+ .reader-mode .breadcrumb,
832
+ .reader-mode .meta-table,
833
+ .reader-mode .backlinks { display: none !important; }
834
+ .reader-mode .main-content { margin-left: 0 !important; }
835
+ .reader-mode .container { max-width: 720px; }
836
+ .reader-toggle-btn {
837
+ background: none;
838
+ border: 1px solid var(--border);
839
+ color: var(--text-muted);
840
+ border-radius: 6px;
841
+ padding: 0.3rem 0.6rem;
842
+ cursor: pointer;
843
+ font-size: 0.8rem;
844
+ }
845
+ .reader-toggle-btn:hover { color: var(--text); background: var(--bg-card); }
846
+
847
+ /* \u2500\u2500 Tag badges \u2500\u2500 */
848
+ .tag-list { display: flex; flex-wrap: wrap; gap: 0.3rem; margin-top: 0.35rem; }
849
+ .tag {
850
+ display: inline-block;
851
+ background: var(--bg-secondary);
852
+ border: 1px solid var(--border);
853
+ color: var(--text-muted);
854
+ border-radius: 999px;
855
+ padding: 0.15rem 0.55rem;
856
+ font-size: 0.72rem;
857
+ cursor: pointer;
858
+ transition: background 0.15s, color 0.15s;
859
+ }
860
+ .tag:hover { background: var(--accent); color: #fff; border-color: var(--accent); }
861
+
862
+ /* \u2500\u2500 Search filter dropdowns \u2500\u2500 */
863
+ .search-filter-select {
864
+ background: var(--bg-card);
865
+ border: 1px solid var(--border);
866
+ color: var(--text);
867
+ border-radius: 6px;
868
+ padding: 0.35rem 0.6rem;
869
+ font-size: 0.875rem;
870
+ cursor: pointer;
871
+ }
872
+ .search-filter-select:focus {
873
+ outline: 2px solid var(--accent);
874
+ outline-offset: 1px;
875
+ }
876
+
877
+ /* \u2500\u2500 View Transitions \u2500\u2500 */
878
+ @media (prefers-reduced-motion: no-preference) {
879
+ ::view-transition-old(root) {
880
+ animation: 90ms cubic-bezier(0.4, 0, 1, 1) both vt-fade-out;
881
+ }
882
+ ::view-transition-new(root) {
883
+ animation: 210ms cubic-bezier(0, 0, 0.2, 1) both vt-fade-in;
884
+ }
885
+ }
886
+ @keyframes vt-fade-out { to { opacity: 0; } }
887
+ @keyframes vt-fade-in { from { opacity: 0; } }
888
+ `;
889
+
890
+ // src/render/layout.ts
891
+ function escHtml(text) {
892
+ return text.replace(/&/g, "&amp;").replace(/</g, "&lt;").replace(/>/g, "&gt;").replace(/"/g, "&quot;").replace(/'/g, "&#39;");
893
+ }
894
+ var SIDEBAR_INLINE_JS = `
895
+ <script>
896
+ (function() {
897
+ // Theme
898
+ var saved = localStorage.getItem('mt-theme') || 'dark';
899
+ document.documentElement.setAttribute('data-theme', saved);
900
+ // Sidebar collapse
901
+ if (localStorage.getItem('mt-sidebar') === 'collapsed') document.body.classList.add('sidebar-collapsed');
902
+ function updateThemeBtn() {
903
+ var btn = document.getElementById('theme-toggle');
904
+ if (btn) btn.textContent = document.documentElement.getAttribute('data-theme') === 'dark' ? '\u2600 Light' : '\u263E Dark';
905
+ }
906
+ document.addEventListener('DOMContentLoaded', function() {
907
+ updateThemeBtn();
908
+ var btn = document.getElementById('theme-toggle');
909
+ if (btn) btn.addEventListener('click', function() {
910
+ var next = document.documentElement.getAttribute('data-theme') === 'dark' ? 'light' : 'dark';
911
+ document.documentElement.setAttribute('data-theme', next);
912
+ localStorage.setItem('mt-theme', next);
913
+ updateThemeBtn();
914
+ });
915
+ // Mobile hamburger
916
+ var hb = document.getElementById('hamburger');
917
+ var sb = document.getElementById('sidebar');
918
+ var ov = document.getElementById('sidebar-overlay');
919
+ function closeSidebar() {
920
+ if (sb) sb.classList.remove('open');
921
+ if (ov) ov.classList.remove('open');
922
+ }
923
+ if (hb) hb.addEventListener('click', function() {
924
+ if (sb) sb.classList.toggle('open');
925
+ if (ov) ov.classList.toggle('open');
926
+ });
927
+ if (ov) ov.addEventListener('click', closeSidebar);
928
+ // Sidebar collapse toggle
929
+ var collapseBtn = document.getElementById('sidebar-collapse');
930
+ if (collapseBtn) collapseBtn.addEventListener('click', function() {
931
+ var collapsed = document.body.classList.toggle('sidebar-collapsed');
932
+ localStorage.setItem('mt-sidebar', collapsed ? 'collapsed' : 'expanded');
933
+ });
934
+ // TOC toggle
935
+ var tocToggle = document.getElementById('toc-toggle');
936
+ var tocList = document.getElementById('toc-list');
937
+ if (tocToggle && tocList) tocToggle.addEventListener('click', function() {
938
+ var expanded = tocToggle.getAttribute('aria-expanded') === 'true';
939
+ tocToggle.setAttribute('aria-expanded', expanded ? 'false' : 'true');
940
+ if (expanded) { tocList.setAttribute('hidden', ''); } else { tocList.removeAttribute('hidden'); }
941
+ });
942
+ // Reader Mode toggle
943
+ var readerBtn = document.getElementById('reader-toggle');
944
+ if (readerBtn) {
945
+ if (localStorage.getItem('mt-reader') === '1') document.body.classList.add('reader-mode');
946
+ readerBtn.addEventListener('click', function() {
947
+ var on = document.body.classList.toggle('reader-mode');
948
+ localStorage.setItem('mt-reader', on ? '1' : '0');
949
+ });
950
+ }
951
+ // View Transitions API (progressive enhancement)
952
+ if (document.startViewTransition) {
953
+ document.addEventListener('click', function(e) {
954
+ var a = e.target && (e.target.closest ? e.target.closest('a[href]:not([target]):not([download])') : null);
955
+ if (!a) return;
956
+ var href = a.getAttribute('href');
957
+ if (!href || href.startsWith('#') || href.startsWith('http')) return;
958
+ e.preventDefault();
959
+ document.startViewTransition(function() { window.location.href = href; });
960
+ });
961
+ }
962
+ // Popover preview
963
+ var popover = null;
964
+ document.querySelectorAll('a[data-summary]').forEach(function(el) {
965
+ el.addEventListener('mouseenter', function(e) {
966
+ var summary = el.getAttribute('data-summary') || '';
967
+ var meta = el.getAttribute('data-meta') || '';
968
+ if (!summary && !meta) return;
969
+ if (!popover) {
970
+ popover = document.createElement('div');
971
+ popover.className = 'popover';
972
+ document.body.appendChild(popover);
973
+ }
974
+ popover.innerHTML = '';
975
+ if (meta) {
976
+ var metaDiv = document.createElement('div');
977
+ metaDiv.className = 'popover-meta';
978
+ metaDiv.textContent = meta;
979
+ popover.appendChild(metaDiv);
980
+ }
981
+ if (summary) {
982
+ var sumDiv = document.createElement('div');
983
+ sumDiv.className = 'popover-summary';
984
+ sumDiv.textContent = summary;
985
+ popover.appendChild(sumDiv);
986
+ }
987
+ popover.style.display = 'block';
988
+ positionPopover(e);
989
+ });
990
+ el.addEventListener('mousemove', positionPopover);
991
+ el.addEventListener('mouseleave', function() {
992
+ if (popover) popover.style.display = 'none';
993
+ });
994
+ });
995
+ function positionPopover(e) {
996
+ if (!popover) return;
997
+ var x = e.clientX + 12;
998
+ var y = e.clientY + 12;
999
+ var pw = popover.offsetWidth || 320;
1000
+ var ph = popover.offsetHeight || 80;
1001
+ if (x + pw > window.innerWidth - 8) x = e.clientX - pw - 12;
1002
+ if (y + ph > window.innerHeight - 8) y = e.clientY - ph - 12;
1003
+ popover.style.left = x + 'px';
1004
+ popover.style.top = y + 'px';
1005
+ }
1006
+ });
1007
+ })();
1008
+ </script>`;
1009
+ function buildNavItems(depth) {
1010
+ const p = "../".repeat(depth);
1011
+ return [
1012
+ { id: "dashboard", icon: "\u{1F4CA}", labelKey: "dashboard", href: `${p}index.html` },
1013
+ { id: "transcripts", icon: "\u{1F4AC}", labelKey: "sessions", href: `${p}transcripts/index.html` },
1014
+ { id: "projects", icon: "\u{1F4C1}", labelKey: "projects", href: `${p}projects/index.html` },
1015
+ { id: "graph", icon: "\u{1F578}\uFE0F", labelKey: "graph", href: `${p}graph.html` },
1016
+ { id: "goals", icon: "\u{1F3AF}", labelKey: "goals", href: `${p}goals/index.html` },
1017
+ { id: "todos", icon: "\u2705", labelKey: "todos", href: `${p}todos/index.html` },
1018
+ { id: "knowledge", icon: "\u{1F4DA}", labelKey: "knowledge", href: `${p}knowledge/index.html` },
1019
+ { id: "archive", icon: "\u{1F5C4}\uFE0F", labelKey: "archive", href: `${p}archive/index.html` },
1020
+ { id: "search", icon: "\u{1F50D}", labelKey: "search", href: `${p}search.html` }
1021
+ ];
1022
+ }
1023
+ function renderNav(current, depth, t) {
1024
+ const items = buildNavItems(depth);
1025
+ const label = (item) => {
1026
+ if (t) return t.nav[item.labelKey];
1027
+ const defaults = {
1028
+ dashboard: "Dashboard",
1029
+ sessions: "Sessions",
1030
+ projects: "Projects",
1031
+ graph: "Graph",
1032
+ goals: "Goals",
1033
+ todos: "Todos",
1034
+ knowledge: "Knowledge",
1035
+ archive: "Archive",
1036
+ search: "Search"
1037
+ };
1038
+ return defaults[item.labelKey] ?? item.labelKey;
1039
+ };
1040
+ const themeLabel = "\u2600 Light";
1041
+ const navLinks = items.map((item) => {
1042
+ const cls = item.id === current ? "nav-link active" : "nav-link";
1043
+ return `<a href="${escHtml(item.href)}" class="${cls}" title="${escHtml(label(item))}">${escHtml(item.icon)} <span class="nav-label">${escHtml(label(item))}</span></a>`;
1044
+ }).join("\n ");
1045
+ return `<aside class="sidebar" id="sidebar">
1046
+ <div class="sidebar-header">
1047
+ <span class="sidebar-brand">MemoryTree</span>
1048
+ <button class="sidebar-collapse-btn" id="sidebar-collapse" type="button" title="Collapse sidebar">\xAB</button>
1049
+ </div>
1050
+ <nav class="sidebar-nav">
1051
+ ${navLinks}
1052
+ </nav>
1053
+ <div class="sidebar-footer">
1054
+ <button class="theme-toggle-btn" id="theme-toggle" type="button">${escHtml(themeLabel)}</button>
1055
+ </div>
1056
+ </aside>
1057
+ <div class="sidebar-overlay" id="sidebar-overlay"></div>`;
1058
+ }
1059
+ function manifestStem(m) {
1060
+ const cleanPath = m.repo_clean_path || m.global_clean_path || "";
1061
+ return cleanPath ? basename(cleanPath, ".md") : m.session_id;
1062
+ }
1063
+ function transcriptUrlFromRoot(m) {
1064
+ return `transcripts/${m.client}/${manifestStem(m)}.html`;
1065
+ }
1066
+ function transcriptUrlFromTranscript(m) {
1067
+ return `../${m.client}/${manifestStem(m)}.html`;
1068
+ }
1069
+ var KNOWN_CLIENTS = /* @__PURE__ */ new Set(["codex", "claude", "gemini", "doubao"]);
1070
+ function clientBadge(client) {
1071
+ const safeClass = KNOWN_CLIENTS.has(client) ? client : "unknown";
1072
+ return `<span class="badge badge-${safeClass}">${escHtml(client)}</span>`;
1073
+ }
1074
+ function slugifyName(name) {
1075
+ return name.toLowerCase().replace(/[^a-z0-9]+/g, "-");
1076
+ }
1077
+ function htmlShell(title, content, nav, extraHeadOrOpts = "", lang = "en") {
1078
+ const opts = typeof extraHeadOrOpts === "string" ? { extraHead: extraHeadOrOpts, lang } : extraHeadOrOpts;
1079
+ const extraHead = opts.extraHead ?? "";
1080
+ const resolvedLang = opts.lang ?? lang;
1081
+ const breadcrumb = opts.breadcrumb ?? "";
1082
+ const readerBtn = opts.readerMode !== false ? `<button class="reader-toggle-btn" id="reader-toggle" type="button" title="Toggle reader mode">\u{1F4D6}</button>` : "";
1083
+ const ogDesc = opts.ogDescription ? opts.ogDescription.slice(0, 200) : "";
1084
+ const ogMeta = [
1085
+ `<meta property="og:title" content="${escHtml(title)}">`,
1086
+ `<meta property="og:type" content="website">`,
1087
+ ogDesc ? `<meta property="og:description" content="${escHtml(ogDesc)}">` : "",
1088
+ opts.ogUrl ? `<meta property="og:url" content="${escHtml(opts.ogUrl)}">` : ""
1089
+ ].filter(Boolean).join("\n");
1090
+ const topbar = `<header class="topbar" id="topbar">
1091
+ <span class="topbar-brand">MemoryTree</span>
1092
+ <div class="topbar-actions">
1093
+ <button class="theme-toggle-btn" id="theme-toggle-mobile" type="button">\u2600</button>
1094
+ <button class="hamburger" id="hamburger" type="button">\u2630</button>
1095
+ </div>
1096
+ </header>`;
1097
+ return `<!DOCTYPE html>
1098
+ <html lang="${escHtml(resolvedLang)}" data-theme="dark">
1099
+ <head>
1100
+ <meta charset="UTF-8">
1101
+ <meta name="viewport" content="width=device-width, initial-scale=1.0">
1102
+ <title>${escHtml(title)} \u2014 MemoryTree</title>
1103
+ ${ogMeta}
1104
+ <style>${REPORT_CSS}</style>
1105
+ ${extraHead}
1106
+ ${SIDEBAR_INLINE_JS}
1107
+ </head>
1108
+ <body>
1109
+ ${topbar}
1110
+ ${nav}
1111
+ <div class="main-content">
1112
+ <div class="container">
1113
+ ${breadcrumb ? `${breadcrumb}
1114
+ ` : ""}<div style="display:flex;justify-content:flex-end;margin-bottom:0.5rem">${readerBtn}</div>
1115
+ ${content}
1116
+ </div>
1117
+ </div>
1118
+ </body>
1119
+ </html>`;
1120
+ }
1121
+
1122
+ // src/render/charts.ts
1123
+ var HEATMAP_COLORS = ["#161b22", "#0e4429", "#006d32", "#26a641", "#39d353"];
1124
+ var CLIENT_COLORS = {
1125
+ codex: "#388bfd",
1126
+ claude: "#bc8cff",
1127
+ gemini: "#3fb950"
1128
+ };
1129
+ var FALLBACK_COLORS = ["#58a6ff", "#f78166", "#d29922", "#3fb950", "#bc8cff", "#ff7b72"];
1130
+ function renderHeatmap(dayBuckets) {
1131
+ const today = /* @__PURE__ */ new Date();
1132
+ today.setHours(0, 0, 0, 0);
1133
+ const startDate = new Date(today);
1134
+ startDate.setDate(startDate.getDate() - 364);
1135
+ startDate.setDate(startDate.getDate() - startDate.getDay());
1136
+ const cellSize = 11;
1137
+ const cellGap = 3;
1138
+ const cellStep = cellSize + cellGap;
1139
+ const leftPad = 28;
1140
+ const topPad = 18;
1141
+ const weeks = [];
1142
+ const cur = new Date(startDate);
1143
+ while (weeks.length < 53) {
1144
+ const week = [];
1145
+ for (let d = 0; d < 7; d++) {
1146
+ const dateStr = toDateStr(cur);
1147
+ const isFuture = cur > today;
1148
+ week.push({ date: dateStr, count: isFuture ? 0 : dayBuckets[dateStr] ?? 0, future: isFuture });
1149
+ cur.setDate(cur.getDate() + 1);
1150
+ }
1151
+ weeks.push(week);
1152
+ }
1153
+ const width = weeks.length * cellStep + leftPad;
1154
+ const height = 7 * cellStep + topPad + 4;
1155
+ const dayLabels = ["", "Mon", "", "Wed", "", "Fri", ""];
1156
+ let cells = "";
1157
+ for (let w = 0; w < weeks.length; w++) {
1158
+ const week = weeks[w];
1159
+ if (!week) continue;
1160
+ for (let d = 0; d < 7; d++) {
1161
+ const cell = week[d];
1162
+ if (!cell) continue;
1163
+ const x = leftPad + w * cellStep;
1164
+ const y = topPad + d * cellStep;
1165
+ const color = cell.future ? "#0d1117" : getHeatColor(cell.count);
1166
+ const title = cell.date ? `${escHtml(cell.date)}: ${cell.count} session(s)` : "";
1167
+ cells += `<rect x="${x}" y="${y}" width="${cellSize}" height="${cellSize}" rx="2" fill="${color}">`;
1168
+ if (title) cells += `<title>${title}</title>`;
1169
+ cells += `</rect>`;
1170
+ }
1171
+ }
1172
+ let labels = "";
1173
+ for (let d = 0; d < 7; d++) {
1174
+ const label = dayLabels[d];
1175
+ if (!label) continue;
1176
+ const y = topPad + d * cellStep + cellSize - 1;
1177
+ labels += `<text x="${leftPad - 4}" y="${y}" font-size="9" fill="#8b949e" text-anchor="end">${label}</text>`;
1178
+ }
1179
+ return `<svg viewBox="0 0 ${width} ${height}" xmlns="http://www.w3.org/2000/svg" aria-label="Activity heatmap">
1180
+ ${labels}
1181
+ ${cells}
1182
+ </svg>
1183
+ <div class="heatmap-legend">Less
1184
+ ${HEATMAP_COLORS.map((c) => `<svg width="11" height="11" viewBox="0 0 11 11"><rect width="11" height="11" rx="2" fill="${c}"/></svg>`).join("")}
1185
+ More</div>`;
1186
+ }
1187
+ function getHeatColor(count) {
1188
+ if (count === 0) return HEATMAP_COLORS[0] ?? "#161b22";
1189
+ if (count === 1) return HEATMAP_COLORS[1] ?? "#0e4429";
1190
+ if (count <= 3) return HEATMAP_COLORS[2] ?? "#006d32";
1191
+ if (count <= 6) return HEATMAP_COLORS[3] ?? "#26a641";
1192
+ return HEATMAP_COLORS[4] ?? "#39d353";
1193
+ }
1194
+ function toDateStr(d) {
1195
+ const y = d.getFullYear();
1196
+ const m = String(d.getMonth() + 1).padStart(2, "0");
1197
+ const day = String(d.getDate()).padStart(2, "0");
1198
+ return `${y}-${m}-${day}`;
1199
+ }
1200
+ function renderClientDoughnut(clientCounts) {
1201
+ const cx = 90;
1202
+ const cy = 90;
1203
+ const r = 60;
1204
+ const innerR = 38;
1205
+ const entries = Object.entries(clientCounts).filter(([, v]) => v > 0);
1206
+ const total = entries.reduce((s, [, v]) => s + v, 0);
1207
+ if (total === 0 || entries.length === 0) {
1208
+ return emptyDoughnut(cx, cy, r);
1209
+ }
1210
+ const circumference = 2 * Math.PI * r;
1211
+ let offset = 0;
1212
+ let segments = "";
1213
+ const legendItems = [];
1214
+ entries.forEach(([client, count], i) => {
1215
+ const fraction = count / total;
1216
+ const dashLen = fraction * circumference;
1217
+ const dashGap = circumference - dashLen;
1218
+ const color = CLIENT_COLORS[client] ?? FALLBACK_COLORS[i % FALLBACK_COLORS.length] ?? "#58a6ff";
1219
+ segments += `<circle
1220
+ cx="${cx}" cy="${cy}" r="${r}"
1221
+ fill="none"
1222
+ stroke="${color}"
1223
+ stroke-width="${r - innerR}"
1224
+ stroke-dasharray="${dashLen.toFixed(2)} ${dashGap.toFixed(2)}"
1225
+ stroke-dashoffset="${(-offset * circumference).toFixed(2)}"
1226
+ transform="rotate(-90 ${cx} ${cy})"
1227
+ ><title>${escHtml(client)}: ${count} (${(fraction * 100).toFixed(0)}%)</title></circle>`;
1228
+ offset += fraction;
1229
+ const pct = (fraction * 100).toFixed(0);
1230
+ legendItems.push(
1231
+ `<tr><td><svg width="10" height="10" viewBox="0 0 10 10"><rect width="10" height="10" rx="2" fill="${color}"/></svg></td><td style="padding-left:6px;color:#e6edf3">${escHtml(client)}</td><td style="padding-left:12px;color:#8b949e;text-align:right">${count} (${pct}%)</td></tr>`
1232
+ );
1233
+ return void 0;
1234
+ });
1235
+ const legendX = cx * 2 + 20;
1236
+ const width = legendX + 180;
1237
+ const height = cy * 2;
1238
+ return `<svg viewBox="0 0 ${width} ${height}" xmlns="http://www.w3.org/2000/svg" aria-label="Client distribution">
1239
+ <circle cx="${cx}" cy="${cy}" r="${r}" fill="#161b22"/>
1240
+ ${segments}
1241
+ <circle cx="${cx}" cy="${cy}" r="${innerR}" fill="#0d1117"/>
1242
+ <text x="${cx}" y="${cy - 6}" text-anchor="middle" font-size="20" font-weight="700" fill="#e6edf3">${total}</text>
1243
+ <text x="${cx}" y="${cy + 14}" text-anchor="middle" font-size="10" fill="#8b949e">sessions</text>
1244
+ <foreignObject x="${legendX}" y="${(cy * 2 - legendItems.length * 28) / 2}" width="180" height="${legendItems.length * 28 + 10}">
1245
+ <table xmlns="http://www.w3.org/1999/xhtml" style="font-size:12px;border-collapse:collapse;font-family:sans-serif">
1246
+ ${legendItems.join("")}
1247
+ </table>
1248
+ </foreignObject>
1249
+ </svg>`;
1250
+ }
1251
+ function emptyDoughnut(cx, cy, r) {
1252
+ const width = cx * 2 + 220;
1253
+ const height = cy * 2;
1254
+ return `<svg viewBox="0 0 ${width} ${height}" xmlns="http://www.w3.org/2000/svg">
1255
+ <circle cx="${cx}" cy="${cy}" r="${r}" fill="none" stroke="#30363d" stroke-width="22"/>
1256
+ <circle cx="${cx}" cy="${cy}" r="${r - 21}" fill="#0d1117"/>
1257
+ <text x="${cx}" y="${cy + 5}" text-anchor="middle" font-size="10" fill="#8b949e">No data</text>
1258
+ </svg>`;
1259
+ }
1260
+ function renderWeeklyLine(weekBuckets) {
1261
+ const W = 580;
1262
+ const H = 140;
1263
+ const padL = 38;
1264
+ const padR = 12;
1265
+ const padT = 12;
1266
+ const padB = 24;
1267
+ const plotW = W - padL - padR;
1268
+ const plotH = H - padT - padB;
1269
+ const weeks = getLast52Weeks();
1270
+ const data = weeks.map((w) => weekBuckets[w] ?? 0);
1271
+ const maxVal = Math.max(...data, 1);
1272
+ const points = data.map((v, i) => {
1273
+ const x = data.length > 1 ? padL + i / (data.length - 1) * plotW : padL + plotW / 2;
1274
+ const y = padT + plotH - v / maxVal * plotH;
1275
+ return { x, y };
1276
+ });
1277
+ const linePoints = points.map((p) => `${p.x.toFixed(1)},${p.y.toFixed(1)}`).join(" ");
1278
+ const firstX = points[0]?.x ?? padL;
1279
+ const lastX = points[points.length - 1]?.x ?? padL + plotW;
1280
+ const baseY = padT + plotH;
1281
+ const areaPath = `M${firstX.toFixed(1)},${baseY} ` + points.map((p) => `L${p.x.toFixed(1)},${p.y.toFixed(1)}`).join(" ") + ` L${lastX.toFixed(1)},${baseY} Z`;
1282
+ const yLabels = [0, Math.round(maxVal / 2), maxVal].map((v) => {
1283
+ const y = padT + plotH - v / maxVal * plotH;
1284
+ return `<text x="${padL - 4}" y="${y + 4}" font-size="9" fill="#8b949e" text-anchor="end">${v}</text>`;
1285
+ }).join("");
1286
+ const firstWeek = weeks[0] ?? "";
1287
+ const lastWeek = weeks[weeks.length - 1] ?? "";
1288
+ const xLabels = `<text x="${padL}" y="${H - 4}" font-size="9" fill="#8b949e" text-anchor="start">${escHtml(firstWeek)}</text><text x="${W - padR}" y="${H - 4}" font-size="9" fill="#8b949e" text-anchor="end">${escHtml(lastWeek)}</text>`;
1289
+ return `<svg viewBox="0 0 ${W} ${H}" xmlns="http://www.w3.org/2000/svg" aria-label="Weekly messages">
1290
+ <defs>
1291
+ <linearGradient id="lineGrad" x1="0" y1="0" x2="0" y2="1">
1292
+ <stop offset="0%" stop-color="#58a6ff" stop-opacity="0.3"/>
1293
+ <stop offset="100%" stop-color="#58a6ff" stop-opacity="0.02"/>
1294
+ </linearGradient>
1295
+ </defs>
1296
+ <line x1="${padL}" y1="${padT}" x2="${padL}" y2="${padT + plotH}" stroke="#30363d" stroke-width="1"/>
1297
+ <line x1="${padL}" y1="${padT + plotH}" x2="${padL + plotW}" y2="${padT + plotH}" stroke="#30363d" stroke-width="1"/>
1298
+ ${yLabels}
1299
+ ${xLabels}
1300
+ <path d="${areaPath}" fill="url(#lineGrad)"/>
1301
+ <polyline points="${linePoints}" fill="none" stroke="#58a6ff" stroke-width="1.5" stroke-linejoin="round" stroke-linecap="round"/>
1302
+ </svg>`;
1303
+ }
1304
+ function getLast52Weeks() {
1305
+ const weeks = [];
1306
+ const today = /* @__PURE__ */ new Date();
1307
+ today.setHours(0, 0, 0, 0);
1308
+ for (let i = 51; i >= 0; i--) {
1309
+ const d = new Date(today);
1310
+ d.setDate(d.getDate() - i * 7);
1311
+ weeks.push(isoWeekKey(d));
1312
+ }
1313
+ return weeks;
1314
+ }
1315
+ function renderToolBar(toolCounts) {
1316
+ const entries = Object.entries(toolCounts).sort(([, a], [, b]) => b - a).slice(0, 10);
1317
+ if (entries.length === 0) {
1318
+ return `<svg viewBox="0 0 500 60" xmlns="http://www.w3.org/2000/svg">
1319
+ <text x="250" y="35" text-anchor="middle" font-size="13" fill="#8b949e">No tool data available</text>
1320
+ </svg>`;
1321
+ }
1322
+ const W = 500;
1323
+ const rowH = 22;
1324
+ const rowGap = 6;
1325
+ const labelW = 130;
1326
+ const barArea = W - labelW - 60;
1327
+ const H = entries.length * (rowH + rowGap) + 10;
1328
+ const maxCount = entries[0]?.[1] ?? 1;
1329
+ const bars = entries.map(([name, count], i) => {
1330
+ const y = i * (rowH + rowGap) + 5;
1331
+ const barW = Math.max(2, count / maxCount * barArea);
1332
+ const color = FALLBACK_COLORS[i % FALLBACK_COLORS.length] ?? "#58a6ff";
1333
+ return `<text x="${labelW - 4}" y="${y + rowH - 5}" font-size="10" fill="#8b949e" text-anchor="end">${escHtml(name)}</text><rect x="${labelW}" y="${y + 2}" width="${barW.toFixed(1)}" height="${rowH - 4}" rx="2" fill="${color}" opacity="0.8"><title>${escHtml(name)}: ${count}</title></rect><text x="${labelW + barW + 4}" y="${y + rowH - 5}" font-size="10" fill="#8b949e">${count}</text>`;
1334
+ }).join("");
1335
+ return `<svg viewBox="0 0 ${W} ${H}" xmlns="http://www.w3.org/2000/svg" aria-label="Top tools">
1336
+ ${bars}
1337
+ </svg>`;
1338
+ }
1339
+
1340
+ // src/render/dashboard.ts
1341
+ function renderDashboard(stats, manifests, t) {
1342
+ const nav = renderNav("dashboard", 0, t);
1343
+ const content = [
1344
+ renderPageHeader(stats, t),
1345
+ renderStatsCards(stats, t),
1346
+ renderCharts(stats),
1347
+ renderRecentSessions(manifests, t)
1348
+ ].join("\n");
1349
+ const lang = t ? detectLang(t) : "en";
1350
+ return htmlShell(t?.dashboard.title ?? "Dashboard", content, nav, "", lang);
1351
+ }
1352
+ function renderPageHeader(stats, t) {
1353
+ const from = stats.dateRange.from.slice(0, 10) || "\u2014";
1354
+ const to = stats.dateRange.to.slice(0, 10) || "\u2014";
1355
+ const title = t?.dashboard.title ?? "Memory Dashboard";
1356
+ const subtitle = (t?.dashboard.subtitle ?? "Activity from {from} to {to}").replace("{from}", from).replace("{to}", to);
1357
+ return `<div class="page-header">
1358
+ <h1>${escHtml(title)}</h1>
1359
+ <p class="subtitle">${escHtml(subtitle)}</p>
1360
+ </div>`;
1361
+ }
1362
+ function renderStatsCards(stats, t) {
1363
+ const cards = [
1364
+ { value: fmtNum(stats.totalSessions), label: t?.dashboard.sessions ?? "Sessions" },
1365
+ { value: fmtNum(stats.totalMessages), label: t?.dashboard.messages ?? "Messages" },
1366
+ { value: fmtNum(stats.totalToolEvents), label: t?.dashboard.toolEvents ?? "Tool Events" },
1367
+ { value: fmtNum(stats.activeDays), label: t?.dashboard.activeDays ?? "Active Days" }
1368
+ ];
1369
+ const html = cards.map(
1370
+ (c) => `<div class="card">
1371
+ <div class="card-title">${escHtml(c.label)}</div>
1372
+ <div class="stat-value">${c.value}</div>
1373
+ </div>`
1374
+ ).join("");
1375
+ return `<div class="stats-grid">${html}</div>`;
1376
+ }
1377
+ function renderCharts(stats) {
1378
+ const heatmap = renderHeatmap(stats.dayBuckets);
1379
+ const doughnut = renderClientDoughnut(stats.clientCounts);
1380
+ const line = renderWeeklyLine(stats.weekBuckets);
1381
+ const bar = renderToolBar(stats.toolCounts);
1382
+ return `<div class="chart-grid">
1383
+ <div class="chart-card full-width">
1384
+ <div class="chart-title">Activity (last 365 days)</div>
1385
+ ${heatmap}
1386
+ </div>
1387
+ <div class="chart-card">
1388
+ <div class="chart-title">Client Distribution</div>
1389
+ ${doughnut}
1390
+ </div>
1391
+ <div class="chart-card">
1392
+ <div class="chart-title">Messages / Week (last 52 weeks)</div>
1393
+ ${line}
1394
+ </div>
1395
+ <div class="chart-card full-width">
1396
+ <div class="chart-title">Top 10 Tools</div>
1397
+ ${bar}
1398
+ </div>
1399
+ </div>`;
1400
+ }
1401
+ function renderRecentSessions(manifests, t) {
1402
+ const recent = [...manifests].sort((a, b) => b.started_at.localeCompare(a.started_at)).slice(0, 10);
1403
+ const heading = t?.dashboard.recentSessions ?? "Recent Sessions";
1404
+ const clientLabel = t?.sessions.client ?? "Client";
1405
+ const dateLabel = t?.sessions.date ?? "Date";
1406
+ const msgsLabel = t?.sessions.msgs ?? "Msgs";
1407
+ const toolsLabel = t?.sessions.tools ?? "Tools";
1408
+ if (recent.length === 0) {
1409
+ return `<div class="card"><p style="color:var(--text-muted)">${escHtml(t?.sessions.noSessions ?? "No sessions imported yet.")}</p></div>`;
1410
+ }
1411
+ const rows = recent.map((m) => {
1412
+ const url = transcriptUrlFromRoot(m);
1413
+ const date = m.started_at.slice(0, 10);
1414
+ const badge = clientBadge(m.client);
1415
+ const msgs = m.message_count;
1416
+ const tools = m.tool_event_count;
1417
+ const summary = "";
1418
+ const meta = `${escHtml(m.client)} \xB7 ${escHtml(date)} \xB7 ${msgs} msgs`;
1419
+ return `<tr>
1420
+ <td>${badge}</td>
1421
+ <td><a href="${escHtml(url)}" data-summary="${escHtml(summary)}" data-meta="${escHtml(meta)}">${escHtml(m.title || m.session_id)}</a></td>
1422
+ <td style="color:var(--text-muted)">${escHtml(date)}</td>
1423
+ <td style="color:var(--text-muted);text-align:right">${msgs}</td>
1424
+ <td style="color:var(--text-muted);text-align:right">${tools}</td>
1425
+ </tr>`;
1426
+ }).join("");
1427
+ return `<h2>${escHtml(heading)}</h2>
1428
+ <div class="card" style="padding:0;overflow:hidden">
1429
+ <table>
1430
+ <thead><tr>
1431
+ <th>${escHtml(clientLabel)}</th><th>Title</th><th>${escHtml(dateLabel)}</th>
1432
+ <th style="text-align:right">${escHtml(msgsLabel)}</th>
1433
+ <th style="text-align:right">${escHtml(toolsLabel)}</th>
1434
+ </tr></thead>
1435
+ <tbody>${rows}</tbody>
1436
+ </table>
1437
+ </div>`;
1438
+ }
1439
+ function fmtNum(n) {
1440
+ return n.toLocaleString("en-US");
1441
+ }
1442
+ function detectLang(t) {
1443
+ return /[\u4e00-\u9fff]/.test(t.nav.dashboard) ? "zh-CN" : "en";
1444
+ }
1445
+
1446
+ // src/tags.ts
1447
+ import { existsSync, mkdirSync, readFileSync, writeFileSync } from "fs";
1448
+ import { join } from "path";
1449
+ async function getTags(sha256, summary, options) {
1450
+ if (options.noAi) return [];
1451
+ if (!summary.trim()) return [];
1452
+ const cached = readTagCache(sha256, options.cacheDir);
1453
+ if (cached !== null) return cached;
1454
+ const apiKey = process.env["ANTHROPIC_API_KEY"];
1455
+ if (!apiKey) return [];
1456
+ return callTagApi(sha256, summary, options);
1457
+ }
1458
+ async function callTagApi(sha256, summary, options) {
1459
+ try {
1460
+ const { default: Anthropic } = await import("@anthropic-ai/sdk");
1461
+ const client = new Anthropic();
1462
+ const response = await client.messages.create({
1463
+ model: options.model,
1464
+ max_tokens: 60,
1465
+ system: "You are a tag extractor. Respond with a JSON array only \u2014 no prose, no markdown.",
1466
+ messages: [
1467
+ {
1468
+ role: "user",
1469
+ content: `Extract 3-5 short keyword tags from this session summary. Respond with JSON array only: ["tag1","tag2"]
1470
+
1471
+ ${summary.slice(0, 500)}`
1472
+ }
1473
+ ]
1474
+ });
1475
+ const block = response.content[0];
1476
+ const text = block && block.type === "text" ? block.text.trim() : "";
1477
+ if (!text) return [];
1478
+ const parsed = JSON.parse(text);
1479
+ if (!Array.isArray(parsed)) return [];
1480
+ const tags = parsed.filter((t) => typeof t === "string" && t.length > 0 && t.length <= 50).slice(0, 5).map((t) => t.trim().toLowerCase()).filter((t) => t.length > 0);
1481
+ if (tags.length > 0) {
1482
+ writeTagCache(sha256, tags, options.cacheDir);
1483
+ }
1484
+ return tags;
1485
+ } catch {
1486
+ return [];
1487
+ }
1488
+ }
1489
+ function renderTagBadges(tags) {
1490
+ if (tags.length === 0) return "";
1491
+ const pills = tags.map((t) => `<span class="tag" data-tag="${escHtml(t)}">${escHtml(t)}</span>`).join("");
1492
+ return `<div class="tag-list">${pills}</div>`;
1493
+ }
1494
+ function readTagCache(sha256, cacheDir) {
1495
+ const path = tagCachePath(sha256, cacheDir);
1496
+ if (!existsSync(path)) return null;
1497
+ try {
1498
+ const raw = readFileSync(path, "utf-8");
1499
+ const entry = JSON.parse(raw);
1500
+ return Array.isArray(entry.tags) ? entry.tags : null;
1501
+ } catch {
1502
+ return null;
1503
+ }
1504
+ }
1505
+ function writeTagCache(sha256, tags, cacheDir) {
1506
+ try {
1507
+ const tagsDir = join(cacheDir, "tags");
1508
+ mkdirSync(tagsDir, { recursive: true });
1509
+ const entry = { sha256, tags, generated_at: (/* @__PURE__ */ new Date()).toISOString() };
1510
+ writeFileSync(tagCachePath(sha256, cacheDir), JSON.stringify(entry, null, 2) + "\n", "utf-8");
1511
+ } catch {
1512
+ }
1513
+ }
1514
+ function tagCachePath(sha256, cacheDir) {
1515
+ return join(cacheDir, "tags", `${sha256}.json`);
1516
+ }
1517
+
1518
+ // src/render/transcript-list.ts
1519
+ function renderTranscriptList(manifests, t, summaries, tags) {
1520
+ const nav = renderNav("transcripts", 1, t);
1521
+ const sorted = [...manifests].sort((a, b) => b.started_at.localeCompare(a.started_at));
1522
+ const title = t?.sessions.title ?? "Sessions";
1523
+ const clientLabel = t?.sessions.client ?? "Client";
1524
+ const dateLabel = t?.sessions.date ?? "Date";
1525
+ const idLabel = t?.sessions.id ?? "ID";
1526
+ const msgsLabel = t?.sessions.msgs ?? "Msgs";
1527
+ const toolsLabel = t?.sessions.tools ?? "Tools";
1528
+ const allLabel = t?.sessions.all ?? "All";
1529
+ if (sorted.length === 0) {
1530
+ const content2 = `<div class="page-header">
1531
+ <h1>${escHtml(title)}</h1>
1532
+ <p class="subtitle">${escHtml(t?.sessions.noSessions ?? "No sessions imported yet.")}</p>
1533
+ </div>`;
1534
+ return htmlShell(title, content2, nav);
1535
+ }
1536
+ const clientSet = /* @__PURE__ */ new Set();
1537
+ for (const m of sorted) clientSet.add(m.client);
1538
+ const clients = [...clientSet].sort();
1539
+ const tabBar = renderTabBar(allLabel, clients, sorted);
1540
+ const rows = sorted.map((m) => {
1541
+ const url = transcriptHref(m);
1542
+ const date = m.started_at.slice(0, 10);
1543
+ const time = m.started_at.slice(11, 16);
1544
+ const badge = clientBadge(m.client);
1545
+ const summary = summaries?.[m.session_id] ?? "";
1546
+ const sessionTags = tags?.[m.session_id] ?? [];
1547
+ const tagBadges = renderTagBadges(sessionTags);
1548
+ const meta = `${escHtml(m.client)} \xB7 ${escHtml(date)} \xB7 ${m.message_count} msgs`;
1549
+ return `<tr data-client="${escHtml(m.client)}">
1550
+ <td>${badge}</td>
1551
+ <td><a href="${escHtml(url)}" data-summary="${escHtml(summary)}" data-meta="${escHtml(meta)}">${escHtml(m.title || m.session_id)}</a>${tagBadges}</td>
1552
+ <td style="color:var(--text-muted)">${escHtml(date)} ${escHtml(time)}</td>
1553
+ <td style="color:var(--text-muted);font-family:var(--font-mono);font-size:0.8rem">${escHtml(m.session_id.slice(0, 8))}</td>
1554
+ <td style="text-align:right;color:var(--text-muted)">${m.message_count}</td>
1555
+ <td style="text-align:right;color:var(--text-muted)">${m.tool_event_count}</td>
1556
+ </tr>`;
1557
+ }).join("");
1558
+ const filterJs = `<script>
1559
+ (function() {
1560
+ document.addEventListener('DOMContentLoaded', function() {
1561
+ var tabs = document.querySelectorAll('.tab-btn');
1562
+ var rows = document.querySelectorAll('tr[data-client]');
1563
+ tabs.forEach(function(btn) {
1564
+ btn.addEventListener('click', function() {
1565
+ tabs.forEach(function(b) { b.classList.remove('active'); });
1566
+ btn.classList.add('active');
1567
+ var filter = btn.getAttribute('data-filter');
1568
+ rows.forEach(function(row) {
1569
+ if (!filter || row.getAttribute('data-client') === filter) {
1570
+ row.style.display = '';
1571
+ } else {
1572
+ row.style.display = 'none';
1573
+ }
1574
+ });
1575
+ });
1576
+ });
1577
+ });
1578
+ })();
1579
+ </script>`;
1580
+ const content = `<div class="page-header">
1581
+ <h1>${escHtml(title)}</h1>
1582
+ <p class="subtitle">${sorted.length} session(s) imported</p>
1583
+ </div>
1584
+ ${tabBar}
1585
+ <div class="card" style="padding:0;overflow:hidden">
1586
+ <table>
1587
+ <thead><tr>
1588
+ <th>${escHtml(clientLabel)}</th>
1589
+ <th>Title</th>
1590
+ <th>${escHtml(dateLabel)}</th>
1591
+ <th>${escHtml(idLabel)}</th>
1592
+ <th style="text-align:right">${escHtml(msgsLabel)}</th>
1593
+ <th style="text-align:right">${escHtml(toolsLabel)}</th>
1594
+ </tr></thead>
1595
+ <tbody>${rows}</tbody>
1596
+ </table>
1597
+ </div>`;
1598
+ return htmlShell(title, content, nav, filterJs);
1599
+ }
1600
+ function renderTabBar(allLabel, clients, manifests) {
1601
+ const total = manifests.length;
1602
+ const tabs = [
1603
+ `<button class="tab-btn active" data-filter="" type="button">${escHtml(allLabel)} (${total})</button>`,
1604
+ ...clients.map((client) => {
1605
+ const count = manifests.filter((m) => m.client === client).length;
1606
+ return `<button class="tab-btn" data-filter="${escHtml(client)}" type="button">${escHtml(client)} (${count})</button>`;
1607
+ })
1608
+ ];
1609
+ return `<div class="tab-bar">${tabs.join("")}</div>`;
1610
+ }
1611
+ function transcriptHref(m) {
1612
+ return transcriptUrlFromRoot(m).replace(/^transcripts\//, "");
1613
+ }
1614
+
1615
+ // src/render/transcript.ts
1616
+ function renderTranscript(messages, manifest, summary, backlinks, t, reportBaseUrl = "") {
1617
+ const nav = renderNav("transcripts", 2, t);
1618
+ const content = [
1619
+ renderHeader(manifest, t),
1620
+ summary ? renderSummaryCard(summary, t) : "",
1621
+ backlinks.length > 0 ? renderBacklinks(backlinks, t) : "",
1622
+ renderMessages(messages, t)
1623
+ ].filter(Boolean).join("\n");
1624
+ const title = manifest.title || manifest.session_id;
1625
+ const ogUrl = reportBaseUrl ? `${reportBaseUrl.replace(/\/$/, "")}/${transcriptUrlFromRoot(manifest)}` : "";
1626
+ const shellOptions = {
1627
+ ...summary ? { ogDescription: summary } : {},
1628
+ ...ogUrl ? { ogUrl } : {}
1629
+ };
1630
+ return htmlShell(title, content, nav, shellOptions);
1631
+ }
1632
+ function renderHeader(m, t) {
1633
+ const badge = clientBadge(m.client);
1634
+ const date = m.started_at.slice(0, 10);
1635
+ const time = m.started_at.slice(11, 19);
1636
+ const clientLabel = t?.transcript.client ?? "Client";
1637
+ const sessionIdLabel = t?.transcript.sessionId ?? "Session ID";
1638
+ const msgsLabel = t?.transcript.messages ?? "Messages";
1639
+ const toolEventsLabel = t?.transcript.toolEvents ?? "Tool Events";
1640
+ const branchLabel = t?.transcript.branch ?? "Branch";
1641
+ const cwdLabel = t?.transcript.workingDir ?? "Working Dir";
1642
+ const shaLabel = t?.transcript.sha256 ?? "SHA-256";
1643
+ return `<div class="page-header">
1644
+ <h1>${escHtml(m.title || m.session_id)}</h1>
1645
+ </div>
1646
+ <table class="meta-table card">
1647
+ <tbody>
1648
+ <tr><td>${escHtml(clientLabel)}</td><td>${badge}</td></tr>
1649
+ <tr><td>Date</td><td>${escHtml(date)} ${escHtml(time)}</td></tr>
1650
+ <tr><td>${escHtml(sessionIdLabel)}</td><td><code>${escHtml(m.session_id)}</code></td></tr>
1651
+ <tr><td>${escHtml(msgsLabel)}</td><td>${m.message_count}</td></tr>
1652
+ <tr><td>${escHtml(toolEventsLabel)}</td><td>${m.tool_event_count}</td></tr>
1653
+ <tr><td>${escHtml(branchLabel)}</td><td><code>${escHtml(m.branch || "\u2014")}</code></td></tr>
1654
+ <tr><td>${escHtml(cwdLabel)}</td><td><code style="font-size:0.8rem">${escHtml(m.cwd || "\u2014")}</code></td></tr>
1655
+ <tr><td>${escHtml(shaLabel)}</td><td><code style="font-size:0.75rem">${escHtml(m.raw_sha256.slice(0, 16))}\u2026</code></td></tr>
1656
+ </tbody>
1657
+ </table>`;
1658
+ }
1659
+ function renderSummaryCard(summary, t) {
1660
+ const label = t?.transcript.aiSummary ?? "AI Summary";
1661
+ return `<div class="summary-card">
1662
+ <div class="summary-label">${escHtml(label)}</div>
1663
+ <div>${escHtml(summary)}</div>
1664
+ </div>`;
1665
+ }
1666
+ function renderBacklinks(backlinks, t) {
1667
+ const heading = t?.transcript.referencedBy ?? "Referenced By";
1668
+ const items = backlinks.map((m) => {
1669
+ const url = transcriptHref2(m);
1670
+ return `<li><a href="${escHtml(url)}">${escHtml(m.title || m.session_id)}</a></li>`;
1671
+ }).join("");
1672
+ return `<div class="backlinks">
1673
+ <div class="backlinks-title">${escHtml(heading)} ${backlinks.length} session(s)</div>
1674
+ <ul>${items}</ul>
1675
+ </div>`;
1676
+ }
1677
+ function renderMessages(messages, t) {
1678
+ const heading = t?.transcript.messages ?? "Messages";
1679
+ const noMessages = t?.transcript.noMessages ?? "No messages available for this session.";
1680
+ if (messages.length === 0) {
1681
+ return `<div class="card" style="color:var(--text-muted)">${escHtml(noMessages)}</div>`;
1682
+ }
1683
+ const rendered = messages.map((msg) => {
1684
+ const roleClass = msg.role === "user" ? "message-user" : "message-assistant";
1685
+ const ts = msg.timestamp ? `<span style="margin-left:auto">${escHtml(msg.timestamp.slice(11, 19))}</span>` : "";
1686
+ const body = renderMessageBody(msg.text);
1687
+ return `<div class="message ${roleClass}">
1688
+ <div class="message-header">
1689
+ <span class="message-role">${escHtml(msg.role)}</span>${ts}
1690
+ </div>
1691
+ <div class="message-body">${body}</div>
1692
+ </div>`;
1693
+ }).join("\n");
1694
+ return `<h2 style="margin-top:1.5rem">${escHtml(heading)}</h2>
1695
+ <div class="messages">${rendered}</div>`;
1696
+ }
1697
+ function renderMessageBody(text) {
1698
+ const parts = [];
1699
+ const codeBlockRe = /```[\w]*\n?([\s\S]*?)```/g;
1700
+ let lastIndex = 0;
1701
+ let match;
1702
+ while ((match = codeBlockRe.exec(text)) !== null) {
1703
+ if (match.index > lastIndex) {
1704
+ parts.push(renderInlineText(text.slice(lastIndex, match.index)));
1705
+ }
1706
+ const code = match[1] ?? "";
1707
+ parts.push(`<pre><code>${escHtml(code)}</code></pre>`);
1708
+ lastIndex = match.index + match[0].length;
1709
+ }
1710
+ if (lastIndex < text.length) {
1711
+ parts.push(renderInlineText(text.slice(lastIndex)));
1712
+ }
1713
+ return parts.join("");
1714
+ }
1715
+ function renderInlineText(text) {
1716
+ const paragraphs = text.split(/\n{2,}/).filter((p) => p.trim());
1717
+ return paragraphs.map((p) => `<p>${applyInlineMarkdown(escHtml(p.trim()))}</p>`).join("");
1718
+ }
1719
+ function applyInlineMarkdown(html) {
1720
+ return html.replace(/\*\*(.+?)\*\*/g, "<strong>$1</strong>").replace(/\*(.+?)\*/g, "<em>$1</em>").replace(/`([^`]+)`/g, "<code>$1</code>").replace(/\n/g, "<br>");
1721
+ }
1722
+ function transcriptHref2(m) {
1723
+ return transcriptUrlFromTranscript(m);
1724
+ }
1725
+
1726
+ // src/render/markdown.ts
1727
+ var CALLOUT_ICONS = {
1728
+ note: "\u{1F4DD}",
1729
+ tip: "\u{1F4A1}",
1730
+ important: "\u2757",
1731
+ warning: "\u26A0\uFE0F",
1732
+ caution: "\u{1F525}",
1733
+ info: "\u2139\uFE0F",
1734
+ success: "\u2705",
1735
+ error: "\u274C"
1736
+ };
1737
+ function hasMermaidBlocks(md) {
1738
+ return /^```mermaid\s*$/m.test(md);
1739
+ }
1740
+ var MERMAID_CDN_SCRIPT = `<script type="module">
1741
+ import mermaid from 'https://cdn.jsdelivr.net/npm/mermaid@11/dist/mermaid.esm.min.mjs';
1742
+ mermaid.initialize({ startOnLoad: true, theme: document.documentElement.getAttribute('data-theme') === 'light' ? 'default' : 'dark' });
1743
+ </script>`;
1744
+ function markdownToHtml(md) {
1745
+ const lines = md.split("\n");
1746
+ const output = [];
1747
+ let i = 0;
1748
+ let inList = false;
1749
+ let listType = null;
1750
+ function closeList() {
1751
+ if (inList) {
1752
+ output.push(listType === "ol" ? "</ol>" : "</ul>");
1753
+ inList = false;
1754
+ listType = null;
1755
+ }
1756
+ }
1757
+ while (i < lines.length) {
1758
+ const line = lines[i] ?? "";
1759
+ if (line.startsWith("```")) {
1760
+ closeList();
1761
+ const lang = line.slice(3).trim();
1762
+ const codeLines = [];
1763
+ i++;
1764
+ while (i < lines.length && !(lines[i] ?? "").startsWith("```")) {
1765
+ codeLines.push(lines[i] ?? "");
1766
+ i++;
1767
+ }
1768
+ const codeContent = codeLines.join("\n");
1769
+ if (lang === "mermaid") {
1770
+ output.push(`<pre class="mermaid">${escHtml(codeContent)}</pre>`);
1771
+ } else {
1772
+ output.push(`<pre><code${lang ? ` class="language-${escHtml(lang)}"` : ""}>${escHtml(codeContent)}</code></pre>`);
1773
+ }
1774
+ i++;
1775
+ continue;
1776
+ }
1777
+ if (i === 0 && line === "---") {
1778
+ i++;
1779
+ while (i < lines.length && (lines[i] ?? "") !== "---") i++;
1780
+ i++;
1781
+ continue;
1782
+ }
1783
+ const headingMatch = line.match(/^(#{1,6})\s+(.*)$/);
1784
+ if (headingMatch) {
1785
+ closeList();
1786
+ const level = headingMatch[1]?.length ?? 1;
1787
+ const text = headingMatch[2] ?? "";
1788
+ output.push(`<h${level}>${applyInline(text)}</h${level}>`);
1789
+ i++;
1790
+ continue;
1791
+ }
1792
+ if (/^---+$/.test(line.trim())) {
1793
+ closeList();
1794
+ output.push("<hr>");
1795
+ i++;
1796
+ continue;
1797
+ }
1798
+ const olMatch = line.match(/^(\s*)\d+\.\s+(.+)$/);
1799
+ if (olMatch) {
1800
+ if (!inList || listType !== "ol") {
1801
+ closeList();
1802
+ output.push("<ol>");
1803
+ inList = true;
1804
+ listType = "ol";
1805
+ }
1806
+ output.push(`<li>${applyInline(olMatch[2] ?? "")}</li>`);
1807
+ i++;
1808
+ continue;
1809
+ }
1810
+ const ulMatch = line.match(/^(\s*)[-*+]\s+(.+)$/);
1811
+ if (ulMatch) {
1812
+ if (!inList || listType !== "ul") {
1813
+ closeList();
1814
+ output.push("<ul>");
1815
+ inList = true;
1816
+ listType = "ul";
1817
+ }
1818
+ output.push(`<li>${applyInline(ulMatch[2] ?? "")}</li>`);
1819
+ i++;
1820
+ continue;
1821
+ }
1822
+ if (line.startsWith("> ") || line === ">") {
1823
+ closeList();
1824
+ const bqLines = [];
1825
+ while (i < lines.length && ((lines[i] ?? "").startsWith("> ") || (lines[i] ?? "") === ">")) {
1826
+ bqLines.push((lines[i] ?? "").replace(/^> ?/, ""));
1827
+ i++;
1828
+ }
1829
+ output.push(renderBlockquote(bqLines));
1830
+ continue;
1831
+ }
1832
+ if (!line.trim()) {
1833
+ closeList();
1834
+ i++;
1835
+ continue;
1836
+ }
1837
+ closeList();
1838
+ output.push(`<p>${applyInline(line)}</p>`);
1839
+ i++;
1840
+ }
1841
+ closeList();
1842
+ return output.join("\n");
1843
+ }
1844
+ function renderBlockquote(innerLines) {
1845
+ const firstLine = innerLines[0] ?? "";
1846
+ const calloutMatch = firstLine.match(/^\[!(note|tip|important|warning|caution|info|success|error)\]$/i);
1847
+ if (calloutMatch) {
1848
+ const type = calloutMatch[1].toLowerCase();
1849
+ const icon = CALLOUT_ICONS[type] ?? "\u{1F4AC}";
1850
+ const title = type.charAt(0).toUpperCase() + type.slice(1);
1851
+ const bodyLines = innerLines.slice(1).filter((l) => l !== "");
1852
+ const body = bodyLines.map((l) => `<p>${applyInline(l)}</p>`).join("\n");
1853
+ return `<div class="callout callout-${escHtml(type)}">
1854
+ <div class="callout-title">${icon} ${escHtml(title)}</div>
1855
+ <div class="callout-body">${body}</div>
1856
+ </div>`;
1857
+ }
1858
+ const inner = innerLines.map((l) => `<p>${applyInline(l)}</p>`).join("\n");
1859
+ return `<blockquote>${inner}</blockquote>`;
1860
+ }
1861
+ function applyInline(text) {
1862
+ const withLinks = text.replace(/\[([^\]]+)\]\(([^)]+)\)/g, (_, label, href) => {
1863
+ return `<a href="${escHtml(href)}" target="_blank" rel="noopener">${escHtml(label)}</a>`;
1864
+ });
1865
+ const parts = withLinks.split(/(<a [^>]+>.*?<\/a>)/g);
1866
+ const processed = parts.map((part, i) => {
1867
+ if (i % 2 === 1) return part;
1868
+ const escaped = escHtml(part);
1869
+ return escaped.replace(/\*\*(.+?)\*\*/g, "<strong>$1</strong>").replace(/\*(.+?)\*/g, "<em>$1</em>").replace(/__(.+?)__/g, "<strong>$1</strong>").replace(/_(.+?)_/g, "<em>$1</em>").replace(/`([^`]+)`/g, "<code>$1</code>");
1870
+ }).join("");
1871
+ return processed;
1872
+ }
1873
+
1874
+ // src/render/toc.ts
1875
+ function extractToc(md) {
1876
+ const entries = [];
1877
+ const seen = /* @__PURE__ */ new Map();
1878
+ for (const line of md.split("\n")) {
1879
+ const h2 = line.match(/^## (.+)$/);
1880
+ const h3 = line.match(/^### (.+)$/);
1881
+ const match = h2 ?? h3;
1882
+ if (!match) continue;
1883
+ const level = h2 ? 2 : 3;
1884
+ const text = (match[1] ?? "").trim();
1885
+ const base = slugifyHeading(text);
1886
+ const count = seen.get(base) ?? 0;
1887
+ const id = count === 0 ? base : `${base}-${count}`;
1888
+ seen.set(base, count + 1);
1889
+ entries.push({ level, text, id });
1890
+ }
1891
+ return entries;
1892
+ }
1893
+ function renderToc(entries) {
1894
+ if (entries.length === 0) return "";
1895
+ const items = entries.map((e) => {
1896
+ const indent = e.level === 3 ? ' style="padding-left:1.25rem"' : "";
1897
+ return `<li${indent}><a href="#${escHtml(e.id)}">${escHtml(e.text)}</a></li>`;
1898
+ }).join("\n ");
1899
+ return `<nav class="toc" id="toc">
1900
+ <button class="toc-toggle" id="toc-toggle" type="button" aria-expanded="true">
1901
+ Contents <span class="toc-arrow">\u25BE</span>
1902
+ </button>
1903
+ <ul class="toc-list" id="toc-list">
1904
+ ${items}
1905
+ </ul>
1906
+ </nav>`;
1907
+ }
1908
+ function prefixTocIds(entries, prefix) {
1909
+ if (!prefix) return entries;
1910
+ return entries.map((entry) => ({
1911
+ ...entry,
1912
+ id: `${prefix}-${entry.id}`
1913
+ }));
1914
+ }
1915
+ function injectHeadingIds(html, entries) {
1916
+ if (entries.length === 0) return html;
1917
+ const remaining = [...entries];
1918
+ return html.replace(/<h([23])>([\s\S]*?)<\/h[23]>/g, (full, levelStr, innerHtml) => {
1919
+ const level = parseInt(levelStr, 10);
1920
+ const innerText = innerHtml.replace(/<[^>]+>/g, "").trim();
1921
+ const idx = remaining.findIndex((e) => {
1922
+ if (e.level !== level) return false;
1923
+ return stripInlineMarkdown(e.text) === innerText || escHtml(e.text) === innerHtml.trim();
1924
+ });
1925
+ if (idx === -1) return full;
1926
+ const entry = remaining.splice(idx, 1)[0];
1927
+ return `<h${level} id="${escHtml(entry.id)}">${innerHtml}</h${level}>`;
1928
+ });
1929
+ }
1930
+ function slugifyHeading(text) {
1931
+ return text.toLowerCase().replace(/[^\w\s-]/g, "").trim().replace(/[\s_]+/g, "-").replace(/-+/g, "-");
1932
+ }
1933
+ function stripInlineMarkdown(text) {
1934
+ return text.replace(/\*\*([^*]+)\*\*/g, "$1").replace(/\*([^*]+)\*/g, "$1").replace(/`([^`]+)`/g, "$1").replace(/\[([^\]]+)\]\([^)]+\)/g, "$1").trim();
1935
+ }
1936
+
1937
+ // src/render/knowledge.ts
1938
+ function renderKnowledge(files, t) {
1939
+ const nav = renderNav("knowledge", 1, t);
1940
+ const title = t?.nav.knowledge ?? "Knowledge";
1941
+ if (files.length === 0) {
1942
+ const content2 = `<div class="page-header">
1943
+ <h1>${escHtml(title)}</h1>
1944
+ <p class="subtitle">No knowledge files found in Memory/04_knowledge/.</p>
1945
+ </div>`;
1946
+ return htmlShell(title, content2, nav);
1947
+ }
1948
+ const hasMermaid = files.some((f) => hasMermaidBlocks(f.content));
1949
+ const sections = files.map((f) => {
1950
+ const sectionId = slugifyName(f.filename);
1951
+ const tocEntries = prefixTocIds(extractToc(f.content), sectionId);
1952
+ const rawHtml = markdownToHtml(f.content);
1953
+ const htmlContent = injectHeadingIds(rawHtml, tocEntries);
1954
+ const toc = renderToc(tocEntries);
1955
+ return `<div class="card" id="${escHtml(sectionId)}">
1956
+ <h2>${escHtml(f.title)}</h2>
1957
+ <p style="font-size:0.8rem;color:var(--text-muted);margin-bottom:0.5rem">${escHtml(f.filename)}</p>
1958
+ ${toc}
1959
+ <div class="markdown-body">${htmlContent}</div>
1960
+ </div>`;
1961
+ }).join("\n");
1962
+ const content = `<div class="page-header">
1963
+ <h1>${escHtml(title)}</h1>
1964
+ <p class="subtitle">${files.length} knowledge file(s)</p>
1965
+ </div>
1966
+ ${sections}`;
1967
+ return htmlShell(title, content, nav, hasMermaid ? { extraHead: MERMAID_CDN_SCRIPT } : {});
1968
+ }
1969
+
1970
+ // src/render/goals.ts
1971
+ function renderGoals(files, t) {
1972
+ const nav = renderNav("goals", 1, t);
1973
+ const title = t?.nav.goals ?? "Goals";
1974
+ if (files.length === 0) {
1975
+ const content2 = `<div class="page-header">
1976
+ <h1>${escHtml(title)}</h1>
1977
+ <p class="subtitle">No goal files found in Memory/01_goals/.</p>
1978
+ </div>`;
1979
+ return htmlShell(title, content2, nav);
1980
+ }
1981
+ const sections = files.map((f) => {
1982
+ const sectionId = slugifyName(f.filename);
1983
+ const tocEntries = prefixTocIds(extractToc(f.content), sectionId);
1984
+ const rawHtml = markdownToHtml(f.content);
1985
+ const htmlContent = injectHeadingIds(rawHtml, tocEntries);
1986
+ const toc = renderToc(tocEntries);
1987
+ return `<div class="card" id="${escHtml(sectionId)}">
1988
+ <h2>${escHtml(f.title)}</h2>
1989
+ <p style="font-size:0.8rem;color:var(--text-muted);margin-bottom:0.5rem">${escHtml(f.filename)}</p>
1990
+ ${toc}
1991
+ <div class="markdown-body">${htmlContent}</div>
1992
+ </div>`;
1993
+ }).join("\n");
1994
+ const content = `<div class="page-header">
1995
+ <h1>${escHtml(title)}</h1>
1996
+ <p class="subtitle">${files.length} goal file(s)</p>
1997
+ </div>
1998
+ ${sections}`;
1999
+ const hasMermaid = files.some((f) => hasMermaidBlocks(f.content));
2000
+ return htmlShell(title, content, nav, hasMermaid ? { extraHead: MERMAID_CDN_SCRIPT } : {});
2001
+ }
2002
+
2003
+ // src/render/todos.ts
2004
+ function renderTodos(files, t) {
2005
+ const nav = renderNav("todos", 1, t);
2006
+ const title = t?.nav.todos ?? "Todos";
2007
+ if (files.length === 0) {
2008
+ const content2 = `<div class="page-header">
2009
+ <h1>${escHtml(title)}</h1>
2010
+ <p class="subtitle">No todo files found in Memory/02_todos/.</p>
2011
+ </div>`;
2012
+ return htmlShell(title, content2, nav);
2013
+ }
2014
+ const sections = files.map((f) => {
2015
+ const sectionId = slugifyName(f.filename);
2016
+ const tocEntries = prefixTocIds(extractToc(f.content), sectionId);
2017
+ const rawHtml = markdownToHtml(f.content);
2018
+ const htmlContent = injectHeadingIds(rawHtml, tocEntries);
2019
+ const toc = renderToc(tocEntries);
2020
+ return `<div class="card" id="${escHtml(sectionId)}">
2021
+ <h2>${escHtml(f.title)}</h2>
2022
+ <p style="font-size:0.8rem;color:var(--text-muted);margin-bottom:1rem">${escHtml(f.filename)}</p>
2023
+ ${toc}
2024
+ <div class="markdown-body">${htmlContent}</div>
2025
+ </div>`;
2026
+ }).join("\n");
2027
+ const content = `<div class="page-header">
2028
+ <h1>${escHtml(title)}</h1>
2029
+ <p class="subtitle">${files.length} todo file(s)</p>
2030
+ </div>
2031
+ ${sections}`;
2032
+ const hasMermaid = files.some((f) => hasMermaidBlocks(f.content));
2033
+ return htmlShell(title, content, nav, hasMermaid ? { extraHead: MERMAID_CDN_SCRIPT } : {});
2034
+ }
2035
+
2036
+ // src/render/archive.ts
2037
+ function renderArchive(files, t) {
2038
+ const nav = renderNav("archive", 1, t);
2039
+ const title = t?.nav.archive ?? "Archive";
2040
+ if (files.length === 0) {
2041
+ const content2 = `<div class="page-header">
2042
+ <h1>${escHtml(title)}</h1>
2043
+ <p class="subtitle">No archive files found in Memory/05_archive/.</p>
2044
+ </div>`;
2045
+ return htmlShell(title, content2, nav);
2046
+ }
2047
+ const sections = files.map((f) => {
2048
+ const sectionId = slugifyName(f.filename);
2049
+ const tocEntries = prefixTocIds(extractToc(f.content), sectionId);
2050
+ const rawHtml = markdownToHtml(f.content);
2051
+ const htmlContent = injectHeadingIds(rawHtml, tocEntries);
2052
+ const toc = renderToc(tocEntries);
2053
+ return `<div class="card" id="${escHtml(sectionId)}">
2054
+ <h2>${escHtml(f.title)}</h2>
2055
+ <p style="font-size:0.8rem;color:var(--text-muted);margin-bottom:1rem">${escHtml(f.filename)}</p>
2056
+ ${toc}
2057
+ <div class="markdown-body">${htmlContent}</div>
2058
+ </div>`;
2059
+ }).join("\n");
2060
+ const content = `<div class="page-header">
2061
+ <h1>${escHtml(title)}</h1>
2062
+ <p class="subtitle">${files.length} archive file(s)</p>
2063
+ </div>
2064
+ ${sections}`;
2065
+ const hasMermaid = files.some((f) => hasMermaidBlocks(f.content));
2066
+ return htmlShell(title, content, nav, hasMermaid ? { extraHead: MERMAID_CDN_SCRIPT } : {});
2067
+ }
2068
+
2069
+ // src/render/projects.ts
2070
+ function extractProject(cwd) {
2071
+ if (!cwd) return "unknown";
2072
+ return cwd.split(/[/\\]/).filter(Boolean).at(-1) ?? "unknown";
2073
+ }
2074
+ function renderProjects(manifests, t) {
2075
+ const nav = renderNav("projects", 1, t);
2076
+ const title = t?.projects.title ?? "Projects";
2077
+ if (manifests.length === 0) {
2078
+ const content2 = `<div class="page-header">
2079
+ <h1>${escHtml(title)}</h1>
2080
+ <p class="subtitle">${escHtml(t?.projects.noProjects ?? "No projects found.")}</p>
2081
+ </div>`;
2082
+ return htmlShell(title, content2, nav);
2083
+ }
2084
+ const projectMap = /* @__PURE__ */ new Map();
2085
+ for (const m of manifests) {
2086
+ const proj = extractProject(m.cwd);
2087
+ const group = projectMap.get(proj);
2088
+ if (group) {
2089
+ group.push(m);
2090
+ } else {
2091
+ projectMap.set(proj, [m]);
2092
+ }
2093
+ }
2094
+ const sorted = [...projectMap.entries()].sort((a, b) => b[1].length - a[1].length);
2095
+ const sessionLabel = t?.projects.sessions ?? "sessions";
2096
+ const cards = sorted.map(([name, sessions]) => {
2097
+ const count = sessions.length;
2098
+ const lastActive = sessions.map((s) => s.started_at).sort().at(-1)?.slice(0, 10) ?? "\u2014";
2099
+ const clients = [...new Set(sessions.map((s) => s.client))];
2100
+ const badges = clients.map((c) => clientBadge(c)).join(" ");
2101
+ return `<div class="project-card">
2102
+ <div class="project-card-name">${escHtml(name)}</div>
2103
+ <div class="project-card-meta">
2104
+ ${count} ${escHtml(sessionLabel)} \xB7 last active ${escHtml(lastActive)}
2105
+ </div>
2106
+ <div class="project-card-clients">${badges}</div>
2107
+ </div>`;
2108
+ }).join("\n");
2109
+ const content = `<div class="page-header">
2110
+ <h1>${escHtml(title)}</h1>
2111
+ <p class="subtitle">${sorted.length} project(s)</p>
2112
+ </div>
2113
+ <div class="project-grid">
2114
+ ${cards}
2115
+ </div>`;
2116
+ return htmlShell(title, content, nav);
2117
+ }
2118
+
2119
+ // src/render/graph.ts
2120
+ var MAX_NODES = 500;
2121
+ function buildGraphData(manifests, knowledgeFiles, linkGraph) {
2122
+ const sorted = [...manifests].sort((a, b) => b.started_at.localeCompare(a.started_at)).slice(0, MAX_NODES);
2123
+ const nodeSet = /* @__PURE__ */ new Set();
2124
+ const nodes = [];
2125
+ const edges = [];
2126
+ for (const m of sorted) {
2127
+ if (!nodeSet.has(m.session_id)) {
2128
+ nodeSet.add(m.session_id);
2129
+ nodes.push({
2130
+ id: m.session_id,
2131
+ label: m.title || m.session_id.slice(0, 8),
2132
+ type: "session",
2133
+ client: m.client,
2134
+ url: transcriptUrlFromRoot(m)
2135
+ });
2136
+ }
2137
+ }
2138
+ for (const kf of knowledgeFiles) {
2139
+ const id = `knowledge:${kf.filename}`;
2140
+ if (!nodeSet.has(id)) {
2141
+ nodeSet.add(id);
2142
+ nodes.push({
2143
+ id,
2144
+ label: kf.title || kf.filename,
2145
+ type: "knowledge",
2146
+ url: `knowledge/index.html#${slugifyName(kf.filename)}`
2147
+ });
2148
+ }
2149
+ }
2150
+ for (const [sourceId, targets] of Object.entries(linkGraph.forwardLinks)) {
2151
+ if (!nodeSet.has(sourceId)) continue;
2152
+ for (const targetId of targets) {
2153
+ if (nodeSet.has(targetId)) {
2154
+ edges.push({ source: sourceId, target: targetId });
2155
+ }
2156
+ }
2157
+ }
2158
+ return { nodes, edges };
2159
+ }
2160
+ function renderGraph(manifests, knowledgeFiles, linkGraph, t) {
2161
+ const nav = renderNav("graph", 0, t);
2162
+ const title = t?.graph.title ?? "Knowledge Graph";
2163
+ const subtitle = t?.graph.subtitle ?? "Connections between sessions and knowledge files";
2164
+ const { nodes, edges } = buildGraphData(manifests, knowledgeFiles, linkGraph);
2165
+ if (nodes.length === 0) {
2166
+ const content2 = `<div class="page-header">
2167
+ <h1>${escHtml(title)}</h1>
2168
+ <p class="subtitle">${escHtml(t?.graph.noData ?? "No graph data available.")}</p>
2169
+ </div>`;
2170
+ return htmlShell(title, content2, nav);
2171
+ }
2172
+ const graphDataJson = JSON.stringify({ nodes, edges }).replace(/<\//g, "<\\/").replace(/\u2028/g, "\\u2028").replace(/\u2029/g, "\\u2029");
2173
+ const graphScript = `<script>
2174
+ (function() {
2175
+ var GRAPH_DATA = ${graphDataJson};
2176
+
2177
+ // Client color map
2178
+ var CLIENT_COLORS = { claude: '#bc8cff', codex: '#58a6ff', gemini: '#3fb950' };
2179
+ function nodeColor(n) {
2180
+ if (n.type === 'knowledge') return '#f0c040';
2181
+ return CLIENT_COLORS[n.client] || '#8b949e';
2182
+ }
2183
+ function nodeRadius(n) { return n.type === 'knowledge' ? 7 : 5; }
2184
+
2185
+ var canvas = document.getElementById('graph-canvas');
2186
+ if (!canvas) return;
2187
+ var ctx = canvas.getContext('2d');
2188
+
2189
+ // High-DPI
2190
+ function resize() {
2191
+ var rect = canvas.getBoundingClientRect();
2192
+ var dpr = window.devicePixelRatio || 1;
2193
+ canvas.width = rect.width * dpr;
2194
+ canvas.height = rect.height * dpr;
2195
+ ctx.scale(dpr, dpr);
2196
+ }
2197
+ resize();
2198
+ window.addEventListener('resize', function() { resize(); draw(); });
2199
+
2200
+ var W = canvas.getBoundingClientRect().width;
2201
+ var H = canvas.getBoundingClientRect().height;
2202
+
2203
+ // Init positions (random spread)
2204
+ var nodeMap = {};
2205
+ var simNodes = GRAPH_DATA.nodes.map(function(n) {
2206
+ var sn = { id: n.id, label: n.label, type: n.type, client: n.client, url: n.url,
2207
+ x: W/2 + (Math.random()-0.5)*W*0.7, y: H/2 + (Math.random()-0.5)*H*0.7, vx: 0, vy: 0 };
2208
+ nodeMap[n.id] = sn;
2209
+ return sn;
2210
+ });
2211
+
2212
+ var simEdges = GRAPH_DATA.edges.map(function(e) {
2213
+ return { source: nodeMap[e.source], target: nodeMap[e.target] };
2214
+ }).filter(function(e) { return e.source && e.target; });
2215
+
2216
+ // Simulation params \u2014 tuned for ~50\u2013200 node graphs on a 900px canvas
2217
+ var ALPHA = 1.0; // initial energy; decays each tick
2218
+ var ALPHA_DECAY = 0.0228; // decay rate \u2248 ln(0.001)/300 (settles in ~300 ticks)
2219
+ var ALPHA_MIN = 0.001; // stop simulation below this threshold
2220
+ var CHARGE = -120; // repulsion strength between nodes (negative = push apart)
2221
+ var LINK_DIST = 80; // spring rest length in pixels
2222
+ var CENTER_FORCE = 0.05; // gravity toward canvas center (prevents drift)
2223
+ var DAMPING = 0.6; // velocity damping per tick (higher = faster settling)
2224
+
2225
+ // Pan/zoom state
2226
+ var panX = 0, panY = 0, scale = 1;
2227
+ var dragging = false, dragNode = null, dragOffX = 0, dragOffY = 0;
2228
+ var isPanning = false, panStartX = 0, panStartY = 0, panStartPanX = 0, panStartPanY = 0;
2229
+
2230
+ function worldToScreen(wx, wy) {
2231
+ return { x: (wx + panX) * scale + W/2, y: (wy + panY) * scale + H/2 };
2232
+ }
2233
+ function screenToWorld(sx, sy) {
2234
+ return { x: (sx - W/2) / scale - panX, y: (sy - H/2) / scale - panY };
2235
+ }
2236
+
2237
+ function tick() {
2238
+ if (ALPHA < ALPHA_MIN) return;
2239
+ ALPHA *= (1 - ALPHA_DECAY);
2240
+
2241
+ // Repulsion (n^2, capped)
2242
+ var N = simNodes.length;
2243
+ for (var i = 0; i < N; i++) {
2244
+ for (var j = i+1; j < N; j++) {
2245
+ var ni = simNodes[i], nj = simNodes[j];
2246
+ var dx = nj.x - ni.x, dy = nj.y - ni.y;
2247
+ var dist = Math.sqrt(dx*dx + dy*dy) || 1;
2248
+ var force = CHARGE / (dist * dist) * ALPHA;
2249
+ var fx = dx / dist * force, fy = dy / dist * force;
2250
+ ni.vx -= fx; ni.vy -= fy;
2251
+ nj.vx += fx; nj.vy += fy;
2252
+ }
2253
+ }
2254
+
2255
+ // Link attraction
2256
+ for (var k = 0; k < simEdges.length; k++) {
2257
+ var e = simEdges[k];
2258
+ var dx2 = e.target.x - e.source.x, dy2 = e.target.y - e.source.y;
2259
+ var dist2 = Math.sqrt(dx2*dx2 + dy2*dy2) || 1;
2260
+ var strength = (dist2 - LINK_DIST) / dist2 * 0.5 * ALPHA;
2261
+ var fx2 = dx2 * strength, fy2 = dy2 * strength;
2262
+ e.source.vx += fx2; e.source.vy += fy2;
2263
+ e.target.vx -= fx2; e.target.vy -= fy2;
2264
+ }
2265
+
2266
+ // Center gravity
2267
+ for (var m = 0; m < N; m++) {
2268
+ var node = simNodes[m];
2269
+ node.vx += -node.x * CENTER_FORCE * ALPHA;
2270
+ node.vy += -node.y * CENTER_FORCE * ALPHA;
2271
+ if (node === dragNode) continue;
2272
+ node.vx *= DAMPING;
2273
+ node.vy *= DAMPING;
2274
+ node.x += node.vx;
2275
+ node.y += node.vy;
2276
+ }
2277
+ }
2278
+
2279
+ function draw() {
2280
+ W = canvas.getBoundingClientRect().width;
2281
+ H = canvas.getBoundingClientRect().height;
2282
+ ctx.clearRect(0, 0, W, H);
2283
+ ctx.save();
2284
+
2285
+ // Draw edges
2286
+ ctx.strokeStyle = 'rgba(139,148,158,0.3)';
2287
+ ctx.lineWidth = 1;
2288
+ for (var k = 0; k < simEdges.length; k++) {
2289
+ var e = simEdges[k];
2290
+ var s = worldToScreen(e.source.x, e.source.y);
2291
+ var t2 = worldToScreen(e.target.x, e.target.y);
2292
+ ctx.beginPath();
2293
+ ctx.moveTo(s.x, s.y);
2294
+ ctx.lineTo(t2.x, t2.y);
2295
+ ctx.stroke();
2296
+ }
2297
+
2298
+ // Draw nodes
2299
+ for (var i = 0; i < simNodes.length; i++) {
2300
+ var n = simNodes[i];
2301
+ var pos = worldToScreen(n.x, n.y);
2302
+ var r = nodeRadius(n) * scale;
2303
+ ctx.beginPath();
2304
+ ctx.arc(pos.x, pos.y, Math.max(r, 3), 0, Math.PI*2);
2305
+ ctx.fillStyle = nodeColor(n);
2306
+ ctx.fill();
2307
+ if (n === hoverNode) {
2308
+ ctx.strokeStyle = '#fff';
2309
+ ctx.lineWidth = 1.5;
2310
+ ctx.stroke();
2311
+ }
2312
+ }
2313
+
2314
+ // Draw hover label
2315
+ if (hoverNode) {
2316
+ var hpos = worldToScreen(hoverNode.x, hoverNode.y);
2317
+ ctx.font = '11px sans-serif';
2318
+ ctx.fillStyle = '#e6edf3';
2319
+ ctx.fillText(hoverNode.label.slice(0, 30), hpos.x + 8, hpos.y - 4);
2320
+ }
2321
+
2322
+ ctx.restore();
2323
+ }
2324
+
2325
+ var hoverNode = null;
2326
+
2327
+ function findNode(sx, sy) {
2328
+ var w = screenToWorld(sx, sy);
2329
+ var best = null, bestDist = Infinity;
2330
+ for (var i = 0; i < simNodes.length; i++) {
2331
+ var n = simNodes[i];
2332
+ var dx = n.x - w.x, dy = n.y - w.y;
2333
+ var dist = Math.sqrt(dx*dx + dy*dy);
2334
+ if (dist < 15 / scale && dist < bestDist) { best = n; bestDist = dist; }
2335
+ }
2336
+ return best;
2337
+ }
2338
+
2339
+ function getCanvasPos(evt) {
2340
+ var rect = canvas.getBoundingClientRect();
2341
+ return { x: evt.clientX - rect.left, y: evt.clientY - rect.top };
2342
+ }
2343
+
2344
+ canvas.addEventListener('mousemove', function(e) {
2345
+ var pos = getCanvasPos(e);
2346
+ if (isPanning) {
2347
+ panX = panStartPanX + (pos.x - panStartX) / scale;
2348
+ panY = panStartPanY + (pos.y - panStartY) / scale;
2349
+ return;
2350
+ }
2351
+ if (dragNode) {
2352
+ var w = screenToWorld(pos.x, pos.y);
2353
+ dragNode.x = w.x + dragOffX;
2354
+ dragNode.y = w.y + dragOffY;
2355
+ dragNode.vx = 0; dragNode.vy = 0;
2356
+ return;
2357
+ }
2358
+ var node = findNode(pos.x, pos.y);
2359
+ hoverNode = node;
2360
+ canvas.style.cursor = node ? 'pointer' : 'grab';
2361
+ });
2362
+
2363
+ canvas.addEventListener('mousedown', function(e) {
2364
+ var pos = getCanvasPos(e);
2365
+ var node = findNode(pos.x, pos.y);
2366
+ if (node) {
2367
+ dragNode = node;
2368
+ var w = screenToWorld(pos.x, pos.y);
2369
+ dragOffX = node.x - w.x;
2370
+ dragOffY = node.y - w.y;
2371
+ ALPHA = 0.3;
2372
+ } else {
2373
+ isPanning = true;
2374
+ panStartX = pos.x; panStartY = pos.y;
2375
+ panStartPanX = panX; panStartPanY = panY;
2376
+ }
2377
+ });
2378
+
2379
+ canvas.addEventListener('mouseup', function(e) {
2380
+ if (dragNode) { dragNode = null; return; }
2381
+ isPanning = false;
2382
+ });
2383
+
2384
+ canvas.addEventListener('click', function(e) {
2385
+ var pos = getCanvasPos(e);
2386
+ var node = findNode(pos.x, pos.y);
2387
+ if (node && node.url) { window.location.href = node.url; }
2388
+ });
2389
+
2390
+ canvas.addEventListener('wheel', function(e) {
2391
+ e.preventDefault();
2392
+ var pos = getCanvasPos(e);
2393
+ var factor = e.deltaY > 0 ? 0.9 : 1.1;
2394
+ var wx = (pos.x - W/2) / scale - panX;
2395
+ var wy = (pos.y - H/2) / scale - panY;
2396
+ scale *= factor;
2397
+ scale = Math.max(0.1, Math.min(10, scale));
2398
+ panX = (pos.x - W/2) / scale - wx;
2399
+ panY = (pos.y - H/2) / scale - wy;
2400
+ }, { passive: false });
2401
+
2402
+ // Animation loop \u2014 stops automatically once simulation settles
2403
+ function loop() {
2404
+ tick();
2405
+ draw();
2406
+ if (ALPHA >= ALPHA_MIN) {
2407
+ requestAnimationFrame(loop);
2408
+ }
2409
+ }
2410
+ loop();
2411
+ })();
2412
+ </script>`;
2413
+ const legendItems = [
2414
+ { color: "#bc8cff", label: "Claude" },
2415
+ { color: "#58a6ff", label: "Codex" },
2416
+ { color: "#3fb950", label: "Gemini" },
2417
+ { color: "#8b949e", label: "Other" },
2418
+ { color: "#f0c040", label: "Knowledge" }
2419
+ ];
2420
+ const legend = legendItems.map(
2421
+ (item) => `<div class="graph-legend-item">
2422
+ <span class="graph-legend-dot" style="background:${escHtml(item.color)}"></span>
2423
+ ${escHtml(item.label)}
2424
+ </div>`
2425
+ ).join("");
2426
+ const content = `<div class="page-header">
2427
+ <h1>${escHtml(title)}</h1>
2428
+ <p class="subtitle">${escHtml(subtitle)} \xB7 ${nodes.length} nodes, ${edges.length} edges</p>
2429
+ </div>
2430
+ <canvas id="graph-canvas"></canvas>
2431
+ <div class="graph-legend">${legend}</div>`;
2432
+ return htmlShell(title, content, nav, graphScript);
2433
+ }
2434
+
2435
+ // src/render/search.ts
2436
+ var MAX_INDEX_BYTES = 5e4;
2437
+ var SNIPPET_LEN = 200;
2438
+ function buildSearchIndex(manifests, getSnippet) {
2439
+ const entries = [];
2440
+ let totalBytes = 0;
2441
+ for (const m of manifests) {
2442
+ let snippet = getSnippet(m).slice(0, SNIPPET_LEN);
2443
+ const project = m.project || lastPathSegment(m.cwd) || "unknown";
2444
+ const baseBytes = JSON.stringify({
2445
+ url: transcriptUrl(m),
2446
+ title: m.title || m.session_id,
2447
+ client: m.client,
2448
+ project,
2449
+ date: m.started_at.slice(0, 10),
2450
+ snippet: ""
2451
+ }).length;
2452
+ const budgetForSnippet = MAX_INDEX_BYTES - totalBytes - baseBytes - 10;
2453
+ if (budgetForSnippet < 20) break;
2454
+ if (baseBytes + snippet.length > MAX_INDEX_BYTES - totalBytes) {
2455
+ snippet = snippet.slice(0, Math.max(0, budgetForSnippet));
2456
+ }
2457
+ const entry = {
2458
+ url: transcriptUrl(m),
2459
+ title: m.title || m.session_id,
2460
+ client: m.client,
2461
+ project,
2462
+ date: m.started_at.slice(0, 10),
2463
+ snippet
2464
+ };
2465
+ totalBytes += JSON.stringify(entry).length;
2466
+ entries.push(entry);
2467
+ }
2468
+ return entries;
2469
+ }
2470
+ function transcriptUrl(m) {
2471
+ return transcriptUrlFromRoot(m);
2472
+ }
2473
+ function lastPathSegment(p) {
2474
+ return p.split(/[/\\]/).filter(Boolean).at(-1) ?? "";
2475
+ }
2476
+ function renderSearchPage(index, t) {
2477
+ const indexJson = JSON.stringify(index).replace(/<\//g, "<\\/");
2478
+ const nav = renderNav("search", 0, t);
2479
+ const title = t?.search.title ?? "Search";
2480
+ const placeholder = t?.search.placeholder ?? "Search sessions, messages, and content...";
2481
+ const clients = [...new Set(index.map((e) => e.client).filter(Boolean))].sort();
2482
+ const projects = [...new Set(index.map((e) => e.project).filter(Boolean))].sort();
2483
+ const clientOptions = clients.map((c) => `<option value="${escHtml(c)}">${escHtml(c)}</option>`).join("");
2484
+ const projectOptions = projects.map((p) => `<option value="${escHtml(p)}">${escHtml(p)}</option>`).join("");
2485
+ const extraHead = `<script>
2486
+ const SEARCH_INDEX = ${indexJson};
2487
+ </script>`;
2488
+ const content = `
2489
+ <div class="page-header">
2490
+ <h1>${escHtml(title)}</h1>
2491
+ <p class="subtitle">Full-text search across all imported sessions</p>
2492
+ </div>
2493
+
2494
+ <div style="display:flex;gap:0.5rem;margin-bottom:0.75rem;flex-wrap:wrap">
2495
+ <select id="filter-client" class="search-filter-select">
2496
+ <option value="">All Clients</option>
2497
+ ${clientOptions}
2498
+ </select>
2499
+ <select id="filter-project" class="search-filter-select">
2500
+ <option value="">All Projects</option>
2501
+ ${projectOptions}
2502
+ </select>
2503
+ </div>
2504
+ <input
2505
+ type="search"
2506
+ id="search-input"
2507
+ class="search-box"
2508
+ placeholder="${escHtml(placeholder)}"
2509
+ autofocus
2510
+ >
2511
+ <div id="search-count"></div>
2512
+ <div id="search-results" class="search-results"></div>
2513
+
2514
+ <script>
2515
+ (function() {
2516
+ var input = document.getElementById('search-input');
2517
+ var resultsEl = document.getElementById('search-results');
2518
+ var countEl = document.getElementById('search-count');
2519
+ var clientSel = document.getElementById('filter-client');
2520
+ var projectSel = document.getElementById('filter-project');
2521
+
2522
+ function highlight(text, query) {
2523
+ if (!query) return escHtml(text);
2524
+ var escaped = escHtml(text);
2525
+ var escapedQ = escHtml(query);
2526
+ var lower = escaped.toLowerCase();
2527
+ var lowerQ = escapedQ.toLowerCase();
2528
+ if (!lowerQ) return escaped;
2529
+ var result = '';
2530
+ var i = 0;
2531
+ while (i < escaped.length) {
2532
+ var idx = lower.indexOf(lowerQ, i);
2533
+ if (idx === -1) { result += escaped.slice(i); break; }
2534
+ result += escaped.slice(i, idx) + '<mark>' + escaped.slice(idx, idx + lowerQ.length) + '</mark>';
2535
+ i = idx + lowerQ.length;
2536
+ }
2537
+ return result;
2538
+ }
2539
+
2540
+ function escHtml(s) {
2541
+ return String(s).replace(/&/g,'&amp;').replace(/</g,'&lt;').replace(/>/g,'&gt;').replace(/"/g,'&quot;');
2542
+ }
2543
+
2544
+ function renderResults() {
2545
+ var query = (input.value || '');
2546
+ var q = query.toLowerCase().trim();
2547
+ var filterClient = clientSel ? clientSel.value : '';
2548
+ var filterProject = projectSel ? projectSel.value : '';
2549
+ var filtered = SEARCH_INDEX.filter(function(e) {
2550
+ if (filterClient && e.client !== filterClient) return false;
2551
+ if (filterProject && e.project !== filterProject) return false;
2552
+ if (!q) return true;
2553
+ return (e.title || '').toLowerCase().includes(q) ||
2554
+ (e.snippet || '').toLowerCase().includes(q);
2555
+ });
2556
+ if (!q && !filterClient && !filterProject) {
2557
+ resultsEl.innerHTML = '';
2558
+ countEl.textContent = SEARCH_INDEX.length + ' session(s) indexed';
2559
+ return;
2560
+ }
2561
+ countEl.textContent = filtered.length + ' result(s)' + (q ? ' for "' + escHtml(q) + '"' : '');
2562
+ resultsEl.innerHTML = filtered.map(function(e) {
2563
+ return '<div class="search-result">' +
2564
+ '<div class="search-result-title"><a href="' + escHtml(e.url) + '">' + highlight(e.title, query) + '</a></div>' +
2565
+ '<div class="search-result-meta"><span class="badge badge-' + escHtml(e.client) + '">' + escHtml(e.client) + '</span> &nbsp; ' + escHtml(e.date) + '</div>' +
2566
+ '<div class="search-result-snippet">' + highlight(e.snippet, query) + '</div>' +
2567
+ '</div>';
2568
+ }).join('');
2569
+ }
2570
+
2571
+ renderResults();
2572
+ input.addEventListener('input', renderResults);
2573
+ if (clientSel) clientSel.addEventListener('change', renderResults);
2574
+ if (projectSel) projectSel.addEventListener('change', renderResults);
2575
+ })();
2576
+ </script>`;
2577
+ return htmlShell(title, content, nav, extraHead);
2578
+ }
2579
+
2580
+ // src/render/links.ts
2581
+ import { existsSync as existsSync2, readFileSync as readFileSync2 } from "fs";
2582
+ import { basename as basename2, join as join2 } from "path";
2583
+ function buildLinkGraph(manifests, root) {
2584
+ const forwardLinks = {};
2585
+ const backlinks = {};
2586
+ const lookup = {};
2587
+ for (const m of manifests) {
2588
+ lookup[m.session_id] = m.session_id;
2589
+ const cleanPath = m.repo_clean_path || m.global_clean_path;
2590
+ if (cleanPath) {
2591
+ const stem = basename2(cleanPath, ".md");
2592
+ if (stem) lookup[stem] = m.session_id;
2593
+ }
2594
+ }
2595
+ for (const m of manifests) {
2596
+ const cleanPath = resolveCleanPath(m, root);
2597
+ if (!cleanPath || !existsSync2(cleanPath)) continue;
2598
+ let content;
2599
+ try {
2600
+ content = readFileSync2(cleanPath, "utf-8");
2601
+ } catch {
2602
+ continue;
2603
+ }
2604
+ const refs = extractLinks(content);
2605
+ for (const ref of refs) {
2606
+ const targetId = lookup[ref];
2607
+ if (!targetId || targetId === m.session_id) continue;
2608
+ const fwd = forwardLinks[m.session_id] ?? [];
2609
+ if (!fwd.includes(targetId)) {
2610
+ forwardLinks[m.session_id] = [...fwd, targetId];
2611
+ }
2612
+ const back = backlinks[targetId] ?? [];
2613
+ if (!back.includes(m.session_id)) {
2614
+ backlinks[targetId] = [...back, m.session_id];
2615
+ }
2616
+ }
2617
+ }
2618
+ return { backlinks, forwardLinks };
2619
+ }
2620
+ function extractLinks(content) {
2621
+ const results = [];
2622
+ const re = /\[\[([^\][\n]+)\]\]/g;
2623
+ let match;
2624
+ while ((match = re.exec(content)) !== null) {
2625
+ const ref = match[1]?.trim();
2626
+ if (ref && ref.length > 0) {
2627
+ results.push(ref);
2628
+ }
2629
+ }
2630
+ return results;
2631
+ }
2632
+ function resolveCleanPath(m, root) {
2633
+ if (m.repo_clean_path) {
2634
+ return join2(root, m.repo_clean_path);
2635
+ }
2636
+ if (m.global_clean_path) {
2637
+ return m.global_clean_path;
2638
+ }
2639
+ return null;
2640
+ }
2641
+
2642
+ // src/render/rss.ts
2643
+ function xmlEsc(text) {
2644
+ return text.replace(/&/g, "&amp;").replace(/</g, "&lt;").replace(/>/g, "&gt;").replace(/"/g, "&quot;").replace(/'/g, "&apos;");
2645
+ }
2646
+ function renderRssFeed(manifests, summaries, baseUrl) {
2647
+ const MAX_ITEMS = 100;
2648
+ const sorted = [...manifests].sort((a, b) => b.started_at.localeCompare(a.started_at)).slice(0, MAX_ITEMS);
2649
+ const channelLink = baseUrl ? `
2650
+ <link>${xmlEsc(baseUrl)}</link>` : "";
2651
+ const items = sorted.map((m) => {
2652
+ const title = m.title || m.session_id;
2653
+ const summary = summaries[m.session_id] ?? "";
2654
+ const pubDate = toRfc2822(m.started_at);
2655
+ const relUrl = transcriptUrlFromRoot(m);
2656
+ const absUrl = baseUrl ? `${baseUrl.replace(/\/$/, "")}/${relUrl}` : "";
2657
+ const linkTag = absUrl ? `
2658
+ <link>${xmlEsc(absUrl)}</link>` : "";
2659
+ const guidTag = absUrl ? `
2660
+ <guid isPermaLink="true">${xmlEsc(absUrl)}</guid>` : `
2661
+ <guid isPermaLink="false">${xmlEsc(m.session_id)}</guid>`;
2662
+ const descTag = summary ? `
2663
+ <description>${xmlEsc(summary.slice(0, 500))}</description>` : "";
2664
+ return ` <item>
2665
+ <title>${xmlEsc(title)}</title>${linkTag}${guidTag}
2666
+ <pubDate>${xmlEsc(pubDate)}</pubDate>${descTag}
2667
+ </item>`;
2668
+ }).join("\n");
2669
+ return `<?xml version="1.0" encoding="UTF-8"?>
2670
+ <rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom">
2671
+ <channel>
2672
+ <title>MemoryTree Sessions</title>${channelLink}
2673
+ <description>AI session transcripts exported by MemoryTree</description>
2674
+ <generator>MemoryTree</generator>
2675
+ ${items}
2676
+ </channel>
2677
+ </rss>`;
2678
+ }
2679
+ function toRfc2822(iso) {
2680
+ const d = new Date(iso);
2681
+ return isNaN(d.getTime()) ? (/* @__PURE__ */ new Date()).toUTCString() : d.toUTCString();
2682
+ }
2683
+
2684
+ // src/summarize.ts
2685
+ import { existsSync as existsSync3, mkdirSync as mkdirSync2, readFileSync as readFileSync3, writeFileSync as writeFileSync2 } from "fs";
2686
+ import { join as join3 } from "path";
2687
+ var activeCalls = 0;
2688
+ var MAX_CONCURRENT = 5;
2689
+ async function withSemaphore(fn) {
2690
+ while (activeCalls >= MAX_CONCURRENT) {
2691
+ await new Promise((resolve) => setTimeout(resolve, 100));
2692
+ }
2693
+ activeCalls++;
2694
+ try {
2695
+ return await fn();
2696
+ } finally {
2697
+ activeCalls--;
2698
+ }
2699
+ }
2700
+ async function getSummary(sha256, messages, options) {
2701
+ const cached = readCache(sha256, options.cacheDir);
2702
+ if (cached !== null) {
2703
+ return cached;
2704
+ }
2705
+ if (options.noAi) return "";
2706
+ const apiKey = process.env["ANTHROPIC_API_KEY"];
2707
+ if (!apiKey) return "";
2708
+ return withSemaphore(() => callApi(sha256, messages, options));
2709
+ }
2710
+ async function callApi(sha256, messages, options) {
2711
+ try {
2712
+ const { default: Anthropic } = await import("@anthropic-ai/sdk");
2713
+ const client = new Anthropic();
2714
+ const prompt = buildSummaryPrompt(messages);
2715
+ const response = await client.messages.create({
2716
+ model: options.model,
2717
+ max_tokens: 200,
2718
+ system: "Summarize in 2-3 sentences. What was accomplished? What problems were solved? Be specific. Use past tense.",
2719
+ messages: [{ role: "user", content: prompt }]
2720
+ });
2721
+ const block = response.content[0];
2722
+ const text = block && block.type === "text" ? block.text : "";
2723
+ if (text) {
2724
+ writeCache(sha256, text, options.cacheDir);
2725
+ }
2726
+ return text;
2727
+ } catch (err) {
2728
+ const code = err.code;
2729
+ if (code !== "MODULE_NOT_FOUND") {
2730
+ console.warn(`[summarize] API call failed: ${String(err)}`);
2731
+ }
2732
+ return "";
2733
+ }
2734
+ }
2735
+ function buildSummaryPrompt(messages) {
2736
+ const slice = messages.slice(0, 30);
2737
+ const lines = slice.map((m) => {
2738
+ const text = m.text.slice(0, 500);
2739
+ return `${m.role.toUpperCase()}: ${text}`;
2740
+ });
2741
+ return lines.join("\n\n");
2742
+ }
2743
+ function readCache(sha256, cacheDir) {
2744
+ const path = cachePath(sha256, cacheDir);
2745
+ if (!existsSync3(path)) return null;
2746
+ try {
2747
+ const raw = readFileSync3(path, "utf-8");
2748
+ const entry = JSON.parse(raw);
2749
+ return entry.summary ?? null;
2750
+ } catch {
2751
+ return null;
2752
+ }
2753
+ }
2754
+ function writeCache(sha256, summary, cacheDir) {
2755
+ try {
2756
+ mkdirSync2(cacheDir, { recursive: true });
2757
+ const entry = {
2758
+ sha256,
2759
+ summary,
2760
+ generated_at: (/* @__PURE__ */ new Date()).toISOString()
2761
+ };
2762
+ writeFileSync2(cachePath(sha256, cacheDir), JSON.stringify(entry, null, 2) + "\n", "utf-8");
2763
+ } catch {
2764
+ }
2765
+ }
2766
+ function cachePath(sha256, cacheDir) {
2767
+ return join3(cacheDir, `${sha256}.json`);
2768
+ }
2769
+
2770
+ // src/i18n/en.ts
2771
+ var en = {
2772
+ nav: {
2773
+ dashboard: "Dashboard",
2774
+ sessions: "Sessions",
2775
+ projects: "Projects",
2776
+ graph: "Graph",
2777
+ goals: "Goals",
2778
+ todos: "Todos",
2779
+ knowledge: "Knowledge",
2780
+ archive: "Archive",
2781
+ search: "Search"
2782
+ },
2783
+ dashboard: {
2784
+ title: "Memory Dashboard",
2785
+ subtitle: "Activity from {from} to {to}",
2786
+ sessions: "Sessions",
2787
+ messages: "Messages",
2788
+ toolEvents: "Tool Events",
2789
+ activeDays: "Active Days",
2790
+ recentSessions: "Recent Sessions"
2791
+ },
2792
+ sessions: {
2793
+ title: "Sessions",
2794
+ noSessions: "No sessions imported yet.",
2795
+ client: "Client",
2796
+ date: "Date",
2797
+ id: "ID",
2798
+ msgs: "Msgs",
2799
+ tools: "Tools",
2800
+ all: "All"
2801
+ },
2802
+ transcript: {
2803
+ aiSummary: "AI Summary",
2804
+ referencedBy: "Referenced By",
2805
+ messages: "Messages",
2806
+ noMessages: "No messages found.",
2807
+ client: "Client",
2808
+ sessionId: "Session ID",
2809
+ branch: "Branch",
2810
+ workingDir: "Working Dir",
2811
+ sha256: "SHA-256",
2812
+ toolEvents: "Tool Events"
2813
+ },
2814
+ graph: {
2815
+ title: "Knowledge Graph",
2816
+ subtitle: "Connections between sessions and knowledge files",
2817
+ noData: "No graph data available."
2818
+ },
2819
+ projects: {
2820
+ title: "Projects",
2821
+ noProjects: "No projects found.",
2822
+ sessions: "sessions"
2823
+ },
2824
+ search: {
2825
+ title: "Search",
2826
+ placeholder: "Search sessions...",
2827
+ results: "results",
2828
+ noResults: "No results found."
2829
+ },
2830
+ common: {
2831
+ loading: "Loading...",
2832
+ unknown: "unknown"
2833
+ }
2834
+ };
2835
+
2836
+ // src/i18n/zh-CN.ts
2837
+ var zhCN = {
2838
+ nav: {
2839
+ dashboard: "\u4EEA\u8868\u76D8",
2840
+ sessions: "\u4F1A\u8BDD",
2841
+ projects: "\u9879\u76EE",
2842
+ graph: "\u77E5\u8BC6\u56FE\u8C31",
2843
+ goals: "\u76EE\u6807",
2844
+ todos: "\u5F85\u529E",
2845
+ knowledge: "\u77E5\u8BC6\u5E93",
2846
+ archive: "\u5F52\u6863",
2847
+ search: "\u641C\u7D22"
2848
+ },
2849
+ dashboard: {
2850
+ title: "\u8BB0\u5FC6\u4EEA\u8868\u76D8",
2851
+ subtitle: "\u6D3B\u52A8\u65F6\u95F4\uFF1A{from} \u81F3 {to}",
2852
+ sessions: "\u4F1A\u8BDD\u6570",
2853
+ messages: "\u6D88\u606F\u6570",
2854
+ toolEvents: "\u5DE5\u5177\u8C03\u7528",
2855
+ activeDays: "\u6D3B\u8DC3\u5929\u6570",
2856
+ recentSessions: "\u6700\u8FD1\u4F1A\u8BDD"
2857
+ },
2858
+ sessions: {
2859
+ title: "\u4F1A\u8BDD\u5217\u8868",
2860
+ noSessions: "\u6682\u65E0\u5BFC\u5165\u7684\u4F1A\u8BDD\u3002",
2861
+ client: "\u5BA2\u6237\u7AEF",
2862
+ date: "\u65E5\u671F",
2863
+ id: "ID",
2864
+ msgs: "\u6D88\u606F",
2865
+ tools: "\u5DE5\u5177",
2866
+ all: "\u5168\u90E8"
2867
+ },
2868
+ transcript: {
2869
+ aiSummary: "AI \u6458\u8981",
2870
+ referencedBy: "\u88AB\u5F15\u7528",
2871
+ messages: "\u6D88\u606F",
2872
+ noMessages: "\u672A\u627E\u5230\u6D88\u606F\u3002",
2873
+ client: "\u5BA2\u6237\u7AEF",
2874
+ sessionId: "\u4F1A\u8BDD ID",
2875
+ branch: "\u5206\u652F",
2876
+ workingDir: "\u5DE5\u4F5C\u76EE\u5F55",
2877
+ sha256: "SHA-256",
2878
+ toolEvents: "\u5DE5\u5177\u8C03\u7528"
2879
+ },
2880
+ graph: {
2881
+ title: "\u77E5\u8BC6\u56FE\u8C31",
2882
+ subtitle: "\u4F1A\u8BDD\u4E0E\u77E5\u8BC6\u6587\u4EF6\u4E4B\u95F4\u7684\u5173\u8054",
2883
+ noData: "\u6682\u65E0\u56FE\u8C31\u6570\u636E\u3002"
2884
+ },
2885
+ projects: {
2886
+ title: "\u9879\u76EE",
2887
+ noProjects: "\u672A\u627E\u5230\u9879\u76EE\u3002",
2888
+ sessions: "\u4E2A\u4F1A\u8BDD"
2889
+ },
2890
+ search: {
2891
+ title: "\u641C\u7D22",
2892
+ placeholder: "\u641C\u7D22\u4F1A\u8BDD...",
2893
+ results: "\u6761\u7ED3\u679C",
2894
+ noResults: "\u672A\u627E\u5230\u7ED3\u679C\u3002"
2895
+ },
2896
+ common: {
2897
+ loading: "\u52A0\u8F7D\u4E2D...",
2898
+ unknown: "\u672A\u77E5"
2899
+ }
2900
+ };
2901
+
2902
+ // src/i18n/index.ts
2903
+ var LOCALES = {
2904
+ "en": en,
2905
+ "zh-CN": zhCN
2906
+ };
2907
+ function loadLocale(locale) {
2908
+ return LOCALES[locale] ?? en;
2909
+ }
2910
+
2911
+ // src/build.ts
2912
+ var DEFAULT_MODEL = "claude-haiku-4-5-20251001";
2913
+ async function buildReport(options) {
2914
+ const { root, output } = options;
2915
+ const noAi = options.noAi ?? false;
2916
+ const model = options.model ?? DEFAULT_MODEL;
2917
+ const locale = options.locale ?? "en";
2918
+ const ghPagesBranch = options.ghPagesBranch ?? "";
2919
+ const cname = options.cname ?? "";
2920
+ const webhookUrl = options.webhookUrl ?? "";
2921
+ const newSessionIds = options.newSessionIds ?? [];
2922
+ const reportBaseUrl = options.reportBaseUrl ?? "";
2923
+ const cacheDir = join4(root, "Memory", ".report-cache");
2924
+ const t = loadLocale(locale);
2925
+ clearOutputDir(output);
2926
+ mkdirSync3(output, { recursive: true });
2927
+ mkdirSync3(join4(output, "transcripts"), { recursive: true });
2928
+ mkdirSync3(join4(output, "goals"), { recursive: true });
2929
+ mkdirSync3(join4(output, "knowledge"), { recursive: true });
2930
+ mkdirSync3(join4(output, "todos"), { recursive: true });
2931
+ mkdirSync3(join4(output, "archive"), { recursive: true });
2932
+ mkdirSync3(join4(output, "projects"), { recursive: true });
2933
+ mkdirSync3(cacheDir, { recursive: true });
2934
+ ensureGitignore(root, "Memory/07_reports/");
2935
+ const manifests = loadManifests(root);
2936
+ const linkGraph = buildLinkGraph(manifests, root);
2937
+ let toolCounts = {};
2938
+ const summaryOptions = { cacheDir, noAi, model };
2939
+ const snippets = {};
2940
+ const summaries = {};
2941
+ const summaryPromises = [];
2942
+ for (const m of manifests) {
2943
+ const messages = parseCleanMarkdownMessages(m, root);
2944
+ const rawPath = resolveRawPath(m, root);
2945
+ if (rawPath && existsSync4(rawPath)) {
2946
+ try {
2947
+ const toolSummaries = extractToolSummariesFromRaw(rawPath, m.client);
2948
+ const names = extractToolNames(toolSummaries);
2949
+ toolCounts = accumulateToolCounts(toolCounts, names);
2950
+ } catch {
2951
+ }
2952
+ }
2953
+ const firstMsg = messages[0];
2954
+ snippets[m.session_id] = firstMsg ? firstMsg.text.slice(0, 300) : "";
2955
+ const manifest = m;
2956
+ const msgs = messages;
2957
+ summaryPromises.push(
2958
+ (async () => {
2959
+ try {
2960
+ const summary = await getSummary(manifest.raw_sha256, msgs, summaryOptions);
2961
+ summaries[manifest.session_id] = summary;
2962
+ const backlinkIds = linkGraph.backlinks[manifest.session_id] ?? [];
2963
+ const backlinkManifests = backlinkIds.map((id) => manifests.find((x) => x.session_id === id)).filter((x) => x !== void 0);
2964
+ const html = renderTranscript(msgs, manifest, summary, backlinkManifests, t, reportBaseUrl);
2965
+ const outPath = transcriptOutputPath(output, manifest);
2966
+ mkdirSync3(dirname(outPath), { recursive: true });
2967
+ writeFileSync3(outPath, html, "utf-8");
2968
+ } catch (err) {
2969
+ getLogger().warn(`[build] Failed to render ${manifest.session_id}: ${String(err)}`);
2970
+ }
2971
+ })()
2972
+ );
2973
+ }
2974
+ await Promise.all(summaryPromises);
2975
+ const tagOptions = { cacheDir, noAi, model };
2976
+ const tags = {};
2977
+ await Promise.all(
2978
+ manifests.map(async (m) => {
2979
+ const summary = summaries[m.session_id] ?? "";
2980
+ tags[m.session_id] = await getTags(m.raw_sha256, summary, tagOptions);
2981
+ })
2982
+ );
2983
+ const stats = computeStats(manifests, toolCounts);
2984
+ const goalFiles = loadMarkdownFiles(join4(root, "Memory", "01_goals"));
2985
+ const todoFiles = loadMarkdownFiles(join4(root, "Memory", "02_todos"));
2986
+ const knowledgeFiles = loadMarkdownFiles(join4(root, "Memory", "04_knowledge"));
2987
+ const archiveFiles = loadMarkdownFiles(join4(root, "Memory", "05_archive"));
2988
+ writeFileSync3(join4(output, "index.html"), renderDashboard(stats, manifests, t), "utf-8");
2989
+ writeFileSync3(
2990
+ join4(output, "transcripts", "index.html"),
2991
+ renderTranscriptList(manifests, t, summaries, tags),
2992
+ "utf-8"
2993
+ );
2994
+ writeFileSync3(
2995
+ join4(output, "goals", "index.html"),
2996
+ renderGoals(goalFiles, t),
2997
+ "utf-8"
2998
+ );
2999
+ writeFileSync3(
3000
+ join4(output, "knowledge", "index.html"),
3001
+ renderKnowledge(knowledgeFiles, t),
3002
+ "utf-8"
3003
+ );
3004
+ writeFileSync3(
3005
+ join4(output, "todos", "index.html"),
3006
+ renderTodos(todoFiles, t),
3007
+ "utf-8"
3008
+ );
3009
+ writeFileSync3(
3010
+ join4(output, "archive", "index.html"),
3011
+ renderArchive(archiveFiles, t),
3012
+ "utf-8"
3013
+ );
3014
+ writeFileSync3(
3015
+ join4(output, "projects", "index.html"),
3016
+ renderProjects(manifests, t),
3017
+ "utf-8"
3018
+ );
3019
+ writeFileSync3(
3020
+ join4(output, "graph.html"),
3021
+ renderGraph(manifests, knowledgeFiles, linkGraph, t),
3022
+ "utf-8"
3023
+ );
3024
+ const searchIndex = buildSearchIndex(manifests, (m) => snippets[m.session_id] ?? "");
3025
+ writeFileSync3(join4(output, "search.html"), renderSearchPage(searchIndex, t), "utf-8");
3026
+ writeFileSync3(join4(output, "feed.xml"), renderRssFeed(manifests, summaries, reportBaseUrl), "utf-8");
3027
+ if (ghPagesBranch) {
3028
+ try {
3029
+ const { deployGithubPages } = await import("./github-pages-XPQKYB2E.js");
3030
+ await deployGithubPages({ repoRoot: root, outputDir: output, branch: ghPagesBranch, cname });
3031
+ } catch {
3032
+ }
3033
+ } else if (cname) {
3034
+ writeFileSync3(join4(output, "CNAME"), cname + "\n", "utf-8");
3035
+ }
3036
+ if (webhookUrl) {
3037
+ try {
3038
+ const { sendWebhook } = await import("./webhook-6KVPOX3O.js");
3039
+ await sendWebhook({
3040
+ url: webhookUrl,
3041
+ sessionCount: manifests.length,
3042
+ newSessionIds
3043
+ });
3044
+ } catch {
3045
+ }
3046
+ }
3047
+ }
3048
+ function loadManifests(root) {
3049
+ const manifestsDir = join4(root, "Memory", "06_transcripts", "manifests");
3050
+ if (!existsSync4(manifestsDir)) return [];
3051
+ const manifests = [];
3052
+ loadManifestsRecursive(manifestsDir, manifests);
3053
+ return manifests;
3054
+ }
3055
+ function loadManifestsRecursive(dir, out) {
3056
+ let entries;
3057
+ try {
3058
+ entries = readdirSync(dir);
3059
+ } catch {
3060
+ return;
3061
+ }
3062
+ for (const entry of entries) {
3063
+ const fullPath = join4(dir, entry);
3064
+ if (entry.endsWith(".json")) {
3065
+ try {
3066
+ const raw = readFileSync4(fullPath, "utf-8");
3067
+ const parsed = JSON.parse(raw);
3068
+ if (parsed !== null && typeof parsed === "object" && typeof parsed["session_id"] === "string" && typeof parsed["client"] === "string") {
3069
+ out.push(parsed);
3070
+ }
3071
+ } catch {
3072
+ }
3073
+ } else {
3074
+ loadManifestsRecursive(fullPath, out);
3075
+ }
3076
+ }
3077
+ }
3078
+ function parseCleanMarkdownMessages(m, root) {
3079
+ const cleanPath = m.repo_clean_path ? join4(root, m.repo_clean_path) : m.global_clean_path;
3080
+ if (!cleanPath || !existsSync4(cleanPath)) return [];
3081
+ let content;
3082
+ try {
3083
+ content = readFileSync4(cleanPath, "utf-8");
3084
+ } catch {
3085
+ return [];
3086
+ }
3087
+ return parseMessagesFromMarkdown(content);
3088
+ }
3089
+ function parseMessagesFromMarkdown(content) {
3090
+ const messages = [];
3091
+ const msgSection = content.indexOf("\n## Messages");
3092
+ if (msgSection === -1) return [];
3093
+ const body = content.slice(msgSection + 12);
3094
+ const msgRe = /^### \d+\.\s+(\w+)/gm;
3095
+ let match;
3096
+ const starts = [];
3097
+ while ((match = msgRe.exec(body)) !== null) {
3098
+ starts.push({ index: match.index, role: match[1] ?? "unknown" });
3099
+ }
3100
+ for (let i = 0; i < starts.length; i++) {
3101
+ const start = starts[i];
3102
+ if (!start) continue;
3103
+ const nextStart = starts[i + 1];
3104
+ const chunk = nextStart ? body.slice(start.index, nextStart.index) : body.slice(start.index);
3105
+ const lines = chunk.split("\n");
3106
+ let timestamp = "";
3107
+ const textLines = [];
3108
+ let pastHeader = false;
3109
+ for (const line of lines) {
3110
+ if (!pastHeader && line.startsWith("### ")) {
3111
+ pastHeader = true;
3112
+ continue;
3113
+ }
3114
+ if (!pastHeader) continue;
3115
+ const tsMatch = line.match(/^- Timestamp:\s*`([^`]+)`/);
3116
+ if (tsMatch && !timestamp) {
3117
+ timestamp = tsMatch[1] ?? "";
3118
+ continue;
3119
+ }
3120
+ textLines.push(line);
3121
+ }
3122
+ const text = textLines.join("\n").trim();
3123
+ if (text || timestamp) {
3124
+ messages.push({ role: start.role, timestamp, text });
3125
+ }
3126
+ }
3127
+ return messages;
3128
+ }
3129
+ function extractToolSummariesFromRaw(rawPath, _client) {
3130
+ const summaries = [];
3131
+ let content;
3132
+ try {
3133
+ content = readFileSync4(rawPath, "utf-8");
3134
+ } catch {
3135
+ return [];
3136
+ }
3137
+ for (const line of content.split("\n")) {
3138
+ const trimmed = line.trim();
3139
+ if (!trimmed) continue;
3140
+ try {
3141
+ const record = JSON.parse(trimmed);
3142
+ const type = record["type"];
3143
+ if (type === "function_call" || type === "custom_tool_call") {
3144
+ const name = record["name"] ?? type;
3145
+ summaries.push(name);
3146
+ continue;
3147
+ }
3148
+ const payload = record["message"];
3149
+ const content2 = payload?.["content"] ?? record["content"];
3150
+ if (Array.isArray(content2)) {
3151
+ for (const block of content2) {
3152
+ if (block !== null && typeof block === "object" && block["type"] === "tool_use") {
3153
+ const name = block["name"] ?? "tool_use";
3154
+ summaries.push(name);
3155
+ }
3156
+ }
3157
+ }
3158
+ } catch {
3159
+ }
3160
+ }
3161
+ return summaries;
3162
+ }
3163
+ function transcriptOutputPath(output, m) {
3164
+ const cleanPath = m.repo_clean_path || m.global_clean_path || "";
3165
+ const stem = cleanPath ? basename3(cleanPath, ".md") : m.session_id;
3166
+ return join4(output, "transcripts", m.client, `${stem}.html`);
3167
+ }
3168
+ function resolveRawPath(m, root) {
3169
+ if (m.repo_raw_path) {
3170
+ return join4(root, m.repo_raw_path);
3171
+ }
3172
+ if (m.global_raw_path) {
3173
+ return m.global_raw_path;
3174
+ }
3175
+ return null;
3176
+ }
3177
+ function loadMarkdownFiles(dir) {
3178
+ if (!existsSync4(dir)) return [];
3179
+ let entries;
3180
+ try {
3181
+ entries = readdirSync(dir);
3182
+ } catch {
3183
+ return [];
3184
+ }
3185
+ return entries.filter((e) => e.endsWith(".md")).sort().map((filename) => {
3186
+ const fullPath = join4(dir, filename);
3187
+ let content = "";
3188
+ try {
3189
+ content = readFileSync4(fullPath, "utf-8");
3190
+ } catch {
3191
+ }
3192
+ const title = extractMarkdownTitle(content) || basename3(filename, ".md");
3193
+ return { filename, title, content };
3194
+ });
3195
+ }
3196
+ function clearOutputDir(output) {
3197
+ if (!existsSync4(output)) return;
3198
+ let entries;
3199
+ try {
3200
+ entries = readdirSync(output);
3201
+ } catch {
3202
+ return;
3203
+ }
3204
+ for (const entry of entries) {
3205
+ try {
3206
+ rmSync(join4(output, entry), { recursive: true, force: true });
3207
+ } catch {
3208
+ }
3209
+ }
3210
+ }
3211
+ function extractMarkdownTitle(content) {
3212
+ const match = content.match(/^#\s+(.+)$/m);
3213
+ return match?.[1]?.trim() ?? "";
3214
+ }
3215
+ function ensureGitignore(root, entry) {
3216
+ const gitignorePath = join4(root, ".gitignore");
3217
+ let existing = "";
3218
+ if (existsSync4(gitignorePath)) {
3219
+ try {
3220
+ existing = readFileSync4(gitignorePath, "utf-8");
3221
+ } catch {
3222
+ return;
3223
+ }
3224
+ }
3225
+ const lines = existing.replace(/\r\n/g, "\n").split("\n");
3226
+ if (lines.some((l) => l.trim() === entry.trim())) return;
3227
+ const append = existing.endsWith("\n") ? entry + "\n" : "\n" + entry + "\n";
3228
+ try {
3229
+ appendFileSync(gitignorePath, append, "utf-8");
3230
+ } catch {
3231
+ }
3232
+ }
3233
+ export {
3234
+ buildReport
3235
+ };