@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.
- package/README.md +17 -1
- package/dist/bin.mjs +276 -0
- package/dist/client/assets/WorkspaceCenter-F8ctl0ZL.js +1 -0
- package/dist/client/assets/WorkspaceMemoryDetail-BLFoEX3q.js +1 -0
- package/dist/client/assets/_projectId-D3UHHv9-.js +1 -0
- package/dist/client/assets/_projectId._memoryId-CZOJ7L4F.js +1 -0
- package/dist/client/assets/_projectId.index-Dc3jTh6p.js +1 -0
- package/dist/client/assets/collections-y7eMFwFc.js +19 -0
- package/dist/client/assets/empty-C4Q9vAt-.js +1 -0
- package/dist/client/assets/global-BZgjloNm.js +1 -0
- package/dist/client/assets/global._memoryId-DbiHUsW4.js +1 -0
- package/dist/client/assets/global.index-w6voN-YE.js +1 -0
- package/dist/client/assets/index-B3Ej81eU.js +10 -0
- package/dist/client/assets/index-DHtBroPC.css +2 -0
- package/dist/client/assets/input-BdBFU9te.js +1 -0
- package/dist/client/assets/preload-helper-DYD3iRAO.js +1 -0
- package/dist/client/assets/routes-DdtLVWUF.js +1 -0
- package/dist/client/assets/schemas-jSuIeSIL.js +39 -0
- package/dist/client/assets/skeleton-BDvkvqhf.js +1 -0
- package/dist/client/assets/types-DT_fBrSI.js +1 -0
- package/dist/client/assets/useRenderElement-DhejdWAQ.js +1 -0
- package/dist/client/index.html +7 -2
- package/dist/index.cjs +130 -108
- package/dist/index.d.cts +4 -2
- package/dist/index.d.mts +4 -2
- package/dist/index.mjs +131 -109
- package/package.json +15 -6
- package/dist/client/assets/MagnifyingGlass.es-CGxS0MwI.js +0 -1
- package/dist/client/assets/index-DZxM00o8.js +0 -119
- package/dist/client/assets/index-GVEqDPxR.css +0 -1
- package/dist/client/assets/memories-ja38P73o.js +0 -1
- package/dist/client/assets/memories._id-DeaZiiQb.js +0 -1
- package/dist/client/assets/memories.index-DUtHo-Be.js +0 -1
- 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
|
|
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
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
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
|
|
142
|
-
|
|
143
|
-
|
|
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 (!
|
|
124
|
+
if (!repo.findById(id)) return c.json({ error: "Not found" }, 404);
|
|
151
125
|
const body = await c.req.json();
|
|
152
|
-
|
|
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
|
|
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
|
-
|
|
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",
|
|
178
|
-
|
|
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(
|
|
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
|
|
212
|
-
|
|
213
|
-
const
|
|
214
|
-
const
|
|
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
|
-
|
|
218
|
-
|
|
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 =
|
|
228
|
-
const
|
|
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
|
-
(
|
|
245
|
-
|
|
246
|
-
|
|
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
|
-
|
|
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 {
|
|
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(
|
|
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 {
|
|
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(
|
|
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,
|
|
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
|
|
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
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
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
|
|
118
|
-
|
|
119
|
-
|
|
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 (!
|
|
100
|
+
if (!repo.findById(id)) return c.json({ error: "Not found" }, 404);
|
|
127
101
|
const body = await c.req.json();
|
|
128
|
-
|
|
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
|
|
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
|
-
|
|
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",
|
|
154
|
-
|
|
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(
|
|
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
|
|
188
|
-
|
|
189
|
-
const
|
|
190
|
-
const
|
|
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
|
-
|
|
194
|
-
|
|
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 =
|
|
204
|
-
const
|
|
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
|
-
|
|
221
|
-
|
|
222
|
-
|
|
239
|
+
await new Promise((resolve) => {
|
|
240
|
+
serve({
|
|
241
|
+
fetch: app.fetch,
|
|
242
|
+
port
|
|
243
|
+
}, () => {
|
|
244
|
+
opts?.onReady?.(port);
|
|
245
|
+
resolve();
|
|
246
|
+
});
|
|
223
247
|
});
|
|
224
|
-
|
|
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
|