@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
|
@@ -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
|
+
}
|