@membank/dashboard 0.5.5 → 0.7.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 (34) hide show
  1. package/README.md +17 -1
  2. package/dist/bin.mjs +276 -0
  3. package/dist/client/assets/WorkspaceCenter-F8ctl0ZL.js +1 -0
  4. package/dist/client/assets/WorkspaceMemoryDetail-BLFoEX3q.js +1 -0
  5. package/dist/client/assets/_projectId-D3UHHv9-.js +1 -0
  6. package/dist/client/assets/_projectId._memoryId-CZOJ7L4F.js +1 -0
  7. package/dist/client/assets/_projectId.index-Dc3jTh6p.js +1 -0
  8. package/dist/client/assets/collections-y7eMFwFc.js +19 -0
  9. package/dist/client/assets/empty-C4Q9vAt-.js +1 -0
  10. package/dist/client/assets/global-BZgjloNm.js +1 -0
  11. package/dist/client/assets/global._memoryId-DbiHUsW4.js +1 -0
  12. package/dist/client/assets/global.index-w6voN-YE.js +1 -0
  13. package/dist/client/assets/index-B3Ej81eU.js +10 -0
  14. package/dist/client/assets/index-DHtBroPC.css +2 -0
  15. package/dist/client/assets/input-BdBFU9te.js +1 -0
  16. package/dist/client/assets/preload-helper-DYD3iRAO.js +1 -0
  17. package/dist/client/assets/routes-DdtLVWUF.js +1 -0
  18. package/dist/client/assets/schemas-jSuIeSIL.js +39 -0
  19. package/dist/client/assets/skeleton-BDvkvqhf.js +1 -0
  20. package/dist/client/assets/types-DT_fBrSI.js +1 -0
  21. package/dist/client/assets/useRenderElement-DhejdWAQ.js +1 -0
  22. package/dist/client/index.html +7 -2
  23. package/dist/index.cjs +130 -108
  24. package/dist/index.d.cts +4 -2
  25. package/dist/index.d.mts +4 -2
  26. package/dist/index.mjs +131 -109
  27. package/package.json +15 -6
  28. package/dist/client/assets/MagnifyingGlass.es-CGxS0MwI.js +0 -1
  29. package/dist/client/assets/index-DZxM00o8.js +0 -119
  30. package/dist/client/assets/index-GVEqDPxR.css +0 -1
  31. package/dist/client/assets/memories-ja38P73o.js +0 -1
  32. package/dist/client/assets/memories._id-DeaZiiQb.js +0 -1
  33. package/dist/client/assets/memories.index-DUtHo-Be.js +0 -1
  34. package/dist/client/assets/native-select-BpuM7pgJ.js +0 -1
package/dist/index.cjs CHANGED
@@ -32,6 +32,20 @@ let open = require("open");
32
32
  open = __toESM(open, 1);
33
33
  //#region src/server/index.ts
34
34
  const PREFERRED_PORT = 3847;
35
+ function buildSynthesisTools(repo, querier) {
36
+ return {
37
+ queryMemory: async (args) => {
38
+ const results = await querier.query({
39
+ query: args.query,
40
+ projectHash: args.global === true ? void 0 : args.projectHash,
41
+ limit: args.limit ?? 20,
42
+ includePinned: true
43
+ });
44
+ return JSON.stringify(results);
45
+ },
46
+ getMemorySummary: async () => JSON.stringify(repo.stats())
47
+ };
48
+ }
35
49
  const MIME = {
36
50
  ".js": "application/javascript",
37
51
  ".mjs": "application/javascript",
@@ -45,46 +59,6 @@ const MIME = {
45
59
  ".png": "image/png",
46
60
  ".json": "application/json"
47
61
  };
48
- function parseReviewEvent(row) {
49
- return {
50
- id: row.id,
51
- memoryId: row.memory_id,
52
- conflictingMemoryId: row.conflicting_memory_id,
53
- similarity: row.similarity,
54
- conflictContentSnapshot: row.conflict_content_snapshot,
55
- reason: row.reason,
56
- createdAt: row.created_at,
57
- resolvedAt: row.resolved_at
58
- };
59
- }
60
- function getReviewEventsForMemories(db, ids) {
61
- if (ids.length === 0) return /* @__PURE__ */ new Map();
62
- const placeholders = ids.map(() => "?").join(", ");
63
- const rows = db.db.prepare(`SELECT * FROM memory_review_events WHERE memory_id IN (${placeholders}) AND resolved_at IS NULL ORDER BY created_at DESC`).all(...ids);
64
- const map = /* @__PURE__ */ new Map();
65
- for (const row of rows) {
66
- const event = parseReviewEvent(row);
67
- const existing = map.get(event.memoryId) ?? [];
68
- existing.push(event);
69
- map.set(event.memoryId, existing);
70
- }
71
- return map;
72
- }
73
- function parseRow(row, projects = [], reviewEvents = []) {
74
- return {
75
- id: row.id,
76
- content: row.content,
77
- type: row.type,
78
- tags: JSON.parse(row.tags),
79
- projects,
80
- sourceHarness: row.source,
81
- accessCount: row.access_count,
82
- pinned: row.pinned !== 0,
83
- reviewEvents,
84
- createdAt: row.created_at,
85
- updatedAt: row.updated_at
86
- };
87
- }
88
62
  function tryPort(port) {
89
63
  return new Promise((resolve, reject) => {
90
64
  const server = (0, node_net.createServer)();
@@ -108,29 +82,32 @@ async function findFreePort(preferred) {
108
82
  });
109
83
  }
110
84
  }
111
- function createApiApp(db, repo, projectRepo) {
85
+ function aggregateActivity(memories, days) {
86
+ const cutoff = /* @__PURE__ */ new Date();
87
+ cutoff.setDate(cutoff.getDate() - days);
88
+ const cutoffStr = cutoff.toISOString().slice(0, 10);
89
+ const dayCounts = {};
90
+ for (const m of memories) {
91
+ const day = m.createdAt.slice(0, 10);
92
+ if (day >= cutoffStr) dayCounts[day] = (dayCounts[day] ?? 0) + 1;
93
+ const updateDay = m.updatedAt.slice(0, 10);
94
+ if (updateDay !== day && updateDay >= cutoffStr) dayCounts[updateDay] = (dayCounts[updateDay] ?? 0) + 1;
95
+ }
96
+ return Object.entries(dayCounts).map(([date, count]) => ({
97
+ date,
98
+ count
99
+ })).sort((a, b) => a.date.localeCompare(b.date));
100
+ }
101
+ function createApiApp(repo, projectRepo, embedder, queryEngine, synthRepo) {
112
102
  const app = new hono.Hono();
113
103
  app.get("/api/memories", (c) => {
114
104
  const { type, pinned, needsReview, search, projectId } = c.req.query();
115
- const conditions = [];
116
- const params = [];
117
- if (type) {
118
- conditions.push("m.type = ?");
119
- params.push(type);
120
- }
121
- if (pinned === "true") conditions.push("m.pinned = 1");
122
- if (needsReview === "true") conditions.push("EXISTS (SELECT 1 FROM memory_review_events e WHERE e.memory_id = m.id AND e.resolved_at IS NULL)");
123
- if (projectId === "global") conditions.push("m.id NOT IN (SELECT memory_id FROM memory_projects)");
124
- else if (projectId) {
125
- conditions.push("m.id IN (SELECT memory_id FROM memory_projects WHERE project_id = ?)");
126
- params.push(projectId);
127
- }
128
- const where = conditions.length > 0 ? `WHERE ${conditions.join(" AND ")}` : "";
129
- const rows = db.db.prepare(`SELECT m.* FROM memories m ${where} ORDER BY m.created_at DESC`).all(...params);
130
- const ids = rows.map((r) => r.id);
131
- const projectMap = projectRepo.getProjectsForMemories(ids);
132
- const eventMap = getReviewEventsForMemories(db, ids);
133
- let memories = rows.map((r) => parseRow(r, projectMap.get(r.id) ?? [], eventMap.get(r.id) ?? []));
105
+ let memories = repo.list({
106
+ type,
107
+ pinned: pinned === "true" ? true : void 0,
108
+ needsReview: needsReview === "true" ? true : void 0,
109
+ projectId
110
+ });
134
111
  if (search) {
135
112
  const q = search.toLowerCase();
136
113
  memories = memories.filter((m) => m.content.toLowerCase().includes(q));
@@ -138,50 +115,33 @@ function createApiApp(db, repo, projectRepo) {
138
115
  return c.json(memories);
139
116
  });
140
117
  app.get("/api/memories/:id", (c) => {
141
- const id = c.req.param("id");
142
- const row = db.db.prepare("SELECT * FROM memories WHERE id = ?").get(id);
143
- if (!row) return c.json({ error: "Not found" }, 404);
144
- const projectMap = projectRepo.getProjectsForMemories([id]);
145
- const eventMap = getReviewEventsForMemories(db, [id]);
146
- return c.json(parseRow(row, projectMap.get(id) ?? [], eventMap.get(id) ?? []));
118
+ const memory = repo.findById(c.req.param("id"));
119
+ if (!memory) return c.json({ error: "Not found" }, 404);
120
+ return c.json(memory);
147
121
  });
148
122
  app.patch("/api/memories/:id", async (c) => {
149
123
  const id = c.req.param("id");
150
- if (!db.db.prepare("SELECT id FROM memories WHERE id = ?").get(id)) return c.json({ error: "Not found" }, 404);
124
+ if (!repo.findById(id)) return c.json({ error: "Not found" }, 404);
151
125
  const body = await c.req.json();
152
- const sets = [];
153
- const sqlParams = [];
154
- if (body.pinned !== void 0) {
155
- sets.push("pinned = ?");
156
- sqlParams.push(body.pinned ? 1 : 0);
157
- }
158
- if (body.type !== void 0) {
159
- sets.push("type = ?");
160
- sqlParams.push(body.type);
161
- }
162
- if (sets.length > 0) {
163
- sets.push("updated_at = ?");
164
- sqlParams.push((/* @__PURE__ */ new Date()).toISOString(), id);
165
- db.db.prepare(`UPDATE memories SET ${sets.join(", ")} WHERE id = ?`).run(...sqlParams);
166
- }
126
+ if (body.pinned !== void 0) repo.setPin(id, body.pinned);
167
127
  if (body.needsReview === false) repo.resolveReviewEvents(id);
168
- if (body.content !== void 0 || body.tags !== void 0) await repo.update(id, {
128
+ if (body.content !== void 0 || body.tags !== void 0 || body.type !== void 0) await (0, _membank_core.updateMemory)(id, {
169
129
  content: body.content,
170
- tags: body.tags
130
+ tags: body.tags,
131
+ type: body.type
132
+ }, {
133
+ repo,
134
+ embedder
171
135
  });
172
- const updated = db.db.prepare("SELECT * FROM memories WHERE id = ?").get(id);
173
- const projectMap = projectRepo.getProjectsForMemories([id]);
174
- const eventMap = getReviewEventsForMemories(db, [id]);
175
- return c.json(parseRow(updated, projectMap.get(id) ?? [], eventMap.get(id) ?? []));
136
+ return c.json(repo.findById(id));
176
137
  });
177
- app.delete("/api/memories/:id", async (c) => {
178
- await repo.delete(c.req.param("id"));
138
+ app.delete("/api/memories/:id", (c) => {
139
+ repo.delete(c.req.param("id"));
179
140
  return c.json({ ok: true });
180
141
  });
181
142
  app.post("/api/memories/:id/projects", async (c) => {
182
- const memoryId = c.req.param("id");
183
143
  const body = await c.req.json();
184
- projectRepo.addAssociation(memoryId, body.projectId);
144
+ projectRepo.addAssociation(c.req.param("id"), body.projectId);
185
145
  return c.json({ ok: true });
186
146
  });
187
147
  app.delete("/api/memories/:id/projects/:projectId", (c) => {
@@ -192,15 +152,50 @@ function createApiApp(db, repo, projectRepo) {
192
152
  return c.json(projectRepo.list());
193
153
  });
194
154
  app.patch("/api/projects/:id", async (c) => {
195
- const id = c.req.param("id");
196
155
  const body = await c.req.json();
197
156
  try {
198
- return c.json(projectRepo.rename(id, body.name));
157
+ return c.json(projectRepo.rename(c.req.param("id"), body.name));
199
158
  } catch {
200
159
  return c.json({ error: "Not found" }, 404);
201
160
  }
202
161
  });
203
162
  app.get("/api/stats", (c) => {
163
+ const { byType, total, needsReview } = repo.stats();
164
+ return c.json({
165
+ byType,
166
+ total,
167
+ needsReview
168
+ });
169
+ });
170
+ app.get("/api/syntheses", (c) => {
171
+ return c.json(synthRepo.listAll());
172
+ });
173
+ app.get("/api/projects/:id/synthesis", (c) => {
174
+ const project = projectRepo.list().find((p) => p.id === c.req.param("id"));
175
+ if (!project) return c.json({ error: "Not found" }, 404);
176
+ return c.json(synthRepo.getSynthesis(project.scopeHash) ?? null);
177
+ });
178
+ app.post("/api/projects/:id/synthesis", (c) => {
179
+ if (!(0, _membank_core.isSynthesisEnabled)()) return c.json({ error: "Synthesis is disabled" }, 503);
180
+ const project = projectRepo.list().find((p) => p.id === c.req.param("id"));
181
+ if (!project) return c.json({ error: "Not found" }, 404);
182
+ const agentRunner = (0, _membank_core.createSynthesisAgentRunner)(buildSynthesisTools(repo, queryEngine), { enabled: true });
183
+ (0, _membank_core.runSynthesis)(project.scopeHash, {
184
+ synthRepo,
185
+ agentRunner
186
+ });
187
+ return c.json({ ok: true }, 202);
188
+ });
189
+ app.delete("/api/projects/:id/synthesis/in-flight", (c) => {
190
+ const project = projectRepo.list().find((p) => p.id === c.req.param("id"));
191
+ if (!project) return c.json({ error: "Not found" }, 404);
192
+ synthRepo.clearInFlight(project.scopeHash);
193
+ return c.json({ ok: true });
194
+ });
195
+ app.get("/api/projects/:id/stats", (c) => {
196
+ const project = projectRepo.list().find((p) => p.id === c.req.param("id"));
197
+ if (!project) return c.json({ error: "Not found" }, 404);
198
+ const memories = repo.list({ projectId: project.id });
204
199
  const byType = {
205
200
  correction: 0,
206
201
  preference: 0,
@@ -208,24 +203,48 @@ function createApiApp(db, repo, projectRepo) {
208
203
  learning: 0,
209
204
  fact: 0
210
205
  };
211
- const typeRows = db.db.prepare("SELECT type, COUNT(*) as count FROM memories GROUP BY type").all();
212
- for (const row of typeRows) if (row.type in byType) byType[row.type] = row.count;
213
- const totals = db.db.prepare("SELECT COUNT(*) as total FROM memories").get() ?? { total: 0 };
214
- const reviewRow = db.db.prepare("SELECT COUNT(DISTINCT memory_id) as needsReview FROM memory_review_events WHERE resolved_at IS NULL").get() ?? { needsReview: 0 };
206
+ for (const m of memories) byType[m.type] = (byType[m.type] ?? 0) + 1;
207
+ const mostCommonType = memories.length > 0 ? Object.entries(byType).sort((a, b) => b[1] - a[1])[0]?.[0] ?? null : null;
208
+ const needsReview = memories.filter((m) => m.reviewEvents.length > 0).length;
209
+ const pinned = memories.filter((m) => m.pinned).length;
210
+ const lastActive = memories.reduce((latest, m) => {
211
+ const d = m.updatedAt > m.createdAt ? m.updatedAt : m.createdAt;
212
+ return d > latest ? d : latest;
213
+ }, "");
214
+ const activeDaySet = new Set(memories.map((m) => m.createdAt.slice(0, 10)));
215
+ const harnessCounts = {};
216
+ for (const m of memories) if (m.sourceHarness) harnessCounts[m.sourceHarness] = (harnessCounts[m.sourceHarness] ?? 0) + 1;
217
+ const harness = Object.entries(harnessCounts).sort((a, b) => b[1] - a[1])[0]?.[0] ?? null;
215
218
  return c.json({
219
+ total: memories.length,
216
220
  byType,
217
- total: totals.total,
218
- needsReview: reviewRow.needsReview
221
+ needsReview,
222
+ pinned,
223
+ mostCommonType,
224
+ lastActive: lastActive || null,
225
+ harness,
226
+ activeDays: activeDaySet.size
219
227
  });
220
228
  });
229
+ app.get("/api/projects/:id/activity", (c) => {
230
+ const project = projectRepo.list().find((p) => p.id === c.req.param("id"));
231
+ if (!project) return c.json({ error: "Not found" }, 404);
232
+ const daysParam = Math.max(1, parseInt(c.req.query("days") ?? "365", 10));
233
+ return c.json(aggregateActivity(repo.list({ projectId: project.id }), daysParam));
234
+ });
235
+ app.get("/api/activity", (c) => {
236
+ const daysParam = Math.max(1, parseInt(c.req.query("days") ?? "365", 10));
237
+ return c.json(aggregateActivity(repo.list(), daysParam));
238
+ });
221
239
  return app;
222
240
  }
223
241
  async function startDashboard(opts) {
224
242
  const port = await findFreePort(opts?.port ?? PREFERRED_PORT);
225
243
  const db = _membank_core.DatabaseManager.open();
226
244
  const embedding = new _membank_core.EmbeddingService();
227
- const projects = new _membank_core.ProjectRepository(db);
228
- const app = createApiApp(db, new _membank_core.MemoryRepository(db, embedding, projects), projects);
245
+ const projects = (0, _membank_core.createProjectRepository)(db);
246
+ const repo = (0, _membank_core.createMemoryRepository)(db, projects);
247
+ const app = createApiApp(repo, projects, embedding, new _membank_core.QueryEngine(db, embedding, repo), (0, _membank_core.createSynthesisRepository)(db));
229
248
  const clientDir = (0, node_path.join)((0, node_path.dirname)((0, node_url.fileURLToPath)(require("url").pathToFileURL(__filename).href)), "client");
230
249
  app.get("*", (c) => {
231
250
  const filePath = (0, node_path.join)(clientDir, c.req.path === "/" ? "/index.html" : c.req.path);
@@ -241,13 +260,16 @@ async function startDashboard(opts) {
241
260
  db.close();
242
261
  process.exit(0);
243
262
  });
244
- (0, _hono_node_server.serve)({
245
- fetch: app.fetch,
246
- port
263
+ await new Promise((resolve) => {
264
+ (0, _hono_node_server.serve)({
265
+ fetch: app.fetch,
266
+ port
267
+ }, () => {
268
+ opts?.onReady?.(port);
269
+ resolve();
270
+ });
247
271
  });
248
- process.stdout.write(`\n Membank dashboard → http://localhost:${port}\n`);
249
- process.stdout.write(` Press Ctrl+C to stop\n\n`);
250
- await (0, open.default)(`http://localhost:${port}`);
272
+ if (opts?.open) await (0, open.default)(`http://localhost:${port}`);
251
273
  await new Promise(() => {});
252
274
  }
253
275
  //#endregion
package/dist/index.d.cts CHANGED
@@ -1,10 +1,12 @@
1
- import { DatabaseManager, MemoryRepository, ProjectRepository } from "@membank/core";
1
+ import { Embedder, MemoryRepository, ProjectRepository, QueryEngine, SynthesisRepository } from "@membank/core";
2
2
  import { Hono } from "hono";
3
3
 
4
4
  //#region src/server/index.d.ts
5
- declare function createApiApp(db: DatabaseManager, repo: MemoryRepository, projectRepo: ProjectRepository): Hono;
5
+ declare function createApiApp(repo: MemoryRepository, projectRepo: ProjectRepository, embedder: Embedder, queryEngine: QueryEngine, synthRepo: SynthesisRepository): Hono;
6
6
  declare function startDashboard(opts?: {
7
7
  port?: number;
8
+ open?: boolean;
9
+ onReady?: (port: number) => void;
8
10
  }): Promise<void>;
9
11
  //#endregion
10
12
  export { createApiApp, startDashboard };
package/dist/index.d.mts CHANGED
@@ -1,10 +1,12 @@
1
- import { DatabaseManager, MemoryRepository, ProjectRepository } from "@membank/core";
1
+ import { Embedder, MemoryRepository, ProjectRepository, QueryEngine, SynthesisRepository } from "@membank/core";
2
2
  import { Hono } from "hono";
3
3
 
4
4
  //#region src/server/index.d.ts
5
- declare function createApiApp(db: DatabaseManager, repo: MemoryRepository, projectRepo: ProjectRepository): Hono;
5
+ declare function createApiApp(repo: MemoryRepository, projectRepo: ProjectRepository, embedder: Embedder, queryEngine: QueryEngine, synthRepo: SynthesisRepository): Hono;
6
6
  declare function startDashboard(opts?: {
7
7
  port?: number;
8
+ open?: boolean;
9
+ onReady?: (port: number) => void;
8
10
  }): Promise<void>;
9
11
  //#endregion
10
12
  export { createApiApp, startDashboard };
package/dist/index.mjs CHANGED
@@ -3,11 +3,25 @@ import { createServer } from "node:net";
3
3
  import { dirname, extname, join } from "node:path";
4
4
  import { fileURLToPath } from "node:url";
5
5
  import { serve } from "@hono/node-server";
6
- import { DatabaseManager, EmbeddingService, MemoryRepository, ProjectRepository } from "@membank/core";
6
+ import { DatabaseManager, EmbeddingService, QueryEngine, createMemoryRepository, createProjectRepository, createSynthesisAgentRunner, createSynthesisRepository, isSynthesisEnabled, runSynthesis, updateMemory } from "@membank/core";
7
7
  import { Hono } from "hono";
8
8
  import open from "open";
9
9
  //#region src/server/index.ts
10
10
  const PREFERRED_PORT = 3847;
11
+ function buildSynthesisTools(repo, querier) {
12
+ return {
13
+ queryMemory: async (args) => {
14
+ const results = await querier.query({
15
+ query: args.query,
16
+ projectHash: args.global === true ? void 0 : args.projectHash,
17
+ limit: args.limit ?? 20,
18
+ includePinned: true
19
+ });
20
+ return JSON.stringify(results);
21
+ },
22
+ getMemorySummary: async () => JSON.stringify(repo.stats())
23
+ };
24
+ }
11
25
  const MIME = {
12
26
  ".js": "application/javascript",
13
27
  ".mjs": "application/javascript",
@@ -21,46 +35,6 @@ const MIME = {
21
35
  ".png": "image/png",
22
36
  ".json": "application/json"
23
37
  };
24
- function parseReviewEvent(row) {
25
- return {
26
- id: row.id,
27
- memoryId: row.memory_id,
28
- conflictingMemoryId: row.conflicting_memory_id,
29
- similarity: row.similarity,
30
- conflictContentSnapshot: row.conflict_content_snapshot,
31
- reason: row.reason,
32
- createdAt: row.created_at,
33
- resolvedAt: row.resolved_at
34
- };
35
- }
36
- function getReviewEventsForMemories(db, ids) {
37
- if (ids.length === 0) return /* @__PURE__ */ new Map();
38
- const placeholders = ids.map(() => "?").join(", ");
39
- const rows = db.db.prepare(`SELECT * FROM memory_review_events WHERE memory_id IN (${placeholders}) AND resolved_at IS NULL ORDER BY created_at DESC`).all(...ids);
40
- const map = /* @__PURE__ */ new Map();
41
- for (const row of rows) {
42
- const event = parseReviewEvent(row);
43
- const existing = map.get(event.memoryId) ?? [];
44
- existing.push(event);
45
- map.set(event.memoryId, existing);
46
- }
47
- return map;
48
- }
49
- function parseRow(row, projects = [], reviewEvents = []) {
50
- return {
51
- id: row.id,
52
- content: row.content,
53
- type: row.type,
54
- tags: JSON.parse(row.tags),
55
- projects,
56
- sourceHarness: row.source,
57
- accessCount: row.access_count,
58
- pinned: row.pinned !== 0,
59
- reviewEvents,
60
- createdAt: row.created_at,
61
- updatedAt: row.updated_at
62
- };
63
- }
64
38
  function tryPort(port) {
65
39
  return new Promise((resolve, reject) => {
66
40
  const server = createServer();
@@ -84,29 +58,32 @@ async function findFreePort(preferred) {
84
58
  });
85
59
  }
86
60
  }
87
- function createApiApp(db, repo, projectRepo) {
61
+ function aggregateActivity(memories, days) {
62
+ const cutoff = /* @__PURE__ */ new Date();
63
+ cutoff.setDate(cutoff.getDate() - days);
64
+ const cutoffStr = cutoff.toISOString().slice(0, 10);
65
+ const dayCounts = {};
66
+ for (const m of memories) {
67
+ const day = m.createdAt.slice(0, 10);
68
+ if (day >= cutoffStr) dayCounts[day] = (dayCounts[day] ?? 0) + 1;
69
+ const updateDay = m.updatedAt.slice(0, 10);
70
+ if (updateDay !== day && updateDay >= cutoffStr) dayCounts[updateDay] = (dayCounts[updateDay] ?? 0) + 1;
71
+ }
72
+ return Object.entries(dayCounts).map(([date, count]) => ({
73
+ date,
74
+ count
75
+ })).sort((a, b) => a.date.localeCompare(b.date));
76
+ }
77
+ function createApiApp(repo, projectRepo, embedder, queryEngine, synthRepo) {
88
78
  const app = new Hono();
89
79
  app.get("/api/memories", (c) => {
90
80
  const { type, pinned, needsReview, search, projectId } = c.req.query();
91
- const conditions = [];
92
- const params = [];
93
- if (type) {
94
- conditions.push("m.type = ?");
95
- params.push(type);
96
- }
97
- if (pinned === "true") conditions.push("m.pinned = 1");
98
- if (needsReview === "true") conditions.push("EXISTS (SELECT 1 FROM memory_review_events e WHERE e.memory_id = m.id AND e.resolved_at IS NULL)");
99
- if (projectId === "global") conditions.push("m.id NOT IN (SELECT memory_id FROM memory_projects)");
100
- else if (projectId) {
101
- conditions.push("m.id IN (SELECT memory_id FROM memory_projects WHERE project_id = ?)");
102
- params.push(projectId);
103
- }
104
- const where = conditions.length > 0 ? `WHERE ${conditions.join(" AND ")}` : "";
105
- const rows = db.db.prepare(`SELECT m.* FROM memories m ${where} ORDER BY m.created_at DESC`).all(...params);
106
- const ids = rows.map((r) => r.id);
107
- const projectMap = projectRepo.getProjectsForMemories(ids);
108
- const eventMap = getReviewEventsForMemories(db, ids);
109
- let memories = rows.map((r) => parseRow(r, projectMap.get(r.id) ?? [], eventMap.get(r.id) ?? []));
81
+ let memories = repo.list({
82
+ type,
83
+ pinned: pinned === "true" ? true : void 0,
84
+ needsReview: needsReview === "true" ? true : void 0,
85
+ projectId
86
+ });
110
87
  if (search) {
111
88
  const q = search.toLowerCase();
112
89
  memories = memories.filter((m) => m.content.toLowerCase().includes(q));
@@ -114,50 +91,33 @@ function createApiApp(db, repo, projectRepo) {
114
91
  return c.json(memories);
115
92
  });
116
93
  app.get("/api/memories/:id", (c) => {
117
- const id = c.req.param("id");
118
- const row = db.db.prepare("SELECT * FROM memories WHERE id = ?").get(id);
119
- if (!row) return c.json({ error: "Not found" }, 404);
120
- const projectMap = projectRepo.getProjectsForMemories([id]);
121
- const eventMap = getReviewEventsForMemories(db, [id]);
122
- return c.json(parseRow(row, projectMap.get(id) ?? [], eventMap.get(id) ?? []));
94
+ const memory = repo.findById(c.req.param("id"));
95
+ if (!memory) return c.json({ error: "Not found" }, 404);
96
+ return c.json(memory);
123
97
  });
124
98
  app.patch("/api/memories/:id", async (c) => {
125
99
  const id = c.req.param("id");
126
- if (!db.db.prepare("SELECT id FROM memories WHERE id = ?").get(id)) return c.json({ error: "Not found" }, 404);
100
+ if (!repo.findById(id)) return c.json({ error: "Not found" }, 404);
127
101
  const body = await c.req.json();
128
- const sets = [];
129
- const sqlParams = [];
130
- if (body.pinned !== void 0) {
131
- sets.push("pinned = ?");
132
- sqlParams.push(body.pinned ? 1 : 0);
133
- }
134
- if (body.type !== void 0) {
135
- sets.push("type = ?");
136
- sqlParams.push(body.type);
137
- }
138
- if (sets.length > 0) {
139
- sets.push("updated_at = ?");
140
- sqlParams.push((/* @__PURE__ */ new Date()).toISOString(), id);
141
- db.db.prepare(`UPDATE memories SET ${sets.join(", ")} WHERE id = ?`).run(...sqlParams);
142
- }
102
+ if (body.pinned !== void 0) repo.setPin(id, body.pinned);
143
103
  if (body.needsReview === false) repo.resolveReviewEvents(id);
144
- if (body.content !== void 0 || body.tags !== void 0) await repo.update(id, {
104
+ if (body.content !== void 0 || body.tags !== void 0 || body.type !== void 0) await updateMemory(id, {
145
105
  content: body.content,
146
- tags: body.tags
106
+ tags: body.tags,
107
+ type: body.type
108
+ }, {
109
+ repo,
110
+ embedder
147
111
  });
148
- const updated = db.db.prepare("SELECT * FROM memories WHERE id = ?").get(id);
149
- const projectMap = projectRepo.getProjectsForMemories([id]);
150
- const eventMap = getReviewEventsForMemories(db, [id]);
151
- return c.json(parseRow(updated, projectMap.get(id) ?? [], eventMap.get(id) ?? []));
112
+ return c.json(repo.findById(id));
152
113
  });
153
- app.delete("/api/memories/:id", async (c) => {
154
- await repo.delete(c.req.param("id"));
114
+ app.delete("/api/memories/:id", (c) => {
115
+ repo.delete(c.req.param("id"));
155
116
  return c.json({ ok: true });
156
117
  });
157
118
  app.post("/api/memories/:id/projects", async (c) => {
158
- const memoryId = c.req.param("id");
159
119
  const body = await c.req.json();
160
- projectRepo.addAssociation(memoryId, body.projectId);
120
+ projectRepo.addAssociation(c.req.param("id"), body.projectId);
161
121
  return c.json({ ok: true });
162
122
  });
163
123
  app.delete("/api/memories/:id/projects/:projectId", (c) => {
@@ -168,15 +128,50 @@ function createApiApp(db, repo, projectRepo) {
168
128
  return c.json(projectRepo.list());
169
129
  });
170
130
  app.patch("/api/projects/:id", async (c) => {
171
- const id = c.req.param("id");
172
131
  const body = await c.req.json();
173
132
  try {
174
- return c.json(projectRepo.rename(id, body.name));
133
+ return c.json(projectRepo.rename(c.req.param("id"), body.name));
175
134
  } catch {
176
135
  return c.json({ error: "Not found" }, 404);
177
136
  }
178
137
  });
179
138
  app.get("/api/stats", (c) => {
139
+ const { byType, total, needsReview } = repo.stats();
140
+ return c.json({
141
+ byType,
142
+ total,
143
+ needsReview
144
+ });
145
+ });
146
+ app.get("/api/syntheses", (c) => {
147
+ return c.json(synthRepo.listAll());
148
+ });
149
+ app.get("/api/projects/:id/synthesis", (c) => {
150
+ const project = projectRepo.list().find((p) => p.id === c.req.param("id"));
151
+ if (!project) return c.json({ error: "Not found" }, 404);
152
+ return c.json(synthRepo.getSynthesis(project.scopeHash) ?? null);
153
+ });
154
+ app.post("/api/projects/:id/synthesis", (c) => {
155
+ if (!isSynthesisEnabled()) return c.json({ error: "Synthesis is disabled" }, 503);
156
+ const project = projectRepo.list().find((p) => p.id === c.req.param("id"));
157
+ if (!project) return c.json({ error: "Not found" }, 404);
158
+ const agentRunner = createSynthesisAgentRunner(buildSynthesisTools(repo, queryEngine), { enabled: true });
159
+ runSynthesis(project.scopeHash, {
160
+ synthRepo,
161
+ agentRunner
162
+ });
163
+ return c.json({ ok: true }, 202);
164
+ });
165
+ app.delete("/api/projects/:id/synthesis/in-flight", (c) => {
166
+ const project = projectRepo.list().find((p) => p.id === c.req.param("id"));
167
+ if (!project) return c.json({ error: "Not found" }, 404);
168
+ synthRepo.clearInFlight(project.scopeHash);
169
+ return c.json({ ok: true });
170
+ });
171
+ app.get("/api/projects/:id/stats", (c) => {
172
+ const project = projectRepo.list().find((p) => p.id === c.req.param("id"));
173
+ if (!project) return c.json({ error: "Not found" }, 404);
174
+ const memories = repo.list({ projectId: project.id });
180
175
  const byType = {
181
176
  correction: 0,
182
177
  preference: 0,
@@ -184,24 +179,48 @@ function createApiApp(db, repo, projectRepo) {
184
179
  learning: 0,
185
180
  fact: 0
186
181
  };
187
- const typeRows = db.db.prepare("SELECT type, COUNT(*) as count FROM memories GROUP BY type").all();
188
- for (const row of typeRows) if (row.type in byType) byType[row.type] = row.count;
189
- const totals = db.db.prepare("SELECT COUNT(*) as total FROM memories").get() ?? { total: 0 };
190
- const reviewRow = db.db.prepare("SELECT COUNT(DISTINCT memory_id) as needsReview FROM memory_review_events WHERE resolved_at IS NULL").get() ?? { needsReview: 0 };
182
+ for (const m of memories) byType[m.type] = (byType[m.type] ?? 0) + 1;
183
+ const mostCommonType = memories.length > 0 ? Object.entries(byType).sort((a, b) => b[1] - a[1])[0]?.[0] ?? null : null;
184
+ const needsReview = memories.filter((m) => m.reviewEvents.length > 0).length;
185
+ const pinned = memories.filter((m) => m.pinned).length;
186
+ const lastActive = memories.reduce((latest, m) => {
187
+ const d = m.updatedAt > m.createdAt ? m.updatedAt : m.createdAt;
188
+ return d > latest ? d : latest;
189
+ }, "");
190
+ const activeDaySet = new Set(memories.map((m) => m.createdAt.slice(0, 10)));
191
+ const harnessCounts = {};
192
+ for (const m of memories) if (m.sourceHarness) harnessCounts[m.sourceHarness] = (harnessCounts[m.sourceHarness] ?? 0) + 1;
193
+ const harness = Object.entries(harnessCounts).sort((a, b) => b[1] - a[1])[0]?.[0] ?? null;
191
194
  return c.json({
195
+ total: memories.length,
192
196
  byType,
193
- total: totals.total,
194
- needsReview: reviewRow.needsReview
197
+ needsReview,
198
+ pinned,
199
+ mostCommonType,
200
+ lastActive: lastActive || null,
201
+ harness,
202
+ activeDays: activeDaySet.size
195
203
  });
196
204
  });
205
+ app.get("/api/projects/:id/activity", (c) => {
206
+ const project = projectRepo.list().find((p) => p.id === c.req.param("id"));
207
+ if (!project) return c.json({ error: "Not found" }, 404);
208
+ const daysParam = Math.max(1, parseInt(c.req.query("days") ?? "365", 10));
209
+ return c.json(aggregateActivity(repo.list({ projectId: project.id }), daysParam));
210
+ });
211
+ app.get("/api/activity", (c) => {
212
+ const daysParam = Math.max(1, parseInt(c.req.query("days") ?? "365", 10));
213
+ return c.json(aggregateActivity(repo.list(), daysParam));
214
+ });
197
215
  return app;
198
216
  }
199
217
  async function startDashboard(opts) {
200
218
  const port = await findFreePort(opts?.port ?? PREFERRED_PORT);
201
219
  const db = DatabaseManager.open();
202
220
  const embedding = new EmbeddingService();
203
- const projects = new ProjectRepository(db);
204
- const app = createApiApp(db, new MemoryRepository(db, embedding, projects), projects);
221
+ const projects = createProjectRepository(db);
222
+ const repo = createMemoryRepository(db, projects);
223
+ const app = createApiApp(repo, projects, embedding, new QueryEngine(db, embedding, repo), createSynthesisRepository(db));
205
224
  const clientDir = join(dirname(fileURLToPath(import.meta.url)), "client");
206
225
  app.get("*", (c) => {
207
226
  const filePath = join(clientDir, c.req.path === "/" ? "/index.html" : c.req.path);
@@ -217,13 +236,16 @@ async function startDashboard(opts) {
217
236
  db.close();
218
237
  process.exit(0);
219
238
  });
220
- serve({
221
- fetch: app.fetch,
222
- port
239
+ await new Promise((resolve) => {
240
+ serve({
241
+ fetch: app.fetch,
242
+ port
243
+ }, () => {
244
+ opts?.onReady?.(port);
245
+ resolve();
246
+ });
223
247
  });
224
- process.stdout.write(`\n Membank dashboard → http://localhost:${port}\n`);
225
- process.stdout.write(` Press Ctrl+C to stop\n\n`);
226
- await open(`http://localhost:${port}`);
248
+ if (opts?.open) await open(`http://localhost:${port}`);
227
249
  await new Promise(() => {});
228
250
  }
229
251
  //#endregion