@melandlabs/memory-store 0.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +17 -0
- package/README.md +377 -0
- package/dist/config-MlK9f9CM.d.ts +147 -0
- package/dist/contracts.d.ts +62 -0
- package/dist/contracts.js +27 -0
- package/dist/contracts.js.map +1 -0
- package/dist/http.d.ts +38 -0
- package/dist/http.js +545 -0
- package/dist/http.js.map +1 -0
- package/dist/index.d.ts +35 -0
- package/dist/index.js +442 -0
- package/dist/index.js.map +1 -0
- package/dist/mcp.d.ts +29 -0
- package/dist/mcp.js +589 -0
- package/dist/mcp.js.map +1 -0
- package/dist/policies/memory-graph-correction-policy.d.ts +14 -0
- package/dist/policies/memory-graph-correction-policy.js +41 -0
- package/dist/policies/memory-graph-correction-policy.js.map +1 -0
- package/dist/policies/memory-graph-write-policy.d.ts +35 -0
- package/dist/policies/memory-graph-write-policy.js +78 -0
- package/dist/policies/memory-graph-write-policy.js.map +1 -0
- package/dist/search/unified-search.d.ts +81 -0
- package/dist/search/unified-search.js +399 -0
- package/dist/search/unified-search.js.map +1 -0
- package/dist/server/cli-http.d.ts +1 -0
- package/dist/server/cli-http.js +584 -0
- package/dist/server/cli-http.js.map +1 -0
- package/dist/server/cli-mcp.d.ts +1 -0
- package/dist/server/cli-mcp.js +603 -0
- package/dist/server/cli-mcp.js.map +1 -0
- package/dist/storage/chroma-memory-index.d.ts +76 -0
- package/dist/storage/chroma-memory-index.js +249 -0
- package/dist/storage/chroma-memory-index.js.map +1 -0
- package/dist/storage/postgres-raw-message-factory.d.ts +48 -0
- package/dist/storage/postgres-raw-message-factory.js +19 -0
- package/dist/storage/postgres-raw-message-factory.js.map +1 -0
- package/dist/storage/raw-message-store.d.ts +47 -0
- package/dist/storage/raw-message-store.js +140 -0
- package/dist/storage/raw-message-store.js.map +1 -0
- package/dist/storage/sqlite-raw-message-store.d.ts +19 -0
- package/dist/storage/sqlite-raw-message-store.js +68 -0
- package/dist/storage/sqlite-raw-message-store.js.map +1 -0
- package/dist/storage/sqlite-vector-index.d.ts +42 -0
- package/dist/storage/sqlite-vector-index.js +92 -0
- package/dist/storage/sqlite-vector-index.js.map +1 -0
- package/package.json +87 -0
package/LICENSE
ADDED
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
Apache License
|
|
2
|
+
Version 2.0, January 2004
|
|
3
|
+
http://www.apache.org/licenses/
|
|
4
|
+
|
|
5
|
+
Copyright 2026 Meland Labs
|
|
6
|
+
|
|
7
|
+
Licensed under the Apache License, Version 2.0 (the "License");
|
|
8
|
+
you may not use this file except in compliance with the License.
|
|
9
|
+
You may obtain a copy of the License at
|
|
10
|
+
|
|
11
|
+
http://www.apache.org/licenses/LICENSE-2.0
|
|
12
|
+
|
|
13
|
+
Unless required by applicable law or agreed to in writing, software
|
|
14
|
+
distributed under the License is distributed on an "AS IS" BASIS,
|
|
15
|
+
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|
16
|
+
See the License for the specific language governing permissions and
|
|
17
|
+
limitations under the License.
|
package/README.md
ADDED
|
@@ -0,0 +1,377 @@
|
|
|
1
|
+
# memory-store (workspace)
|
|
2
|
+
|
|
3
|
+
> **Workspace package.** Internal monorepo build artifact; not published to npm.
|
|
4
|
+
> End users install [`@melandlabs/opencontext`](https://www.npmjs.com/package/@melandlabs/opencontext)
|
|
5
|
+
> (the facade) instead. Monorepo contributors depend on this package via
|
|
6
|
+
> the workspace protocol.
|
|
7
|
+
|
|
8
|
+
|
|
9
|
+
OpenContext memory storage + search SDK with optional HTTP and MCP server
|
|
10
|
+
entry points. The package is intentionally decoupled from the opencontext
|
|
11
|
+
web app's database and env layers — every consumer wires up its own
|
|
12
|
+
implementation via `MemoryStoreConfig`.
|
|
13
|
+
|
|
14
|
+
- **SDK** (`memory-store`) — embed in a Node.js host
|
|
15
|
+
- **HTTP** (`memory-store/http`) — REST daemon
|
|
16
|
+
- **MCP** (`memory-store/mcp`) — stdio tools for Claude Desktop / Cursor
|
|
17
|
+
|
|
18
|
+
## Install
|
|
19
|
+
|
|
20
|
+
```bash
|
|
21
|
+
pnpm add @melandlabs/opencontext
|
|
22
|
+
```
|
|
23
|
+
|
|
24
|
+
## Quick start
|
|
25
|
+
|
|
26
|
+
```ts
|
|
27
|
+
import { createMemoryStore } from "memory-store";
|
|
28
|
+
|
|
29
|
+
const store = await createMemoryStore({
|
|
30
|
+
db: { getDb: () => drizzleDb() },
|
|
31
|
+
env: { isTauriMode: () => false },
|
|
32
|
+
unified: {
|
|
33
|
+
embedQuery: async ({ query }) => myEmbed(query),
|
|
34
|
+
},
|
|
35
|
+
});
|
|
36
|
+
|
|
37
|
+
await store.getRawMessageManager();
|
|
38
|
+
const hits = await store.searchUnifiedMemory({ userId, query });
|
|
39
|
+
```
|
|
40
|
+
|
|
41
|
+
## Entry points
|
|
42
|
+
|
|
43
|
+
| Import | What you get |
|
|
44
|
+
| -------------------------------- | ----------------------------------------------- |
|
|
45
|
+
| `memory-store` | `createMemoryStore(config)` facade |
|
|
46
|
+
| `memory-store/http` | `startHttpServer(options)` — Hono app on a port |
|
|
47
|
+
| `memory-store/mcp` | `startMcpServer()` — stdio MCP server |
|
|
48
|
+
|
|
49
|
+
CLI bins ship with the package:
|
|
50
|
+
|
|
51
|
+
```bash
|
|
52
|
+
opencontext-memory-http --port 7421 --host 127.0.0.1
|
|
53
|
+
opencontext-memory-mcp
|
|
54
|
+
```
|
|
55
|
+
|
|
56
|
+
## Configuration matrix
|
|
57
|
+
|
|
58
|
+
| Key | Required | Description |
|
|
59
|
+
| ----------------------------------------------------- | ------------------------------- | --------------------------------------------------------- |
|
|
60
|
+
| `db.getDb()` | yes for persistence | Drizzle DB handle factory |
|
|
61
|
+
| `db.tables.rawMessages` / `db.tables.memorySummaries` | yes for postgres backend | Drizzle table references owned by the host |
|
|
62
|
+
| `env.isTauriMode()` | yes | selects sqlite vs postgres backend |
|
|
63
|
+
| `env.getTauriDbPath()` / `env.getTauriDataDir()` | for Tauri | where to place the SQLite files |
|
|
64
|
+
| `vector.backend` | one of `sqlite-vec` or `chroma` | vector index backend (auto-detected if omitted) |
|
|
65
|
+
| `vector.sqliteVec.dbPath` | when backend = sqlite-vec | sqlite-vec DB path |
|
|
66
|
+
| `vector.chroma.url` | when backend = chroma | chroma server URL |
|
|
67
|
+
| `unified.embedQuery` | yes for unified search | query embedder |
|
|
68
|
+
| `unified.searchKnowledge` | optional | RAG over uploaded documents |
|
|
69
|
+
| `unified.searchInsights` | optional | semantic search over extracted insights |
|
|
70
|
+
| `unified.searchRawMessagesAnn` | optional | postgres-side ANN over `raw_messages` (database fallback) |
|
|
71
|
+
| `logger` | optional | `console`-shaped logger; defaults to `console` |
|
|
72
|
+
|
|
73
|
+
If any of `unified.embedQuery`, `unified.searchKnowledge`, `unified.searchInsights`,
|
|
74
|
+
or `unified.searchRawMessagesAnn` are absent, the corresponding source in
|
|
75
|
+
`searchUnifiedMemory` returns empty results with a warning. The SDK still
|
|
76
|
+
works — fine for a read-only memory daemon or a chroma-only deployment.
|
|
77
|
+
|
|
78
|
+
See `src/config.ts` for the full type surface.
|
|
79
|
+
|
|
80
|
+
## Recipes
|
|
81
|
+
|
|
82
|
+
### 1. Postgres backend (server / agent daemon)
|
|
83
|
+
|
|
84
|
+
```ts
|
|
85
|
+
import { drizzle } from "drizzle-orm/postgres-js";
|
|
86
|
+
import {
|
|
87
|
+
createMemoryStore,
|
|
88
|
+
registerPostgresFactory,
|
|
89
|
+
type PostgresRawMessageManagerLike,
|
|
90
|
+
} from "memory-store";
|
|
91
|
+
import { myPostgresRawMessageManager } from "./postgres-raw-message-store";
|
|
92
|
+
import * as schema from "./schema";
|
|
93
|
+
|
|
94
|
+
registerPostgresFactory<typeof myPostgresRawMessageManager>(
|
|
95
|
+
async () =>
|
|
96
|
+
myPostgresRawMessageManager as unknown as PostgresRawMessageManagerLike,
|
|
97
|
+
);
|
|
98
|
+
|
|
99
|
+
const db = drizzle(process.env.DATABASE_URL!, { schema });
|
|
100
|
+
|
|
101
|
+
const store = await createMemoryStore({
|
|
102
|
+
db: {
|
|
103
|
+
getDb: () => db,
|
|
104
|
+
tables: {
|
|
105
|
+
rawMessages: schema.rawMessages,
|
|
106
|
+
memorySummaries: schema.memorySummaries,
|
|
107
|
+
},
|
|
108
|
+
},
|
|
109
|
+
env: { isTauriMode: () => false },
|
|
110
|
+
unified: {
|
|
111
|
+
embedQuery: async ({ query }) => myEmbedder.embedQuery(query),
|
|
112
|
+
searchInsights: mySearchInsights,
|
|
113
|
+
searchKnowledge: mySearchKnowledge,
|
|
114
|
+
searchRawMessagesAnn: async ({
|
|
115
|
+
userId,
|
|
116
|
+
queryEmbedding,
|
|
117
|
+
limit,
|
|
118
|
+
threshold,
|
|
119
|
+
botId,
|
|
120
|
+
}) => myAnnSearch({ userId, queryEmbedding, limit, threshold, botId }),
|
|
121
|
+
},
|
|
122
|
+
});
|
|
123
|
+
```
|
|
124
|
+
|
|
125
|
+
### 2. Tauri / SQLite-vec backend (desktop)
|
|
126
|
+
|
|
127
|
+
```ts
|
|
128
|
+
const store = await createMemoryStore({
|
|
129
|
+
env: {
|
|
130
|
+
isTauriMode: () => true,
|
|
131
|
+
getTauriDataDir: () => appDataDir(),
|
|
132
|
+
getTauriDbPath: () => `${appDataDir()}/memory.sqlite`,
|
|
133
|
+
},
|
|
134
|
+
vector: {
|
|
135
|
+
backend: "sqlite-vec",
|
|
136
|
+
sqliteVec: { dbPath: `${appDataDir()}/vectors.sqlite` },
|
|
137
|
+
},
|
|
138
|
+
unified: {
|
|
139
|
+
embedQuery: async ({ query }) => localOnnxEmbedder.embed(query),
|
|
140
|
+
},
|
|
141
|
+
});
|
|
142
|
+
```
|
|
143
|
+
|
|
144
|
+
### 3. Chroma backend (managed vector store)
|
|
145
|
+
|
|
146
|
+
```ts
|
|
147
|
+
const store = await createMemoryStore({
|
|
148
|
+
env: { isTauriMode: () => false },
|
|
149
|
+
vector: {
|
|
150
|
+
backend: "chroma",
|
|
151
|
+
chroma: {
|
|
152
|
+
url: process.env.CHROMA_URL ?? "http://127.0.0.1:8000",
|
|
153
|
+
rawMessagesCollection: "opencontext_raw_messages",
|
|
154
|
+
insightsCollection: "opencontext_insights",
|
|
155
|
+
},
|
|
156
|
+
},
|
|
157
|
+
unified: { embedQuery: myEmbedder },
|
|
158
|
+
});
|
|
159
|
+
```
|
|
160
|
+
|
|
161
|
+
When chroma is reachable and the host's postgres manager exposes
|
|
162
|
+
`searchMessagesSemantically`, chroma wins and the database path becomes
|
|
163
|
+
the fallback — same semantics as the web app.
|
|
164
|
+
|
|
165
|
+
### 4. Cross-source search wiring
|
|
166
|
+
|
|
167
|
+
`createUnifiedSearch(deps)` accepts the per-source searchers independently.
|
|
168
|
+
You don't need to wire all of them; the ones you omit just emit a warning:
|
|
169
|
+
|
|
170
|
+
```ts
|
|
171
|
+
import { createUnifiedSearch } from "memory-store/unified-search";
|
|
172
|
+
|
|
173
|
+
const search = createUnifiedSearch({
|
|
174
|
+
embedQuery: async ({ userId, query, authToken }) =>
|
|
175
|
+
embedder.embed(query, { userId, authToken }),
|
|
176
|
+
|
|
177
|
+
searchRawMessagesAnn: async ({
|
|
178
|
+
userId,
|
|
179
|
+
queryEmbedding,
|
|
180
|
+
limit,
|
|
181
|
+
threshold,
|
|
182
|
+
botId,
|
|
183
|
+
}) =>
|
|
184
|
+
db.rawMessages.searchAnn({
|
|
185
|
+
userId,
|
|
186
|
+
embedding: queryEmbedding,
|
|
187
|
+
limit,
|
|
188
|
+
threshold,
|
|
189
|
+
botId,
|
|
190
|
+
}),
|
|
191
|
+
|
|
192
|
+
searchInsights: async ({ userId, query, limit, threshold, botIds }) =>
|
|
193
|
+
insightIndex.search({ userId, query, limit, threshold, botIds }),
|
|
194
|
+
|
|
195
|
+
searchKnowledge: async ({ userId, query, options, authToken }) =>
|
|
196
|
+
ragIndex.search({ userId, query, ...options, authToken }),
|
|
197
|
+
});
|
|
198
|
+
|
|
199
|
+
const result = await search.searchUnifiedMemory({
|
|
200
|
+
userId: "u-1",
|
|
201
|
+
query: "what changed since yesterday?",
|
|
202
|
+
sources: ["memory", "insights", "knowledge"],
|
|
203
|
+
limit: 10,
|
|
204
|
+
threshold: 0.7,
|
|
205
|
+
botIds: ["bot-42"],
|
|
206
|
+
});
|
|
207
|
+
|
|
208
|
+
// result.results: UnifiedMemorySearchResult[]
|
|
209
|
+
// result.warnings: UnifiedMemorySearchWarning[]
|
|
210
|
+
// e.g. { source: "memory", code: "raw_message_storage_unavailable", ... }
|
|
211
|
+
```
|
|
212
|
+
|
|
213
|
+
### 5. Postgres manager factory (lazy registration)
|
|
214
|
+
|
|
215
|
+
If the host application owns the postgres raw-message manager (e.g. an
|
|
216
|
+
existing Drizzle-based repo), register it via the factory pattern. The
|
|
217
|
+
memory-store package will resolve it on first use:
|
|
218
|
+
|
|
219
|
+
```ts
|
|
220
|
+
import { registerPostgresFactory } from "memory-store/postgres-raw-message-factory";
|
|
221
|
+
import { myPostgresManager } from "./managers/postgres-raw-message";
|
|
222
|
+
|
|
223
|
+
registerPostgresFactory(async () => myPostgresManager);
|
|
224
|
+
// Later — anywhere in the codebase:
|
|
225
|
+
import { getRawMessageManager } from "memory-store";
|
|
226
|
+
const manager = await getRawMessageManager();
|
|
227
|
+
```
|
|
228
|
+
|
|
229
|
+
If you don't register a factory, the package falls back to the legacy
|
|
230
|
+
sqlite path (`env.isTauriMode()` must return true). For non-Tauri hosts,
|
|
231
|
+
registering the factory is mandatory.
|
|
232
|
+
|
|
233
|
+
### 6. HTTP daemon
|
|
234
|
+
|
|
235
|
+
```ts
|
|
236
|
+
import { startHttpServer } from "memory-store/http";
|
|
237
|
+
|
|
238
|
+
const { url, port, stop } = await startHttpServer({
|
|
239
|
+
port: 7421,
|
|
240
|
+
host: "127.0.0.1",
|
|
241
|
+
// MemoryStoreConfig passthrough:
|
|
242
|
+
env: { isTauriMode: () => false },
|
|
243
|
+
vector: { backend: "chroma", chroma: { url: process.env.CHROMA_URL! } },
|
|
244
|
+
unified: { embedQuery: myEmbedder },
|
|
245
|
+
});
|
|
246
|
+
|
|
247
|
+
console.log(`listening at ${url}`);
|
|
248
|
+
// later: await stop();
|
|
249
|
+
```
|
|
250
|
+
|
|
251
|
+
Or run the CLI:
|
|
252
|
+
|
|
253
|
+
```bash
|
|
254
|
+
MEMORY_HTTP_HOST=0.0.0.0 MEMORY_HTTP_PORT=7421 opencontext-memory-http
|
|
255
|
+
```
|
|
256
|
+
|
|
257
|
+
Endpoints:
|
|
258
|
+
|
|
259
|
+
| Method + path | Body |
|
|
260
|
+
| -------------------------------------- | ------------------------------------------------------------------------ |
|
|
261
|
+
| `GET /health` | — |
|
|
262
|
+
| `POST /v1/search` | `{ userId, query, limit?, threshold?, sources?, botIds?, documentIds? }` |
|
|
263
|
+
| `POST /v1/raw-messages` | `{ userId, messages: RawMessage[] }` |
|
|
264
|
+
| `GET /v1/raw-messages/:id?userId=...` | — |
|
|
265
|
+
|
|
266
|
+
For container / LAN deployment, put it behind a reverse proxy:
|
|
267
|
+
|
|
268
|
+
```nginx
|
|
269
|
+
location /memory/ {
|
|
270
|
+
proxy_pass http://127.0.0.1:7421/;
|
|
271
|
+
proxy_set_header X-Forwarded-User $remote_user;
|
|
272
|
+
proxy_read_timeout 60s;
|
|
273
|
+
}
|
|
274
|
+
```
|
|
275
|
+
|
|
276
|
+
### 7. MCP daemon
|
|
277
|
+
|
|
278
|
+
```bash
|
|
279
|
+
opencontext-memory-mcp
|
|
280
|
+
```
|
|
281
|
+
|
|
282
|
+
Tools exposed over stdio:
|
|
283
|
+
|
|
284
|
+
| Tool | Required args |
|
|
285
|
+
| ------------------------ | ------------------------------------------------------------------------------------ |
|
|
286
|
+
| `memory.health` | — |
|
|
287
|
+
| `memory.searchUnified` | `userId`, `query`, optional `limit`, `threshold`, `sources`, `botIds`, `documentIds` |
|
|
288
|
+
| `memory.writeRawMessage` | `userId`, `message: { role, content, platform?, botId?, ... }` |
|
|
289
|
+
| `memory.getRawMessage` | `userId`, `messageId` |
|
|
290
|
+
|
|
291
|
+
#### Claude Desktop (`claude_desktop_config.json`)
|
|
292
|
+
|
|
293
|
+
```json
|
|
294
|
+
{
|
|
295
|
+
"mcpServers": {
|
|
296
|
+
"opencontext-memory": {
|
|
297
|
+
"command": "npx",
|
|
298
|
+
"args": ["-y", "memory-store", "memory-mcp"],
|
|
299
|
+
"env": {
|
|
300
|
+
"DATABASE_URL": "postgres://user:pass@host:5432/opencontext"
|
|
301
|
+
}
|
|
302
|
+
}
|
|
303
|
+
}
|
|
304
|
+
}
|
|
305
|
+
```
|
|
306
|
+
|
|
307
|
+
For local dev against this monorepo, point at the built CLI directly:
|
|
308
|
+
|
|
309
|
+
```json
|
|
310
|
+
{
|
|
311
|
+
"mcpServers": {
|
|
312
|
+
"opencontext-memory": {
|
|
313
|
+
"command": "node",
|
|
314
|
+
"args": [
|
|
315
|
+
"/path/to/opencontext/packages/memory-store/dist/server/cli-mcp.js"
|
|
316
|
+
]
|
|
317
|
+
}
|
|
318
|
+
}
|
|
319
|
+
}
|
|
320
|
+
```
|
|
321
|
+
|
|
322
|
+
#### Cursor
|
|
323
|
+
|
|
324
|
+
Same shape — Cursor reads `mcpServers` from its MCP settings
|
|
325
|
+
(Cursor → Settings → MCP → Add new global MCP server):
|
|
326
|
+
|
|
327
|
+
```json
|
|
328
|
+
{
|
|
329
|
+
"mcpServers": {
|
|
330
|
+
"opencontext-memory": {
|
|
331
|
+
"command": "opencontext-memory-mcp"
|
|
332
|
+
}
|
|
333
|
+
}
|
|
334
|
+
}
|
|
335
|
+
```
|
|
336
|
+
|
|
337
|
+
After registering, the four `memory.*` tools become available inside the
|
|
338
|
+
editor. Tool calls appear in the chat like any other MCP tool.
|
|
339
|
+
|
|
340
|
+
## Subpath exports
|
|
341
|
+
|
|
342
|
+
| Subpath | Contents |
|
|
343
|
+
| ---------------------------------------------------------- | ------------------------------------------------------------------------------- |
|
|
344
|
+
| `memory-store` | `createMemoryStore`, top-level types |
|
|
345
|
+
| `memory-store/http` | `startHttpServer`, `StartedHttpServer` |
|
|
346
|
+
| `memory-store/mcp` | `startMcpServer` |
|
|
347
|
+
| `memory-store/unified-search` | `createUnifiedSearch(deps)` factory + result types |
|
|
348
|
+
| `memory-store/raw-message-store` | `createRawMessageStore`, `getRawMessageManager`, `isRawMessageStorageAvailable` |
|
|
349
|
+
| `memory-store/sqlite-raw-message-store` | SQLite-vec raw-message manager (Tauri / desktop) |
|
|
350
|
+
| `memory-store/postgres-raw-message-factory` | `registerPostgresFactory`, `resolvePostgresFactory` |
|
|
351
|
+
| `memory-store/sqlite-vector-index` | Direct sqlite-vec insight index helpers |
|
|
352
|
+
| `memory-store/chroma-memory-index` | Direct chroma upsert / search helpers |
|
|
353
|
+
| `memory-store/memory-graph-write-policy` | `resolveMemoryGraphWritePolicy`, allowlist gating |
|
|
354
|
+
| `memory-store/memory-graph-correction-policy` | `resolveMemoryGraphCorrectionPolicy` |
|
|
355
|
+
|
|
356
|
+
## Behaviour notes
|
|
357
|
+
|
|
358
|
+
- An empty / whitespace `query` short-circuits and returns
|
|
359
|
+
`{ query, sources, results: [], count: 0, warnings: [] }` without
|
|
360
|
+
touching any backend.
|
|
361
|
+
- If `isRawMessageStorageAvailable()` returns `false`, the memory source
|
|
362
|
+
emits a `raw_message_storage_unavailable` warning instead of running
|
|
363
|
+
Chroma / postgres search.
|
|
364
|
+
- When `botIds` is provided, the memory source fans out across each
|
|
365
|
+
`botId` filter in parallel (`Promise.all(filters.map(...))`) and
|
|
366
|
+
flattens the results. If `botIds` is empty, a single unfiltered query
|
|
367
|
+
is sent.
|
|
368
|
+
- `limit` is passed straight through to each underlying backend — no
|
|
369
|
+
silent inflation. `threshold` is clamped to `[-1, 1]`.
|
|
370
|
+
- Chroma failures fall back to `unified.searchRawMessagesAnn` when both
|
|
371
|
+
are configured; otherwise the memory source returns empty.
|
|
372
|
+
|
|
373
|
+
## See also
|
|
374
|
+
|
|
375
|
+
- [`docs/architecture.md`](../../docs/architecture.md) — the overall data
|
|
376
|
+
model, lifecycle, and transport surfaces that the memory primitives
|
|
377
|
+
plug into.
|
|
@@ -0,0 +1,147 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Memory store configuration — injected by the host application.
|
|
3
|
+
*
|
|
4
|
+
* The memory-store package is intentionally decoupled from the
|
|
5
|
+
* opencontext web app's database and env layers. Each consumer wires up
|
|
6
|
+
* its own implementation of these contracts.
|
|
7
|
+
*
|
|
8
|
+
* Required for raw-message persistence:
|
|
9
|
+
* - `db.getDb()` — returns a Drizzle DB handle
|
|
10
|
+
* - `env.isTauriMode()` — for sqlite vs postgres selection
|
|
11
|
+
*
|
|
12
|
+
* Required for vector indexing:
|
|
13
|
+
* - one of `vector.sqlite-vec.dbPath` or `vector.chroma.url`
|
|
14
|
+
*
|
|
15
|
+
* Required for unified semantic search:
|
|
16
|
+
* - `unified.embedQuery` — the embedding provider's query embedder
|
|
17
|
+
*
|
|
18
|
+
* Optional for cross-source unified search:
|
|
19
|
+
* - `unified.searchKnowledge` — RAG over uploaded documents
|
|
20
|
+
* - `unified.searchInsights` — over extracted insights
|
|
21
|
+
*
|
|
22
|
+
* If `unified.*` are absent, the SDK still works but the corresponding
|
|
23
|
+
* sources in `searchUnifiedMemory` will return empty results with a
|
|
24
|
+
* warning — fine for a standalone memory daemon.
|
|
25
|
+
*/
|
|
26
|
+
|
|
27
|
+
interface MemoryStoreDb {
|
|
28
|
+
/** Resolve the active Drizzle DB handle. Must be server-side. */
|
|
29
|
+
getDb(): unknown;
|
|
30
|
+
/** Whether the DB has finished initializing (optional, for startup gating). */
|
|
31
|
+
isDbInitialized?(): boolean;
|
|
32
|
+
/** Initialize the DB (optional, called by `createMemoryStore` if present). */
|
|
33
|
+
initDb?(): unknown;
|
|
34
|
+
/**
|
|
35
|
+
* Drizzle table references for the postgres backend. Required when
|
|
36
|
+
* running outside Tauri mode without a registered factory. The host
|
|
37
|
+
* application owns the actual schema.
|
|
38
|
+
*/
|
|
39
|
+
tables?: {
|
|
40
|
+
rawMessages?: unknown;
|
|
41
|
+
memorySummaries?: unknown;
|
|
42
|
+
};
|
|
43
|
+
}
|
|
44
|
+
interface MemoryStoreEnv {
|
|
45
|
+
/** True when running under Tauri (sqlite local). */
|
|
46
|
+
isTauriMode(): boolean;
|
|
47
|
+
/** Where to place the SQLite DB file (Tauri mode only). */
|
|
48
|
+
getTauriDbPath?(): string;
|
|
49
|
+
/**
|
|
50
|
+
* Tauri data dir for mkdirSync. Defaults to dirname(getTauriDbPath()).
|
|
51
|
+
* Provided so callers that use a custom layout don't need to expose
|
|
52
|
+
* their internal helpers.
|
|
53
|
+
*/
|
|
54
|
+
getTauriDataDir?(): string;
|
|
55
|
+
}
|
|
56
|
+
type VectorBackend = "sqlite-vec" | "chroma";
|
|
57
|
+
interface MemoryStoreVectorConfig {
|
|
58
|
+
/** Active backend selector. Defaults to env-based detection. */
|
|
59
|
+
backend?: VectorBackend;
|
|
60
|
+
/** SQLite-vec options (used when backend === "sqlite-vec"). */
|
|
61
|
+
sqliteVec?: {
|
|
62
|
+
dbPath?: string;
|
|
63
|
+
insightsCollection?: string;
|
|
64
|
+
};
|
|
65
|
+
/** Chroma options (used when backend === "chroma"). */
|
|
66
|
+
chroma?: {
|
|
67
|
+
url?: string;
|
|
68
|
+
rawMessagesCollection?: string;
|
|
69
|
+
insightsCollection?: string;
|
|
70
|
+
};
|
|
71
|
+
}
|
|
72
|
+
type EmbedQueryFn = (input: {
|
|
73
|
+
userId: string;
|
|
74
|
+
query: string;
|
|
75
|
+
authToken?: string;
|
|
76
|
+
}) => Promise<number[]>;
|
|
77
|
+
interface UnifiedSearchKnowledgeResult {
|
|
78
|
+
chunkId: string;
|
|
79
|
+
documentId: string;
|
|
80
|
+
documentName: string;
|
|
81
|
+
content: string;
|
|
82
|
+
similarity: number;
|
|
83
|
+
chunkIndex: number;
|
|
84
|
+
}
|
|
85
|
+
interface UnifiedSearchInsightsResult {
|
|
86
|
+
id: string;
|
|
87
|
+
content: string;
|
|
88
|
+
similarity: number;
|
|
89
|
+
metadata: Record<string, unknown>;
|
|
90
|
+
}
|
|
91
|
+
interface UnifiedSearchDeps {
|
|
92
|
+
/** Embed a query string using the active user's provider. */
|
|
93
|
+
embedQuery?: EmbedQueryFn;
|
|
94
|
+
/**
|
|
95
|
+
* Postgres-side ANN search over `raw_messages`. Used when chroma is
|
|
96
|
+
* not enabled but the host has a postgres-backed manager. If both
|
|
97
|
+
* chroma and this are absent, the memory source will return empty
|
|
98
|
+
* (with a warning) in standalone mode.
|
|
99
|
+
*/
|
|
100
|
+
searchRawMessagesAnn?: (input: {
|
|
101
|
+
userId: string;
|
|
102
|
+
queryEmbedding: number[];
|
|
103
|
+
limit: number;
|
|
104
|
+
threshold: number;
|
|
105
|
+
botId?: string;
|
|
106
|
+
}) => Promise<Array<{
|
|
107
|
+
id: string;
|
|
108
|
+
content: string;
|
|
109
|
+
similarity: number;
|
|
110
|
+
metadata: Record<string, unknown>;
|
|
111
|
+
}>>;
|
|
112
|
+
/** RAG over uploaded knowledge documents. Returns a list of chunk hits. */
|
|
113
|
+
searchKnowledge?: (input: {
|
|
114
|
+
userId: string;
|
|
115
|
+
query: string;
|
|
116
|
+
options: {
|
|
117
|
+
limit: number;
|
|
118
|
+
threshold: number;
|
|
119
|
+
documentIds?: string[];
|
|
120
|
+
};
|
|
121
|
+
authToken?: string;
|
|
122
|
+
}) => Promise<UnifiedSearchKnowledgeResult[]>;
|
|
123
|
+
/** Semantic search over extracted insights. */
|
|
124
|
+
searchInsights?: (input: {
|
|
125
|
+
userId: string;
|
|
126
|
+
query: string;
|
|
127
|
+
limit: number;
|
|
128
|
+
threshold: number;
|
|
129
|
+
botIds?: string[];
|
|
130
|
+
includeArchived?: boolean;
|
|
131
|
+
authToken?: string;
|
|
132
|
+
}) => Promise<UnifiedSearchInsightsResult[]>;
|
|
133
|
+
}
|
|
134
|
+
interface MemoryStoreConfig {
|
|
135
|
+
/** Database handle factory. Required for any persistence. */
|
|
136
|
+
db?: MemoryStoreDb;
|
|
137
|
+
/** Environment helpers. Defaults to a process.env-only stub if omitted. */
|
|
138
|
+
env?: MemoryStoreEnv;
|
|
139
|
+
/** Vector backend config. Defaults to env-based detection. */
|
|
140
|
+
vector?: MemoryStoreVectorConfig;
|
|
141
|
+
/** Optional cross-source search providers. */
|
|
142
|
+
unified?: UnifiedSearchDeps;
|
|
143
|
+
/** Optional logger. Defaults to console. */
|
|
144
|
+
logger?: Pick<Console, "log" | "warn" | "error">;
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
export type { EmbedQueryFn as E, MemoryStoreConfig as M, UnifiedSearchInsightsResult as U, VectorBackend as V, MemoryStoreDb as a, MemoryStoreEnv as b, UnifiedSearchKnowledgeResult as c, UnifiedSearchDeps as d };
|
|
@@ -0,0 +1,62 @@
|
|
|
1
|
+
import { MemoryRecord } from '@melandlabs/ai/memory';
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Memory-store contracts — types and constants that were originally re-exported
|
|
5
|
+
* from `@melandlabs/indexeddb` but pulled in browser-only code (IndexedDB
|
|
6
|
+
* globals) at module evaluation time, breaking the standalone HTTP/MCP
|
|
7
|
+
* daemons.
|
|
8
|
+
*
|
|
9
|
+
* Anything defined here can be safely imported on the server without any
|
|
10
|
+
* browser globals being resolved. The `@melandlabs/indexeddb` peer dependency
|
|
11
|
+
* is optional in `@melandlabs/memory-store/package.json`; consumers can
|
|
12
|
+
* depend on it directly for IndexedDB-backed storage.
|
|
13
|
+
*
|
|
14
|
+
* Re-export from this module is stable. New domain types belong in
|
|
15
|
+
* dedicated files; this module only hosts the boundary contracts.
|
|
16
|
+
*/
|
|
17
|
+
|
|
18
|
+
/** Prefix used to reserve chat-memory evidence IDs that must not collide. */
|
|
19
|
+
declare const CHAT_MEMORY_EVIDENCE_ID_PREFIX = "opencontext-chat:";
|
|
20
|
+
/** A locally-defined re-export of the raw-message shape used by the SDK.
|
|
21
|
+
* Kept as a structural type so consumers (SQLite, postgres factories)
|
|
22
|
+
* can pass either the IndexedDB `RawMessage` or this shape. */
|
|
23
|
+
interface RawMessage {
|
|
24
|
+
id?: number;
|
|
25
|
+
messageId: string;
|
|
26
|
+
platform: string;
|
|
27
|
+
botId: string;
|
|
28
|
+
userId: string;
|
|
29
|
+
channel?: string;
|
|
30
|
+
person?: string;
|
|
31
|
+
timestamp: number;
|
|
32
|
+
content: string;
|
|
33
|
+
attachments?: Array<{
|
|
34
|
+
name: string;
|
|
35
|
+
url: string;
|
|
36
|
+
contentType?: string;
|
|
37
|
+
sizeBytes?: number;
|
|
38
|
+
}>;
|
|
39
|
+
embedding?: number[];
|
|
40
|
+
embeddingModel?: string;
|
|
41
|
+
embeddingContentHash?: string;
|
|
42
|
+
embeddingDimensions?: number;
|
|
43
|
+
embeddingUpdatedAt?: number;
|
|
44
|
+
metadata?: Record<string, any>;
|
|
45
|
+
createdAt: number;
|
|
46
|
+
memoryStage?: "short" | "mid" | "long";
|
|
47
|
+
accessCount?: number;
|
|
48
|
+
lastAccessAt?: number;
|
|
49
|
+
importanceScore?: number;
|
|
50
|
+
archivedAt?: number;
|
|
51
|
+
isPinned?: boolean;
|
|
52
|
+
summaryRefId?: string;
|
|
53
|
+
deprecatedAt?: number;
|
|
54
|
+
deprecationReason?: string;
|
|
55
|
+
supersededBySummaryId?: string;
|
|
56
|
+
}
|
|
57
|
+
/** Convert a RawMessage to a MemoryRecord for embedding. Local copy of the
|
|
58
|
+
* helper that previously lived in `@melandlabs/indexeddb/embedding` and pulled
|
|
59
|
+
* in browser-only modules. */
|
|
60
|
+
declare function rawMessageToMemoryRecord(message: RawMessage): MemoryRecord;
|
|
61
|
+
|
|
62
|
+
export { CHAT_MEMORY_EVIDENCE_ID_PREFIX, type RawMessage, rawMessageToMemoryRecord };
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
// src/contracts.ts
|
|
2
|
+
var CHAT_MEMORY_EVIDENCE_ID_PREFIX = "opencontext-chat:";
|
|
3
|
+
function normalizeTimestampToMs(value) {
|
|
4
|
+
if (!Number.isFinite(value)) {
|
|
5
|
+
return 0;
|
|
6
|
+
}
|
|
7
|
+
if (value < 1e11) {
|
|
8
|
+
return Math.floor(value * 1e3);
|
|
9
|
+
}
|
|
10
|
+
return Math.floor(value);
|
|
11
|
+
}
|
|
12
|
+
function rawMessageToMemoryRecord(message) {
|
|
13
|
+
return {
|
|
14
|
+
id: message.messageId,
|
|
15
|
+
userId: message.userId,
|
|
16
|
+
timestamp: normalizeTimestampToMs(message.timestamp),
|
|
17
|
+
text: message.archivedAt ? void 0 : message.content,
|
|
18
|
+
mediaRefs: message.attachments?.map((item) => item.url).filter(Boolean),
|
|
19
|
+
embedding: message.embedding,
|
|
20
|
+
embeddingModel: message.embeddingModel,
|
|
21
|
+
embeddingContentHash: message.embeddingContentHash
|
|
22
|
+
};
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
export { CHAT_MEMORY_EVIDENCE_ID_PREFIX, rawMessageToMemoryRecord };
|
|
26
|
+
//# sourceMappingURL=contracts.js.map
|
|
27
|
+
//# sourceMappingURL=contracts.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":["../src/contracts.ts"],"names":[],"mappings":";AAkBO,IAAM,8BAAA,GAAiC;AA0C9C,SAAS,uBAAuB,KAAA,EAAmC;AAClE,EAAA,IAAI,CAAC,MAAA,CAAO,QAAA,CAAS,KAAK,CAAA,EAAG;AAC5B,IAAA,OAAO,CAAA;AAAA,EACR;AACA,EAAA,IAAK,QAAmB,IAAA,EAAM;AAC7B,IAAA,OAAO,IAAA,CAAK,KAAA,CAAO,KAAA,GAAmB,GAAI,CAAA;AAAA,EAC3C;AACA,EAAA,OAAO,IAAA,CAAK,MAAM,KAAe,CAAA;AAClC;AAKO,SAAS,yBAAyB,OAAA,EAAmC;AAC3E,EAAA,OAAO;AAAA,IACN,IAAI,OAAA,CAAQ,SAAA;AAAA,IACZ,QAAQ,OAAA,CAAQ,MAAA;AAAA,IAChB,SAAA,EAAW,sBAAA,CAAuB,OAAA,CAAQ,SAAS,CAAA;AAAA,IACnD,IAAA,EAAM,OAAA,CAAQ,UAAA,GAAa,MAAA,GAAY,OAAA,CAAQ,OAAA;AAAA,IAC/C,SAAA,EAAW,OAAA,CAAQ,WAAA,EAAa,GAAA,CAAI,CAAC,SAAS,IAAA,CAAK,GAAG,CAAA,CAAE,MAAA,CAAO,OAAO,CAAA;AAAA,IACtE,WAAW,OAAA,CAAQ,SAAA;AAAA,IACnB,gBAAgB,OAAA,CAAQ,cAAA;AAAA,IACxB,sBAAsB,OAAA,CAAQ;AAAA,GAC/B;AACD","file":"contracts.js","sourcesContent":["/**\n * Memory-store contracts — types and constants that were originally re-exported\n * from `@melandlabs/indexeddb` but pulled in browser-only code (IndexedDB\n * globals) at module evaluation time, breaking the standalone HTTP/MCP\n * daemons.\n *\n * Anything defined here can be safely imported on the server without any\n * browser globals being resolved. The `@melandlabs/indexeddb` peer dependency\n * is optional in `@melandlabs/memory-store/package.json`; consumers can\n * depend on it directly for IndexedDB-backed storage.\n *\n * Re-export from this module is stable. New domain types belong in\n * dedicated files; this module only hosts the boundary contracts.\n */\n\nimport type { MemoryRecord } from \"@melandlabs/ai/memory\";\n\n/** Prefix used to reserve chat-memory evidence IDs that must not collide. */\nexport const CHAT_MEMORY_EVIDENCE_ID_PREFIX = \"opencontext-chat:\";\n\n/** A locally-defined re-export of the raw-message shape used by the SDK.\n * Kept as a structural type so consumers (SQLite, postgres factories)\n * can pass either the IndexedDB `RawMessage` or this shape. */\n// biome-ignore lint/suspicious/noExplicitAny: structural mirror of `@melandlabs/indexeddb/storage`'s `RawMessage`.\nexport interface RawMessage {\n\tid?: number;\n\tmessageId: string;\n\tplatform: string;\n\tbotId: string;\n\tuserId: string;\n\tchannel?: string;\n\tperson?: string;\n\ttimestamp: number;\n\tcontent: string;\n\tattachments?: Array<{\n\t\tname: string;\n\t\turl: string;\n\t\tcontentType?: string;\n\t\tsizeBytes?: number;\n\t}>;\n\tembedding?: number[];\n\tembeddingModel?: string;\n\tembeddingContentHash?: string;\n\tembeddingDimensions?: number;\n\tembeddingUpdatedAt?: number;\n\t// biome-ignore lint/suspicious/noExplicitAny: Preserve the public raw metadata contract.\n\tmetadata?: Record<string, any>;\n\tcreatedAt: number;\n\tmemoryStage?: \"short\" | \"mid\" | \"long\";\n\taccessCount?: number;\n\tlastAccessAt?: number;\n\timportanceScore?: number;\n\tarchivedAt?: number;\n\tisPinned?: boolean;\n\tsummaryRefId?: string;\n\tdeprecatedAt?: number;\n\tdeprecationReason?: string;\n\tsupersededBySummaryId?: string;\n}\n\nfunction normalizeTimestampToMs(value: number | undefined): number {\n\tif (!Number.isFinite(value)) {\n\t\treturn 0;\n\t}\n\tif ((value as number) < 1e11) {\n\t\treturn Math.floor((value as number) * 1000);\n\t}\n\treturn Math.floor(value as number);\n}\n\n/** Convert a RawMessage to a MemoryRecord for embedding. Local copy of the\n * helper that previously lived in `@melandlabs/indexeddb/embedding` and pulled\n * in browser-only modules. */\nexport function rawMessageToMemoryRecord(message: RawMessage): MemoryRecord {\n\treturn {\n\t\tid: message.messageId,\n\t\tuserId: message.userId,\n\t\ttimestamp: normalizeTimestampToMs(message.timestamp),\n\t\ttext: message.archivedAt ? undefined : message.content,\n\t\tmediaRefs: message.attachments?.map((item) => item.url).filter(Boolean),\n\t\tembedding: message.embedding,\n\t\tembeddingModel: message.embeddingModel,\n\t\tembeddingContentHash: message.embeddingContentHash,\n\t} as unknown as MemoryRecord;\n}\n"]}
|
package/dist/http.d.ts
ADDED
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
import { M as MemoryStoreConfig } from './config-MlK9f9CM.js';
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* @melandlabs/memory-store/http — HTTP server entry.
|
|
5
|
+
*
|
|
6
|
+
* Usage:
|
|
7
|
+
* import { startHttpServer } from "@melandlabs/memory-store/http";
|
|
8
|
+
* const { url, stop } = await startHttpServer({
|
|
9
|
+
* port: 7421,
|
|
10
|
+
* db: { getDb: () => drizzleDb() },
|
|
11
|
+
* env: { isTauriMode: () => false },
|
|
12
|
+
* });
|
|
13
|
+
*
|
|
14
|
+
* Endpoints (all POST, JSON in/out):
|
|
15
|
+
* GET /health → { ok: true }
|
|
16
|
+
* POST /v1/search → UnifiedMemorySearchOutput
|
|
17
|
+
* POST /v1/raw-messages → upsert raw messages (returns count)
|
|
18
|
+
* GET /v1/raw-messages/:id → single raw message
|
|
19
|
+
*
|
|
20
|
+
* The HTTP server speaks only to the raw-message + vector layers; it does
|
|
21
|
+
* not expose RAG/insights cross-source search (callers wire those up
|
|
22
|
+
* server-side or use the MCP server with a fully-wired store).
|
|
23
|
+
*/
|
|
24
|
+
|
|
25
|
+
interface StartHttpServerOptions extends MemoryStoreConfig {
|
|
26
|
+
/** Port to bind. Defaults to 7421. */
|
|
27
|
+
port?: number;
|
|
28
|
+
/** Bind address. Defaults to 127.0.0.1 (loopback only — change to 0.0.0.0 for LAN). */
|
|
29
|
+
host?: string;
|
|
30
|
+
}
|
|
31
|
+
interface StartedHttpServer {
|
|
32
|
+
url: string;
|
|
33
|
+
port: number;
|
|
34
|
+
stop(): Promise<void>;
|
|
35
|
+
}
|
|
36
|
+
declare function startHttpServer(options?: StartHttpServerOptions): Promise<StartedHttpServer>;
|
|
37
|
+
|
|
38
|
+
export { MemoryStoreConfig, type StartHttpServerOptions, type StartedHttpServer, startHttpServer };
|