@modusensus/dsh-mneme 0.7.10 → 0.7.12
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.en.md +437 -0
- package/README.md +364 -166
- package/bin/cli.mjs +603 -0
- package/{dsh-mneme/cordis.patch.yml → cordis.patch.yml} +1 -0
- package/lib/api-standalone.js +264 -0
- package/{dsh-mneme/lib → lib}/api.js +133 -289
- package/{dsh-mneme/lib → lib}/client.js +1008 -922
- package/{dsh-mneme/lib → lib}/config.js +54 -99
- package/{dsh-mneme/lib → lib}/dream/decisions.js +94 -143
- package/{dsh-mneme/lib → lib}/dream/sleep.js +12 -152
- package/{dsh-mneme/lib → lib}/dream.js +22 -168
- package/{dsh-mneme/src → lib}/hot-memory.js +0 -7
- package/{dsh-mneme/lib → lib}/index.js +45 -42
- package/{dsh-mneme/lib → lib}/inject.js +11 -53
- package/{dsh-mneme/src → lib}/mirror.js +1 -12
- package/{dsh-mneme/lib → lib}/quality-filter.js +1 -3
- package/{dsh-mneme/lib → lib}/reranker.js +1 -4
- package/{dsh-mneme/lib → lib}/service.js +20 -410
- package/{dsh-mneme/src → lib}/settings.js +34 -64
- package/{dsh-mneme/lib → lib}/store.js +38 -450
- package/{dsh-mneme/lib → lib}/summarize.js +10 -10
- package/lib/tools.js +274 -0
- package/package.json +45 -19
- package/{dsh-mneme/scripts → scripts}/e2e-dsh.js +3 -4
- package/{dsh-mneme/scripts → scripts}/sync-lib.js +5 -10
- package/src/api-standalone.js +264 -0
- package/{dsh-mneme/src → src}/api.js +133 -289
- package/{dsh-mneme/src → src}/config.js +54 -99
- package/{dsh-mneme/src → src}/dream/decisions.js +94 -143
- package/{dsh-mneme/src → src}/dream/sleep.js +12 -152
- package/{dsh-mneme/src → src}/dream.js +22 -168
- package/{dsh-mneme/lib → src}/hot-memory.js +0 -7
- package/{dsh-mneme/src → src}/index.js +45 -42
- package/{dsh-mneme/src → src}/inject.js +11 -53
- package/{dsh-mneme/lib → src}/mirror.js +1 -12
- package/{dsh-mneme/src → src}/quality-filter.js +1 -3
- package/{dsh-mneme/src → src}/reranker.js +1 -4
- package/{dsh-mneme/src → src}/service.js +20 -410
- package/{dsh-mneme/lib → src}/settings.js +34 -64
- package/{dsh-mneme/src → src}/store.js +38 -450
- package/{dsh-mneme/src → src}/summarize.js +10 -10
- package/src/tools.js +274 -0
- package/{dsh-mneme/test → test}/api.test.js +45 -156
- package/{dsh-mneme/test → test}/client.test.js +10 -206
- package/{dsh-mneme/test → test}/config.test.js +0 -21
- package/{dsh-mneme/test → test}/dream.test.js +1 -160
- package/{dsh-mneme/test → test}/graph-api.test.js +0 -34
- package/{dsh-mneme/test → test}/hot-memory.test.js +0 -29
- package/test/inject.test.js +120 -0
- package/{dsh-mneme/test → test}/llm-audit.test.js +4 -4
- package/{dsh-mneme/test → test}/reasoning-effort.test.js +0 -27
- package/{dsh-mneme/test → test}/recall-layer.test.js +5 -26
- package/{dsh-mneme/test → test}/reranker.test.js +0 -43
- package/{dsh-mneme/test → test}/service-search.test.js +0 -25
- package/{dsh-mneme/test → test}/service.test.js +0 -106
- package/{dsh-mneme/test → test}/settings.test.js +16 -35
- package/{dsh-mneme/test → test}/sleep.test.js +4 -171
- package/test/standalone-api.test.js +326 -0
- package/{dsh-mneme/test → test}/store.test.js +0 -76
- package/{dsh-mneme/test → test}/summarize.test.js +16 -15
- package/test/tools.test.js +265 -0
- package/.github/workflows/publish.yml +0 -58
- package/.github/workflows/test.yml +0 -32
- package/CHANGELOG.md +0 -91
- package/CONTRIBUTING.md +0 -293
- package/SECURITY.md +0 -718
- package/docs/devlog/2026-08-14-dsh-mneme-dev-log.md +0 -247
- package/docs/devlog/2026-08-15-dsh-mneme-audit-stress-dev-log.md +0 -145
- package/docs/devlog/2026-08-15-dsh-mneme-pipeline-dev-log.md +0 -56
- package/docs/devlog/2026-08-15-dsh-mneme-reflection-dev-log.md +0 -77
- package/docs/devlog/2026-08-15-dsh-mneme-review-fixes-dev-log.md +0 -64
- package/docs/devlog/2026-08-15-dsh-mneme-semantic-dev-log.md +0 -90
- package/dsh-mneme/CHANGELOG.md +0 -419
- package/dsh-mneme/LICENSE +0 -21
- package/dsh-mneme/README.md +0 -501
- package/dsh-mneme/docs/AGENT_MEMORY_RESEARCH.md +0 -183
- package/dsh-mneme/docs/ENTITIES.md +0 -245
- package/dsh-mneme/docs/LOCAL_MODEL.md +0 -141
- package/dsh-mneme/docs/MIGRATION.md +0 -127
- package/dsh-mneme/docs/SEMANTIC.md +0 -256
- package/dsh-mneme/docs/SLEEP.md +0 -163
- package/dsh-mneme/lib/dream/tag-extractor.js +0 -156
- package/dsh-mneme/lib/heat.js +0 -136
- package/dsh-mneme/lib/parser/tag.js +0 -59
- package/dsh-mneme/lib/parser/wiki-link.js +0 -38
- package/dsh-mneme/lib/search/tag-boost.js +0 -61
- package/dsh-mneme/lib/tools.js +0 -458
- package/dsh-mneme/package-lock.json +0 -1936
- package/dsh-mneme/package.json +0 -83
- package/dsh-mneme/scripts/check-sync.js +0 -42
- package/dsh-mneme/src/client.js +0 -2242
- package/dsh-mneme/src/dream/tag-extractor.js +0 -156
- package/dsh-mneme/src/heat.js +0 -136
- package/dsh-mneme/src/parser/tag.js +0 -59
- package/dsh-mneme/src/parser/wiki-link.js +0 -38
- package/dsh-mneme/src/search/tag-boost.js +0 -61
- package/dsh-mneme/src/tools.js +0 -458
- package/dsh-mneme/test/boundary-v0625.test.js +0 -82
- package/dsh-mneme/test/directory.test.js +0 -134
- package/dsh-mneme/test/heat.test.js +0 -148
- package/dsh-mneme/test/inject.test.js +0 -206
- package/dsh-mneme/test/layered-types-stats.test.js +0 -144
- package/dsh-mneme/test/lib-smoke.test.js +0 -109
- package/dsh-mneme/test/normalize-decisions.test.js +0 -120
- package/dsh-mneme/test/provenance.test.js +0 -103
- package/dsh-mneme/test/recall-runs.test.js +0 -93
- package/dsh-mneme/test/sleep-heat.test.js +0 -112
- package/dsh-mneme/test/tag-boost.test.js +0 -125
- package/dsh-mneme/test/tag.test.js +0 -426
- package/dsh-mneme/test/tools.test.js +0 -674
- package/dsh-mneme/test/updated-at-semantics.test.js +0 -113
- package/dsh-mneme/test/wiki-link.test.js +0 -332
- package//346/250/252/345/271/205.png +0 -0
- /package/{dsh-mneme/lib → lib}/commands.js +0 -0
- /package/{dsh-mneme/lib → lib}/dream/clustering.js +0 -0
- /package/{dsh-mneme/lib → lib}/embedding.js +0 -0
- /package/{dsh-mneme/lib → lib}/entities/extractor.js +0 -0
- /package/{dsh-mneme/lib → lib}/local-embedder.js +0 -0
- /package/{dsh-mneme/lib → lib}/search/adaptive.js +0 -0
- /package/{dsh-mneme/lib → lib}/search/bm25.js +0 -0
- /package/{dsh-mneme/lib → lib}/vector-index.js +0 -0
- /package/{dsh-mneme/scripts → scripts}/benchmark-embed.js +0 -0
- /package/{dsh-mneme/scripts → scripts}/benchmark-recall.js +0 -0
- /package/{dsh-mneme/scripts → scripts}/benchmark-rerank.js +0 -0
- /package/{dsh-mneme/scripts → scripts}/stress-dsh.js +0 -0
- /package/{dsh-mneme/src → src}/commands.js +0 -0
- /package/{dsh-mneme/src → src}/dream/clustering.js +0 -0
- /package/{dsh-mneme/src → src}/embedding.js +0 -0
- /package/{dsh-mneme/src → src}/entities/extractor.js +0 -0
- /package/{dsh-mneme/src → src}/local-embedder.js +0 -0
- /package/{dsh-mneme/src → src}/search/adaptive.js +0 -0
- /package/{dsh-mneme/src → src}/search/bm25.js +0 -0
- /package/{dsh-mneme/src → src}/vector-index.js +0 -0
- /package/{dsh-mneme/test → test}/audit.test.js +0 -0
- /package/{dsh-mneme/test → test}/benchmark.test.js +0 -0
- /package/{dsh-mneme/test → test}/clustering.test.js +0 -0
- /package/{dsh-mneme/test → test}/commands.test.js +0 -0
- /package/{dsh-mneme/test → test}/conflict-freeze.test.js +0 -0
- /package/{dsh-mneme/test → test}/entities.test.js +0 -0
- /package/{dsh-mneme/test → test}/epistemic.test.js +0 -0
- /package/{dsh-mneme/test → test}/fnew-0112.test.js +0 -0
- /package/{dsh-mneme/test → test}/fnew-03.test.js +0 -0
- /package/{dsh-mneme/test → test}/helpers/dream-mock.js +0 -0
- /package/{dsh-mneme/test → test}/local-embedder.test.js +0 -0
- /package/{dsh-mneme/test → test}/mirror-dirty.test.js +0 -0
- /package/{dsh-mneme/test → test}/mirror-edit-digest.test.js +0 -0
- /package/{dsh-mneme/test → test}/mirror-generation.test.js +0 -0
- /package/{dsh-mneme/test → test}/mirror.test.js +0 -0
- /package/{dsh-mneme/test → test}/peer-blockers.test.js +0 -0
- /package/{dsh-mneme/test → test}/policy-epoch.test.js +0 -0
- /package/{dsh-mneme/test → test}/quality-filter.test.js +0 -0
- /package/{dsh-mneme/test → test}/recall-evals.test.js +0 -0
- /package/{dsh-mneme/test → test}/receipt-chain.test.js +0 -0
- /package/{dsh-mneme/test → test}/reflection.test.js +0 -0
- /package/{dsh-mneme/test → test}/search-fusion.test.js +0 -0
- /package/{dsh-mneme/test → test}/semantic.test.js +0 -0
- /package/{dsh-mneme/test → test}/stress.test.js +0 -0
- /package/{dsh-mneme/test → test}/vector-index.test.js +0 -0
|
@@ -1,11 +1,25 @@
|
|
|
1
1
|
import { URL } from "node:url";
|
|
2
|
-
import { timingSafeEqual } from "node:crypto";
|
|
2
|
+
import { timingSafeEqual, randomBytes } from "node:crypto";
|
|
3
3
|
|
|
4
4
|
function sendJson(res, status, payload) {
|
|
5
5
|
res.writeHead(status, { "Content-Type": "application/json; charset=utf-8" });
|
|
6
6
|
res.end(JSON.stringify(payload));
|
|
7
7
|
}
|
|
8
8
|
|
|
9
|
+
// Defaults for the standalone external API — keep in step with the schema
|
|
10
|
+
// defaults in config.js (externalApiPort / externalApiHost).
|
|
11
|
+
const EXTERNAL_API_DEFAULTS = { enabled: false, port: 8790, host: "127.0.0.1" };
|
|
12
|
+
|
|
13
|
+
/** Merge persisted external-api kv over the defaults for client display. */
|
|
14
|
+
function fullExternalConfig(kv = {}) {
|
|
15
|
+
return {
|
|
16
|
+
enabled: kv.enabled === true,
|
|
17
|
+
port: Number.isInteger(kv.port) && kv.port > 0 ? kv.port : EXTERNAL_API_DEFAULTS.port,
|
|
18
|
+
host: kv.host || EXTERNAL_API_DEFAULTS.host,
|
|
19
|
+
token: kv.token || ""
|
|
20
|
+
};
|
|
21
|
+
}
|
|
22
|
+
|
|
9
23
|
/**
|
|
10
24
|
* Mask an API key for client display: keep a recognizable prefix and suffix,
|
|
11
25
|
* hide the middle. Empty keys stay empty; short keys are fully hidden.
|
|
@@ -88,23 +102,20 @@ export function createApi(ctx, service, settings, commands, embedder, semantic =
|
|
|
88
102
|
const type = url.searchParams.get("type") ?? undefined;
|
|
89
103
|
const limit = Number(url.searchParams.get("limit") ?? 50);
|
|
90
104
|
const offset = Number(url.searchParams.get("offset") ?? 0);
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
const raw = url.searchParams.get("days");
|
|
106
|
-
const days = Math.min(30, Math.max(1, parseInt(raw ?? "7", 10) || 7));
|
|
107
|
-
sendJson(res, 200, service.stats({ days }));
|
|
105
|
+
// order=chrono: pure newest-first for paged browsing; default keeps
|
|
106
|
+
// the importance-ranked order other callers rely on.
|
|
107
|
+
const order = url.searchParams.get("order") ?? undefined;
|
|
108
|
+
// minImportance: numeric lower bound on importance (absent/NaN → no
|
|
109
|
+
// floor); source: exact match on the source column (empty → no filter).
|
|
110
|
+
const minRaw = url.searchParams.get("minImportance");
|
|
111
|
+
const minImportance = minRaw !== null && minRaw !== "" && !Number.isNaN(Number(minRaw))
|
|
112
|
+
? Number(minRaw)
|
|
113
|
+
: undefined;
|
|
114
|
+
const source = url.searchParams.get("source") || undefined;
|
|
115
|
+
const items = service.toApiList(service.list({ type, limit, offset, order, minImportance, source }));
|
|
116
|
+
// Total honors the same filters as the rows, or the pager's
|
|
117
|
+
// has-more math breaks whenever minImportance/source is active.
|
|
118
|
+
sendJson(res, 200, { items, total: service.count(type, { minImportance, source }) });
|
|
108
119
|
} catch {
|
|
109
120
|
sendJson(res, 500, { error: "internal" });
|
|
110
121
|
}
|
|
@@ -175,6 +186,40 @@ export function createApi(ctx, service, settings, commands, embedder, semantic =
|
|
|
175
186
|
}
|
|
176
187
|
});
|
|
177
188
|
|
|
189
|
+
// --- delete one memory by id ---
|
|
190
|
+
// Mutation route: apiToken-gated like the profile/rules writes above. POST
|
|
191
|
+
// body is JSON { id }. store.remove deletes silently, so existence is checked
|
|
192
|
+
// up front to give clients a distinguishable 404 instead of a fake success.
|
|
193
|
+
register({
|
|
194
|
+
kind: "exact",
|
|
195
|
+
path: "/api/dsh-mneme/delete",
|
|
196
|
+
handler(req, res) {
|
|
197
|
+
try {
|
|
198
|
+
if (req.method !== "POST") {
|
|
199
|
+
sendJson(res, 404, { error: "not-found" });
|
|
200
|
+
return;
|
|
201
|
+
}
|
|
202
|
+
if (!requireAuth(req, res, apiToken)) return;
|
|
203
|
+
return readBody(req).then((text) => {
|
|
204
|
+
const body = parseBody(text);
|
|
205
|
+
const id = typeof body.id === "string" ? body.id.trim() : "";
|
|
206
|
+
if (!id) {
|
|
207
|
+
sendJson(res, 400, { error: "missing-id" });
|
|
208
|
+
return;
|
|
209
|
+
}
|
|
210
|
+
if (!service.getById(id)) {
|
|
211
|
+
sendJson(res, 404, { error: "not-found" });
|
|
212
|
+
return;
|
|
213
|
+
}
|
|
214
|
+
service.remove(id);
|
|
215
|
+
sendJson(res, 200, { ok: true });
|
|
216
|
+
});
|
|
217
|
+
} catch {
|
|
218
|
+
sendJson(res, 500, { error: "internal" });
|
|
219
|
+
}
|
|
220
|
+
}
|
|
221
|
+
});
|
|
222
|
+
|
|
178
223
|
// --- rules ---
|
|
179
224
|
register({
|
|
180
225
|
kind: "exact",
|
|
@@ -233,102 +278,6 @@ export function createApi(ctx, service, settings, commands, embedder, semantic =
|
|
|
233
278
|
}
|
|
234
279
|
});
|
|
235
280
|
|
|
236
|
-
// --- app config read/write (partial: pass only the fields to change) ---
|
|
237
|
-
register({
|
|
238
|
-
kind: "exact",
|
|
239
|
-
path: "/api/dsh-mneme/config",
|
|
240
|
-
handler: async (req, res) => {
|
|
241
|
-
try {
|
|
242
|
-
if (!requireAuth(req, res, apiToken)) return;
|
|
243
|
-
if (req.method === "PUT" || req.method === "POST") {
|
|
244
|
-
const body = parseBody(await readBody(req));
|
|
245
|
-
const patch = {};
|
|
246
|
-
const setIfBoolean = (key) => {
|
|
247
|
-
if (Object.prototype.hasOwnProperty.call(body, key)) {
|
|
248
|
-
if (typeof body[key] !== "boolean") {
|
|
249
|
-
sendJson(res, 400, { error: `${key} must be a boolean` });
|
|
250
|
-
return false;
|
|
251
|
-
}
|
|
252
|
-
patch[key] = body[key];
|
|
253
|
-
}
|
|
254
|
-
return true;
|
|
255
|
-
};
|
|
256
|
-
if (!setIfBoolean("autoTagEnabled") || !setIfBoolean("manualTagEnabled") || !setIfBoolean("showSidebarTrigger")) return;
|
|
257
|
-
if (Object.keys(patch).length === 0) {
|
|
258
|
-
sendJson(res, 400, { error: "no valid config field provided" });
|
|
259
|
-
return;
|
|
260
|
-
}
|
|
261
|
-
// Each setter ignores the keys it doesn't own, so one patch can feed
|
|
262
|
-
// both stores safely (tag toggles → "autoTag", UI prefs → "ui").
|
|
263
|
-
settings.setAutoTagConfig(patch);
|
|
264
|
-
settings.setUiConfig(patch);
|
|
265
|
-
// Respond with the effective (settings-over-config merged) toggles
|
|
266
|
-
// so the panel always sees booleans that actually gate runtime
|
|
267
|
-
// behaviour (issue #31).
|
|
268
|
-
sendJson(res, 200, { config: service.getAppConfig() });
|
|
269
|
-
return;
|
|
270
|
-
}
|
|
271
|
-
sendJson(res, 200, { config: service.getAppConfig() });
|
|
272
|
-
} catch {
|
|
273
|
-
sendJson(res, 500, { error: "internal" });
|
|
274
|
-
}
|
|
275
|
-
}
|
|
276
|
-
});
|
|
277
|
-
|
|
278
|
-
// --- delete a memory by exact id or best query match (mirrors memory_delete tool) ---
|
|
279
|
-
register({
|
|
280
|
-
kind: "exact",
|
|
281
|
-
path: "/api/dsh-mneme/memories",
|
|
282
|
-
handler: async (req, res) => {
|
|
283
|
-
try {
|
|
284
|
-
if (!requireAuth(req, res, apiToken)) return;
|
|
285
|
-
if (req.method !== "DELETE") {
|
|
286
|
-
sendJson(res, 405, { error: "method not allowed" });
|
|
287
|
-
return;
|
|
288
|
-
}
|
|
289
|
-
const body = parseBody(await readBody(req));
|
|
290
|
-
const hasId = Object.prototype.hasOwnProperty.call(body, "id");
|
|
291
|
-
const hasQuery = Object.prototype.hasOwnProperty.call(body, "query");
|
|
292
|
-
if ((hasId && hasQuery) || (!hasId && !hasQuery)) {
|
|
293
|
-
sendJson(res, 400, { error: "provide exactly one of id or query" });
|
|
294
|
-
return;
|
|
295
|
-
}
|
|
296
|
-
if (hasId) {
|
|
297
|
-
if (typeof body.id !== "string" || !body.id.trim()) {
|
|
298
|
-
sendJson(res, 400, { error: "invalid id" });
|
|
299
|
-
return;
|
|
300
|
-
}
|
|
301
|
-
const mem = service.getById(body.id);
|
|
302
|
-
if (!mem) {
|
|
303
|
-
sendJson(res, 200, { deleted: false });
|
|
304
|
-
return;
|
|
305
|
-
}
|
|
306
|
-
service.remove(body.id);
|
|
307
|
-
sendJson(res, 200, { deleted: true, id: body.id });
|
|
308
|
-
return;
|
|
309
|
-
}
|
|
310
|
-
if (typeof body.query !== "string" || !body.query.trim()) {
|
|
311
|
-
sendJson(res, 400, { error: "invalid query" });
|
|
312
|
-
return;
|
|
313
|
-
}
|
|
314
|
-
const results = await service.searchMemories(body.query, { mode: "auto", topK: 1, useRerank: true });
|
|
315
|
-
if (!Array.isArray(results) || results.length === 0) {
|
|
316
|
-
sendJson(res, 200, { deleted: false });
|
|
317
|
-
return;
|
|
318
|
-
}
|
|
319
|
-
const mem = results[0];
|
|
320
|
-
if (!mem || !mem.id) {
|
|
321
|
-
sendJson(res, 200, { deleted: false });
|
|
322
|
-
return;
|
|
323
|
-
}
|
|
324
|
-
service.remove(mem.id);
|
|
325
|
-
sendJson(res, 200, { deleted: true, id: mem.id });
|
|
326
|
-
} catch {
|
|
327
|
-
sendJson(res, 500, { error: "internal" });
|
|
328
|
-
}
|
|
329
|
-
}
|
|
330
|
-
});
|
|
331
|
-
|
|
332
281
|
// --- vector re-index (backfill embeddings for rows missing them) ---
|
|
333
282
|
register({
|
|
334
283
|
kind: "exact",
|
|
@@ -479,10 +428,7 @@ export function createApi(ctx, service, settings, commands, embedder, semantic =
|
|
|
479
428
|
name: n.name,
|
|
480
429
|
type: n.type ?? null,
|
|
481
430
|
mention_count: n.mention_count ?? 1,
|
|
482
|
-
distance: n.distance
|
|
483
|
-
// v0.7.0 实体热投影:实体热 = 关联记忆 heat 聚合(max),前端据此
|
|
484
|
-
// 缩放节点大小/明暗。heatEnabled=false 时 entityHeat 返回 null。
|
|
485
|
-
heat: service.entityHeat?.(n.id) ?? null
|
|
431
|
+
distance: n.distance
|
|
486
432
|
})),
|
|
487
433
|
edges: [...edgeMap.values()].map((e) => ({
|
|
488
434
|
id: e.id,
|
|
@@ -536,107 +482,6 @@ export function createApi(ctx, service, settings, commands, embedder, semantic =
|
|
|
536
482
|
}
|
|
537
483
|
});
|
|
538
484
|
|
|
539
|
-
// --- wiki-link back links (v0.6.1) --------------------------------------
|
|
540
|
-
// Read-only like the graph endpoints, so it stays open when apiToken is set.
|
|
541
|
-
// GET /api/dsh-mneme/wikilinks/backlinks?id=<memoryId> → memories whose
|
|
542
|
-
// content carries a [[wiki-link]] resolving to the given memory.
|
|
543
|
-
register({
|
|
544
|
-
kind: "exact",
|
|
545
|
-
path: "/api/dsh-mneme/wikilinks/backlinks",
|
|
546
|
-
handler(req, res) {
|
|
547
|
-
try {
|
|
548
|
-
const url = new URL(req.url, "http://localhost");
|
|
549
|
-
const id = (url.searchParams.get("id") ?? "").trim();
|
|
550
|
-
if (!id) {
|
|
551
|
-
sendJson(res, 400, { error: "missing-id" });
|
|
552
|
-
return;
|
|
553
|
-
}
|
|
554
|
-
const memory = service.getById?.(id) ?? null;
|
|
555
|
-
const backlinks = (service.getBacklinks?.(id) ?? []).map(({ source, relation }) => ({
|
|
556
|
-
id: source.id,
|
|
557
|
-
title: source.title,
|
|
558
|
-
type: source.type,
|
|
559
|
-
created_at: relation.created_at
|
|
560
|
-
}));
|
|
561
|
-
sendJson(res, 200, {
|
|
562
|
-
memoryId: id,
|
|
563
|
-
memory: memory ? { id: memory.id, title: memory.title, type: memory.type } : null,
|
|
564
|
-
backlinks
|
|
565
|
-
});
|
|
566
|
-
} catch {
|
|
567
|
-
sendJson(res, 500, { error: "internal" });
|
|
568
|
-
}
|
|
569
|
-
}
|
|
570
|
-
});
|
|
571
|
-
|
|
572
|
-
// --- wiki-link forward links (v0.6.1) -----------------------------------
|
|
573
|
-
// GET /api/dsh-mneme/wikilinks/forward?id=<memoryId> → memories the given
|
|
574
|
-
// memory explicitly links to. Unresolved target titles surface with id:null.
|
|
575
|
-
register({
|
|
576
|
-
kind: "exact",
|
|
577
|
-
path: "/api/dsh-mneme/wikilinks/forward",
|
|
578
|
-
handler(req, res) {
|
|
579
|
-
try {
|
|
580
|
-
const url = new URL(req.url, "http://localhost");
|
|
581
|
-
const id = (url.searchParams.get("id") ?? "").trim();
|
|
582
|
-
if (!id) {
|
|
583
|
-
sendJson(res, 400, { error: "missing-id" });
|
|
584
|
-
return;
|
|
585
|
-
}
|
|
586
|
-
const memory = service.getById?.(id) ?? null;
|
|
587
|
-
const links = (service.getForwardLinks?.(id) ?? []).map(({ target, relation }) => ({
|
|
588
|
-
id: target?.id ?? null,
|
|
589
|
-
title: target?.title ?? relation.to_entity,
|
|
590
|
-
type: target?.type ?? null,
|
|
591
|
-
created_at: relation.created_at
|
|
592
|
-
}));
|
|
593
|
-
sendJson(res, 200, {
|
|
594
|
-
memoryId: id,
|
|
595
|
-
memory: memory ? { id: memory.id, title: memory.title, type: memory.type } : null,
|
|
596
|
-
links
|
|
597
|
-
});
|
|
598
|
-
} catch {
|
|
599
|
-
sendJson(res, 500, { error: "internal" });
|
|
600
|
-
}
|
|
601
|
-
}
|
|
602
|
-
});
|
|
603
|
-
|
|
604
|
-
// --- wiki-link resolve (v0.6.1) -----------------------------------------
|
|
605
|
-
// GET /api/dsh-mneme/wikilinks/resolve?title=<title> → case-insensitive exact
|
|
606
|
-
// title match against the memories table (the resolution used when writing
|
|
607
|
-
// links_to relations). 404 when no memory matches.
|
|
608
|
-
register({
|
|
609
|
-
kind: "exact",
|
|
610
|
-
path: "/api/dsh-mneme/wikilinks/resolve",
|
|
611
|
-
handler(req, res) {
|
|
612
|
-
try {
|
|
613
|
-
const url = new URL(req.url, "http://localhost");
|
|
614
|
-
const title = (url.searchParams.get("title") ?? "").trim();
|
|
615
|
-
if (!title) {
|
|
616
|
-
sendJson(res, 400, { error: "missing-title" });
|
|
617
|
-
return;
|
|
618
|
-
}
|
|
619
|
-
const memory = service.resolveWikiLink?.(title) ?? null;
|
|
620
|
-
if (!memory) {
|
|
621
|
-
sendJson(res, 404, { error: "memory-not-found", title });
|
|
622
|
-
return;
|
|
623
|
-
}
|
|
624
|
-
// Note: no `source` field — it may carry file paths/internal host info
|
|
625
|
-
// and this endpoint is read-only without auth when apiToken is set.
|
|
626
|
-
sendJson(res, 200, {
|
|
627
|
-
title,
|
|
628
|
-
memory: {
|
|
629
|
-
id: memory.id,
|
|
630
|
-
title: memory.title,
|
|
631
|
-
type: memory.type
|
|
632
|
-
}
|
|
633
|
-
});
|
|
634
|
-
} catch {
|
|
635
|
-
sendJson(res, 500, { error: "internal" });
|
|
636
|
-
}
|
|
637
|
-
}
|
|
638
|
-
});
|
|
639
|
-
|
|
640
485
|
// --- entity directory (entity gene v0.3.0): read-only list for the UI ---
|
|
641
486
|
// Flat entity list (newest-seen first). Attributes and relations stay on
|
|
642
487
|
// their own per-entity endpoints, the directory only carries the columns
|
|
@@ -704,113 +549,112 @@ export function createApi(ctx, service, settings, commands, embedder, semantic =
|
|
|
704
549
|
}
|
|
705
550
|
});
|
|
706
551
|
|
|
707
|
-
// ---
|
|
552
|
+
// --- panel mode (v0.7.12): light / standard -------------------------------
|
|
553
|
+
// Persists the Web panel's feature preset into the settings kv
|
|
554
|
+
// ("panel_mode"). PUT requires auth like every other settings write; the
|
|
555
|
+
// value is validated against the enum. index.js applies the light preset on
|
|
556
|
+
// the next boot (persisted mode wins over the bundle config).
|
|
708
557
|
register({
|
|
709
558
|
kind: "exact",
|
|
710
|
-
path: "/api/dsh-mneme/
|
|
559
|
+
path: "/api/dsh-mneme/mode",
|
|
711
560
|
handler(req, res) {
|
|
712
561
|
try {
|
|
713
|
-
if (req.method === "POST") {
|
|
562
|
+
if (req.method === "PUT" || req.method === "POST") {
|
|
714
563
|
if (!requireAuth(req, res, apiToken)) return;
|
|
715
564
|
return readBody(req).then((text) => {
|
|
716
565
|
const body = parseBody(text);
|
|
717
|
-
|
|
718
|
-
|
|
719
|
-
|
|
720
|
-
description: body.description,
|
|
721
|
-
instruction: body.instruction
|
|
722
|
-
});
|
|
723
|
-
sendJson(res, 200, { command });
|
|
724
|
-
} catch (error) {
|
|
725
|
-
sendJson(res, 400, { error: error.message });
|
|
566
|
+
if (body.mode !== "light" && body.mode !== "standard") {
|
|
567
|
+
sendJson(res, 400, { error: "invalid-mode" });
|
|
568
|
+
return;
|
|
726
569
|
}
|
|
570
|
+
settings.setPanelMode(body.mode);
|
|
571
|
+
sendJson(res, 200, { mode: settings.getPanelMode() });
|
|
727
572
|
});
|
|
728
573
|
}
|
|
729
|
-
|
|
730
|
-
if (!requireAuth(req, res, apiToken)) return;
|
|
731
|
-
const url = new URL(req.url, "http://localhost");
|
|
732
|
-
const id = url.searchParams.get("id");
|
|
733
|
-
const removed = id ? commands.remove(id) : false;
|
|
734
|
-
sendJson(res, 200, { removed });
|
|
735
|
-
return;
|
|
736
|
-
}
|
|
737
|
-
sendJson(res, 200, { commands: commands.list() });
|
|
574
|
+
sendJson(res, 200, { mode: settings.getPanelMode() });
|
|
738
575
|
} catch {
|
|
739
576
|
sendJson(res, 500, { error: "internal" });
|
|
740
577
|
}
|
|
741
578
|
}
|
|
742
579
|
});
|
|
743
580
|
|
|
744
|
-
// ---
|
|
745
|
-
// GET /api/dsh-mneme/memory/tags?id=<memoryId> → live entity_attrs-backed
|
|
746
|
-
// tag set for one memory plus the manualTagEnabled gate (so the panel
|
|
747
|
-
// can hide tag editing when the manual path is off). Read-only, stays
|
|
748
|
-
// open when apiToken is set (like list/search/semantic).
|
|
749
|
-
// POST /api/dsh-mneme/memory/tags { id, tags } → overwrite the live tag set
|
|
750
|
-
// via service.setMemoryTags (manualTagEnabled gate); 409 when the gate
|
|
751
|
-
// is closed. Auth-gated like the other write endpoints.
|
|
581
|
+
// --- external API settings (the standalone server's panel-facing config) ---
|
|
752
582
|
register({
|
|
753
583
|
kind: "exact",
|
|
754
|
-
path: "/api/dsh-mneme/
|
|
584
|
+
path: "/api/dsh-mneme/external-api",
|
|
755
585
|
handler(req, res) {
|
|
756
586
|
try {
|
|
757
|
-
if (req.method === "
|
|
587
|
+
if (req.method === "PUT" || req.method === "POST") {
|
|
758
588
|
if (!requireAuth(req, res, apiToken)) return;
|
|
759
589
|
return readBody(req).then((text) => {
|
|
760
590
|
const body = parseBody(text);
|
|
761
|
-
const
|
|
762
|
-
if (
|
|
763
|
-
|
|
764
|
-
|
|
765
|
-
|
|
766
|
-
|
|
767
|
-
|
|
768
|
-
|
|
769
|
-
|
|
591
|
+
const patch = {};
|
|
592
|
+
if (body.enabled !== undefined) patch.enabled = body.enabled === true;
|
|
593
|
+
if (body.port !== undefined) {
|
|
594
|
+
const port = Number(body.port);
|
|
595
|
+
if (!Number.isInteger(port) || port < 1 || port > 65535) {
|
|
596
|
+
sendJson(res, 400, { error: "invalid-port" });
|
|
597
|
+
return;
|
|
598
|
+
}
|
|
599
|
+
patch.port = port;
|
|
770
600
|
}
|
|
771
|
-
|
|
772
|
-
|
|
773
|
-
|
|
774
|
-
|
|
601
|
+
if (body.host !== undefined) {
|
|
602
|
+
const host = String(body.host).trim();
|
|
603
|
+
if (!host || host.includes("://")) {
|
|
604
|
+
sendJson(res, 400, { error: "invalid-host" });
|
|
605
|
+
return;
|
|
606
|
+
}
|
|
607
|
+
patch.host = host;
|
|
775
608
|
}
|
|
776
|
-
sendJson(res, 200, {
|
|
609
|
+
sendJson(res, 200, { config: fullExternalConfig(settings.setExternalApi(patch)) });
|
|
777
610
|
});
|
|
778
611
|
}
|
|
779
|
-
|
|
780
|
-
|
|
781
|
-
|
|
782
|
-
|
|
783
|
-
|
|
784
|
-
|
|
785
|
-
|
|
786
|
-
if (!memory) {
|
|
787
|
-
sendJson(res, 404, { error: "memory-not-found" });
|
|
788
|
-
return;
|
|
612
|
+
// GET: always hand back a token — the standalone server generates its
|
|
613
|
+
// own on first enabled boot, but the panel displays it before that,
|
|
614
|
+
// so materialize and persist one here.
|
|
615
|
+
const kv = settings.getExternalApi() ?? {};
|
|
616
|
+
if (!kv.token) {
|
|
617
|
+
kv.token = randomBytes(24).toString("base64url");
|
|
618
|
+
settings.setExternalApi({ token: kv.token });
|
|
789
619
|
}
|
|
790
|
-
|
|
791
|
-
sendJson(res, 200, {
|
|
792
|
-
memoryId: id,
|
|
793
|
-
tags: Array.isArray(tags) ? tags : [],
|
|
794
|
-
manualTagEnabled: service.manualTagEnabled?.() ?? true
|
|
795
|
-
});
|
|
620
|
+
sendJson(res, 200, { config: fullExternalConfig(kv) });
|
|
796
621
|
} catch {
|
|
797
622
|
sendJson(res, 500, { error: "internal" });
|
|
798
623
|
}
|
|
799
624
|
}
|
|
800
625
|
});
|
|
801
626
|
|
|
802
|
-
// ---
|
|
803
|
-
// GET /api/dsh-mneme/directory → memories grouped by tag as
|
|
804
|
-
// { groups: [{ tag, memories: [...] }], untagged: [...] }. Live-only
|
|
805
|
-
// (forgotten/archived/session-disposed excluded), groups tag-sorted, members
|
|
806
|
-
// importance+updated DESC. Read-only, stays open when apiToken is set.
|
|
627
|
+
// --- custom commands ---
|
|
807
628
|
register({
|
|
808
629
|
kind: "exact",
|
|
809
|
-
path: "/api/dsh-mneme/
|
|
630
|
+
path: "/api/dsh-mneme/commands",
|
|
810
631
|
handler(req, res) {
|
|
811
632
|
try {
|
|
812
|
-
|
|
813
|
-
|
|
633
|
+
if (req.method === "POST") {
|
|
634
|
+
if (!requireAuth(req, res, apiToken)) return;
|
|
635
|
+
return readBody(req).then((text) => {
|
|
636
|
+
const body = parseBody(text);
|
|
637
|
+
try {
|
|
638
|
+
const command = commands.add({
|
|
639
|
+
name: body.name,
|
|
640
|
+
description: body.description,
|
|
641
|
+
instruction: body.instruction
|
|
642
|
+
});
|
|
643
|
+
sendJson(res, 200, { command });
|
|
644
|
+
} catch (error) {
|
|
645
|
+
sendJson(res, 400, { error: error.message });
|
|
646
|
+
}
|
|
647
|
+
});
|
|
648
|
+
}
|
|
649
|
+
if (req.method === "DELETE") {
|
|
650
|
+
if (!requireAuth(req, res, apiToken)) return;
|
|
651
|
+
const url = new URL(req.url, "http://localhost");
|
|
652
|
+
const id = url.searchParams.get("id");
|
|
653
|
+
const removed = id ? commands.remove(id) : false;
|
|
654
|
+
sendJson(res, 200, { removed });
|
|
655
|
+
return;
|
|
656
|
+
}
|
|
657
|
+
sendJson(res, 200, { commands: commands.list() });
|
|
814
658
|
} catch {
|
|
815
659
|
sendJson(res, 500, { error: "internal" });
|
|
816
660
|
}
|
|
@@ -818,7 +662,7 @@ export function createApi(ctx, service, settings, commands, embedder, semantic =
|
|
|
818
662
|
});
|
|
819
663
|
|
|
820
664
|
return {
|
|
821
|
-
routes:
|
|
665
|
+
routes: 17,
|
|
822
666
|
dispose: () => {
|
|
823
667
|
for (const dispose of disposers) dispose();
|
|
824
668
|
}
|