@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.
Files changed (158) hide show
  1. package/README.en.md +437 -0
  2. package/README.md +364 -166
  3. package/bin/cli.mjs +603 -0
  4. package/{dsh-mneme/cordis.patch.yml → cordis.patch.yml} +1 -0
  5. package/lib/api-standalone.js +264 -0
  6. package/{dsh-mneme/lib → lib}/api.js +133 -289
  7. package/{dsh-mneme/lib → lib}/client.js +1008 -922
  8. package/{dsh-mneme/lib → lib}/config.js +54 -99
  9. package/{dsh-mneme/lib → lib}/dream/decisions.js +94 -143
  10. package/{dsh-mneme/lib → lib}/dream/sleep.js +12 -152
  11. package/{dsh-mneme/lib → lib}/dream.js +22 -168
  12. package/{dsh-mneme/src → lib}/hot-memory.js +0 -7
  13. package/{dsh-mneme/lib → lib}/index.js +45 -42
  14. package/{dsh-mneme/lib → lib}/inject.js +11 -53
  15. package/{dsh-mneme/src → lib}/mirror.js +1 -12
  16. package/{dsh-mneme/lib → lib}/quality-filter.js +1 -3
  17. package/{dsh-mneme/lib → lib}/reranker.js +1 -4
  18. package/{dsh-mneme/lib → lib}/service.js +20 -410
  19. package/{dsh-mneme/src → lib}/settings.js +34 -64
  20. package/{dsh-mneme/lib → lib}/store.js +38 -450
  21. package/{dsh-mneme/lib → lib}/summarize.js +10 -10
  22. package/lib/tools.js +274 -0
  23. package/package.json +45 -19
  24. package/{dsh-mneme/scripts → scripts}/e2e-dsh.js +3 -4
  25. package/{dsh-mneme/scripts → scripts}/sync-lib.js +5 -10
  26. package/src/api-standalone.js +264 -0
  27. package/{dsh-mneme/src → src}/api.js +133 -289
  28. package/{dsh-mneme/src → src}/config.js +54 -99
  29. package/{dsh-mneme/src → src}/dream/decisions.js +94 -143
  30. package/{dsh-mneme/src → src}/dream/sleep.js +12 -152
  31. package/{dsh-mneme/src → src}/dream.js +22 -168
  32. package/{dsh-mneme/lib → src}/hot-memory.js +0 -7
  33. package/{dsh-mneme/src → src}/index.js +45 -42
  34. package/{dsh-mneme/src → src}/inject.js +11 -53
  35. package/{dsh-mneme/lib → src}/mirror.js +1 -12
  36. package/{dsh-mneme/src → src}/quality-filter.js +1 -3
  37. package/{dsh-mneme/src → src}/reranker.js +1 -4
  38. package/{dsh-mneme/src → src}/service.js +20 -410
  39. package/{dsh-mneme/lib → src}/settings.js +34 -64
  40. package/{dsh-mneme/src → src}/store.js +38 -450
  41. package/{dsh-mneme/src → src}/summarize.js +10 -10
  42. package/src/tools.js +274 -0
  43. package/{dsh-mneme/test → test}/api.test.js +45 -156
  44. package/{dsh-mneme/test → test}/client.test.js +10 -206
  45. package/{dsh-mneme/test → test}/config.test.js +0 -21
  46. package/{dsh-mneme/test → test}/dream.test.js +1 -160
  47. package/{dsh-mneme/test → test}/graph-api.test.js +0 -34
  48. package/{dsh-mneme/test → test}/hot-memory.test.js +0 -29
  49. package/test/inject.test.js +120 -0
  50. package/{dsh-mneme/test → test}/llm-audit.test.js +4 -4
  51. package/{dsh-mneme/test → test}/reasoning-effort.test.js +0 -27
  52. package/{dsh-mneme/test → test}/recall-layer.test.js +5 -26
  53. package/{dsh-mneme/test → test}/reranker.test.js +0 -43
  54. package/{dsh-mneme/test → test}/service-search.test.js +0 -25
  55. package/{dsh-mneme/test → test}/service.test.js +0 -106
  56. package/{dsh-mneme/test → test}/settings.test.js +16 -35
  57. package/{dsh-mneme/test → test}/sleep.test.js +4 -171
  58. package/test/standalone-api.test.js +326 -0
  59. package/{dsh-mneme/test → test}/store.test.js +0 -76
  60. package/{dsh-mneme/test → test}/summarize.test.js +16 -15
  61. package/test/tools.test.js +265 -0
  62. package/.github/workflows/publish.yml +0 -58
  63. package/.github/workflows/test.yml +0 -32
  64. package/CHANGELOG.md +0 -91
  65. package/CONTRIBUTING.md +0 -293
  66. package/SECURITY.md +0 -718
  67. package/docs/devlog/2026-08-14-dsh-mneme-dev-log.md +0 -247
  68. package/docs/devlog/2026-08-15-dsh-mneme-audit-stress-dev-log.md +0 -145
  69. package/docs/devlog/2026-08-15-dsh-mneme-pipeline-dev-log.md +0 -56
  70. package/docs/devlog/2026-08-15-dsh-mneme-reflection-dev-log.md +0 -77
  71. package/docs/devlog/2026-08-15-dsh-mneme-review-fixes-dev-log.md +0 -64
  72. package/docs/devlog/2026-08-15-dsh-mneme-semantic-dev-log.md +0 -90
  73. package/dsh-mneme/CHANGELOG.md +0 -419
  74. package/dsh-mneme/LICENSE +0 -21
  75. package/dsh-mneme/README.md +0 -501
  76. package/dsh-mneme/docs/AGENT_MEMORY_RESEARCH.md +0 -183
  77. package/dsh-mneme/docs/ENTITIES.md +0 -245
  78. package/dsh-mneme/docs/LOCAL_MODEL.md +0 -141
  79. package/dsh-mneme/docs/MIGRATION.md +0 -127
  80. package/dsh-mneme/docs/SEMANTIC.md +0 -256
  81. package/dsh-mneme/docs/SLEEP.md +0 -163
  82. package/dsh-mneme/lib/dream/tag-extractor.js +0 -156
  83. package/dsh-mneme/lib/heat.js +0 -136
  84. package/dsh-mneme/lib/parser/tag.js +0 -59
  85. package/dsh-mneme/lib/parser/wiki-link.js +0 -38
  86. package/dsh-mneme/lib/search/tag-boost.js +0 -61
  87. package/dsh-mneme/lib/tools.js +0 -458
  88. package/dsh-mneme/package-lock.json +0 -1936
  89. package/dsh-mneme/package.json +0 -83
  90. package/dsh-mneme/scripts/check-sync.js +0 -42
  91. package/dsh-mneme/src/client.js +0 -2242
  92. package/dsh-mneme/src/dream/tag-extractor.js +0 -156
  93. package/dsh-mneme/src/heat.js +0 -136
  94. package/dsh-mneme/src/parser/tag.js +0 -59
  95. package/dsh-mneme/src/parser/wiki-link.js +0 -38
  96. package/dsh-mneme/src/search/tag-boost.js +0 -61
  97. package/dsh-mneme/src/tools.js +0 -458
  98. package/dsh-mneme/test/boundary-v0625.test.js +0 -82
  99. package/dsh-mneme/test/directory.test.js +0 -134
  100. package/dsh-mneme/test/heat.test.js +0 -148
  101. package/dsh-mneme/test/inject.test.js +0 -206
  102. package/dsh-mneme/test/layered-types-stats.test.js +0 -144
  103. package/dsh-mneme/test/lib-smoke.test.js +0 -109
  104. package/dsh-mneme/test/normalize-decisions.test.js +0 -120
  105. package/dsh-mneme/test/provenance.test.js +0 -103
  106. package/dsh-mneme/test/recall-runs.test.js +0 -93
  107. package/dsh-mneme/test/sleep-heat.test.js +0 -112
  108. package/dsh-mneme/test/tag-boost.test.js +0 -125
  109. package/dsh-mneme/test/tag.test.js +0 -426
  110. package/dsh-mneme/test/tools.test.js +0 -674
  111. package/dsh-mneme/test/updated-at-semantics.test.js +0 -113
  112. package/dsh-mneme/test/wiki-link.test.js +0 -332
  113. package//346/250/252/345/271/205.png +0 -0
  114. /package/{dsh-mneme/lib → lib}/commands.js +0 -0
  115. /package/{dsh-mneme/lib → lib}/dream/clustering.js +0 -0
  116. /package/{dsh-mneme/lib → lib}/embedding.js +0 -0
  117. /package/{dsh-mneme/lib → lib}/entities/extractor.js +0 -0
  118. /package/{dsh-mneme/lib → lib}/local-embedder.js +0 -0
  119. /package/{dsh-mneme/lib → lib}/search/adaptive.js +0 -0
  120. /package/{dsh-mneme/lib → lib}/search/bm25.js +0 -0
  121. /package/{dsh-mneme/lib → lib}/vector-index.js +0 -0
  122. /package/{dsh-mneme/scripts → scripts}/benchmark-embed.js +0 -0
  123. /package/{dsh-mneme/scripts → scripts}/benchmark-recall.js +0 -0
  124. /package/{dsh-mneme/scripts → scripts}/benchmark-rerank.js +0 -0
  125. /package/{dsh-mneme/scripts → scripts}/stress-dsh.js +0 -0
  126. /package/{dsh-mneme/src → src}/commands.js +0 -0
  127. /package/{dsh-mneme/src → src}/dream/clustering.js +0 -0
  128. /package/{dsh-mneme/src → src}/embedding.js +0 -0
  129. /package/{dsh-mneme/src → src}/entities/extractor.js +0 -0
  130. /package/{dsh-mneme/src → src}/local-embedder.js +0 -0
  131. /package/{dsh-mneme/src → src}/search/adaptive.js +0 -0
  132. /package/{dsh-mneme/src → src}/search/bm25.js +0 -0
  133. /package/{dsh-mneme/src → src}/vector-index.js +0 -0
  134. /package/{dsh-mneme/test → test}/audit.test.js +0 -0
  135. /package/{dsh-mneme/test → test}/benchmark.test.js +0 -0
  136. /package/{dsh-mneme/test → test}/clustering.test.js +0 -0
  137. /package/{dsh-mneme/test → test}/commands.test.js +0 -0
  138. /package/{dsh-mneme/test → test}/conflict-freeze.test.js +0 -0
  139. /package/{dsh-mneme/test → test}/entities.test.js +0 -0
  140. /package/{dsh-mneme/test → test}/epistemic.test.js +0 -0
  141. /package/{dsh-mneme/test → test}/fnew-0112.test.js +0 -0
  142. /package/{dsh-mneme/test → test}/fnew-03.test.js +0 -0
  143. /package/{dsh-mneme/test → test}/helpers/dream-mock.js +0 -0
  144. /package/{dsh-mneme/test → test}/local-embedder.test.js +0 -0
  145. /package/{dsh-mneme/test → test}/mirror-dirty.test.js +0 -0
  146. /package/{dsh-mneme/test → test}/mirror-edit-digest.test.js +0 -0
  147. /package/{dsh-mneme/test → test}/mirror-generation.test.js +0 -0
  148. /package/{dsh-mneme/test → test}/mirror.test.js +0 -0
  149. /package/{dsh-mneme/test → test}/peer-blockers.test.js +0 -0
  150. /package/{dsh-mneme/test → test}/policy-epoch.test.js +0 -0
  151. /package/{dsh-mneme/test → test}/quality-filter.test.js +0 -0
  152. /package/{dsh-mneme/test → test}/recall-evals.test.js +0 -0
  153. /package/{dsh-mneme/test → test}/receipt-chain.test.js +0 -0
  154. /package/{dsh-mneme/test → test}/reflection.test.js +0 -0
  155. /package/{dsh-mneme/test → test}/search-fusion.test.js +0 -0
  156. /package/{dsh-mneme/test → test}/semantic.test.js +0 -0
  157. /package/{dsh-mneme/test → test}/stress.test.js +0 -0
  158. /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
- const items = service.toApiList(service.list({ type, limit, offset }));
92
- sendJson(res, 200, { items, total: service.count(type) });
93
- } catch {
94
- sendJson(res, 500, { error: "internal" });
95
- }
96
- }
97
- });
98
-
99
- register({
100
- kind: "exact",
101
- path: "/api/dsh-mneme/stats",
102
- handler(req, res) {
103
- try {
104
- const url = new URL(req.url, "http://localhost");
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
- // --- custom commands ---
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/commands",
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
- try {
718
- const command = commands.add({
719
- name: body.name,
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
- if (req.method === "DELETE") {
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
- // --- memory tags (v0.6.2) ------------------------------------------------
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/memory/tags",
584
+ path: "/api/dsh-mneme/external-api",
755
585
  handler(req, res) {
756
586
  try {
757
- if (req.method === "POST" || req.method === "PUT") {
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 id = typeof body.id === "string" ? body.id.trim() : "";
762
- if (!id) {
763
- sendJson(res, 400, { error: "missing-id" });
764
- return;
765
- }
766
- const memory = service.getById?.(id) ?? null;
767
- if (!memory) {
768
- sendJson(res, 404, { error: "memory-not-found" });
769
- return;
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
- const result = service.setMemoryTags(id, Array.isArray(body.tags) ? body.tags : []);
772
- if (result?.ok === false) {
773
- sendJson(res, 409, { error: result.error || "tags-disabled" });
774
- return;
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, { ok: true, memoryId: id, tags: result?.tags ?? [] });
609
+ sendJson(res, 200, { config: fullExternalConfig(settings.setExternalApi(patch)) });
777
610
  });
778
611
  }
779
- const url = new URL(req.url, "http://localhost");
780
- const id = (url.searchParams.get("id") ?? "").trim();
781
- if (!id) {
782
- sendJson(res, 400, { error: "missing-id" });
783
- return;
784
- }
785
- const memory = service.getById?.(id) ?? null;
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
- const tags = service.getMemoryTags?.(id) ?? [];
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
- // --- directory view (v0.6.3) ---------------------------------------------
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/directory",
630
+ path: "/api/dsh-mneme/commands",
810
631
  handler(req, res) {
811
632
  try {
812
- const dir = service.getDirectory?.() ?? { groups: [], untagged: [] };
813
- sendJson(res, 200, dir);
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: 20,
665
+ routes: 17,
822
666
  dispose: () => {
823
667
  for (const dispose of disposers) dispose();
824
668
  }