memor-cli 0.1.0__py3-none-any.whl

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.
Files changed (48) hide show
  1. memor/__init__.py +0 -0
  2. memor/cli.py +463 -0
  3. memor/daemon.py +294 -0
  4. memor/dashboard/__init__.py +0 -0
  5. memor/dashboard/server.py +153 -0
  6. memor/dashboard/static/index.html +688 -0
  7. memor/distill/__init__.py +0 -0
  8. memor/distill/distiller.py +112 -0
  9. memor/distill/extractive.py +161 -0
  10. memor/embed/__init__.py +0 -0
  11. memor/embed/api.py +15 -0
  12. memor/embed/fake.py +16 -0
  13. memor/embed/local.py +16 -0
  14. memor/eval/__init__.py +0 -0
  15. memor/eval/baselines/__init__.py +5 -0
  16. memor/eval/baselines/base.py +15 -0
  17. memor/eval/baselines/claude_mem.py +19 -0
  18. memor/eval/baselines/graphiti.py +25 -0
  19. memor/eval/dataset.py +48 -0
  20. memor/eval/embed_benchmark.py +67 -0
  21. memor/eval/judge.py +137 -0
  22. memor/eval/metrics.py +13 -0
  23. memor/eval/runner.py +78 -0
  24. memor/feedback.py +96 -0
  25. memor/hook_server.py +144 -0
  26. memor/ingest/__init__.py +0 -0
  27. memor/ingest/claude_code.py +135 -0
  28. memor/ingest/documents.py +28 -0
  29. memor/interfaces.py +20 -0
  30. memor/llm/__init__.py +0 -0
  31. memor/llm/anthropic.py +14 -0
  32. memor/llm/base.py +7 -0
  33. memor/llm/openai_compat.py +20 -0
  34. memor/project.py +69 -0
  35. memor/recall.py +115 -0
  36. memor/redact.py +129 -0
  37. memor/retrieve/__init__.py +0 -0
  38. memor/retrieve/retriever.py +78 -0
  39. memor/store/__init__.py +0 -0
  40. memor/store/sqlite_store.py +336 -0
  41. memor/tokencount.py +9 -0
  42. memor/types.py +45 -0
  43. memor_cli-0.1.0.dist-info/METADATA +273 -0
  44. memor_cli-0.1.0.dist-info/RECORD +48 -0
  45. memor_cli-0.1.0.dist-info/WHEEL +5 -0
  46. memor_cli-0.1.0.dist-info/entry_points.txt +2 -0
  47. memor_cli-0.1.0.dist-info/licenses/LICENSE +21 -0
  48. memor_cli-0.1.0.dist-info/top_level.txt +1 -0
@@ -0,0 +1,688 @@
1
+ <!DOCTYPE html>
2
+ <html lang="en" data-theme="dark">
3
+ <head>
4
+ <meta charset="UTF-8">
5
+ <meta name="viewport" content="width=device-width, initial-scale=1.0">
6
+ <title>Memor Dashboard</title>
7
+ <style>
8
+ /* ── Reset & custom properties ─────────────────────────────── */
9
+ *, *::before, *::after { box-sizing: border-box; margin: 0; padding: 0; }
10
+
11
+ :root {
12
+ --bg: #0f1117;
13
+ --surface: #1a1d27;
14
+ --surface2: #22263a;
15
+ --border: #2e3245;
16
+ --text: #e2e8f0;
17
+ --text-muted: #8892a8;
18
+ --accent: #6c8eff;
19
+ --accent2: #a78bfa;
20
+ --ok: #34d399;
21
+ --warn: #fbbf24;
22
+ --neutral: #64748b;
23
+ --hover: #252a40;
24
+ --card-shadow: 0 2px 8px rgba(0,0,0,0.4);
25
+ --radius: 10px;
26
+ --radius-sm: 6px;
27
+ --font: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, 'Helvetica Neue', sans-serif;
28
+ }
29
+
30
+ [data-theme="light"] {
31
+ --bg: #f4f6fc;
32
+ --surface: #ffffff;
33
+ --surface2: #eef0f8;
34
+ --border: #d4d9ef;
35
+ --text: #1a1d27;
36
+ --text-muted: #5a6481;
37
+ --hover: #eef0f8;
38
+ --card-shadow: 0 2px 8px rgba(0,0,0,0.08);
39
+ }
40
+
41
+ /* ── Base ────────────────────────────────────────────────────── */
42
+ body {
43
+ font-family: var(--font);
44
+ background: var(--bg);
45
+ color: var(--text);
46
+ min-height: 100vh;
47
+ font-size: 14px;
48
+ line-height: 1.5;
49
+ }
50
+
51
+ /* ── Utilities ───────────────────────────────────────────────── */
52
+ .muted { color: var(--text-muted); }
53
+ .mono { font-family: 'SF Mono', 'Fira Code', monospace; font-size: 13px; }
54
+ .truncate { overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
55
+ .sr-only { position: absolute; width: 1px; height: 1px; overflow: hidden; clip: rect(0,0,0,0); }
56
+
57
+ /* ── Layout ──────────────────────────────────────────────────── */
58
+ .container { max-width: 1280px; margin: 0 auto; padding: 0 24px; }
59
+
60
+ /* ── Header ──────────────────────────────────────────────────── */
61
+ header {
62
+ background: var(--surface);
63
+ border-bottom: 1px solid var(--border);
64
+ padding: 14px 0;
65
+ position: sticky; top: 0; z-index: 50;
66
+ }
67
+ .header-inner {
68
+ display: flex; align-items: center; gap: 12px;
69
+ }
70
+ .logo {
71
+ font-size: 18px; font-weight: 700; letter-spacing: -0.5px;
72
+ background: linear-gradient(135deg, var(--accent), var(--accent2));
73
+ -webkit-background-clip: text; -webkit-text-fill-color: transparent;
74
+ background-clip: text;
75
+ }
76
+ .header-right { margin-left: auto; display: flex; align-items: center; gap: 12px; }
77
+ .last-updated { font-size: 12px; color: var(--text-muted); }
78
+
79
+ /* theme toggle */
80
+ .theme-btn {
81
+ background: var(--surface2); border: 1px solid var(--border);
82
+ color: var(--text); cursor: pointer; border-radius: var(--radius-sm);
83
+ padding: 5px 10px; font-size: 13px; transition: background 0.15s;
84
+ }
85
+ .theme-btn:hover { background: var(--hover); }
86
+
87
+ /* ── Banner ──────────────────────────────────────────────────── */
88
+ .banner {
89
+ background: linear-gradient(135deg, rgba(108,142,255,0.15), rgba(167,139,250,0.10));
90
+ border: 1px solid rgba(108,142,255,0.35);
91
+ border-radius: var(--radius); padding: 16px 20px; margin: 20px 0;
92
+ display: flex; align-items: flex-start; gap: 14px;
93
+ }
94
+ .banner-icon { font-size: 20px; flex-shrink: 0; margin-top: 2px; }
95
+ .banner h3 { font-size: 14px; font-weight: 600; margin-bottom: 4px; }
96
+ .banner p { font-size: 13px; color: var(--text-muted); }
97
+ .banner code {
98
+ font-family: 'SF Mono', 'Fira Code', monospace;
99
+ background: var(--surface2); border-radius: 4px;
100
+ padding: 1px 5px; font-size: 12px;
101
+ }
102
+ #banner { display: none; }
103
+
104
+ /* ── Section title ───────────────────────────────────────────── */
105
+ .section-title {
106
+ font-size: 13px; font-weight: 600; text-transform: uppercase;
107
+ letter-spacing: 0.06em; color: var(--text-muted); margin-bottom: 12px;
108
+ }
109
+ section { margin-top: 28px; }
110
+
111
+ /* ── Hero metric cards ────────────────────────────────────────── */
112
+ .metric-grid {
113
+ display: grid;
114
+ grid-template-columns: repeat(auto-fit, minmax(200px, 1fr));
115
+ gap: 14px;
116
+ }
117
+ .metric-card {
118
+ background: var(--surface); border: 1px solid var(--border);
119
+ border-radius: var(--radius); padding: 18px 20px;
120
+ box-shadow: var(--card-shadow); transition: border-color 0.15s;
121
+ }
122
+ .metric-card:hover { border-color: var(--accent); }
123
+ .metric-label {
124
+ font-size: 12px; font-weight: 500; color: var(--text-muted);
125
+ text-transform: uppercase; letter-spacing: 0.05em;
126
+ }
127
+ .metric-value {
128
+ font-size: 32px; font-weight: 700; letter-spacing: -1px;
129
+ color: var(--text); line-height: 1.2; margin: 6px 0 4px;
130
+ }
131
+ .metric-sub { font-size: 12px; color: var(--text-muted); }
132
+
133
+ /* ── Savings bars ────────────────────────────────────────────── */
134
+ .savings-grid {
135
+ display: grid; grid-template-columns: repeat(auto-fill, minmax(280px, 1fr));
136
+ gap: 14px;
137
+ }
138
+ .savings-card {
139
+ background: var(--surface); border: 1px solid var(--border);
140
+ border-radius: var(--radius); padding: 16px 18px;
141
+ box-shadow: var(--card-shadow);
142
+ }
143
+ .savings-header { display: flex; justify-content: space-between; margin-bottom: 10px; }
144
+ .savings-project { font-weight: 600; font-size: 14px; }
145
+ .savings-pct {
146
+ font-weight: 700; font-size: 14px;
147
+ color: var(--ok);
148
+ }
149
+ .bar-track {
150
+ background: var(--surface2); border-radius: 99px; height: 6px; overflow: hidden;
151
+ }
152
+ .bar-fill {
153
+ height: 100%; border-radius: 99px;
154
+ background: linear-gradient(90deg, var(--accent), var(--ok));
155
+ transition: width 0.6s ease;
156
+ }
157
+ .savings-meta {
158
+ display: flex; justify-content: space-between;
159
+ font-size: 12px; color: var(--text-muted); margin-top: 8px;
160
+ }
161
+
162
+ /* ── Tables ──────────────────────────────────────────────────── */
163
+ .table-wrap {
164
+ background: var(--surface); border: 1px solid var(--border);
165
+ border-radius: var(--radius); overflow: hidden;
166
+ box-shadow: var(--card-shadow);
167
+ }
168
+ table { width: 100%; border-collapse: collapse; }
169
+ thead { background: var(--surface2); }
170
+ th {
171
+ padding: 11px 14px; text-align: left; font-size: 12px;
172
+ font-weight: 600; color: var(--text-muted);
173
+ text-transform: uppercase; letter-spacing: 0.05em;
174
+ user-select: none;
175
+ }
176
+ th.sortable { cursor: pointer; }
177
+ th.sortable:hover { color: var(--text); }
178
+ th .sort-icon { display: inline-block; margin-left: 4px; opacity: 0.4; font-size: 10px; }
179
+ th.sort-asc .sort-icon, th.sort-desc .sort-icon { opacity: 1; color: var(--accent); }
180
+ td {
181
+ padding: 10px 14px; border-top: 1px solid var(--border);
182
+ font-size: 13px; color: var(--text);
183
+ }
184
+ tr:hover td { background: var(--hover); }
185
+ tr.selected td { background: rgba(108,142,255,0.12); }
186
+
187
+ /* clickable project rows */
188
+ .projects-body tr { cursor: pointer; }
189
+ .projects-body tr:hover td { background: var(--hover); }
190
+ .projects-body tr.selected td { background: rgba(108,142,255,0.12); }
191
+
192
+ /* ── Status badges ────────────────────────────────────────────── */
193
+ .badge {
194
+ display: inline-block; border-radius: 99px; padding: 2px 8px;
195
+ font-size: 11px; font-weight: 600; letter-spacing: 0.04em;
196
+ }
197
+ .badge-ok { background: rgba(52,211,153,0.18); color: var(--ok); }
198
+ .badge-extractive { background: rgba(251,191,36,0.18); color: var(--warn); }
199
+ .badge-no_hits { background: rgba(100,116,139,0.2); color: var(--neutral); }
200
+
201
+ /* ── Filter pill ─────────────────────────────────────────────── */
202
+ .filter-bar { display: flex; align-items: center; gap: 8px; margin-bottom: 10px; min-height: 28px; }
203
+ .filter-pill {
204
+ background: rgba(108,142,255,0.15); border: 1px solid rgba(108,142,255,0.4);
205
+ border-radius: 99px; padding: 3px 10px; font-size: 12px; font-weight: 500;
206
+ color: var(--accent); display: flex; align-items: center; gap: 6px;
207
+ }
208
+ .filter-pill button {
209
+ background: none; border: none; color: var(--accent); cursor: pointer;
210
+ font-size: 14px; line-height: 1; padding: 0;
211
+ }
212
+
213
+ /* ── Disclaimer note ─────────────────────────────────────────── */
214
+ .note {
215
+ font-size: 12px; color: var(--text-muted); margin-top: 12px;
216
+ padding: 10px 14px; background: var(--surface);
217
+ border-left: 3px solid var(--border); border-radius: 0 var(--radius-sm) var(--radius-sm) 0;
218
+ }
219
+
220
+ /* ── Empty state ─────────────────────────────────────────────── */
221
+ .empty-state {
222
+ text-align: center; padding: 40px 20px;
223
+ color: var(--text-muted); font-size: 13px;
224
+ }
225
+
226
+ /* ── Footer ──────────────────────────────────────────────────── */
227
+ footer {
228
+ margin-top: 40px; padding: 20px 0;
229
+ border-top: 1px solid var(--border);
230
+ text-align: center; font-size: 12px; color: var(--text-muted);
231
+ }
232
+ </style>
233
+ </head>
234
+ <body>
235
+
236
+ <header>
237
+ <div class="container header-inner">
238
+ <span class="logo">memor</span>
239
+ <span class="muted" style="font-size:12px;">context intelligence</span>
240
+ <div class="header-right">
241
+ <span class="last-updated" id="last-updated">–</span>
242
+ <button class="theme-btn" id="theme-toggle" title="Toggle theme">☀ Light</button>
243
+ </div>
244
+ </div>
245
+ </header>
246
+
247
+ <main class="container">
248
+
249
+ <!-- Onboarding banner (hidden until health check says not full) -->
250
+ <div id="banner" class="banner" role="alert">
251
+ <div class="banner-icon">💡</div>
252
+ <div>
253
+ <h3>Getting started with Memor</h3>
254
+ <p id="banner-msg">No artifacts ingested yet. Run <code>memor ingest &lt;path&gt;</code> to index a project, then add the Claude Code hook so recalls are logged automatically.</p>
255
+ </div>
256
+ </div>
257
+
258
+ <!-- ── Hero metrics ───────────────────────────────────────────── -->
259
+ <section aria-label="Hero metrics">
260
+ <div class="section-title">Memory Bank</div>
261
+ <div class="metric-grid" id="metric-grid">
262
+ <div class="metric-card">
263
+ <div class="metric-label">Session Chunks</div>
264
+ <div class="metric-value" id="m-chunks">–</div>
265
+ <div class="metric-sub">ingested from transcripts</div>
266
+ </div>
267
+ <div class="metric-card">
268
+ <div class="metric-label">Memories</div>
269
+ <div class="metric-value" id="m-memories">–</div>
270
+ <div class="metric-sub">distilled insights</div>
271
+ </div>
272
+ <div class="metric-card">
273
+ <div class="metric-label">Projects</div>
274
+ <div class="metric-value" id="m-projects">–</div>
275
+ <div class="metric-sub">tracked projects</div>
276
+ </div>
277
+ <div class="metric-card">
278
+ <div class="metric-label">Total Recalls</div>
279
+ <div class="metric-value" id="m-recalls">–</div>
280
+ <div class="metric-sub" id="m-recalls-sub">hook recall events</div>
281
+ </div>
282
+ </div>
283
+ </section>
284
+
285
+ <!-- ── Context efficiency ──────────────────────────────────────── -->
286
+ <section aria-label="Context efficiency">
287
+ <div class="section-title">Context Efficiency</div>
288
+ <div class="metric-grid" id="efficiency-metrics">
289
+ <div class="metric-card">
290
+ <div class="metric-label">Context Overhead</div>
291
+ <div class="metric-value" id="e-overhead">–</div>
292
+ <div class="metric-sub">avg % of context window used by Memor per session</div>
293
+ </div>
294
+ <div class="metric-card">
295
+ <div class="metric-label">Recall Precision</div>
296
+ <div class="metric-value" id="e-precision">–</div>
297
+ <div class="metric-sub">% of prompts that got relevant context</div>
298
+ </div>
299
+ <div class="metric-card">
300
+ <div class="metric-label">Avg Quality</div>
301
+ <div class="metric-value" id="e-quality">–</div>
302
+ <div class="metric-sub">mean relevance score when hits found</div>
303
+ </div>
304
+ <div class="metric-card">
305
+ <div class="metric-label">Sessions Tracked</div>
306
+ <div class="metric-value" id="e-sessions">–</div>
307
+ <div class="metric-sub" id="e-sessions-sub">with recall data</div>
308
+ </div>
309
+ </div>
310
+ <div class="table-wrap" style="margin-top: 14px;">
311
+ <table>
312
+ <thead>
313
+ <tr>
314
+ <th>Session</th>
315
+ <th>Prompts</th>
316
+ <th>Tokens Injected</th>
317
+ <th>Overhead %</th>
318
+ <th>Precision</th>
319
+ <th>Quality</th>
320
+ <th>Last Active</th>
321
+ </tr>
322
+ </thead>
323
+ <tbody id="efficiency-body"></tbody>
324
+ </table>
325
+ </div>
326
+ <p class="note">
327
+ Overhead = total tokens Memor injected as a % of the 200K context window. Lower is better.
328
+ Precision = % of prompts where relevant memories were found. Quality = avg similarity score.
329
+ Good efficiency means low overhead with high precision — Memor adds value without bloating context.
330
+ </p>
331
+ </section>
332
+
333
+ <!-- ── Project breakdown ─────────────────────────────────────── -->
334
+ <section aria-label="Project breakdown">
335
+ <div class="section-title">Project Breakdown</div>
336
+ <div class="table-wrap">
337
+ <table>
338
+ <thead>
339
+ <tr>
340
+ <th class="sortable" data-col="project" data-type="str">Project <span class="sort-icon">↕</span></th>
341
+ <th class="sortable" data-col="artifacts" data-type="num"><span id="th-col1">Chunks</span> <span class="sort-icon">↕</span></th>
342
+ <th class="sortable" data-col="memories" data-type="num"><span id="th-col2">Memories</span> <span class="sort-icon">↕</span></th>
343
+ <th class="sortable" data-col="total_tokens" data-type="num"><span id="th-col3">Tokens</span> <span class="sort-icon">↕</span></th>
344
+ <th id="th-col4">Status</th>
345
+ <th id="th-col5"></th>
346
+ <th id="th-col6"></th>
347
+ </tr>
348
+ </thead>
349
+ <tbody id="projects-body" class="projects-body"></tbody>
350
+ </table>
351
+ </div>
352
+ </section>
353
+
354
+ <!-- ── Recent recalls ────────────────────────────────────────── -->
355
+ <section aria-label="Recent recalls">
356
+ <div class="section-title">Recent Recalls</div>
357
+ <div class="filter-bar" id="filter-bar"></div>
358
+ <div class="table-wrap">
359
+ <table>
360
+ <thead>
361
+ <tr>
362
+ <th>Time</th>
363
+ <th>Project</th>
364
+ <th>Query</th>
365
+ <th>Hits</th>
366
+ <th>Score</th>
367
+ <th>Tokens</th>
368
+ <th>Latency</th>
369
+ <th>Status</th>
370
+ </tr>
371
+ </thead>
372
+ <tbody id="recalls-body"></tbody>
373
+ </table>
374
+ </div>
375
+ </section>
376
+
377
+ </main>
378
+
379
+ <footer>
380
+ <div class="container">
381
+ Memor Dashboard &mdash; metrics refresh every 30 s
382
+ </div>
383
+ </footer>
384
+
385
+ <script>
386
+ (function () {
387
+ 'use strict';
388
+
389
+ /* ── theme ─────────────────────────────────────────────── */
390
+ const html = document.documentElement;
391
+ const themeBtn = document.getElementById('theme-toggle');
392
+ let theme = localStorage.getItem('memor-theme') || 'dark';
393
+
394
+ function applyTheme(t) {
395
+ html.setAttribute('data-theme', t);
396
+ themeBtn.textContent = t === 'dark' ? '☀ Light' : '🌙 Dark';
397
+ localStorage.setItem('memor-theme', t);
398
+ }
399
+ applyTheme(theme);
400
+ themeBtn.addEventListener('click', () => {
401
+ theme = theme === 'dark' ? 'light' : 'dark';
402
+ applyTheme(theme);
403
+ });
404
+
405
+ /* ── state ─────────────────────────────────────────────── */
406
+ let projectFilter = null;
407
+ let sortCol = 'artifacts';
408
+ let sortDir = 'desc';
409
+ let projectsData = [];
410
+
411
+ /* ── helpers ───────────────────────────────────────────── */
412
+ function fmt(n) {
413
+ if (n == null) return '–';
414
+ if (n >= 1e6) return (n / 1e6).toFixed(1) + 'M';
415
+ if (n >= 1e3) return (n / 1e3).toFixed(1) + 'K';
416
+ return String(n);
417
+ }
418
+ function fmtPct(n) { return n == null ? '–' : n.toFixed(1) + '%'; }
419
+ function fmtMs(n) { return n == null ? '–' : n.toFixed(0) + ' ms'; }
420
+ function fmtScore(n) { return n == null ? '–' : n.toFixed(3); }
421
+
422
+ function relativeTime(ts) {
423
+ if (!ts) return '–';
424
+ const d = new Date(typeof ts === 'number' ? ts * 1000 : ts);
425
+ const diff = (Date.now() - d) / 1000;
426
+ if (diff < 60) return Math.round(diff) + 's ago';
427
+ if (diff < 3600) return Math.round(diff / 60) + 'm ago';
428
+ if (diff < 86400)return Math.round(diff / 3600) + 'h ago';
429
+ return d.toLocaleDateString();
430
+ }
431
+
432
+ function badge(status) {
433
+ const map = {
434
+ ok: ['badge-ok', 'ok'],
435
+ extractive_only: ['badge-extractive', 'extractive'],
436
+ no_hits: ['badge-no_hits', 'no hits'],
437
+ };
438
+ const [cls, label] = map[status] || ['badge-no_hits', status];
439
+ return `<span class="badge ${cls}">${label}</span>`;
440
+ }
441
+
442
+ function el(tag, attrs, inner) {
443
+ const e = document.createElement(tag);
444
+ for (const [k, v] of Object.entries(attrs || {})) e.setAttribute(k, v);
445
+ if (inner != null) e.innerHTML = inner;
446
+ return e;
447
+ }
448
+
449
+ /* ── fetch helpers ─────────────────────────────────────── */
450
+ async function api(path) {
451
+ const r = await fetch(path);
452
+ if (!r.ok) throw new Error(`${r.status} ${r.statusText}`);
453
+ return r.json();
454
+ }
455
+
456
+ /* ── render summary ────────────────────────────────────── */
457
+ function renderSummary(d) {
458
+ const ing = d.ingestion || {};
459
+ const chunks = (ing.session_chunk || {}).count || 0;
460
+ const memories = (ing.memory || {}).count || 0;
461
+ document.getElementById('m-chunks').textContent = fmt(chunks);
462
+ document.getElementById('m-memories').textContent = fmt(memories);
463
+ document.getElementById('m-projects').textContent = fmt(d.project_count || 0);
464
+ document.getElementById('m-recalls').textContent = fmt(d.total_recalls);
465
+ if (d.total_recalls > 0) {
466
+ document.getElementById('m-recalls-sub').textContent =
467
+ fmtPct(d.hit_rate * 100) + ' hit rate · ' + fmtMs(d.avg_latency_ms) + ' avg';
468
+ }
469
+ }
470
+
471
+ /* ── render efficiency ──────────────────────────────────── */
472
+ function renderEfficiency(data) {
473
+ document.getElementById('e-overhead').textContent = data.avg_overhead_pct + '%';
474
+ document.getElementById('e-precision').textContent = fmtPct(data.precision * 100);
475
+ document.getElementById('e-quality').textContent = fmtScore(data.avg_quality);
476
+ document.getElementById('e-sessions').textContent = fmt(data.total_sessions);
477
+ if (data.total_sessions > 0) {
478
+ document.getElementById('e-sessions-sub').textContent =
479
+ fmt(data.total_tokens_injected) + ' total tokens injected';
480
+ }
481
+
482
+ const tbody = document.getElementById('efficiency-body');
483
+ tbody.innerHTML = '';
484
+ if (!data.sessions || !data.sessions.length) {
485
+ tbody.innerHTML = '<tr><td colspan="7" class="empty-state">No session data yet.</td></tr>';
486
+ return;
487
+ }
488
+ data.sessions.forEach(s => {
489
+ const overheadColor = s.overhead_pct < 2 ? 'var(--ok)' : s.overhead_pct < 5 ? 'var(--warn)' : 'var(--text)';
490
+ const precColor = s.precision > 0.7 ? 'var(--ok)' : s.precision > 0.3 ? 'var(--warn)' : 'var(--neutral)';
491
+ const shortId = s.session_id.length > 12 ? s.session_id.slice(0, 8) + '…' : s.session_id;
492
+ const tr = el('tr', {});
493
+ tr.innerHTML = `
494
+ <td class="mono truncate" style="max-width:120px" title="${esc(s.session_id)}">${esc(shortId)}</td>
495
+ <td>${s.prompts}</td>
496
+ <td>${fmt(s.tokens_injected)}</td>
497
+ <td style="color:${overheadColor}">${s.overhead_pct}%</td>
498
+ <td style="color:${precColor}">${fmtPct(s.precision * 100)}</td>
499
+ <td>${fmtScore(s.avg_quality)}</td>
500
+ <td class="mono muted">${relativeTime(s.last_recall)}</td>
501
+ `;
502
+ tbody.appendChild(tr);
503
+ });
504
+ }
505
+
506
+ /* ── render projects table ─────────────────────────────── */
507
+ function renderProjects(data) {
508
+ projectsData = data;
509
+ // apply sort
510
+ const sorted = [...data].sort((a, b) => {
511
+ let va = a[sortCol], vb = b[sortCol];
512
+ if (typeof va === 'string') va = va.toLowerCase(), vb = (vb||'').toLowerCase();
513
+ if (va < vb) return sortDir === 'asc' ? -1 : 1;
514
+ if (va > vb) return sortDir === 'asc' ? 1 : -1;
515
+ return 0;
516
+ });
517
+
518
+ // update sort header icons
519
+ document.querySelectorAll('th.sortable').forEach(th => {
520
+ const icon = th.querySelector('.sort-icon');
521
+ if (th.dataset.col === sortCol) {
522
+ th.classList.add(sortDir === 'asc' ? 'sort-asc' : 'sort-desc');
523
+ th.classList.remove(sortDir === 'asc' ? 'sort-desc' : 'sort-asc');
524
+ icon.textContent = sortDir === 'asc' ? '↑' : '↓';
525
+ } else {
526
+ th.classList.remove('sort-asc', 'sort-desc');
527
+ icon.textContent = '↕';
528
+ }
529
+ });
530
+
531
+ const tbody = document.getElementById('projects-body');
532
+ tbody.innerHTML = '';
533
+ if (!sorted.length) {
534
+ tbody.innerHTML = '<tr><td colspan="7" class="empty-state">No projects yet.</td></tr>';
535
+ return;
536
+ }
537
+ const isRecallMode = sorted.length > 0 && sorted[0].recalls !== undefined;
538
+ if (isRecallMode) {
539
+ document.getElementById('th-col1').textContent = 'Recalls';
540
+ document.getElementById('th-col2').textContent = 'Tokens';
541
+ document.getElementById('th-col3').textContent = 'Avg Score';
542
+ document.getElementById('th-col4').textContent = 'OK';
543
+ document.getElementById('th-col5').textContent = 'No Hits';
544
+ document.getElementById('th-col6').textContent = 'Extractive';
545
+ }
546
+
547
+ sorted.forEach(row => {
548
+ const tr = el('tr', {});
549
+ const isSelected = projectFilter === row.project;
550
+ if (isSelected) tr.classList.add('selected');
551
+ if (isRecallMode) {
552
+ tr.innerHTML = `
553
+ <td class="truncate mono" title="${esc(row.project)}">${esc(row.project)}</td>
554
+ <td>${fmt(row.recalls)}</td>
555
+ <td>${fmt(row.tokens)}</td>
556
+ <td>${fmtScore(row.avg_score)}</td>
557
+ <td style="color:var(--ok)">${row.ok_count ?? 0}</td>
558
+ <td style="color:var(--neutral)">${row.no_hits_count ?? 0}</td>
559
+ <td style="color:var(--warn)">${row.extractive_count ?? 0}</td>
560
+ `;
561
+ } else {
562
+ const hasMemories = (row.memories || 0) > 0;
563
+ const status = hasMemories ? 'extractive' : 'ingested';
564
+ const statusColor = hasMemories ? 'var(--ok)' : 'var(--warn)';
565
+ tr.innerHTML = `
566
+ <td class="truncate mono" title="${esc(row.project)}">${esc(row.project)}</td>
567
+ <td>${fmt(row.chunks || 0)}</td>
568
+ <td>${fmt(row.memories || 0)}</td>
569
+ <td>${fmt(row.total_tokens || 0)}</td>
570
+ <td style="color:${statusColor}">${status}</td>
571
+ <td></td>
572
+ <td></td>
573
+ `;
574
+ }
575
+ tr.addEventListener('click', () => {
576
+ if (projectFilter === row.project) {
577
+ projectFilter = null;
578
+ } else {
579
+ projectFilter = row.project;
580
+ }
581
+ loadRecalls();
582
+ renderProjects(projectsData);
583
+ });
584
+ tbody.appendChild(tr);
585
+ });
586
+ }
587
+
588
+ /* ── render recalls table ──────────────────────────────── */
589
+ function renderRecalls(data) {
590
+ const bar = document.getElementById('filter-bar');
591
+ bar.innerHTML = '';
592
+ if (projectFilter) {
593
+ const pill = el('div', { class: 'filter-pill' },
594
+ `Filtered: ${esc(projectFilter)} <button title="Clear filter">×</button>`);
595
+ pill.querySelector('button').addEventListener('click', () => {
596
+ projectFilter = null;
597
+ loadRecalls();
598
+ renderProjects(projectsData);
599
+ });
600
+ bar.appendChild(pill);
601
+ }
602
+
603
+ const tbody = document.getElementById('recalls-body');
604
+ tbody.innerHTML = '';
605
+ if (!data.length) {
606
+ tbody.innerHTML = '<tr><td colspan="8" class="empty-state">No recall events yet.</td></tr>';
607
+ return;
608
+ }
609
+ data.forEach(row => {
610
+ const tr = el('tr', {});
611
+ tr.innerHTML = `
612
+ <td class="mono muted" style="white-space:nowrap">${relativeTime(row.timestamp)}</td>
613
+ <td class="truncate mono" style="max-width:120px" title="${esc(row.project)}">${esc(row.project)}</td>
614
+ <td class="truncate" style="max-width:220px" title="${esc(row.query_preview)}">${esc(row.query_preview)}</td>
615
+ <td>${row.hits_count ?? 0}</td>
616
+ <td>${fmtScore(row.top_score)}</td>
617
+ <td>${fmt(row.tokens_injected)}</td>
618
+ <td class="mono">${fmtMs(row.latency_ms)}</td>
619
+ <td>${badge(row.status)}</td>
620
+ `;
621
+ tbody.appendChild(tr);
622
+ });
623
+ }
624
+
625
+ /* ── render health / banner ────────────────────────────── */
626
+ function renderHealth(h) {
627
+ const banner = document.getElementById('banner');
628
+ const msg = document.getElementById('banner-msg');
629
+ const status = h.onboarding_status;
630
+ if (status === 'full') {
631
+ banner.style.display = 'none';
632
+ } else {
633
+ banner.style.display = 'flex';
634
+ if (status === 'no_artifacts') {
635
+ msg.innerHTML = 'No artifacts ingested yet. Run <code>memor ingest &lt;path&gt;</code> to index a project, then add the Claude Code hook so recalls are logged automatically.';
636
+ } else if (status === 'no_recalls') {
637
+ msg.innerHTML = 'Artifacts are indexed but no recalls yet. Add the hook: <code>memor install-hook</code> — Memor will start injecting context automatically on each Claude call.';
638
+ } else {
639
+ msg.innerHTML = `Status: <strong>${esc(status)}</strong>. Run <code>memor --help</code> for setup guidance.`;
640
+ }
641
+ }
642
+ }
643
+
644
+ /* ── load data ─────────────────────────────────────────── */
645
+ async function loadSummary() { try { renderSummary(await api('/api/summary')); } catch(e) { console.warn('summary', e); } }
646
+ async function loadProjects() { try { renderProjects(await api('/api/projects')); } catch(e) { console.warn('projects', e); } }
647
+ async function loadEfficiency() { try { renderEfficiency(await api('/api/efficiency')); } catch(e) { console.warn('efficiency', e); } }
648
+ async function loadHealth() { try { renderHealth(await api('/api/health')); } catch(e) { console.warn('health', e); } }
649
+ async function loadRecalls() {
650
+ try {
651
+ const url = '/api/recalls?limit=50' + (projectFilter ? '&project=' + encodeURIComponent(projectFilter) : '');
652
+ renderRecalls(await api(url));
653
+ } catch(e) { console.warn('recalls', e); }
654
+ }
655
+
656
+ async function refresh() {
657
+ await Promise.allSettled([loadSummary(), loadProjects(), loadEfficiency(), loadHealth(), loadRecalls()]);
658
+ document.getElementById('last-updated').textContent = 'Updated ' + new Date().toLocaleTimeString();
659
+ }
660
+
661
+ /* ── sort headers ──────────────────────────────────────── */
662
+ document.querySelectorAll('th.sortable').forEach(th => {
663
+ th.addEventListener('click', () => {
664
+ if (sortCol === th.dataset.col) {
665
+ sortDir = sortDir === 'asc' ? 'desc' : 'asc';
666
+ } else {
667
+ sortCol = th.dataset.col;
668
+ sortDir = th.dataset.type === 'str' ? 'asc' : 'desc';
669
+ }
670
+ renderProjects(projectsData);
671
+ });
672
+ });
673
+
674
+ /* ── escape helper ─────────────────────────────────────── */
675
+ function esc(s) {
676
+ if (s == null) return '';
677
+ return String(s)
678
+ .replace(/&/g, '&amp;').replace(/</g, '&lt;').replace(/>/g, '&gt;')
679
+ .replace(/"/g, '&quot;').replace(/'/g, '&#39;');
680
+ }
681
+
682
+ /* ── boot ──────────────────────────────────────────────── */
683
+ refresh();
684
+ setInterval(refresh, 30000);
685
+ })();
686
+ </script>
687
+ </body>
688
+ </html>