@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
@@ -0,0 +1,264 @@
1
+ // Standalone HTTP API (v0.7.12): a plain node:http server for ecosystem
2
+ // integrations that live outside the DSH host and cannot reach the plugin's
3
+ // internal webServer routes (/api/dsh-mneme/*). Mirrors the JSON semantics of
4
+ // those routes but with mandatory Bearer-token auth on everything except
5
+ // GET /health, so the store can be exposed safely on loopback.
6
+ //
7
+ // Security: the default bind host is 127.0.0.1. Pointing externalApiHost at a
8
+ // non-loopback address exposes the whole memory store to the network — that is
9
+ // the operator's explicit responsibility (documented in README).
10
+ import { createServer } from "node:http";
11
+ import { randomBytes, timingSafeEqual } from "node:crypto";
12
+ import { TYPES } from "./store.js";
13
+
14
+ const DEFAULT_PORT = 8790;
15
+ const DEFAULT_HOST = "127.0.0.1";
16
+ // Hardcoded release version (package.json is bumped at publish time and may
17
+ // lag the code that ships in between).
18
+ const VERSION = "0.7.12";
19
+
20
+ function sendJson(res, status, payload) {
21
+ res.writeHead(status, { "Content-Type": "application/json; charset=utf-8" });
22
+ res.end(JSON.stringify(payload));
23
+ }
24
+
25
+ /** True when the request carries the configured token. */
26
+ function isAuthorized(req, apiToken) {
27
+ const raw = req.headers?.authorization ?? req.headers?.["x-dsh-mneme-token"] ?? "";
28
+ const token = raw.startsWith("Bearer ") ? raw.slice(7).trim() : raw.trim();
29
+ if (token === "" || token.length !== apiToken.length) return false;
30
+ // Constant-time comparison: no timing oracle on the token.
31
+ return timingSafeEqual(Buffer.from(token), Buffer.from(apiToken));
32
+ }
33
+
34
+ /** Collect the request body as text (tolerant of transport errors). */
35
+ function readBody(req) {
36
+ return new Promise((resolve) => {
37
+ let body = "";
38
+ req.on("data", (chunk) => { body += chunk; });
39
+ req.on("end", () => resolve(body));
40
+ req.on("error", () => resolve(""));
41
+ });
42
+ }
43
+
44
+ /**
45
+ * Create (and start) the standalone API server.
46
+ * Accepts { service, store, config, logger, settings, port, host }:
47
+ * - token: persisted settings kv "external_api" wins; auto-generated
48
+ * (crypto.randomBytes(24).toString("base64url")) and persisted when empty.
49
+ * - port: explicit arg > persisted settings > config.externalApiPort > 8790.
50
+ * - host: explicit arg > config.externalApiHost > "127.0.0.1".
51
+ * Returns { server, port, host, token, ready }: `port` is the effective bound
52
+ * port (updated to the OS-assigned one after `ready` resolves when asked to
53
+ * bind port 0), `ready` resolves once listening and rejects if the bind fails.
54
+ */
55
+ export function createStandaloneApi({ service, store, config = {}, logger, settings, port, host }) {
56
+ const persisted = settings?.getExternalApi?.() ?? {};
57
+
58
+ let token = typeof persisted.token === "string" ? persisted.token : "";
59
+ if (!token) {
60
+ token = randomBytes(24).toString("base64url");
61
+ // Persist only the token; enabled/port keys stay as they are (merge).
62
+ try {
63
+ settings?.setExternalApi?.({ token });
64
+ } catch (error) {
65
+ logger?.warn?.(`[dsh-mneme] standalone API token persistence failed: ${String(error)}`);
66
+ }
67
+ }
68
+
69
+ // Persisted host (set from the panel) wins over the bundle config, same
70
+ // precedence as port; the explicit argument still wins over both.
71
+ const boundHost = host
72
+ ?? (typeof persisted.host === "string" && persisted.host ? persisted.host : undefined)
73
+ ?? config.externalApiHost
74
+ ?? DEFAULT_HOST;
75
+ const persistedPort = Number(persisted.port);
76
+ const boundPort = port
77
+ ?? (Number.isInteger(persistedPort) && persistedPort > 0 ? persistedPort : undefined)
78
+ ?? config.externalApiPort
79
+ ?? DEFAULT_PORT;
80
+
81
+ const server = createServer((req, res) => {
82
+ try {
83
+ const url = new URL(req.url ?? "/", "http://localhost");
84
+ const pathname = url.pathname;
85
+
86
+ // Health is the single unauthenticated probe (monitor checks).
87
+ if (req.method === "GET" && pathname === "/health") {
88
+ sendJson(res, 200, { ok: true });
89
+ return;
90
+ }
91
+
92
+ // Everything else requires the Bearer token.
93
+ if (!isAuthorized(req, token)) {
94
+ sendJson(res, 401, { error: "unauthorized" });
95
+ return;
96
+ }
97
+
98
+ // --- GET /status: version + store shape + uptime -----------------------
99
+ if (req.method === "GET" && pathname === "/status") {
100
+ const byType = {};
101
+ for (const type of TYPES) byType[type] = service.count(type);
102
+ let entities = 0;
103
+ try {
104
+ entities = store.db.prepare("SELECT count(*) AS c FROM entities").get().c;
105
+ } catch { /* entities storage unavailable → 0 */ }
106
+ sendJson(res, 200, {
107
+ version: VERSION,
108
+ memories: { total: service.count(), byType },
109
+ entities,
110
+ uptime_s: Math.floor(process.uptime())
111
+ });
112
+ return;
113
+ }
114
+
115
+ // --- GET /memories: paged + filtered list (same semantics as the
116
+ // internal /api/dsh-mneme/list) ------------------------------------
117
+ if (req.method === "GET" && pathname === "/memories") {
118
+ const type = url.searchParams.get("type") ?? undefined;
119
+ const limit = Number(url.searchParams.get("limit") ?? 50);
120
+ const offset = Number(url.searchParams.get("offset") ?? 0);
121
+ const order = url.searchParams.get("order") ?? undefined;
122
+ const minRaw = url.searchParams.get("minImportance");
123
+ const minImportance = minRaw !== null && minRaw !== "" && !Number.isNaN(Number(minRaw))
124
+ ? Number(minRaw)
125
+ : undefined;
126
+ const source = url.searchParams.get("source") || undefined;
127
+ const items = service.toApiList(service.list({ type, limit, offset, order, minImportance, source }));
128
+ // Total honors the same filters so pager math stays correct.
129
+ sendJson(res, 200, { items, total: service.count(type, { minImportance, source }) });
130
+ return;
131
+ }
132
+
133
+ // --- GET/DELETE /memories/:id ------------------------------------------
134
+ const idMatch = pathname.match(/^\/memories\/([^/]+)$/);
135
+ if (idMatch) {
136
+ let id = idMatch[1];
137
+ try { id = decodeURIComponent(id); } catch { /* keep raw */ }
138
+ if (req.method === "GET") {
139
+ const row = service.getById(id);
140
+ if (!row) {
141
+ sendJson(res, 404, { error: "not-found" });
142
+ return;
143
+ }
144
+ sendJson(res, 200, service.toApiList([row])[0]);
145
+ return;
146
+ }
147
+ if (req.method === "DELETE") {
148
+ // store.remove deletes silently — precheck for a distinguishable 404.
149
+ if (!service.getById(id)) {
150
+ sendJson(res, 404, { error: "not-found" });
151
+ return;
152
+ }
153
+ service.remove(id);
154
+ sendJson(res, 200, { ok: true });
155
+ return;
156
+ }
157
+ sendJson(res, 404, { error: "not-found" });
158
+ return;
159
+ }
160
+
161
+ // --- POST /memories: save with title-dedupe (safer than raw save) ------
162
+ if (req.method === "POST" && pathname === "/memories") {
163
+ void readBody(req).then((text) => {
164
+ let body;
165
+ try {
166
+ body = JSON.parse(text || "{}");
167
+ } catch {
168
+ sendJson(res, 400, { error: "invalid-json" });
169
+ return;
170
+ }
171
+ if (body === null || typeof body !== "object" || Array.isArray(body)) {
172
+ sendJson(res, 400, { error: "invalid-body" });
173
+ return;
174
+ }
175
+ // Pre-validate what store.save would throw on, so clients get a
176
+ // clean 400 instead of a leaked SQLite error.
177
+ if (!TYPES.has(body.type)) {
178
+ sendJson(res, 400, { error: "invalid-type" });
179
+ return;
180
+ }
181
+ if (typeof body.title !== "string" || !body.title.trim()) {
182
+ sendJson(res, 400, { error: "missing-title" });
183
+ return;
184
+ }
185
+ if (typeof body.content !== "string") {
186
+ sendJson(res, 400, { error: "missing-content" });
187
+ return;
188
+ }
189
+ if (body.tags !== undefined && !Array.isArray(body.tags)) {
190
+ sendJson(res, 400, { error: "tags-must-be-an-array" });
191
+ return;
192
+ }
193
+ try {
194
+ const { action, memory } = service.saveWithDedupe({
195
+ type: body.type,
196
+ title: body.title,
197
+ content: body.content,
198
+ importance: body.importance,
199
+ tags: body.tags,
200
+ source: body.source
201
+ });
202
+ sendJson(res, action === "created" ? 201 : 200, service.toApiList([memory])[0]);
203
+ } catch (error) {
204
+ logger?.warn?.(`[dsh-mneme] standalone API save failed: ${String(error)}`);
205
+ sendJson(res, 500, { error: "internal" });
206
+ }
207
+ });
208
+ return;
209
+ }
210
+
211
+ // --- GET /search: unified recall pipeline (keyword + vector + BM25) ----
212
+ if (req.method === "GET" && pathname === "/search") {
213
+ const q = url.searchParams.get("q") ?? "";
214
+ const limit = Number(url.searchParams.get("topK") ?? url.searchParams.get("limit") ?? 20);
215
+ const mode = url.searchParams.get("mode") ?? "auto";
216
+ const rerank = url.searchParams.get("rerank") !== "false";
217
+ const query = q.trim();
218
+ if (!query) {
219
+ sendJson(res, 200, { items: [], mode: "keyword" });
220
+ return;
221
+ }
222
+ // Any vector/rerank failure degrades to keyword inside searchMemories.
223
+ void Promise.resolve(
224
+ service.searchMemories(query, { mode, topK: limit, useRerank: rerank })
225
+ ).then((rows) => {
226
+ const used = rows.some((m) => m.vector === true) ? "vector" : "keyword";
227
+ sendJson(res, 200, { items: service.toApiList(rows), mode: used });
228
+ }).catch(() => {
229
+ sendJson(res, 200, { items: service.toApiList(service.search(query, { limit })), mode: "keyword" });
230
+ });
231
+ return;
232
+ }
233
+
234
+ sendJson(res, 404, { error: "not-found" });
235
+ } catch {
236
+ sendJson(res, 500, { error: "internal" });
237
+ }
238
+ });
239
+
240
+ // A permanent error listener keeps an EADDRINUSE / runtime socket error from
241
+ // crashing the host process; `ready` still surfaces the first bind failure.
242
+ server.on("error", (error) => {
243
+ logger?.warn?.(`[dsh-mneme] standalone API error: ${String(error)}`);
244
+ });
245
+ const ready = new Promise((resolve, reject) => {
246
+ server.once("listening", resolve);
247
+ server.once("error", reject);
248
+ });
249
+ server.listen(boundPort, boundHost);
250
+ ready.then(() => {
251
+ const address = server.address();
252
+ if (address && typeof address === "object") {
253
+ logger?.info?.(`[dsh-mneme] standalone API listening on http://${address.address}:${address.port}`);
254
+ }
255
+ }).catch(() => { /* already logged by the error handler above */ });
256
+
257
+ const api = { server, port: boundPort, host: boundHost, token, ready };
258
+ // After the OS assigns the real port (bind port 0), reflect it for callers.
259
+ ready.then(() => {
260
+ const address = server.address();
261
+ if (address && typeof address === "object") api.port = address.port;
262
+ }).catch(() => { /* bind failed: port stays as configured */ });
263
+ return api;
264
+ }