persisted-memory 1.0.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.
Files changed (56) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +281 -0
  3. package/dist/cli/generate-summary.d.ts +1 -0
  4. package/dist/cli/generate-summary.js +13 -0
  5. package/dist/cli/generate-summary.js.map +1 -0
  6. package/dist/cli/viewer.d.ts +1 -0
  7. package/dist/cli/viewer.js +4 -0
  8. package/dist/cli/viewer.js.map +1 -0
  9. package/dist/embeddings/ollama.d.ts +3 -0
  10. package/dist/embeddings/ollama.js +63 -0
  11. package/dist/embeddings/ollama.js.map +1 -0
  12. package/dist/index.d.ts +2 -0
  13. package/dist/index.js +25 -0
  14. package/dist/index.js.map +1 -0
  15. package/dist/search/hybrid.d.ts +8 -0
  16. package/dist/search/hybrid.js +54 -0
  17. package/dist/search/hybrid.js.map +1 -0
  18. package/dist/server.d.ts +2 -0
  19. package/dist/server.js +399 -0
  20. package/dist/server.js.map +1 -0
  21. package/dist/storage/knowledge-graph.d.ts +32 -0
  22. package/dist/storage/knowledge-graph.js +259 -0
  23. package/dist/storage/knowledge-graph.js.map +1 -0
  24. package/dist/storage/lance-store.d.ts +21 -0
  25. package/dist/storage/lance-store.js +288 -0
  26. package/dist/storage/lance-store.js.map +1 -0
  27. package/dist/storage/markdown-store.d.ts +7 -0
  28. package/dist/storage/markdown-store.js +63 -0
  29. package/dist/storage/markdown-store.js.map +1 -0
  30. package/dist/storage/types.d.ts +19 -0
  31. package/dist/storage/types.js +13 -0
  32. package/dist/storage/types.js.map +1 -0
  33. package/dist/utils/chunking.d.ts +1 -0
  34. package/dist/utils/chunking.js +55 -0
  35. package/dist/utils/chunking.js.map +1 -0
  36. package/dist/utils/privacy.d.ts +13 -0
  37. package/dist/utils/privacy.js +23 -0
  38. package/dist/utils/privacy.js.map +1 -0
  39. package/dist/utils/project.d.ts +3 -0
  40. package/dist/utils/project.js +11 -0
  41. package/dist/utils/project.js.map +1 -0
  42. package/dist/utils/summarize.d.ts +12 -0
  43. package/dist/utils/summarize.js +123 -0
  44. package/dist/utils/summarize.js.map +1 -0
  45. package/dist/viewer/index.html +328 -0
  46. package/dist/viewer/server.d.ts +1 -0
  47. package/dist/viewer/server.js +203 -0
  48. package/dist/viewer/server.js.map +1 -0
  49. package/hooks/on-post-tool.sh +17 -0
  50. package/hooks/on-pre-compact.sh +20 -0
  51. package/hooks/on-session-end.sh +46 -0
  52. package/hooks/on-session-start.sh +14 -0
  53. package/hooks/on-stop.sh +6 -0
  54. package/package.json +60 -0
  55. package/scripts/install.sh +125 -0
  56. package/scripts/uninstall.sh +59 -0
@@ -0,0 +1,328 @@
1
+ <!DOCTYPE html>
2
+ <html lang="en">
3
+ <head>
4
+ <meta charset="UTF-8">
5
+ <meta name="viewport" content="width=device-width, initial-scale=1.0">
6
+ <title>Memory Viewer</title>
7
+ <style>
8
+ :root {
9
+ --bg: #0f0f23;
10
+ --card: #1a1a2e;
11
+ --border: #2d2d44;
12
+ --accent: #7c3aed;
13
+ --accent-hover: #6d28d9;
14
+ --text: #e2e8f0;
15
+ --text-muted: #94a3b8;
16
+ --font-mono: "SF Mono", "Fira Code", "Cascadia Code", Consolas, monospace;
17
+ --font-sans: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, sans-serif;
18
+ --badge-decision: #3b82f6;
19
+ --badge-architecture: #8b5cf6;
20
+ --badge-code-change: #06b6d4;
21
+ --badge-error-fix: #ef4444;
22
+ --badge-pattern: #f59e0b;
23
+ --badge-context: #10b981;
24
+ --badge-preference: #ec4899;
25
+ --imp-critical: #ef4444;
26
+ --imp-high: #f97316;
27
+ --imp-medium: #eab308;
28
+ --imp-low: #64748b;
29
+ }
30
+ *, *::before, *::after { box-sizing: border-box; margin: 0; padding: 0; }
31
+ body { font-family: var(--font-sans); background: var(--bg); color: var(--text); min-height: 100vh; }
32
+ .container { max-width: 960px; margin: 0 auto; padding: 24px 16px; }
33
+ header { margin-bottom: 24px; }
34
+ header h1 { font-size: 1.5rem; font-weight: 600; display: flex; align-items: center; gap: 10px; }
35
+ header h1 span { color: var(--accent); }
36
+ .stats-bar { display: flex; gap: 16px; flex-wrap: wrap; margin: 12px 0; font-size: 0.8rem; color: var(--text-muted); }
37
+ .stats-bar .stat { background: var(--card); border: 1px solid var(--border); border-radius: 6px; padding: 4px 10px; }
38
+ .stats-bar .stat strong { color: var(--text); }
39
+ .search-box { position: relative; margin-bottom: 16px; }
40
+ .search-box input {
41
+ width: 100%; padding: 12px 16px 12px 42px; background: var(--card); border: 1px solid var(--border);
42
+ border-radius: 8px; color: var(--text); font-size: 0.95rem; outline: none; transition: border-color 0.2s;
43
+ }
44
+ .search-box input:focus { border-color: var(--accent); }
45
+ .search-box input::placeholder { color: var(--text-muted); }
46
+ .search-box svg { position: absolute; left: 14px; top: 50%; transform: translateY(-50%); color: var(--text-muted); }
47
+ .filters { display: flex; flex-wrap: wrap; gap: 8px; margin-bottom: 20px; }
48
+ .chip {
49
+ padding: 5px 12px; border-radius: 16px; font-size: 0.75rem; cursor: pointer; border: 1px solid var(--border);
50
+ background: var(--card); color: var(--text-muted); transition: all 0.2s; user-select: none;
51
+ }
52
+ .chip:hover { border-color: var(--accent); color: var(--text); }
53
+ .chip.active { background: var(--accent); border-color: var(--accent); color: #fff; }
54
+ .chip-group-label { font-size: 0.7rem; color: var(--text-muted); align-self: center; text-transform: uppercase; letter-spacing: 0.05em; }
55
+ .cards { display: flex; flex-direction: column; gap: 12px; }
56
+ .card {
57
+ background: var(--card); border: 1px solid var(--border); border-radius: 10px; padding: 16px;
58
+ cursor: pointer; transition: border-color 0.2s, transform 0.15s; animation: fadeIn 0.3s ease;
59
+ }
60
+ .card:hover { border-color: var(--accent); transform: translateY(-1px); }
61
+ .card-header { display: flex; align-items: center; gap: 8px; margin-bottom: 8px; flex-wrap: wrap; }
62
+ .badge {
63
+ font-size: 0.7rem; padding: 2px 8px; border-radius: 4px; font-weight: 600; text-transform: uppercase;
64
+ letter-spacing: 0.03em; color: #fff;
65
+ }
66
+ .badge-decision { background: var(--badge-decision); }
67
+ .badge-architecture { background: var(--badge-architecture); }
68
+ .badge-code_change { background: var(--badge-code-change); }
69
+ .badge-error_fix { background: var(--badge-error-fix); }
70
+ .badge-pattern { background: var(--badge-pattern); color: #000; }
71
+ .badge-context { background: var(--badge-context); }
72
+ .badge-preference { background: var(--badge-preference); }
73
+ .importance-dot { width: 8px; height: 8px; border-radius: 50%; flex-shrink: 0; }
74
+ .imp-critical { background: var(--imp-critical); box-shadow: 0 0 6px var(--imp-critical); }
75
+ .imp-high { background: var(--imp-high); }
76
+ .imp-medium { background: var(--imp-medium); }
77
+ .imp-low { background: var(--imp-low); }
78
+ .card-date { font-size: 0.7rem; color: var(--text-muted); margin-left: auto; }
79
+ .card-text { font-size: 0.85rem; line-height: 1.6; color: var(--text-muted); white-space: pre-wrap; word-break: break-word; }
80
+ .card-text.expanded { color: var(--text); }
81
+ .card-tags { display: flex; flex-wrap: wrap; gap: 4px; margin-top: 8px; }
82
+ .tag { font-size: 0.65rem; padding: 2px 6px; background: var(--bg); border-radius: 3px; color: var(--text-muted); font-family: var(--font-mono); }
83
+ .card-score { font-size: 0.65rem; color: var(--accent); font-family: var(--font-mono); }
84
+ .empty-state { text-align: center; padding: 60px 20px; color: var(--text-muted); }
85
+ .empty-state pre { font-family: var(--font-mono); font-size: 0.75rem; line-height: 1.4; margin-bottom: 16px; color: var(--border); }
86
+ .empty-state p { font-size: 0.9rem; }
87
+ .load-more { display: block; width: 100%; padding: 10px; margin-top: 16px; background: var(--card); border: 1px solid var(--border); border-radius: 8px; color: var(--text-muted); cursor: pointer; font-size: 0.85rem; transition: all 0.2s; }
88
+ .load-more:hover { border-color: var(--accent); color: var(--text); }
89
+ .spinner { display: inline-block; width: 16px; height: 16px; border: 2px solid var(--border); border-top-color: var(--accent); border-radius: 50%; animation: spin 0.6s linear infinite; }
90
+ @keyframes spin { to { transform: rotate(360deg); } }
91
+ @keyframes fadeIn { from { opacity: 0; transform: translateY(8px); } to { opacity: 1; transform: translateY(0); } }
92
+ @media (max-width: 600px) {
93
+ .container { padding: 16px 12px; }
94
+ .card-date { margin-left: 0; width: 100%; margin-top: 4px; }
95
+ }
96
+ </style>
97
+ </head>
98
+ <body>
99
+ <div class="container">
100
+ <header>
101
+ <h1><span>&#9679;</span> Memory Viewer</h1>
102
+ <div class="stats-bar" id="stats"></div>
103
+ </header>
104
+ <div class="search-box">
105
+ <svg width="18" height="18" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><circle cx="11" cy="11" r="8"/><line x1="21" y1="21" x2="16.65" y2="16.65"/></svg>
106
+ <input type="text" id="searchInput" placeholder="Search memories..." autocomplete="off">
107
+ </div>
108
+ <div class="filters" id="filters">
109
+ <span class="chip-group-label">Type</span>
110
+ <span class="chip" data-filter="type" data-value="decision">Decision</span>
111
+ <span class="chip" data-filter="type" data-value="architecture">Architecture</span>
112
+ <span class="chip" data-filter="type" data-value="code_change">Code Change</span>
113
+ <span class="chip" data-filter="type" data-value="error_fix">Error Fix</span>
114
+ <span class="chip" data-filter="type" data-value="pattern">Pattern</span>
115
+ <span class="chip" data-filter="type" data-value="context">Context</span>
116
+ <span class="chip" data-filter="type" data-value="preference">Preference</span>
117
+ <span class="chip-group-label">Importance</span>
118
+ <span class="chip" data-filter="importance" data-value="critical">Critical</span>
119
+ <span class="chip" data-filter="importance" data-value="high">High</span>
120
+ <span class="chip" data-filter="importance" data-value="medium">Medium</span>
121
+ <span class="chip" data-filter="importance" data-value="low">Low</span>
122
+ </div>
123
+ <div class="cards" id="cards"></div>
124
+ </div>
125
+ <script>
126
+ (function() {
127
+ const LIMIT = 50;
128
+ let offset = 0;
129
+ let activeType = null;
130
+ let activeImportance = null;
131
+ let searchQuery = "";
132
+ let debounceTimer = null;
133
+ let loading = false;
134
+ let allLoaded = false;
135
+
136
+ const $cards = document.getElementById("cards");
137
+ const $stats = document.getElementById("stats");
138
+ const $search = document.getElementById("searchInput");
139
+
140
+ async function fetchJSON(url) {
141
+ const res = await fetch(url);
142
+ if (!res.ok) throw new Error(res.statusText);
143
+ return res.json();
144
+ }
145
+
146
+ function formatDate(ts) {
147
+ const d = new Date(ts);
148
+ return d.toLocaleDateString("en-US", { month: "short", day: "numeric", year: "numeric" })
149
+ + " " + d.toLocaleTimeString("en-US", { hour: "2-digit", minute: "2-digit" });
150
+ }
151
+
152
+ function truncate(text, len) { return text.length > len ? text.slice(0, len) + "..." : text; }
153
+
154
+ function renderCard(m, isSearch) {
155
+ const card = document.createElement("div");
156
+ card.className = "card";
157
+ const impClass = "imp-" + (m.importance || "low");
158
+ const truncatedText = truncate(m.text, 200);
159
+ const isLong = m.text.length > 200;
160
+ let tagsHTML = "";
161
+ const tags = m.tags || [];
162
+ if (tags.length) { tagsHTML = '<div class="card-tags">' + tags.map(function(t) { return '<span class="tag">' + esc(t) + "</span>"; }).join("") + "</div>"; }
163
+ const scoreHTML = isSearch && m.score != null ? ' <span class="card-score">score: ' + m.score.toFixed(3) + "</span>" : "";
164
+ card.innerHTML =
165
+ '<div class="card-header">' +
166
+ '<span class="badge badge-' + m.type + '">' + esc(m.type.replace("_", " ")) + "</span>" +
167
+ '<span class="importance-dot ' + impClass + '" title="' + esc(m.importance) + '"></span>' +
168
+ scoreHTML +
169
+ '<span class="card-date">' + formatDate(m.timestamp) + "</span>" +
170
+ "</div>" +
171
+ '<div class="card-text" data-full="' + escAttr(m.text) + '">' + esc(truncatedText) + "</div>" +
172
+ tagsHTML;
173
+ if (isLong) {
174
+ card.addEventListener("click", function() {
175
+ const textEl = card.querySelector(".card-text");
176
+ if (textEl.classList.contains("expanded")) {
177
+ textEl.textContent = truncatedText;
178
+ textEl.classList.remove("expanded");
179
+ } else {
180
+ textEl.textContent = textEl.getAttribute("data-full");
181
+ textEl.classList.add("expanded");
182
+ }
183
+ });
184
+ }
185
+ return card;
186
+ }
187
+
188
+ function esc(s) { const d = document.createElement("div"); d.textContent = s; return d.innerHTML; }
189
+ function escAttr(s) { return s.replace(/&/g,"&amp;").replace(/"/g,"&quot;").replace(/'/g,"&#39;").replace(/</g,"&lt;").replace(/>/g,"&gt;"); }
190
+
191
+ function showEmpty() {
192
+ $cards.innerHTML =
193
+ '<div class="empty-state"><pre>' +
194
+ " _____\n" +
195
+ " / \\\n" +
196
+ " | () () |\n" +
197
+ " \\ ^ /\n" +
198
+ " |||||\n" +
199
+ " |||||\n" +
200
+ "</pre><p>No memories found.</p></div>";
201
+ }
202
+
203
+ function showSpinner() {
204
+ const el = document.createElement("div");
205
+ el.style.textAlign = "center";
206
+ el.style.padding = "20px";
207
+ el.innerHTML = '<span class="spinner"></span>';
208
+ el.id = "loadingSpinner";
209
+ $cards.appendChild(el);
210
+ }
211
+
212
+ function removeSpinner() {
213
+ const el = document.getElementById("loadingSpinner");
214
+ if (el) el.remove();
215
+ }
216
+
217
+ function addLoadMore() {
218
+ const existing = document.getElementById("loadMoreBtn");
219
+ if (existing) existing.remove();
220
+ if (allLoaded || searchQuery) return;
221
+ const btn = document.createElement("button");
222
+ btn.className = "load-more";
223
+ btn.id = "loadMoreBtn";
224
+ btn.textContent = "Load more";
225
+ btn.addEventListener("click", function() { loadMemories(true); });
226
+ $cards.appendChild(btn);
227
+ }
228
+
229
+ async function loadMemories(append) {
230
+ if (loading) return;
231
+ loading = true;
232
+ if (!append) { $cards.innerHTML = ""; offset = 0; allLoaded = false; }
233
+ const existing = document.getElementById("loadMoreBtn");
234
+ if (existing) existing.remove();
235
+ showSpinner();
236
+ try {
237
+ let url = "/api/memories?limit=" + LIMIT + "&offset=" + offset;
238
+ if (activeType) url += "&type=" + activeType;
239
+ const data = await fetchJSON(url);
240
+ removeSpinner();
241
+ const entries = data.entries || [];
242
+ if (entries.length === 0 && offset === 0) { showEmpty(); return; }
243
+ if (entries.length < LIMIT) allLoaded = true;
244
+ let filtered = entries;
245
+ if (activeImportance) { filtered = filtered.filter(function(e) { return e.importance === activeImportance; }); }
246
+ filtered.forEach(function(m) { $cards.appendChild(renderCard(m, false)); });
247
+ offset += entries.length;
248
+ addLoadMore();
249
+ } catch (err) {
250
+ removeSpinner();
251
+ $cards.innerHTML = '<div class="empty-state"><p>Error loading memories: ' + esc(err.message) + "</p></div>";
252
+ } finally { loading = false; }
253
+ }
254
+
255
+ async function doSearch(query) {
256
+ if (loading) return;
257
+ loading = true;
258
+ $cards.innerHTML = "";
259
+ showSpinner();
260
+ try {
261
+ const data = await fetchJSON("/api/search?q=" + encodeURIComponent(query) + "&limit=30");
262
+ removeSpinner();
263
+ let results = data.results || [];
264
+ if (activeType) { results = results.filter(function(r) { return r.type === activeType; }); }
265
+ if (activeImportance) { results = results.filter(function(r) { return r.importance === activeImportance; }); }
266
+ if (results.length === 0) { showEmpty(); return; }
267
+ results.forEach(function(m) { $cards.appendChild(renderCard(m, true)); });
268
+ } catch (err) {
269
+ removeSpinner();
270
+ $cards.innerHTML = '<div class="empty-state"><p>Search error: ' + esc(err.message) + "</p></div>";
271
+ } finally { loading = false; }
272
+ }
273
+
274
+ async function loadStatus() {
275
+ try {
276
+ const data = await fetchJSON("/api/status");
277
+ let html = '<span class="stat"><strong>' + (data.project || "unknown") + "</strong> project</span>";
278
+ html += '<span class="stat"><strong>' + (data.total || 0) + "</strong> memories</span>";
279
+ html += '<span class="stat">Ollama: <strong>' + (data.ollama_available ? "online" : "offline") + "</strong></span>";
280
+ const byType = data.by_type || {};
281
+ Object.keys(byType).forEach(function(t) {
282
+ html += '<span class="stat">' + t.replace("_", " ") + ": <strong>" + byType[t] + "</strong></span>";
283
+ });
284
+ $stats.innerHTML = html;
285
+ } catch (e) {
286
+ $stats.innerHTML = '<span class="stat">Could not load status</span>';
287
+ }
288
+ }
289
+
290
+ function refresh() {
291
+ if (searchQuery) { doSearch(searchQuery); } else { loadMemories(false); }
292
+ }
293
+
294
+ $search.addEventListener("input", function() {
295
+ clearTimeout(debounceTimer);
296
+ debounceTimer = setTimeout(function() {
297
+ searchQuery = $search.value.trim();
298
+ refresh();
299
+ }, 300);
300
+ });
301
+
302
+ document.querySelectorAll(".chip[data-filter]").forEach(function(chip) {
303
+ chip.addEventListener("click", function() {
304
+ const filter = chip.getAttribute("data-filter");
305
+ const value = chip.getAttribute("data-value");
306
+ if (filter === "type") {
307
+ if (activeType === value) { activeType = null; chip.classList.remove("active"); }
308
+ else {
309
+ document.querySelectorAll('.chip[data-filter="type"]').forEach(function(c) { c.classList.remove("active"); });
310
+ activeType = value; chip.classList.add("active");
311
+ }
312
+ } else if (filter === "importance") {
313
+ if (activeImportance === value) { activeImportance = null; chip.classList.remove("active"); }
314
+ else {
315
+ document.querySelectorAll('.chip[data-filter="importance"]').forEach(function(c) { c.classList.remove("active"); });
316
+ activeImportance = value; chip.classList.add("active");
317
+ }
318
+ }
319
+ refresh();
320
+ });
321
+ });
322
+
323
+ loadStatus();
324
+ loadMemories(false);
325
+ })();
326
+ </script>
327
+ </body>
328
+ </html>
@@ -0,0 +1 @@
1
+ export declare function startViewer(port?: number): Promise<void>;
@@ -0,0 +1,203 @@
1
+ import { createServer as createHttpServer } from "node:http";
2
+ import { readFileSync } from "node:fs";
3
+ import { join, dirname } from "node:path";
4
+ import { fileURLToPath } from "node:url";
5
+ import * as lanceStore from "../storage/lance-store.js";
6
+ import * as mdStore from "../storage/markdown-store.js";
7
+ import { hybridSearch } from "../search/hybrid.js";
8
+ import { getMemoryDir, getProjectName } from "../utils/project.js";
9
+ import { isAvailable } from "../embeddings/ollama.js";
10
+ import { stripPrivate } from "../utils/privacy.js";
11
+ const __filename = fileURLToPath(import.meta.url);
12
+ const __dirname = dirname(__filename);
13
+ function corsHeaders(req) {
14
+ const origin = req?.headers.origin || "";
15
+ const allowed = /^https?:\/\/(localhost|127\.0\.0\.1)(:\d+)?$/.test(origin);
16
+ return {
17
+ "Access-Control-Allow-Origin": allowed ? origin : "http://localhost:3777",
18
+ "Access-Control-Allow-Methods": "GET, OPTIONS",
19
+ "Access-Control-Allow-Headers": "Content-Type",
20
+ };
21
+ }
22
+ function jsonResponse(res, data, status = 200, extraHeaders = {}) {
23
+ const body = JSON.stringify(data);
24
+ res.writeHead(status, {
25
+ ...extraHeaders,
26
+ "Content-Type": "application/json; charset=utf-8",
27
+ });
28
+ res.end(body);
29
+ }
30
+ function htmlResponse(res, html) {
31
+ res.writeHead(200, {
32
+ "Content-Type": "text/html; charset=utf-8",
33
+ });
34
+ res.end(html);
35
+ }
36
+ function parseQuery(url) {
37
+ const idx = url.indexOf("?");
38
+ if (idx === -1)
39
+ return {};
40
+ const params = {};
41
+ const search = url.slice(idx + 1);
42
+ for (const pair of search.split("&")) {
43
+ const [key, val] = pair.split("=");
44
+ if (key)
45
+ params[decodeURIComponent(key)] = decodeURIComponent(val || "");
46
+ }
47
+ return params;
48
+ }
49
+ function getPathname(url) {
50
+ const idx = url.indexOf("?");
51
+ return idx === -1 ? url : url.slice(0, idx);
52
+ }
53
+ let indexHtml = null;
54
+ // Cached on first read; restart server to pick up HTML changes during development
55
+ function getIndexHtml() {
56
+ if (!indexHtml) {
57
+ indexHtml = readFileSync(join(__dirname, "index.html"), "utf-8");
58
+ }
59
+ return indexHtml;
60
+ }
61
+ const VALID_TYPES = new Set(["decision", "architecture", "code_change", "error_fix", "pattern", "context", "preference"]);
62
+ // --- Rate limiting ---
63
+ const requestCounts = new Map();
64
+ const RATE_LIMIT = 100; // requests per window
65
+ const RATE_WINDOW_MS = 60_000; // 1 minute
66
+ function isRateLimited(ip) {
67
+ const now = Date.now();
68
+ const entry = requestCounts.get(ip);
69
+ if (!entry || now > entry.resetAt) {
70
+ requestCounts.set(ip, { count: 1, resetAt: now + RATE_WINDOW_MS });
71
+ return false;
72
+ }
73
+ entry.count++;
74
+ return entry.count > RATE_LIMIT;
75
+ }
76
+ async function handleRequest(req, res) {
77
+ const url = req.url || "/";
78
+ const cors = corsHeaders(req);
79
+ // Limit URL length to prevent abuse
80
+ if (url.length > 2048) {
81
+ jsonResponse(res, { error: "URI too long" }, 414, cors);
82
+ return;
83
+ }
84
+ // Rate limiting
85
+ const clientIp = req.socket.remoteAddress || "unknown";
86
+ if (isRateLimited(clientIp)) {
87
+ jsonResponse(res, { error: "Too many requests" }, 429, cors);
88
+ return;
89
+ }
90
+ const pathname = getPathname(url);
91
+ const query = parseQuery(url);
92
+ // Handle CORS preflight
93
+ if (req.method === "OPTIONS") {
94
+ res.writeHead(204, cors);
95
+ res.end();
96
+ return;
97
+ }
98
+ if (req.method !== "GET") {
99
+ jsonResponse(res, { error: "Method not allowed" }, 405, cors);
100
+ return;
101
+ }
102
+ try {
103
+ if (pathname === "/" || pathname === "/index.html") {
104
+ htmlResponse(res, getIndexHtml());
105
+ return;
106
+ }
107
+ if (pathname === "/api/memories") {
108
+ const limit = Math.min(Math.max(parseInt(query.limit || "50", 10) || 50, 1), 200);
109
+ const offset = Math.max(parseInt(query.offset || "0", 10) || 0, 0);
110
+ const type = query.type && VALID_TYPES.has(query.type) ? query.type : undefined;
111
+ const entries = await lanceStore.list(limit, offset, type || undefined);
112
+ const total = await lanceStore.count();
113
+ jsonResponse(res, {
114
+ entries: entries.map((e) => ({
115
+ id: e.id,
116
+ text: stripPrivate(e.text),
117
+ type: e.type,
118
+ importance: e.importance,
119
+ timestamp: e.timestamp,
120
+ file_paths: e.file_paths,
121
+ tags: e.tags,
122
+ })),
123
+ total,
124
+ limit,
125
+ offset,
126
+ project: getProjectName(),
127
+ }, 200, cors);
128
+ return;
129
+ }
130
+ if (pathname === "/api/search") {
131
+ const q = query.q || "";
132
+ if (!q) {
133
+ jsonResponse(res, { results: [], count: 0 }, 200, cors);
134
+ return;
135
+ }
136
+ const limit = Math.min(Math.max(parseInt(query.limit || "20", 10) || 20, 1), 100);
137
+ const results = await hybridSearch(q, limit);
138
+ jsonResponse(res, {
139
+ results: results.map((r) => ({
140
+ id: r.id,
141
+ text: stripPrivate(r.text),
142
+ type: r.type,
143
+ importance: r.importance,
144
+ score: Math.round(r.score * 1000) / 1000,
145
+ timestamp: r.timestamp,
146
+ file_paths: r.file_paths,
147
+ tags: r.tags,
148
+ })),
149
+ count: results.length,
150
+ project: getProjectName(),
151
+ }, 200, cors);
152
+ return;
153
+ }
154
+ if (pathname === "/api/status") {
155
+ const [total, byType, ollamaAvailable] = await Promise.all([
156
+ lanceStore.count(),
157
+ lanceStore.countByType(),
158
+ isAvailable(),
159
+ ]);
160
+ const memoryDir = getMemoryDir();
161
+ const summary = mdStore.readSummary(memoryDir);
162
+ jsonResponse(res, {
163
+ total,
164
+ by_type: byType,
165
+ ollama_available: ollamaAvailable,
166
+ memory_dir: memoryDir,
167
+ project: getProjectName(),
168
+ has_summary: summary !== null,
169
+ }, 200, cors);
170
+ return;
171
+ }
172
+ // 404 for everything else
173
+ jsonResponse(res, { error: "Not found" }, 404, cors);
174
+ }
175
+ catch (err) {
176
+ console.error("Request error:", pathname, err);
177
+ jsonResponse(res, { error: "Internal server error" }, 500, cors);
178
+ }
179
+ }
180
+ export async function startViewer(port = 3777) {
181
+ // Initialize LanceDB before accepting requests
182
+ const memoryDir = getMemoryDir();
183
+ mdStore.ensureDirs(memoryDir);
184
+ await lanceStore.init(memoryDir);
185
+ const server = createHttpServer((req, res) => {
186
+ handleRequest(req, res).catch((err) => {
187
+ console.error("Unhandled error:", err);
188
+ jsonResponse(res, { error: "Internal server error" }, 500, corsHeaders(req));
189
+ });
190
+ });
191
+ server.listen(port, () => {
192
+ console.log(`Memory viewer running at http://localhost:${port}`);
193
+ });
194
+ // Graceful shutdown
195
+ const shutdown = () => {
196
+ console.log("\nShutting down viewer...");
197
+ server.close();
198
+ process.exit(0);
199
+ };
200
+ process.on("SIGINT", shutdown);
201
+ process.on("SIGTERM", shutdown);
202
+ }
203
+ //# sourceMappingURL=server.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"server.js","sourceRoot":"","sources":["../../src/viewer/server.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,17 @@
1
+ #!/bin/bash
2
+ # Capture Edit/Write file paths to daily log
3
+ INPUT=$(cat)
4
+ FILE_PATH=$(echo "$INPUT" | node -e "
5
+ let d=''; process.stdin.on('data',c=>d+=c);
6
+ process.stdin.on('end',()=>{try{
7
+ const j=JSON.parse(d);
8
+ console.log(j.tool_input?.file_path||j.tool_input?.path||'')
9
+ }catch{console.log('')}})
10
+ " 2>/dev/null)
11
+
12
+ if [ -n "$FILE_PATH" ]; then
13
+ MEMORY_DIR="${CLAUDE_PROJECT_DIR:-.}/.claude/memory"
14
+ mkdir -p "$MEMORY_DIR/daily"
15
+ DATE=$(date +%Y-%m-%d)
16
+ echo "- $(date +%H:%M) Modified: $FILE_PATH" >> "$MEMORY_DIR/daily/$DATE.md"
17
+ fi
@@ -0,0 +1,20 @@
1
+ #!/bin/bash
2
+ # Save context before compaction — captures last portion of transcript
3
+ INPUT=$(cat)
4
+ MEMORY_DIR="${CLAUDE_PROJECT_DIR:-.}/.claude/memory"
5
+ mkdir -p "$MEMORY_DIR/daily"
6
+ DATE=$(date +%Y-%m-%d)
7
+ TIME=$(date +%H:%M)
8
+ echo -e "\n## Pre-compact save ($TIME)\n" >> "$MEMORY_DIR/daily/$DATE.md"
9
+
10
+ # Try to extract and save transcript path from stdin JSON
11
+ TRANSCRIPT_PATH=$(echo "$INPUT" | node -e "
12
+ let d=''; process.stdin.on('data',c=>d+=c);
13
+ process.stdin.on('end',()=>{try{console.log(JSON.parse(d).transcript_path||'')}catch{console.log('')}})
14
+ " 2>/dev/null)
15
+
16
+ if [ -n "$TRANSCRIPT_PATH" ] && [ -f "$TRANSCRIPT_PATH" ]; then
17
+ tail -200 "$TRANSCRIPT_PATH" >> "$MEMORY_DIR/daily/$DATE.md"
18
+ else
19
+ echo "_Context compacted at $TIME — no transcript available_" >> "$MEMORY_DIR/daily/$DATE.md"
20
+ fi
@@ -0,0 +1,46 @@
1
+ #!/bin/bash
2
+ # Archive session and generate SUMMARY.md (AI-powered with fallback)
3
+ MEMORY_DIR="${CLAUDE_PROJECT_DIR:-.}/.claude/memory"
4
+ mkdir -p "$MEMORY_DIR/daily" "$MEMORY_DIR/sessions"
5
+ DATE=$(date +%Y-%m-%d)
6
+ TIME=$(date +%H-%M)
7
+
8
+ echo -e "\n## Session ended at $(date +%H:%M:%S)\n" >> "$MEMORY_DIR/daily/$DATE.md"
9
+
10
+ # Archive today's daily log
11
+ if [ -f "$MEMORY_DIR/daily/$DATE.md" ]; then
12
+ cp "$MEMORY_DIR/daily/$DATE.md" "$MEMORY_DIR/sessions/$DATE-$TIME.md"
13
+ fi
14
+
15
+ # Try AI-powered summary generation, fall back to tail-based
16
+ SCRIPT_DIR="$(cd "$(dirname "$0")" && pwd)"
17
+ PROJECT_DIR="$(dirname "$SCRIPT_DIR")"
18
+ if MEMORY_DIR="$MEMORY_DIR" node "$PROJECT_DIR/dist/cli/generate-summary.js" 2>/dev/null; then
19
+ exit 0
20
+ fi
21
+
22
+ # Fallback: existing tail-based summary (keep existing logic)
23
+ MAX_SUMMARY_CHARS=4000
24
+ {
25
+ echo "# Memory Summary"
26
+ echo ""
27
+ echo "_Auto-generated $(date '+%Y-%m-%d %H:%M') — use memory_search for full context_"
28
+ echo ""
29
+
30
+ # Include decisions (most valuable, newest first)
31
+ if [ -f "$MEMORY_DIR/decisions.md" ]; then
32
+ echo "## Recent Decisions"
33
+ echo ""
34
+ # Take the last ~1500 chars of decisions
35
+ tail -c 1500 "$MEMORY_DIR/decisions.md"
36
+ echo ""
37
+ fi
38
+
39
+ # Include today's activity log (most recent ~1500 chars)
40
+ if [ -f "$MEMORY_DIR/daily/$DATE.md" ]; then
41
+ echo "## Today's Activity"
42
+ echo ""
43
+ tail -c 1500 "$MEMORY_DIR/daily/$DATE.md"
44
+ echo ""
45
+ fi
46
+ } | head -c "$MAX_SUMMARY_CHARS" > "$MEMORY_DIR/SUMMARY.md"
@@ -0,0 +1,14 @@
1
+ #!/bin/bash
2
+ # Injects SUMMARY.md at session start, capped at 4000 chars to avoid context bloat
3
+ MEMORY_DIR="${CLAUDE_PROJECT_DIR:-.}/.claude/memory"
4
+ MAX_CHARS=4000
5
+
6
+ if [ -f "$MEMORY_DIR/SUMMARY.md" ]; then
7
+ CONTENT=$(head -c "$MAX_CHARS" "$MEMORY_DIR/SUMMARY.md")
8
+ FULL_SIZE=$(wc -c < "$MEMORY_DIR/SUMMARY.md" | tr -d ' ')
9
+ echo "$CONTENT"
10
+ if [ "$FULL_SIZE" -gt "$MAX_CHARS" ]; then
11
+ echo ""
12
+ echo "_[Summary truncated — use memory_search for full context]_"
13
+ fi
14
+ fi
@@ -0,0 +1,6 @@
1
+ #!/bin/bash
2
+ # Brief turn marker in daily log
3
+ MEMORY_DIR="${CLAUDE_PROJECT_DIR:-.}/.claude/memory"
4
+ mkdir -p "$MEMORY_DIR/daily"
5
+ DATE=$(date +%Y-%m-%d)
6
+ echo -e "\n---\n_Turn completed at $(date +%H:%M:%S)_\n" >> "$MEMORY_DIR/daily/$DATE.md"