opencode-self-improvement 0.0.0-stage → 0.1.1

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,362 @@
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">
6
+ <title>Memory Viewer</title>
7
+ <style>
8
+ :root {
9
+ --bg:#0d1117; --panel:#161b22; --surface:#21262d; --border:#30363d;
10
+ --text:#e6edf3; --muted:#8d96a3; --accent:#2f81f7;
11
+ --danger:#f47067; --success:#3fb950; --warn:#d29922;
12
+ --radius:10px; --shadow:0 1px 3px rgba(0,0,0,.45);
13
+ }
14
+ *, *::before, *::after { box-sizing:border-box; }
15
+ html { color-scheme:dark; }
16
+ body { margin:0; background:var(--bg); color:var(--text); font-family:ui-sans-serif, system-ui, -apple-system, "Segoe UI", Roboto, sans-serif; font-size:14px; line-height:1.5; }
17
+ button { cursor:pointer; font-family:inherit; font-size:13px; line-height:1.4; }
18
+ button:disabled { cursor:not-allowed; opacity:.55; }
19
+
20
+ header { position:sticky; top:0; z-index:20; display:flex; align-items:center; gap:14px; flex-wrap:wrap; padding:12px 20px; background:var(--panel); border-bottom:1px solid var(--border); box-shadow:var(--shadow); }
21
+ header h1 { margin:0; font-size:15px; font-weight:650; letter-spacing:-.01em; }
22
+ header h1 span { color:var(--accent); }
23
+ .spacer { flex:1; }
24
+
25
+ .counts { display:flex; gap:6px; font-size:13px; color:var(--muted); }
26
+ .counts b { color:var(--text); font-weight:600; }
27
+
28
+ .card { background:var(--panel); border:1px solid var(--border); border-radius:var(--radius); box-shadow:var(--shadow); }
29
+ .card h2 { margin:0; padding:12px 14px; font-size:13px; font-weight:600; color:var(--muted); text-transform:uppercase; letter-spacing:.04em; border-bottom:1px solid var(--border); }
30
+ .card .body { padding:14px; }
31
+
32
+ main { display:flex; gap:18px; align-items:flex-start; padding:18px 20px 28px; }
33
+
34
+ .toolbar { display:flex; gap:8px; flex-wrap:wrap; padding:14px; }
35
+ .toolbar input[type=search] { flex:1; min-width:200px; }
36
+
37
+ .field, input, select, textarea { font-family:inherit; background:var(--surface); color:var(--text); border:1px solid var(--border); border-radius:8px; padding:7px 10px; }
38
+ input:focus, select:focus, textarea:focus { border-color:var(--accent); outline:none; box-shadow:0 0 0 3px rgba(47,129,247,.25); }
39
+ textarea { resize:vertical; }
40
+
41
+ table { width:100%; border-collapse:separate; border-spacing:0; }
42
+ th, td { text-align:left; padding:9px 12px; border-bottom:1px solid var(--border); vertical-align:top; }
43
+ th { position:sticky; top:0; z-index:1; background:var(--panel); font-size:12px; font-weight:600; color:var(--muted); text-transform:uppercase; letter-spacing:.03em; }
44
+ td.ts { color:var(--muted); font-size:12px; white-space:nowrap; }
45
+ td.content { word-break:break-word; }
46
+
47
+ .btn { padding:5px 11px; background:var(--surface); border-color:var(--border); font-weight:550; }
48
+ .btn:hover { filter:brightness(1.12); }
49
+ .btn:active { filter:brightness(.92); }
50
+ .btn.danger { color:#fff; background:#5d1a17; border-color:var(--danger); }
51
+ .btn.warn { color:#241d05; background:var(--warn); border-color:var(--warn); }
52
+ .btn.primary { color:#fff; background:var(--accent); border-color:var(--accent); }
53
+ .btn.primary:hover { filter:brightness(1.1); }
54
+
55
+ .badge { display:inline-flex; align-items:center; padding:2px 9px; border-radius:999px; font-size:12px; font-weight:600; border:1px solid var(--border); color:var(--muted); }
56
+ .badge.type { color:#79b8ff; border-color:#1f4a77; background:rgba(47,129,247,.12); }
57
+ .badge.scope { color:#e3b341; border-color:#5a4607; background:rgba(210,153,34,.12); }
58
+ .badge.status-active { color:var(--success); border-color:#1f4f27; background:rgba(63,185,80,.12); }
59
+ .badge.status-superseded { color:var(--muted); text-decoration:line-through; }
60
+ .badge.status-blocked { color:#ff8e86; border-color:#5d1a17; background:rgba(244,112,103,.14); }
61
+ .badge.status-deleted { color:#ff8e86; border-color:#5d1a17; background:rgba(244,112,103,.14); }
62
+
63
+ .actions { white-space:nowrap; display:flex; gap:6px; justify-content:flex-end; }
64
+
65
+ .modal { position:fixed; inset:0; z-index:40; display:none; align-items:center; justify-content:center; background:rgba(1,4,9,.68); backdrop-filter:blur(2px); padding:18px; }
66
+ .modal.open { display:flex; }
67
+ .modal .box { width:640px; max-width:94vw; padding:18px; }
68
+ .modal h2 { margin:0 0 6px; font-size:15px; color:var(--text); }
69
+ .modal .muted { margin:0 0 12px; color:var(--muted); white-space:pre-wrap; }
70
+ .modal .row { display:flex; gap:10px; margin-bottom:12px; }
71
+ .modal .row input { flex:1; }
72
+ .modal textarea { width:100%; min-height:96px; margin-bottom:12px; display:block; }
73
+ .modal .btns { display:flex; gap:8px; justify-content:flex-end; }
74
+
75
+ .empty { padding:34px; text-align:center; color:var(--muted); font-size:13px; }
76
+ .toast { position:fixed; right:18px; bottom:18px; z-index:60; max-width:340px; padding:11px 15px; font-size:13px; }
77
+ .toast.error { border-color:var(--danger); color:#ff8e86; }
78
+ </style>
79
+ </head>
80
+ <body>
81
+ <header>
82
+ <h1><span>&#9679;</span> Memory Viewer</h1>
83
+ <div class="counts">
84
+ <span>active <b id="cActive">0</b></span>
85
+ <span>superseded <b id="cSuper">0</b></span>
86
+ <span>blocked <b id="cBlocked">0</b></span>
87
+ <span>deleted <b id="cDeleted">0</b></span>
88
+ </div>
89
+ <span class="spacer"></span>
90
+ <code id="rootLabel" style="color:var(--muted);font-size:12px"></code>
91
+ </header>
92
+
93
+ <main>
94
+ <section class="list">
95
+ <div class="card toolbar">
96
+ <input type="search" id="search" placeholder="Search content / scope / type…" aria-label="Search">
97
+ <select id="fScope"><option value="">any scope</option></select>
98
+ <select id="fType"><option value="">any type</option></select>
99
+ <select id="fStatus" title="Filter by status">
100
+ <option value="active">active</option>
101
+ <option value="all">all (incl. superseded)</option>
102
+ <option value="superseded">superseded</option>
103
+ <option value="blocked">blocked</option>
104
+ <option value="deleted">deleted</option>
105
+ </select>
106
+ </div>
107
+
108
+ <div class="card">
109
+ <table>
110
+ <thead><tr><th>ts</th><th>type</th><th>scope</th><th>content</th><th>status</th><th></th></tr></thead>
111
+ <tbody id="rows"></tbody>
112
+ </table>
113
+ <div id="empty" class="empty" style="display:none">No memories.</div>
114
+ </div>
115
+ </section>
116
+ </main>
117
+
118
+ <div id="editModal" class="modal">
119
+ <div class="box card">
120
+ <h2>Edit memory</h2>
121
+ <div class="row">
122
+ <input id="eType" placeholder="type" title="Memory type">
123
+ <input id="eScope" placeholder="scope" title="Scope">
124
+ </div>
125
+ <textarea id="eContent" placeholder="content"></textarea>
126
+ <input id="eReason" placeholder="reason (required, e.g. corrected after human review)" style="width:100%" title="Reason">
127
+ <div class="btns">
128
+ <button id="eCancel" class="btn">Cancel</button>
129
+ <button id="eSave" class="btn primary" title="Saves a new entry and marks this one superseded">Save (snapshot + supersede)</button>
130
+ </div>
131
+ </div>
132
+ </div>
133
+
134
+ <div id="promptModal" class="modal">
135
+ <div class="box card">
136
+ <h2 id="promptTitle"></h2>
137
+ <p id="promptHint" class="muted"></p>
138
+ <textarea id="promptReason" rows="2" placeholder="reason (required)" title="Reason"></textarea>
139
+ <div class="btns">
140
+ <button id="promptCancel" class="btn">Cancel</button>
141
+ <button id="promptOk" class="btn primary"></button>
142
+ </div>
143
+ </div>
144
+ </div>
145
+
146
+ <script>
147
+ const $ = (id) => document.getElementById(id);
148
+ let DATA = null;
149
+ let editingTs = null;
150
+
151
+ async function api(path, body) {
152
+ const res = await fetch(path, body
153
+ ? { method: "POST", headers: { "content-type": "application/json" }, body: JSON.stringify(body) }
154
+ : undefined);
155
+ const json = await res.json().catch(() => ({}));
156
+ if (!res.ok) throw new Error(json.error || `${res.status} ${res.statusText}`);
157
+ return json;
158
+ }
159
+
160
+ function toast(msg, isError = false) {
161
+ const el = document.createElement("div");
162
+ el.className = "toast card" + (isError ? " error" : "");
163
+ el.textContent = msg;
164
+ document.body.appendChild(el);
165
+ setTimeout(() => el.remove(), 4000);
166
+ }
167
+
168
+ function currentList() {
169
+ const fus = $("fStatus").value;
170
+ if (fus === "blocked") return { list: DATA.blocked, kind: "blocked" };
171
+ if (fus === "deleted") return { list: DATA.deleted, kind: "deleted" };
172
+ if (fus === "superseded") return { list: DATA.memories.filter((m) => m.superseded_by), kind: "superseded" };
173
+ if (fus === "all") return { list: DATA.memories, kind: "all" };
174
+ return { list: DATA.memories.filter((m) => !m.superseded_by), kind: "active" };
175
+ }
176
+
177
+ function render() {
178
+ if (!DATA) return;
179
+ const { list, kind } = currentList();
180
+ const q = $("search").value.toLowerCase();
181
+ const fs = $("fScope").value;
182
+ const ft = $("fType").value;
183
+ let rows = list.slice();
184
+ if (fs) rows = rows.filter((m) => m.scope === fs);
185
+ if (ft) rows = rows.filter((m) => m.type === ft);
186
+ if (q) rows = rows.filter((m) => `${m.type} ${m.scope} ${m.content}`.toLowerCase().includes(q));
187
+
188
+ $("rows").innerHTML = rows.map((m) => {
189
+ const status = kind === "blocked" ? "blocked" : kind === "deleted" ? "deleted"
190
+ : m.superseded_by ? "superseded" : "active";
191
+ const stBadge = status === "superseded" ? badge("status-superseded", `superseded \u2192 ${m.superseded_by.slice(0,8)}`)
192
+ : badge("status-" + status, status);
193
+ const actions = kind === "blocked"
194
+ ? `<button class="btn warn" data-action="unblock" data-ts="${esc(m.ts)}" title="Restore this memory to the read paths">unblock</button>`
195
+ : kind === "deleted"
196
+ ? `<button class="btn warn" data-action="restore-deleted" data-ts="${esc(m.ts)}" data-type="${esc(m.type)}" data-scope="${esc(m.scope)}" data-content="${esc(m.content)}" title="Re-add this deleted entry to memory">restore</button>`
197
+ : `<button class="btn" data-action="edit" data-ts="${esc(m.ts)}" title="Edit: snapshot, supersede this entry, append the replacement">edit</button>
198
+ <button class="btn danger" data-action="delete" data-ts="${esc(m.ts)}" title="Delete: snapshot the file, then remove this entry (audited)">delete</button>
199
+ <button class="btn danger" data-action="block" data-ts="${esc(m.ts)}" title="Block (poison): keep on disk but hide from recall/inject">block</button>`;
200
+ return `<tr>
201
+ <td class="ts">${m.ts.split("T")[0]}<br>${(m.ts.split("T")[1] || "").slice(0,8)}</td>
202
+ <td>${badge("type", esc(m.type))}</td>
203
+ <td>${badge("scope", esc(m.scope))}</td>
204
+ <td class="content">${esc(m.content)}</td>
205
+ <td>${stBadge}</td>
206
+ <td class="actions">${actions}</td>
207
+ </tr>`;
208
+ }).join("");
209
+
210
+ $("empty").style.display = rows.length ? "none" : "block";
211
+ $("cActive").textContent = DATA.memories.filter((m) => !m.superseded_by).length;
212
+ $("cSuper").textContent = DATA.memories.filter((m) => m.superseded_by).length;
213
+ $("cBlocked").textContent = DATA.blocked.length;
214
+ $("cDeleted").textContent = DATA.deleted.length;
215
+ }
216
+
217
+ function esc(s) { return String(s).replace(/[&<>"]/g, (c) => ({ "&":"&amp;", "<":"&lt;", ">":"&gt;", '"':"&quot;" }[c])); }
218
+
219
+ async function refresh() {
220
+ DATA = await api("/api/data");
221
+ const scopes = [...new Set(DATA.memories.map((m) => m.scope))].sort();
222
+ const types = [...new Set(DATA.memories.map((m) => m.type))].sort();
223
+ $("fScope").innerHTML = `<option value="">any scope</option>` + scopes.map((s) => `<option>${esc(s)}</option>`).join("");
224
+ $("fType").innerHTML = `<option value="">any type</option>` + types.map((t) => `<option>${esc(t)}</option>`).join("");
225
+ $("rootLabel").textContent = DATA.roots.memoryRoot;
226
+ render();
227
+ }
228
+
229
+ function badge(cls, text) { return `<span class="badge ${cls}">${text}</span>`; }
230
+
231
+ function openEdit(ts) {
232
+ const m = DATA.memories.find((x) => x.ts === ts);
233
+ editingTs = ts;
234
+ $("eType").value = m.type;
235
+ $("eScope").value = m.scope;
236
+ $("eContent").value = m.content;
237
+ $("eReason").value = m.reason || "";
238
+ $("editModal").classList.add("open");
239
+ }
240
+
241
+ async function saveEdit() {
242
+ const reason = $("eReason").value.trim();
243
+ const content = $("eContent").value.trim();
244
+ if (!reason) { toast("reason required", true); return; }
245
+ if (!content) { toast("content required", true); return; }
246
+ try {
247
+ const r = await api("/api/edit", {
248
+ ts: editingTs, type: $("eType").value.trim(), scope: $("eScope").value.trim(),
249
+ content, reason,
250
+ });
251
+ $("editModal").classList.remove("open");
252
+ toast(`edited \u2192 superseded ${r.superseded_by.slice(0,8)}`);
253
+ await refresh();
254
+ } catch (e) { toast(String(e.message), true); }
255
+ }
256
+
257
+ async function del(ts) {
258
+ askModal(
259
+ `Delete memory ${ts.slice(0, 19)}?`,
260
+ "Removes the entry on disk. A pre-delete snapshot and an audited reason are retained.",
261
+ "Delete",
262
+ async (reason) => {
263
+ try {
264
+ await api("/api/delete", { ts, reason });
265
+ toast("deleted (snapshot retained)");
266
+ await refresh();
267
+ } catch (e) { toast(String(e.message), true); }
268
+ },
269
+ );
270
+ }
271
+
272
+ async function block(ts) {
273
+ const m = DATA.memories.find((x) => x.ts === ts);
274
+ const preview = m ? (m.content.length > 120 ? m.content.slice(0, 120) + "…" : m.content) : "";
275
+ askModal(
276
+ `Block memory ${ts.slice(0, 19)}?`,
277
+ `Poison intervention — it stays on disk but is hidden from recall/inject.\n\n"${preview}"`,
278
+ "Block",
279
+ async (reason) => {
280
+ try {
281
+ await api("/api/block", { ts, reason });
282
+ toast("blocked — suppressed from read paths");
283
+ await refresh();
284
+ } catch (e) { toast(String(e.message), true); }
285
+ },
286
+ );
287
+ }
288
+
289
+ async function unblock(ts) {
290
+ try {
291
+ await api("/api/unblock", { ts });
292
+ toast("unblocked");
293
+ await refresh();
294
+ } catch (e) { toast(String(e.message), true); }
295
+ }
296
+
297
+ function restoreEntry(type, scope, content) {
298
+ askModal(
299
+ "Restore deleted memory?",
300
+ "Re-adds this entry to memory from its audit record.",
301
+ "Restore",
302
+ async () => {
303
+ try {
304
+ await api("/api/restore-entry", { type, scope, content });
305
+ toast("restored to memory");
306
+ await refresh();
307
+ } catch (e) { toast(String(e.message), true); }
308
+ },
309
+ );
310
+ }
311
+
312
+ document.addEventListener("DOMContentLoaded", refresh);
313
+ for (const id of ["search", "fScope", "fType", "fStatus"]) $(id).addEventListener("change", render);
314
+ $("search").addEventListener("input", render);
315
+ $("eCancel").addEventListener("click", () => $("editModal").classList.remove("open"));
316
+ $("eSave").addEventListener("click", saveEdit);
317
+
318
+ $("rows").addEventListener("click", (e) => {
319
+ const btn = e.target.closest("button[data-action]");
320
+ if (!btn) return;
321
+ const { action, ts } = btn.dataset;
322
+ if (action === "edit") openEdit(ts);
323
+ else if (action === "delete") del(ts);
324
+ else if (action === "block") block(ts);
325
+ else if (action === "unblock") unblock(ts);
326
+ else if (action === "restore-deleted") {
327
+ const { type, scope, content } = btn.dataset;
328
+ restoreEntry(type, scope, content);
329
+ }
330
+ });
331
+
332
+ let pending = null;
333
+
334
+ function askModal(title, hint, okLabel, onOk) {
335
+ $("promptTitle").textContent = title;
336
+ $("promptHint").textContent = hint;
337
+ $("promptOk").textContent = okLabel;
338
+ $("promptReason").value = "";
339
+ pending = onOk;
340
+ $("promptModal").classList.add("open");
341
+ $("promptReason").focus();
342
+ }
343
+
344
+ function closePrompt() {
345
+ $("promptModal").classList.remove("open");
346
+ pending = null;
347
+ }
348
+
349
+ $("promptCancel").addEventListener("click", closePrompt);
350
+ $("promptOk").addEventListener("click", () => {
351
+ const reason = $("promptReason").value.trim();
352
+ if (!reason) { toast("reason required", true); return; }
353
+ const fn = pending;
354
+ closePrompt();
355
+ if (fn) fn(reason);
356
+ });
357
+ $("promptModal").addEventListener("click", (e) => {
358
+ if (e.target === $("promptModal")) closePrompt();
359
+ });
360
+ </script>
361
+ </body>
362
+ </html>
@@ -0,0 +1,191 @@
1
+ // Memory Viewer server. Bundled with the plugin: run `bun memory-view` (or
2
+ // `bun run src/viewer/server.ts`) from the repo root and open the printed URL.
3
+ // Binds 127.0.0.1 only — the memory store is local, personal data. All reads go
4
+ // through the same loadAll() used by recall/inject (so blocked entries are
5
+ // hidden), and all mutations are non-destructive (snapshot + audit + supersede).
6
+ // I2: serves only file data; never writes into any repo.
7
+ import { join } from "node:path";
8
+ import { serve, type Server } from "bun";
9
+ import { deletionsFile, memoryRoot, snapshotDir } from "../config.ts";
10
+ import { blockedEntries } from "../blocklist.ts";
11
+ import { listSnapshots, restoreSnapshot } from "../undo.ts";
12
+ import { appendMemory, loadAll, memoryFiles } from "../memory.ts";
13
+ import { parseLine, readLines } from "../logfmt.ts";
14
+ import { deleteMemory, editMemory, addBlock, removeBlock, type MutationResult } from "./mutate.ts";
15
+
16
+ const PAGE = join(import.meta.dir, "page.html");
17
+ const HOST = process.env.OPENCODE_VIEWER_HOST || "127.0.0.1";
18
+ const PORT = Number(process.env.OPENCODE_VIEWER_PORT || 8787);
19
+
20
+ function json(body: unknown, status = 200): Response {
21
+ return new Response(JSON.stringify(body), {
22
+ status,
23
+ headers: { "content-type": "application/json" },
24
+ });
25
+ }
26
+
27
+ function error(msg: string, status = 400): Response {
28
+ return json({ error: msg }, status);
29
+ }
30
+
31
+ interface Row {
32
+ ts: string;
33
+ type: string;
34
+ scope: string;
35
+ content: string;
36
+ reason?: string;
37
+ }
38
+
39
+ // Deleted entries live only in the audit log (their memory line was removed),
40
+ // so surface the recorded data + reason from deletions.logfmt.
41
+ async function deletedRows(): Promise<Row[]> {
42
+ const lines = await readLines(deletionsFile());
43
+ return lines
44
+ .map((l) => parseLine(l))
45
+ .filter((f): f is NonNullable<typeof f> => f !== null)
46
+ .map((f) => ({
47
+ ts: String(f.original_ts ?? f.ts ?? ""),
48
+ type: String(f.type ?? ""),
49
+ scope: String(f.scope ?? ""),
50
+ content: String(f.content ?? ""),
51
+ reason: String(f.reason ?? ""),
52
+ }));
53
+ }
54
+
55
+ // Blocked entries are still on disk but hidden by the blocklist; scan every
56
+ // memory file and keep the ones whose ts is blocked, with their full data.
57
+ async function blockedRows(): Promise<Row[]> {
58
+ const blocked = await blockedEntries();
59
+ const byTs = new Map(blocked.map((b) => [b.ts, b.reason]));
60
+ const out: Row[] = [];
61
+ for (const file of await memoryFiles()) {
62
+ for (const line of await readLines(file)) {
63
+ const f = parseLine(line);
64
+ if (!f || typeof f.ts !== "string") continue;
65
+ if (!byTs.has(f.ts)) continue;
66
+ out.push({
67
+ ts: f.ts,
68
+ type: String(f.type ?? ""),
69
+ scope: String(f.scope ?? ""),
70
+ content: String(f.content ?? ""),
71
+ reason: byTs.get(f.ts) ?? "",
72
+ });
73
+ }
74
+ }
75
+ return out;
76
+ }
77
+
78
+ export interface ViewerServer {
79
+ server: Server<any>;
80
+ url: string;
81
+ }
82
+
83
+ export async function startViewer(port: number = PORT): Promise<ViewerServer> {
84
+ const page = await Bun.file(PAGE).text();
85
+ const server = serve({
86
+ hostname: HOST,
87
+ port,
88
+ fetch(req, srv) {
89
+ const url = new URL(req.url);
90
+ if (url.pathname === "/") {
91
+ return new Response(page, {
92
+ headers: { "content-type": "text/html; charset=utf-8" },
93
+ });
94
+ }
95
+ if (req.method === "GET" && url.pathname === "/api/data") {
96
+ return loaded().then(
97
+ (d) => json(d),
98
+ () => error("failed to load memory data"),
99
+ );
100
+ }
101
+ return route(req, url).catch((e) => error(e instanceof Error ? e.message : String(e)));
102
+ },
103
+ });
104
+ const bound = server.port ?? port;
105
+ return { server, url: `http://${HOST}:${bound}/` };
106
+ }
107
+
108
+ async function loaded(): Promise<Record<string, unknown>> {
109
+ const [memories, blocked, deleted, snaps] = await Promise.all([
110
+ loadAll(),
111
+ blockedRows(),
112
+ deletedRows(),
113
+ listSnapshots(),
114
+ ]);
115
+ const blockedByTs = new Set(blocked.map((b) => b.ts));
116
+ return {
117
+ roots: { memoryRoot: memoryRoot(), snapshotDir: snapshotDir() },
118
+ memories: memories.map((m) => ({
119
+ ts: m.ts,
120
+ type: m.type,
121
+ scope: m.scope,
122
+ content: m.content,
123
+ issue: m.issue,
124
+ tags: m.tags,
125
+ superseded_by: m.superseded_by,
126
+ reason: m.reason,
127
+ blocked: blockedByTs.has(m.ts),
128
+ })),
129
+ blocked,
130
+ deleted,
131
+ snapshots: snaps.map((s) => ({ name: s.name, bytes: s.bytes })),
132
+ };
133
+ }
134
+
135
+ async function route(req: Request, url: URL): Promise<Response> {
136
+ if (req.method === "POST") {
137
+ const body = (await req.json().catch(() => ({}))) as Record<string, unknown>;
138
+ if (url.pathname === "/api/edit") return editRoute(body);
139
+ if (url.pathname === "/api/delete") return deleteRoute(body);
140
+ if (url.pathname === "/api/block") return blockRoute(body);
141
+ if (url.pathname === "/api/unblock") return unblockRoute(body);
142
+ if (url.pathname === "/api/restore-entry") return restoreEntryRoute(body);
143
+ if (url.pathname === "/api/restore") return restoreRoute(body);
144
+ }
145
+ return json({ error: "not found" }, 404);
146
+ }
147
+
148
+ async function editRoute(body: Record<string, unknown>): Promise<Response> {
149
+ const r: MutationResult = await editMemory({
150
+ ts: String(body.ts ?? ""),
151
+ type: String(body.type ?? ""),
152
+ scope: String(body.scope ?? ""),
153
+ content: String(body.content ?? ""),
154
+ issue: typeof body.issue === "string" ? body.issue : undefined,
155
+ tags: Array.isArray(body.tags) ? (body.tags as string[]) : undefined,
156
+ reason: String(body.reason ?? ""),
157
+ });
158
+ return json(r);
159
+ }
160
+
161
+ async function deleteRoute(body: Record<string, unknown>): Promise<Response> {
162
+ return json(await deleteMemory({ ts: String(body.ts ?? ""), reason: String(body.reason ?? "") }));
163
+ }
164
+
165
+ async function blockRoute(body: Record<string, unknown>): Promise<Response> {
166
+ return json({ ts: await addBlock(String(body.ts ?? ""), String(body.reason ?? "")) });
167
+ }
168
+
169
+ async function unblockRoute(body: Record<string, unknown>): Promise<Response> {
170
+ return json({ unblocked: await removeBlock(String(body.ts ?? "")) });
171
+ }
172
+
173
+ async function restoreEntryRoute(body: Record<string, unknown>): Promise<Response> {
174
+ const t = await appendMemory({
175
+ type: String(body.type ?? ""),
176
+ scope: String(body.scope ?? ""),
177
+ content: String(body.content ?? ""),
178
+ });
179
+ return json({ restored: t });
180
+ }
181
+
182
+ async function restoreRoute(body: Record<string, unknown>): Promise<Response> {
183
+ const target = await restoreSnapshot(join(snapshotDir(), String(body.name ?? "")), memoryRoot());
184
+ return json({ restored: target });
185
+ }
186
+
187
+ if (import.meta.main) {
188
+ startViewer(PORT).then(({ url }) => {
189
+ console.log(`\nMemory Viewer: ${url}\n(127.0.0.1 only; snapshot+audit are mandatory on every edit/delete)`);
190
+ });
191
+ }
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2025 Kris
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.