agent-memory-git 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.
@@ -0,0 +1,635 @@
1
+ (() => {
2
+ "use strict";
3
+
4
+ // ------------------------------------------------------------ plumbing
5
+ const token = (location.hash.match(/t=([\w-]+)/) || [])[1] || sessionStorage.getItem("mem-ui-token") || "";
6
+ if (token) sessionStorage.setItem("mem-ui-token", token);
7
+ if (location.hash) history.replaceState(null, "", location.pathname); // keep the token out of sight
8
+ addEventListener("hashchange", () => location.reload()); // a new link from `mem ui`
9
+
10
+ async function api(path, body) {
11
+ const r = await fetch(path, {
12
+ method: body === undefined ? "GET" : "POST",
13
+ headers: { "X-Mem-Token": token, ...(body === undefined ? {} : { "Content-Type": "application/json" }) },
14
+ body: body === undefined ? undefined : JSON.stringify(body),
15
+ });
16
+ const data = await r.json().catch(() => ({}));
17
+ if (!r.ok) throw Object.assign(new Error(data.error || `${r.status} ${r.statusText}`), { status: r.status });
18
+ return data;
19
+ }
20
+ const $ = id => document.getElementById(id);
21
+ function el(tag, attrs, ...kids) {
22
+ const n = document.createElement(tag);
23
+ for (const [k, v] of Object.entries(attrs || {})) {
24
+ if (v == null || v === false) continue;
25
+ if (k === "class") n.className = v;
26
+ else if (k === "text") n.textContent = v;
27
+ else if (k === "value") n.value = v;
28
+ else if (k.startsWith("on")) n.addEventListener(k.slice(2), v);
29
+ else n.setAttribute(k, v === true ? "" : v);
30
+ }
31
+ for (const kid of kids.flat(Infinity)) if (kid != null && kid !== false) n.append(kid instanceof Node ? kid : document.createTextNode(String(kid)));
32
+ return n;
33
+ }
34
+ const ICON = {
35
+ edit: '<path d="M11 2.5l2.5 2.5L6 12.5H3.5V10z"/>',
36
+ trash: '<path d="M3 4.5h10M6.5 4.5V3h3v1.5M4.5 4.5l.6 8.5h5.8l.6-8.5"/>',
37
+ close: '<path d="M4 4l8 8M12 4l-8 8"/>',
38
+ copy: '<rect x="5.5" y="5.5" width="8" height="8" rx="1.5"/><path d="M3 10.5V3.5a1 1 0 0 1 1-1h6.5"/>',
39
+ };
40
+ function iconBtn(name, label, onclick) {
41
+ const b = el("button", { class: "icon-btn", title: label, "aria-label": label, onclick });
42
+ b.innerHTML = `<svg viewBox="0 0 16 16" aria-hidden="true">${ICON[name]}</svg>`; // static markup only
43
+ return b;
44
+ }
45
+ const day = iso => {
46
+ const t = Date.parse(iso || "");
47
+ return t ? new Date(t).toLocaleDateString(undefined, { year: "numeric", month: "short", day: "numeric" }) : "";
48
+ };
49
+ const plural = (n, one, many) => `${n} ${n === 1 ? one : (many || one + "s")}`;
50
+ const local = {
51
+ get(k, d) { try { const v = localStorage.getItem("mem-ui-" + k); return v == null ? d : JSON.parse(v); } catch (e) { return d; } },
52
+ set(k, v) { try { localStorage.setItem("mem-ui-" + k, JSON.stringify(v)); } catch (e) { /* private window */ } },
53
+ };
54
+ function toast(message, opts = {}) {
55
+ const t = el("div", { class: "toast" + (opts.error ? " error" : "") }, el("span", { text: message }));
56
+ if (opts.undo) t.append(el("button", { text: "Undo", onclick: async () => {
57
+ t.remove();
58
+ try { await api("/api/undo", { commit: opts.undo }); await reload(); toast("Undone"); }
59
+ catch (e) { toast(e.message, { error: true }); }
60
+ } }));
61
+ $("toasts").append(t);
62
+ setTimeout(() => t.remove(), opts.undo ? 15000 : opts.error ? 8000 : 2500);
63
+ }
64
+
65
+ // ------------------------------------------------------------ data
66
+ let D = null, M = [], byId = new Map(), byName = new Map(), kwName = new Map();
67
+ const S = {
68
+ open: null, mode: "view", tab: "rendered",
69
+ q: "", lex: [], sem: [],
70
+ f: { type: "", loads: "", tool: "", age: "", kw: "", fmt: "" }, sort: local.get("sort", "new"),
71
+ picked: new Set(), rows: [], cursor: -1,
72
+ };
73
+
74
+ async function reload() {
75
+ const d = await api("/api/state");
76
+ D = d; M = d.memories;
77
+ byId = new Map(M.map((m, i) => [m.id, i]));
78
+ byName = new Map(M.map((m, i) => [m.meta && m.meta.name, i]));
79
+ M.forEach(m => { m.termSet = new Set(m.terms || []); });
80
+ kwName = new Map();
81
+ M.forEach(m => (m.keywords || []).forEach(k => kwName.set(k.k, k.t)));
82
+ S.picked.forEach(id => { if (!byId.has(id)) S.picked.delete(id); });
83
+ if (S.open && !byId.has(S.open)) closeDrawer(true);
84
+ if (S.f.loads.startsWith("place:") && !(D.places || []).some(p => "place:" + p.key === S.f.loads)) S.f.loads = "";
85
+ renderSync();
86
+ if (S.q) runSearch(S.q, false);
87
+ renderFilters();
88
+ renderList();
89
+ if (S.open && S.mode === "view") renderDrawer();
90
+ }
91
+
92
+ // ------------------------------------------------------------ filters
93
+ const TYPES = ["user", "feedback", "project", "reference"];
94
+ const SORTS = [["new", "Newest first"], ["old", "Oldest first"], ["edited", "Recently changed"], ["az", "A to Z"]];
95
+ const AGES = [["7", "Added in the past week"], ["30", "Added in the past month"], ["stale", "Unchanged for 60+ days"]];
96
+ const touched = m => m.updated || m.created || "";
97
+
98
+ function passes(m, skip) {
99
+ const f = S.f, now = Date.now();
100
+ if (skip !== "type" && f.type && m.type !== f.type) return false;
101
+ if (skip !== "loads" && f.loads === "global" && m.scope !== "global") return false;
102
+ if (skip !== "loads" && f.loads.startsWith("place:") && (m.scope !== "here" || m.place !== f.loads.slice(6))) return false;
103
+ if (skip !== "tool" && f.tool && m.tool !== f.tool) return false;
104
+ if (skip !== "kw" && f.kw && !m.termSet.has(f.kw)) return false;
105
+ if (skip !== "fmt" && f.fmt && !(m.issues || []).length) return false;
106
+ if (skip !== "age" && f.age) {
107
+ const t = Date.parse(f.age === "stale" ? touched(m) : m.created || "");
108
+ if (!t) return false;
109
+ if (f.age === "stale" ? now - t < 60 * 864e5 : now - t > Number(f.age) * 864e5) return false;
110
+ }
111
+ return true;
112
+ }
113
+ function pill(key, label, options, value, onchange) {
114
+ const sel = el("select", { "aria-label": label, onchange: e => onchange(e.target.value) },
115
+ options.map(([v, text]) => el("option", { value: v, text, selected: v === value })));
116
+ return el("span", { class: "pill" + (value && key !== "sort" ? " on" : "") }, sel);
117
+ }
118
+ // keywords that tag at least two memories, most common first; the chosen one always listed
119
+ function keywordOptions() {
120
+ const n = new Map();
121
+ M.forEach(m => { if (passes(m, "kw")) m.termSet.forEach(t => { if (kwName.has(t)) n.set(t, (n.get(t) || 0) + 1); }); });
122
+ const out = [...n].filter(([, c]) => c >= 2).sort((a, b) => b[1] - a[1] || kwName.get(a[0]).localeCompare(kwName.get(b[0]))).slice(0, 80);
123
+ if (S.f.kw && !out.some(([k]) => k === S.f.kw)) out.unshift([S.f.kw, n.get(S.f.kw) || 0]);
124
+ return out.map(([k, c]) => [k, `${kwName.get(k) || k} · ${c}`]);
125
+ }
126
+ function setKeyword(k) {
127
+ S.f.kw = S.f.kw === k ? "" : k; S.cursor = -1;
128
+ renderFilters(); renderList(); scrollTo(0, 0);
129
+ if (S.open && S.mode === "view") renderDrawer();
130
+ }
131
+ function keywordChips(m) {
132
+ if (!(m.keywords || []).length) return null;
133
+ return el("div", { class: "kws" }, m.keywords.map(k => el("button", { class: "kw" + (S.f.kw === k.k ? " on" : ""), text: k.t,
134
+ title: S.f.kw === k.k ? "Show all memories again" : `Only memories that mention ${k.t}`,
135
+ onclick: e => { e.stopPropagation(); setKeyword(k.k); } })));
136
+ }
137
+ function renderFilters() {
138
+ const box = $("filters");
139
+ const count = (key, test) => M.filter(m => passes(m, key) && test(m)).length;
140
+ const set = key => v => { S.f[key] = v; S.cursor = -1; renderFilters(); renderList(); scrollTo(0, 0); };
141
+ const tools = [...new Set(M.map(m => m.tool).filter(Boolean))].sort();
142
+ const any = Object.values(S.f).some(Boolean);
143
+ box.replaceChildren(...[
144
+ pill("type", "Type", [["", "All types"], ...TYPES.map(t => [t, `${t} · ${count("type", m => m.type === t)}`])], S.f.type, set("type")),
145
+ pill("loads", "Where it loads", [["", "Loads anywhere"], ["global", `Everywhere · ${count("loads", m => m.scope === "global")}`],
146
+ ...(D.places || []).map(p => ["place:" + p.key, `${p.key} · ${count("loads", m => m.scope === "here" && m.place === p.key)}`])], S.f.loads, set("loads")),
147
+ tools.length > 1 ? pill("tool", "Written by", [["", "Any source"], ...tools.map(t => [t, `${t} · ${count("tool", m => m.tool === t)}`])], S.f.tool, set("tool")) : null,
148
+ pill("kw", "Keyword", [["", "Any keyword"], ...keywordOptions()], S.f.kw, set("kw")),
149
+ pill("age", "When", [["", "Any time"], ...AGES], S.f.age, set("age")),
150
+ pill("fmt", "Format", [["", "Any format"], ["tidy", `Needs tidying · ${count("fmt", m => (m.issues || []).length)}`]], S.f.fmt, set("fmt")),
151
+ any ? el("button", { class: "clear", text: "Clear filters", onclick: () => { S.f = { type: "", loads: "", tool: "", age: "", kw: "", fmt: "" }; renderFilters(); renderList(); } }) : null,
152
+ el("span", { class: "spacer" }),
153
+ el("span", { class: "count", id: "count" }),
154
+ S.q ? null : pill("sort", "Order", SORTS, S.sort, v => { S.sort = v; local.set("sort", v); renderFilters(); renderList(); }),
155
+ ].filter(Boolean));
156
+ }
157
+
158
+ // ------------------------------------------------------------ search
159
+ const q = $("q");
160
+ let semTimer = null, semSeq = 0;
161
+ const terms = text => text.toLowerCase().split(/\s+/).filter(Boolean);
162
+
163
+ function lexical(query) {
164
+ const ts = terms(query);
165
+ if (!ts.length) return [];
166
+ const scored = [];
167
+ M.forEach((m, i) => {
168
+ const desc = m.description.toLowerCase();
169
+ const hay = `${desc}\n${m.body.toLowerCase()}\n${(m.place || "").toLowerCase()}\n${m.id}\n${m.type}`;
170
+ if (!ts.every(t => hay.includes(t))) return;
171
+ const score = ts.reduce((s, t) => s + (desc.includes(t) ? 10 : 0) + Math.min(5, hay.split(t).length - 1), 0);
172
+ scored.push([score, i]);
173
+ });
174
+ return scored.sort((a, b) => b[0] - a[0] || touched(M[b[1]]).localeCompare(touched(M[a[1]]))).map(([, i]) => i);
175
+ }
176
+ function runSearch(text, semantic = true) {
177
+ const was = S.q;
178
+ S.q = text.trim();
179
+ S.lex = S.q ? lexical(S.q) : [];
180
+ if (!S.q) S.sem = [];
181
+ S.cursor = S.q ? 0 : -1;
182
+ S.semPending = !!(semantic && S.q && D.semantic);
183
+ if (!!was !== !!S.q) renderFilters(); // the order menu only shows without a query
184
+ renderList();
185
+ clearTimeout(semTimer);
186
+ if (!S.semPending) return;
187
+ const seq = ++semSeq, query = S.q;
188
+ semTimer = setTimeout(async () => {
189
+ try {
190
+ const r = await api(`/api/search?q=${encodeURIComponent(query)}`);
191
+ if (seq !== semSeq || query !== S.q) return;
192
+ S.sem = (r.hits || []).filter(h => byId.has(h.id));
193
+ } catch (e) { /* word matches stay */ }
194
+ if (seq === semSeq) { S.semPending = false; renderList(); }
195
+ }, 300);
196
+ }
197
+ function highlight(text, ts) {
198
+ const marks = ts.filter(t => t.length > 1 || /[㐀-鿿]/.test(t));
199
+ const frag = document.createDocumentFragment();
200
+ if (!marks.length) { frag.append(text); return frag; }
201
+ const re = new RegExp(`(${marks.map(t => t.replace(/[.*+?^${}()|[\]\\]/g, "\\$&")).join("|")})`, "gi");
202
+ let last = 0;
203
+ text.replace(re, (m, _g, off) => { frag.append(text.slice(last, off), el("mark", { text: m })); last = off + m.length; return m; });
204
+ frag.append(text.slice(last));
205
+ return frag;
206
+ }
207
+ function snippet(m, ts) {
208
+ const desc = m.description.toLowerCase();
209
+ if (ts.every(t => desc.includes(t))) return null;
210
+ const body = m.body.replace(/\s+/g, " "), lower = body.toLowerCase();
211
+ const at = ts.filter(t => !desc.includes(t)).map(t => lower.indexOf(t)).filter(k => k >= 0).sort((a, b) => a - b)[0];
212
+ if (at == null) return null;
213
+ const start = Math.max(0, at - 50);
214
+ return (start ? "…" : "") + body.slice(start, start + 160);
215
+ }
216
+ q.addEventListener("input", () => runSearch(q.value));
217
+ q.addEventListener("keydown", e => {
218
+ if (e.key === "ArrowDown" || e.key === "ArrowUp") { e.preventDefault(); moveCursor(e.key === "ArrowDown" ? 1 : -1); }
219
+ else if (e.key === "Enter") { e.preventDefault(); openCursor(); }
220
+ else if (e.key === "Escape") {
221
+ e.stopPropagation();
222
+ if (q.value) { q.value = ""; runSearch(""); } else q.blur();
223
+ }
224
+ });
225
+
226
+ // ------------------------------------------------------------ list
227
+ const list = $("list");
228
+ function sorted(idx) {
229
+ const by = {
230
+ new: (a, b) => (M[b].created || "").localeCompare(M[a].created || ""),
231
+ old: (a, b) => (M[a].created || "").localeCompare(M[b].created || ""),
232
+ edited: (a, b) => touched(M[b]).localeCompare(touched(M[a])),
233
+ az: (a, b) => M[a].description.localeCompare(M[b].description),
234
+ }[S.sort] || (() => 0);
235
+ return idx.sort(by);
236
+ }
237
+ function renderList() {
238
+ if (!D) return;
239
+ const ts = terms(S.q);
240
+ const groups = [];
241
+ if (S.q) {
242
+ const lex = S.lex.filter(i => passes(M[i]));
243
+ const seen = new Set(S.lex);
244
+ const sem = S.sem.map(h => byId.get(h.id)).filter(i => i != null && !seen.has(i) && passes(M[i])).slice(0, 8);
245
+ groups.push([null, lex]);
246
+ if (sem.length) groups.push(["Close in meaning", sem]);
247
+ } else groups.push([null, sorted(M.map((_, i) => i).filter(i => passes(M[i])))]);
248
+
249
+ S.rows = groups.flatMap(([, idx]) => idx);
250
+ if (S.cursor >= S.rows.length) S.cursor = S.rows.length - 1;
251
+ const shown = S.rows.length, total = M.length;
252
+ const countEl = $("count");
253
+ if (countEl) countEl.textContent = S.q ? `${plural(groups[0][1].length, "match", "matches")}${groups[1] ? ` + ${groups[1][1].length} close in meaning` : ""}${S.semPending ? " · searching by meaning…" : ""}`
254
+ : shown === total ? plural(total, "memory", "memories") : `${shown} of ${total}`;
255
+
256
+ list.classList.toggle("selecting", S.picked.size > 0);
257
+ list.replaceChildren();
258
+ let k = 0;
259
+ for (const [title, idx] of groups) {
260
+ if (title && idx.length) list.append(el("div", { class: "group", text: title }));
261
+ for (const i of idx) list.append(row(i, k++, title ? [] : ts));
262
+ }
263
+ if (!shown) {
264
+ const filtered = Object.values(S.f).some(Boolean);
265
+ list.append(el("div", { class: "empty" },
266
+ el("div", { text: S.q ? (S.semPending ? "No word matches; searching by meaning…" : "No matches.") : "No memories match these filters." }),
267
+ filtered ? el("button", { class: "btn", text: "Clear filters", onclick: () => { S.f = { type: "", loads: "", tool: "", age: "", kw: "", fmt: "" }; renderFilters(); renderList(); } }) : null));
268
+ }
269
+ renderSelbar();
270
+ }
271
+ function row(i, k, ts) {
272
+ const m = M[i], picked = S.picked.has(m.id);
273
+ const snip = ts.length ? snippet(m, ts) : null;
274
+ return el("div", { class: "row" + (S.open === m.id ? " open" : "") + (picked ? " picked" : "") + (k === S.cursor ? " cursor" : ""),
275
+ "data-k": k, onclick: e => { if (e.metaKey || e.shiftKey) { togglePick(m.id); return; } S.cursor = k; openMemory(m.id); } },
276
+ el("input", { type: "checkbox", class: "chk", "aria-label": "Select", checked: picked,
277
+ onclick: e => { e.stopPropagation(); togglePick(m.id); } }),
278
+ el("div", { class: "main" },
279
+ el("div", { class: "desc" }, highlight(m.description, ts)),
280
+ snip ? el("div", { class: "snip" }, highlight(snip, ts)) : keywordChips(m)),
281
+ el("div", { class: "meta" },
282
+ el("span", { class: `t ${m.type}` }, el("i"), m.type),
283
+ el("span", { class: "p", title: m.scope === "global" ? "Loads in every session" : m.place, text: m.scope === "global" ? "everywhere" : m.place }),
284
+ el("span", { class: "d", title: m.updated ? `added ${day(m.created)}, changed ${day(m.updated)}` : `added ${day(m.created)}`, text: day(S.sort === "edited" ? touched(m) : m.created) })));
285
+ }
286
+ function moveCursor(step) {
287
+ if (!S.rows.length) return;
288
+ S.cursor = Math.max(0, Math.min(S.rows.length - 1, (S.cursor < 0 ? (step > 0 ? -1 : S.rows.length) : S.cursor) + step));
289
+ list.querySelectorAll(".row").forEach(r => r.classList.toggle("cursor", Number(r.dataset.k) === S.cursor));
290
+ const r = list.querySelector(`.row[data-k="${S.cursor}"]`);
291
+ if (r) r.scrollIntoView({ block: "nearest" });
292
+ if (S.open && S.mode === "view") openMemory(M[S.rows[S.cursor]].id); // reading along
293
+ }
294
+ function openCursor() { if (S.cursor >= 0 && S.rows[S.cursor] != null) openMemory(M[S.rows[S.cursor]].id); }
295
+
296
+ // ------------------------------------------------------------ selection
297
+ function togglePick(id) { S.picked.has(id) ? S.picked.delete(id) : S.picked.add(id); renderList(); }
298
+ function renderSelbar() {
299
+ const bar = $("selbar");
300
+ bar.hidden = !S.picked.size;
301
+ if (!S.picked.size) return;
302
+ const shown = S.rows.map(i => M[i].id), rest = shown.filter(id => !S.picked.has(id)).length;
303
+ bar.replaceChildren(
304
+ el("span", { class: "n", text: `${S.picked.size} selected` }),
305
+ rest ? el("button", { class: "btn", text: `Select all ${shown.length} shown`, onclick: () => { shown.forEach(id => S.picked.add(id)); renderList(); } }) : null,
306
+ el("button", { class: "btn", text: "Clear", onclick: () => { S.picked.clear(); renderList(); } }),
307
+ el("button", { class: "btn danger", text: "Delete", onclick: () => deleteMany([...S.picked]) }));
308
+ }
309
+ async function deleteMany(ids) {
310
+ const ms = ids.map(id => M[byId.get(id)]).filter(Boolean);
311
+ if (!ms.length) return;
312
+ const text = ms.length === 1 ? `Delete this memory?\n\n${ms[0].description}`
313
+ : `Delete ${ms.length} memories?\n\n` + ms.slice(0, 10).map(m => `• ${m.description}`).join("\n") + (ms.length > 10 ? `\n…and ${ms.length - 10} more` : "");
314
+ if (!confirm(text)) return;
315
+ try {
316
+ const r = await api("/api/delete", { items: ms.map(m => ({ id: m.id, base: m.hash })) });
317
+ ms.forEach(m => S.picked.delete(m.id));
318
+ if (S.open && ms.some(m => m.id === S.open)) closeDrawer(true);
319
+ await reload();
320
+ toast(ms.length === 1 ? "Memory deleted" : `${ms.length} memories deleted`, { undo: r.commit });
321
+ } catch (e) { toast(e.message, { error: true }); }
322
+ }
323
+
324
+ // ------------------------------------------------------------ markdown
325
+ const INLINE = /(`[^`]+`)|(\*\*[^*]+\*\*)|(\[\[[^\]]+\]\])|(\[[^\]]+\]\((?:https?:\/\/)[^)\s]+\))|(\*[^*\s][^*]*\*)|(https?:\/\/[^\s<>()]+)/g;
326
+ function inline(parent, text) {
327
+ let last = 0;
328
+ for (const m of text.matchAll(INLINE)) {
329
+ parent.append(text.slice(last, m.index));
330
+ const s = m[0];
331
+ if (m[1]) parent.append(el("code", { text: s.slice(1, -1) }));
332
+ else if (m[2]) { const b = el("strong"); inline(b, s.slice(2, -2)); parent.append(b); }
333
+ else if (m[3]) {
334
+ const name = s.slice(2, -2), target = byName.get(name);
335
+ parent.append(target != null
336
+ ? el("span", { class: "wikilink", role: "link", tabindex: "0", text: name, onclick: () => pick(M[target].id) })
337
+ : el("span", { text: s }));
338
+ } else if (m[4]) {
339
+ const mm = s.match(/^\[([^\]]+)\]\(([^)\s]+)\)$/);
340
+ parent.append(el("a", { href: mm[2], target: "_blank", rel: "noopener noreferrer", text: mm[1] }));
341
+ } else if (m[5]) { const i = el("em"); inline(i, s.slice(1, -1)); parent.append(i); }
342
+ else if (m[6]) parent.append(el("a", { href: s, target: "_blank", rel: "noopener noreferrer", text: s }));
343
+ last = m.index + s.length;
344
+ }
345
+ parent.append(text.slice(last));
346
+ }
347
+ const LIST = /^(\s*)([-*+]|\d+[.)])\s+(.*)$/;
348
+ function parseList(lines, i) {
349
+ const first = lines[i].match(LIST), indent = first[1].length;
350
+ const list = el(/\d/.test(first[2]) ? "ol" : "ul");
351
+ while (i < lines.length) {
352
+ const m = lines[i].match(LIST);
353
+ if (!m) {
354
+ if (lines[i].trim() && /^\s+/.test(lines[i]) && list.lastElementChild) { list.lastElementChild.append(" "); inline(list.lastElementChild, lines[i].trim()); i++; continue; }
355
+ if (!lines[i].trim() && i + 1 < lines.length && (lines[i + 1].match(LIST) || [])[1] != null && lines[i + 1].match(LIST)[1].length >= indent) { i++; continue; }
356
+ break;
357
+ }
358
+ if (m[1].length < indent) break;
359
+ if (m[1].length > indent) { const sub = parseList(lines, i); (list.lastElementChild || list).append(sub.node); i = sub.next; continue; }
360
+ const li = el("li"); inline(li, m[3]); list.append(li); i++;
361
+ }
362
+ return { node: list, next: i };
363
+ }
364
+ function markdown(src) {
365
+ const root = el("div", { class: "md" });
366
+ const lines = src.replace(/\r\n?/g, "\n").split("\n");
367
+ let i = 0;
368
+ while (i < lines.length) {
369
+ const line = lines[i];
370
+ if (!line.trim()) { i++; continue; }
371
+ let m = line.match(/^\s*(```+|~~~+)/);
372
+ if (m) {
373
+ const buf = []; i++;
374
+ while (i < lines.length && !lines[i].trim().startsWith(m[1])) buf.push(lines[i++]);
375
+ i++;
376
+ root.append(el("pre", {}, el("code", { text: buf.join("\n") })));
377
+ continue;
378
+ }
379
+ m = line.match(/^(#{1,6})\s+(.*)$/);
380
+ if (m) { const h = el("h" + Math.min(4, m[1].length)); inline(h, m[2]); root.append(h); i++; continue; }
381
+ if (/^\s*([-*_])(\s*\1){2,}\s*$/.test(line)) { root.append(el("hr")); i++; continue; }
382
+ if (/^\s*>/.test(line)) {
383
+ const buf = [];
384
+ while (i < lines.length && /^\s*>/.test(lines[i])) buf.push(lines[i++].replace(/^\s*>\s?/, ""));
385
+ root.append(el("blockquote", {}, markdown(buf.join("\n"))));
386
+ continue;
387
+ }
388
+ if (/^\s*\|.*\|\s*$/.test(line) && i + 1 < lines.length && /^\s*\|?\s*:?-{2,}/.test(lines[i + 1])) {
389
+ const cells = l => l.trim().replace(/^\||\|$/g, "").split("|").map(c => c.trim());
390
+ const table = el("table"), head = el("tr");
391
+ cells(line).forEach(c => { const th = el("th"); inline(th, c); head.append(th); });
392
+ table.append(el("thead", {}, head));
393
+ const tbody = el("tbody");
394
+ i += 2;
395
+ while (i < lines.length && /^\s*\|.*\|\s*$/.test(lines[i])) {
396
+ const tr = el("tr"); cells(lines[i++]).forEach(c => { const td = el("td"); inline(td, c); tr.append(td); }); tbody.append(tr);
397
+ }
398
+ table.append(tbody); root.append(table);
399
+ continue;
400
+ }
401
+ if (LIST.test(line)) { const r = parseList(lines, i); root.append(r.node); i = r.next; continue; }
402
+ const buf = [];
403
+ while (i < lines.length && lines[i].trim() && !/^(#{1,6}\s|\s*(```|~~~)|\s*>)/.test(lines[i]) && !LIST.test(lines[i])) buf.push(lines[i++]);
404
+ const p = el("p"); inline(p, buf.join(" ")); root.append(p);
405
+ }
406
+ return root;
407
+ }
408
+
409
+ // ------------------------------------------------------------ drawer
410
+ const drawer = $("drawer");
411
+ const TYPE_HELP = { user: "who you are", feedback: "how you want work done", project: "facts about a project", reference: "where to find things" };
412
+
413
+ function pick(id) { openMemory(id); }
414
+ function showDrawer(on) {
415
+ drawer.classList.toggle("open", on); drawer.setAttribute("aria-hidden", String(!on));
416
+ document.body.classList.toggle("drawer-open", on);
417
+ }
418
+ function openMemory(id) {
419
+ if (S.mode !== "view" && dirty() && !confirm("Discard your unsaved changes?")) return;
420
+ S.open = id; S.mode = "view";
421
+ const k = S.rows.indexOf(byId.get(id));
422
+ if (k >= 0) S.cursor = k;
423
+ renderDrawer();
424
+ showDrawer(true);
425
+ list.querySelectorAll(".row").forEach(r => {
426
+ const on = S.rows[Number(r.dataset.k)] === byId.get(id);
427
+ r.classList.toggle("open", on); r.classList.toggle("cursor", Number(r.dataset.k) === S.cursor);
428
+ if (on) r.scrollIntoView({ block: "nearest" });
429
+ });
430
+ }
431
+ function closeDrawer(force) {
432
+ if (!force && S.mode !== "view" && dirty() && !confirm("Discard your unsaved changes?")) return;
433
+ S.open = null; S.mode = "view";
434
+ showDrawer(false);
435
+ list.querySelectorAll(".row.open").forEach(r => r.classList.remove("open"));
436
+ }
437
+ function startNew() {
438
+ if (S.mode !== "view" && dirty() && !confirm("Discard your unsaved changes?")) return;
439
+ S.open = null; S.mode = "new";
440
+ S.draft = { description: "", type: "feedback", scope: "global", place: null, path: "", body: "" };
441
+ S.base = { ...S.draft };
442
+ renderDrawer();
443
+ showDrawer(true);
444
+ setTimeout(() => { const t = drawer.querySelector("textarea.desc"); if (t) t.focus(); }, 60);
445
+ }
446
+ function startEdit() {
447
+ const m = M[byId.get(S.open)];
448
+ S.mode = "edit";
449
+ S.draft = { description: m.description, type: m.type, scope: m.scope, place: m.scope === "here" ? m.place : null, path: "", body: m.body };
450
+ S.base = { ...S.draft };
451
+ renderDrawer();
452
+ }
453
+ const dirty = () => S.draft && JSON.stringify(S.draft) !== JSON.stringify(S.base);
454
+
455
+ function renderDrawer() {
456
+ drawer.replaceChildren();
457
+ if (S.mode === "view") return renderView();
458
+ return renderForm();
459
+ }
460
+ function propsList(meta) {
461
+ const dl = el("dl", { class: "props" });
462
+ const show = v => Array.isArray(v) ? (v.length ? v.join(", ") : "[]") : v === null ? "null" : String(v);
463
+ for (const [k, v] of Object.entries(meta)) {
464
+ if (v && typeof v === "object" && !Array.isArray(v)) {
465
+ dl.append(el("div", { class: "group", text: k }));
466
+ for (const [k2, v2] of Object.entries(v)) dl.append(el("dt", { class: "sub", text: k2 }), el("dd", { text: show(v2) }));
467
+ } else dl.append(el("dt", { text: k }), el("dd", { text: show(v) }));
468
+ }
469
+ return dl;
470
+ }
471
+ function renderView() {
472
+ const i = byId.get(S.open);
473
+ if (i == null) return;
474
+ const m = M[i];
475
+ const head = el("div", { class: "d-head" },
476
+ el("div", { class: "d-bar" },
477
+ el("span", { class: "where", title: m.path, text: `memories/${m.rel}` }),
478
+ iconBtn("copy", "Copy file path", () => copyText(m.path, "Path copied")),
479
+ iconBtn("edit", "Edit (e)", startEdit),
480
+ iconBtn("trash", "Delete", () => deleteMany([m.id])),
481
+ iconBtn("close", "Close (Esc)", () => closeDrawer())),
482
+ el("h1", { text: m.description }),
483
+ keywordChips(m),
484
+ el("div", { class: "chips" },
485
+ el("span", { class: "chip", text: m.type }),
486
+ el("span", { class: "chip", text: m.scope === "global" ? "loads everywhere" : `loads in ${m.place || "one place"}` }),
487
+ el("span", { class: "chip", text: m.updated ? `changed ${day(m.updated)}` : `added ${day(m.created)}` })),
488
+ (m.issues || []).length ? el("ul", { class: "issues", title: "Differs from the format in PROTOCOL.md" }, m.issues.map(t => el("li", { text: t.replace(/\*\*([^*]+?):?\*\*/g, "“$1”") }))) : null,
489
+ el("div", { class: "tabs", role: "tablist" },
490
+ ["rendered", "file"].map(t => el("button", { role: "tab", "aria-selected": String(S.tab === t), text: t === "rendered" ? "Memory" : "File",
491
+ onclick: () => { S.tab = t; renderDrawer(); } }))));
492
+ const body = el("div", { class: "d-body" });
493
+ if (S.tab === "file") body.append(el("pre", { class: "raw", text: m.raw }));
494
+ else {
495
+ body.append(propsList(m.meta), markdown(m.body));
496
+ const sim = m.similar.filter(s => s.s > 0.05).slice(0, 5);
497
+ if (sim.length) body.append(el("div", { class: "similar" }, el("h2", { text: "Similar memories" }),
498
+ sim.map(s => el("button", { onclick: () => pick(M[s.i].id) }, el("span", { class: "t", text: M[s.i].description }),
499
+ el("span", { class: "p", text: `${Math.round(s.s * 100)}%` })))));
500
+ }
501
+ drawer.append(head, body);
502
+ }
503
+
504
+ function renderForm() {
505
+ const d = S.draft, isNew = S.mode === "new";
506
+ const counter = el("span", { class: "hint" });
507
+ const count = () => { counter.textContent = `${d.description.length} / 150`; counter.classList.toggle("over", d.description.length > 150); };
508
+ const desc = el("textarea", { class: "desc", rows: 2, placeholder: "One line: what should an agent know?", value: d.description,
509
+ oninput: e => { d.description = e.target.value; count(); } });
510
+ count();
511
+ const typeSel = el("select", { onchange: e => { d.type = e.target.value; typeHint.textContent = TYPE_HELP[d.type]; } },
512
+ TYPES.map(t => el("option", { value: t, text: t, selected: d.type === t })));
513
+ const typeHint = el("span", { class: "hint", text: TYPE_HELP[d.type] });
514
+ const placeRow = el("div");
515
+ const drawPlace = () => {
516
+ placeRow.replaceChildren();
517
+ if (d.scope !== "here") return;
518
+ const options = (D.places || []).map(p => p.key);
519
+ if (d.place && !options.includes(d.place)) options.unshift(d.place);
520
+ const sel = el("select", { onchange: e => {
521
+ d.pathMode = e.target.value === "__path";
522
+ d.place = d.pathMode ? null : e.target.value;
523
+ drawPlace();
524
+ } },
525
+ el("option", { value: "", text: "Choose a place…", disabled: true, selected: !d.place && !d.pathMode }),
526
+ options.map(o => el("option", { value: o, text: o, selected: d.place === o })),
527
+ el("option", { value: "__path", text: "Another folder…", selected: !d.place && d.pathMode }));
528
+ placeRow.append(el("label", {}, "Place", sel));
529
+ if (d.pathMode) placeRow.append(el("label", { style: "margin-top:8px" }, "Folder path",
530
+ el("input", { type: "text", value: d.path, placeholder: "~/repos/project", oninput: e => { d.path = e.target.value; } })));
531
+ };
532
+ const seg = el("div", { class: "seg" }, [["global", "Everywhere"], ["here", "One place"]].map(([v, l]) =>
533
+ el("button", { type: "button", "aria-pressed": String(d.scope === v), text: l, onclick: () => {
534
+ d.scope = v; seg.querySelectorAll("button").forEach(b => b.setAttribute("aria-pressed", String(b.textContent === l))); drawPlace(); } })));
535
+ drawPlace();
536
+ const bodyArea = el("textarea", { class: "body", spellcheck: "false", value: d.body,
537
+ placeholder: "The fact itself.\n\n**Why:** …\n\n**How to apply:** …", oninput: e => { d.body = e.target.value; } });
538
+ const err = el("span", { class: "err" });
539
+ const save = el("button", { class: "btn primary", text: isNew ? "Add memory" : "Save", title: "⌘S", onclick: () => submit(err) });
540
+ drawer.append(
541
+ el("div", { class: "d-head" }, el("div", { class: "d-bar" },
542
+ el("span", { class: "where", text: isNew ? "New memory" : `Editing · memories/${M[byId.get(S.open)].rel}` }),
543
+ iconBtn("close", "Cancel (Esc)", () => isNew ? closeDrawer() : cancelEdit()))),
544
+ el("div", { class: "d-body" }, el("div", { class: "form" },
545
+ el("label", {}, el("span", { style: "display:flex;justify-content:space-between" }, "Description", counter), desc),
546
+ el("div", { class: "row" }, el("label", {}, el("span", { style: "display:flex;justify-content:space-between" }, "Type", typeHint), typeSel),
547
+ el("label", {}, "Loads", seg)),
548
+ placeRow,
549
+ el("label", {}, "Memory (Markdown)", bodyArea))),
550
+ el("div", { class: "d-foot" }, err, el("button", { class: "btn", text: "Cancel", onclick: () => isNew ? closeDrawer() : cancelEdit() }), save));
551
+ }
552
+ function cancelEdit() {
553
+ if (dirty() && !confirm("Discard your unsaved changes?")) return;
554
+ S.mode = "view"; renderDrawer();
555
+ }
556
+ async function submit(err) {
557
+ const d = S.draft;
558
+ err.textContent = "";
559
+ if (!d.description.trim()) { err.textContent = "Add a one-line description."; return; }
560
+ let place = null;
561
+ if (d.scope === "here") {
562
+ if (d.pathMode) { if (!d.path.trim()) { err.textContent = "Type the folder path."; return; } place = { path: d.path.trim() }; }
563
+ else if (d.place) place = { key: d.place };
564
+ else { err.textContent = "Choose where it loads."; return; }
565
+ }
566
+ try {
567
+ if (S.mode === "new") {
568
+ const r = await api("/api/create", { type: d.type, scope: d.scope, description: d.description, body: d.body, place });
569
+ S.mode = "view"; S.draft = null; S.open = r.id;
570
+ await reload();
571
+ openMemory(r.id);
572
+ toast("Memory added", { undo: r.commit });
573
+ } else {
574
+ const m = M[byId.get(S.open)], change = {};
575
+ if (d.description !== m.description) change.description = d.description;
576
+ if (d.type !== m.type) change.type = d.type;
577
+ if (d.scope !== m.scope) change.scope = d.scope;
578
+ if (d.body !== m.body) change.body = d.body;
579
+ if (place && (place.path || place.key !== m.place)) change.place = place;
580
+ const r = await api("/api/edit", { id: m.id, base: m.hash, change });
581
+ S.mode = "view"; S.draft = null;
582
+ await reload();
583
+ renderDrawer();
584
+ if (r.commit) toast("Saved", { undo: r.commit });
585
+ }
586
+ } catch (e) { err.textContent = e.message; }
587
+ }
588
+ async function copyText(text, done) {
589
+ try { await navigator.clipboard.writeText(text); toast(done); } catch (e) { prompt("Copy:", text); }
590
+ }
591
+
592
+ // ------------------------------------------------------------ chrome
593
+ function renderSync() {
594
+ const s = $("sync"), g = D.sync || {};
595
+ s.hidden = !(g.remote && g.ahead);
596
+ if (!s.hidden) s.textContent = `${plural(g.ahead, "change")} not pushed yet`;
597
+ }
598
+ $("new").addEventListener("click", startNew);
599
+ document.addEventListener("keydown", e => {
600
+ const typing = /^(INPUT|TEXTAREA|SELECT)$/.test(document.activeElement.tagName) && document.activeElement.type !== "checkbox";
601
+ if ((e.metaKey || e.ctrlKey) && e.key.toLowerCase() === "s" && S.mode !== "view") {
602
+ e.preventDefault(); const btn = drawer.querySelector(".d-foot .btn.primary"); if (btn) btn.click(); return;
603
+ }
604
+ if (e.key === "Escape") {
605
+ if (typing) { document.activeElement.blur(); return; }
606
+ if (S.mode === "edit") return cancelEdit();
607
+ if (S.open || S.mode === "new") return closeDrawer();
608
+ if (S.picked.size) { S.picked.clear(); renderList(); return; }
609
+ if (S.q) { q.value = ""; runSearch(""); }
610
+ return;
611
+ }
612
+ if (typing || e.metaKey || e.ctrlKey || e.altKey) return;
613
+ if (e.key === "/") { e.preventDefault(); q.focus(); q.select(); }
614
+ else if (e.key === "n") { e.preventDefault(); startNew(); }
615
+ else if (e.key === "e" && S.open && S.mode === "view") { e.preventDefault(); startEdit(); }
616
+ else if ((e.key === "j" || e.key === "ArrowDown") && S.mode === "view") { e.preventDefault(); moveCursor(1); }
617
+ else if ((e.key === "k" || e.key === "ArrowUp") && S.mode === "view") { e.preventDefault(); moveCursor(-1); }
618
+ else if (e.key === "Enter" && S.mode === "view") { e.preventDefault(); openCursor(); }
619
+ else if (e.key === "x" && S.cursor >= 0 && S.rows[S.cursor] != null) { e.preventDefault(); togglePick(M[S.rows[S.cursor]].id); }
620
+ });
621
+ addEventListener("beforeunload", e => { if (S.mode !== "view" && dirty()) { e.preventDefault(); e.returnValue = ""; } });
622
+
623
+ // pick up memories that agents write while the page is open
624
+ setInterval(async () => {
625
+ if (document.hidden || !D || S.mode !== "view") return;
626
+ try { const st = await api("/api/stamp"); if (st.stamp !== D.stamp) await reload(); } catch (e) { /* server stopped */ }
627
+ }, 20000);
628
+
629
+ (async () => {
630
+ const boot = $("boot");
631
+ if (!token) { boot.textContent = "Open this page with the link that `mem ui` prints."; return; }
632
+ try { await reload(); boot.remove(); }
633
+ catch (e) { boot.textContent = e.status === 401 ? "This link has expired. Run `mem ui` again and use the new link." : `Could not load memories: ${e.message}`; }
634
+ })();
635
+ })();