mcp-rag-memory 2.4.1 → 2.5.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/.env.example CHANGED
@@ -1,6 +1,7 @@
1
1
  # Copy to `.env` and adjust. Picked up automatically by `npx mcp-rag-memory`,
2
2
  # `npm run dev`, and `npm run start` (see src/env.ts). Host environment
3
- # variables always override these values.
3
+ # variables always override these values, and only RAG_*/OPENCODE_*/EMBEDDING_*/
4
+ # SEARCH_MODE entries in this file are loaded — anything else is ignored.
4
5
 
5
6
  # Where the SQLite store lives (default: <project>/.rag-data)
6
7
  RAG_DB_DIR=.rag-data
@@ -24,6 +25,7 @@ RAG_AUTOSYNC=1
24
25
  RAG_SESSION_CONDENSE=1
25
26
  RAG_SESSION_CONDENSE_MIN_CHARS=120
26
27
  RAG_SESSION_CONDENSE_RATIO=0.35
28
+ # RAG_SESSION_MAX_MSGS=500
27
29
 
28
30
  # Optional pruning during consolidation
29
31
  RAG_PRUNE=0
package/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2025 mcp-rag-memory contributors
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
package/README.md CHANGED
@@ -22,12 +22,16 @@ context **across sessions**.
22
22
 
23
23
  ## Quick start
24
24
 
25
+ Requires **Node.js ≥ 22.12** (for `node:sqlite` read-only handles and
26
+ `busy_timeout`).
27
+
25
28
  ```bash
26
29
  npm install
27
- npm test # 114 checks: unit + MCP round-trip via SDK client
30
+ npm test # 160 checks: unit + MCP round-trip via SDK client
28
31
  npm run lint # oxlint
32
+ npm run typecheck # src + scripts + .opencode/plugin
29
33
  npm run build # compile to dist/
30
- npm run cli -- stats # CLI playground
34
+ npm run cli -- docs # CLI playground
31
35
  ```
32
36
 
33
37
  ### Run from npm (npx)
@@ -133,6 +137,21 @@ The MCP server is registered here so it works in **every project**, not just thi
133
137
  > from this project's `node_modules` and stores data in the same DB
134
138
  > regardless of which project opencode is opened from.
135
139
 
140
+ **Re-assert this global config with one command.** opencode updates have
141
+ been observed to reset/wipe the global config (the MCP server, plugin,
142
+ LSP and instructions disappear from new sessions). `scripts/setup-opencode.*`
143
+ rewrite both `opencode.json` and `opencode.jsonc` (PowerShell on Windows,
144
+ portable bash elsewhere; dispatcher `npm run setup-opencode`), **merging**
145
+ with what is already there — user keys are never clobbered, arrays
146
+ (`instructions`, `plugin`) are unioned, broken files are backed up to
147
+ `.bak-<ts>`, and runs are idempotent. Re-run it (and restart opencode)
148
+ after any opencode update:
149
+
150
+ ```bash
151
+ npm run setup-opencode # merge-in our defaults
152
+ npm run setup-opencode -- --check # exit 0 = up to date, 1 = changes pending
153
+ ```
154
+
136
155
  ### Project config (`opencode.json` in this repo)
137
156
 
138
157
  Keeps project-specific settings (model, LSP, permissions, instructions) — **no `mcp` block here**:
@@ -181,14 +200,14 @@ Restart opencode after any config change.
181
200
  | | `rag_ingest_dir` | Recursively ingest source files |
182
201
  | RAG query | `rag_search` | Hybrid (BM25 + vector) search, ranked chunks + scores |
183
202
  | | `rag_retrieve` | Ready-to-inject context block with token count |
184
- | RAG docs | `rag_list_documents`, `rag_document_stats` | Inventory |
203
+ | RAG docs | `rag_list_documents`, `rag_document_stats` | Inventory (`rag_list_documents` is paginated: `limit` 1–1000, `offset`) |
185
204
  | | `rag_delete_document` | Remove a document + chunks + FTS rows |
186
205
  | Session sync | `rag_sync_session` | Ingest an opencode session's user inputs (or the latest in a workspace) |
187
206
  | Memory | `memory_remember` | Save long-term memory (type/importance/tags) |
188
207
  | | `memory_recall` | Semantic memory search (decay + recall count) |
189
208
  | | `memory_context` | Compact context block from memories for prompts |
190
209
  | | `memory_list`, `memory_get`, `memory_update`, `memory_forget` | CRUD |
191
- | | `memory_consolidate` | Dedupe near-identical + promote hot memories |
210
+ | | `memory_consolidate` | Dedupe near-identical (cosine **and** word overlap) + promote hot memories |
192
211
  | | `memory_stats` | Counts, tokens, avg importance, by type |
193
212
 
194
213
  ## Run in other AI assistants
@@ -279,6 +298,7 @@ npm run cli -- recall "deployment schedule"
279
298
  npm run cli -- ingest-dir ./src/rag
280
299
  npm run cli -- search "vector similarity"
281
300
  npm run cli -- search "auth bug" --source opencode-db # filter by source or doc-id
301
+ npm run cli -- docs --limit 20 --offset 20 # paginated inventory
282
302
  npm run cli -- mem-context "database"
283
303
  npm run cli -- sessions # list recent opencode sessions
284
304
  npm run cli -- sync-session ses_123 # ingest one session's inputs
@@ -383,10 +403,19 @@ Data lives in `.rag-data/rag.sqlite` (git-ignored).
383
403
  | `RAG_PRUNE_IMPORTANCE` | `0.2` | Delete memories below this importance |
384
404
  | `RAG_PRUNE_AGE_DAYS` | `90` | ...and older than this (never-recalled only) |
385
405
  | `RAG_AUTOSYNC` | `1` | Set `0` to disable the session-logger plugin's auto-sync |
386
- | `OPENCODE_DB` / `OPENCODE_DB_LOCAL` | `~/.local/share/opencode/*.db` | Where session transcripts are read from |
406
+ | `OPENCODE_DB` / `OPENCODE_DB_LOCAL` | `~/.local/share/opencode/*.db` | Where session transcripts are read from (re-read on every call, so it can be set late) |
407
+ | `RAG_SESSION_MAX_MSGS` | `500` | Cap on user messages read from one session |
387
408
  | `RAG_SESSION_CONDENSE` | `1` | Set `0` to store session inputs verbatim |
388
409
  | `RAG_SESSION_CONDENSE_MIN_CHARS` | `120` | Inputs at/below this length are saved whole |
389
410
  | `RAG_SESSION_CONDENSE_RATIO` | `0.35` | Fraction of long-input length to keep as key points |
411
+ | `RAG_ENV_FILE` | `<cwd>/.env` | Env file to load at boot |
412
+
413
+ A `.env` file can only set this server's own variables (`RAG_*`,
414
+ `OPENCODE_*`, `EMBEDDING_*`, `SEARCH_MODE`); anything else in it is
415
+ ignored with a warning on stderr, and a real environment variable always
416
+ wins over the file. The loader runs from `dist/index.js` and
417
+ `dist/cli.js` only — if you invoke `src/mcp/rag-server.ts` directly
418
+ (TSX/`npx tsx`), import `src/env.js` yourself.
390
419
 
391
420
  ## Testing
392
421
 
@@ -489,8 +518,13 @@ instant.
489
518
  ### Opencode feels sluggish / UI lag after adding plugins
490
519
 
491
520
  Plugins that run synchronous I/O or write to stderr on every tool call
492
- can slow down the TUI. This project no longer ships any plugins — keep
493
- `opencode.json` plugin-free for clean operation.
521
+ can slow down the TUI. This repo ships exactly one plugin
522
+ (`.opencode/plugin/session-logger.ts`) and it is debounced to at most one
523
+ `npx … sync-latest` per 60s, spawned detached with `stdio: "ignore"` and
524
+ `shell: false`. If it still costs too much, set `RAG_AUTOSYNC=0` (keeps the
525
+ plugin, disables the auto-sync) or drop the `plugin` entry from
526
+ `opencode.json` — the MCP server and its 19 tools keep working, you just have
527
+ to call `rag_sync_session` yourself.
494
528
 
495
529
  ---
496
530
 
package/dist/cli.js CHANGED
@@ -54,25 +54,33 @@ function assertMemoryType(type) {
54
54
  throw new Error(`Invalid memory type "${type}". Valid: ${MEMORY_TYPES.join(", ")}`);
55
55
  }
56
56
  }
57
+ function requireArg(value, usage) {
58
+ if (!value || value.startsWith("--"))
59
+ throw new Error(`Missing argument — usage: ${usage}`);
60
+ return value;
61
+ }
57
62
  async function run(cmd, args) {
58
63
  let out;
59
64
  try {
60
65
  switch (cmd) {
61
66
  case "ingest-text": {
62
- const [title, fileOrDash] = args;
67
+ const parsed = parseOpts(args);
68
+ const [title, fileOrDash] = parsed.positional;
69
+ if (!title || !fileOrDash)
70
+ throw new Error("Missing argument — usage: ingest-text <title> <file|->");
63
71
  const content = fileOrDash === "-" ? fs.readFileSync(0, "utf-8") : fs.readFileSync(fileOrDash, "utf-8");
64
- out = await ingestText(content, title ?? fileOrDash ?? "untitled");
72
+ out = await ingestText(content, title);
65
73
  break;
66
74
  }
67
75
  case "ingest-file":
68
- out = await ingestFile(args[0]);
76
+ out = await ingestFile(requireArg(args[0], "ingest-file <path>"));
69
77
  break;
70
78
  case "ingest-dir":
71
- out = await ingestDirectory(args[0], { recursive: true });
79
+ out = await ingestDirectory(requireArg(args[0], "ingest-dir <dir>"), { recursive: true });
72
80
  break;
73
81
  case "search": {
74
82
  const parsed = parseOpts(args);
75
- out = await searchDocs(parsed.positional.join(" "), undefined, undefined, {
83
+ out = await searchDocs(queryArg(parsed.positional), toNum(parsed.opts["top-k"], 10), toNum(parsed.opts["min-score"], 0.08), {
76
84
  docId: parsed.opts["doc-id"],
77
85
  source: parsed.opts.source,
78
86
  });
@@ -80,67 +88,84 @@ async function run(cmd, args) {
80
88
  }
81
89
  case "retrieve": {
82
90
  const parsed = parseOpts(args);
83
- out = await retrieve(parsed.positional.join(" "), undefined, undefined, {
91
+ out = await retrieve(queryArg(parsed.positional), toNum(parsed.opts["top-k"], 6), toNum(parsed.opts["min-score"], 0.08), {
84
92
  docId: parsed.opts["doc-id"],
85
93
  source: parsed.opts.source,
86
94
  });
87
95
  break;
88
96
  }
89
- case "docs":
90
- out = listDocuments();
97
+ case "docs": {
98
+ const parsed = parseOpts(args);
99
+ out = listDocuments({ limit: toNum(parsed.opts.limit, undefined), offset: toNum(parsed.opts.offset, undefined) });
91
100
  break;
101
+ }
92
102
  case "doc-stats":
93
103
  out = documentStats();
94
104
  break;
95
105
  case "rm-doc":
96
- out = deleteDocument(args[0]);
106
+ out = deleteDocument(requireArg(args[0], "rm-doc <doc_id>"));
97
107
  break;
98
108
  case "remember": {
99
109
  const parsed = parseOpts(args);
110
+ const content = parsed.positional.join(" ");
111
+ if (!content)
112
+ throw new Error("Missing argument — usage: remember <content> [--type fact] [--importance 0.5] [--tag x]");
100
113
  const type = parsed.opts.type ?? "fact";
101
114
  assertMemoryType(type);
102
115
  out = await remember({
103
- content: parsed.positional.join(" "),
116
+ content,
104
117
  type,
105
- importance: parsed.opts.importance !== undefined ? Number(parsed.opts.importance) : 0.5,
118
+ importance: toNum(parsed.opts.importance, 0.5),
106
119
  tags: parsed.opts.tag ? [parsed.opts.tag] : undefined,
107
120
  });
108
121
  break;
109
122
  }
110
- case "recall":
111
- out = await recall(args.join(" "));
123
+ case "recall": {
124
+ const parsed = parseOpts(args);
125
+ out = await recall(queryArg(parsed.positional), toNum(parsed.opts["top-k"], 8), toNum(parsed.opts["min-score"], 0.1));
112
126
  break;
127
+ }
113
128
  case "memory-list": {
114
129
  const parsed = parseOpts(args);
130
+ const type = parsed.opts.type;
131
+ if (type !== undefined)
132
+ assertMemoryType(type);
115
133
  out = listMemories({
116
- type: parsed.opts.type,
134
+ type,
117
135
  tag: parsed.opts.tag,
118
- minImportance: parsed.opts["min-importance"] !== undefined ? Number(parsed.opts["min-importance"]) : undefined,
136
+ minImportance: toNum(parsed.opts["min-importance"], undefined),
137
+ limit: toNum(parsed.opts.limit, undefined),
119
138
  });
120
139
  break;
121
140
  }
122
141
  case "memory-get":
123
- out = getMemory(args[0]);
142
+ out = getMemory(requireArg(args[0], "memory-get <id>"));
124
143
  break;
125
144
  case "memory-update": {
126
145
  const [id, ...restArgs] = args;
127
146
  const parsed = parseOpts(restArgs);
128
- const type = parsed.opts.type ?? "fact";
129
- assertMemoryType(type);
147
+ if (!id)
148
+ throw new Error("Missing argument — usage: memory-update <id> [--content ...] [--importance 0.8] [--type fact] [--tag x]");
149
+ const type = parsed.opts.type;
150
+ if (type !== undefined)
151
+ assertMemoryType(type);
152
+ // --content=... (or --content <text>) sets the body; leftover positional
153
+ // words are the body too. The old `!== "true"` check silently discarded
154
+ // any content that was literally the word "true".
155
+ const content = parsed.opts.content ?? (parsed.positional.length ? parsed.positional.join(" ") : undefined);
156
+ if (content === undefined && type === undefined && parsed.opts.importance === undefined && parsed.opts.tag === undefined) {
157
+ throw new Error("Nothing to update — pass --content, --type, --importance and/or --tag");
158
+ }
130
159
  out = await updateMemory(id, {
131
- content: parsed.opts.content && parsed.opts.content !== "true"
132
- ? parsed.opts.content
133
- : parsed.positional.length
134
- ? parsed.positional.join(" ")
135
- : undefined,
136
- importance: parsed.opts.importance !== undefined ? Number(parsed.opts.importance) : undefined,
160
+ content,
161
+ importance: toNum(parsed.opts.importance, undefined),
137
162
  type,
138
163
  tags: parsed.opts.tag ? [parsed.opts.tag] : undefined,
139
164
  });
140
165
  break;
141
166
  }
142
167
  case "forget":
143
- out = forget(args[0]);
168
+ out = forget(requireArg(args[0], "forget <id>"));
144
169
  break;
145
170
  case "consolidate":
146
171
  out = await consolidate();
@@ -148,15 +173,17 @@ async function run(cmd, args) {
148
173
  case "memory-stats":
149
174
  out = memoryStats();
150
175
  break;
151
- case "mem-context":
152
- out = await contextPrompt(args.join(" "));
176
+ case "mem-context": {
177
+ const parsed = parseOpts(args);
178
+ out = await contextPrompt(queryArg(parsed.positional), toNum(parsed.opts["top-k"], 6));
153
179
  break;
180
+ }
154
181
  case "stats":
155
182
  out = { documents: documentStats(), memories: memoryStats(), dbDir: STORAGE_DIR };
156
183
  break;
157
184
  case "sessions": {
158
185
  const parsed = parseOpts(args);
159
- out = listOpenCodeSessions(parsed.opts.limit ? Number(parsed.opts.limit) : 15);
186
+ out = listOpenCodeSessions(toNum(parsed.opts.limit, 15) ?? 15, { directory: parsed.opts.dir });
160
187
  break;
161
188
  }
162
189
  case "sync-session": {
@@ -164,12 +191,15 @@ async function run(cmd, args) {
164
191
  const parsed = parseOpts(restArgs);
165
192
  if (!id)
166
193
  throw new Error("sync-session <sessionId> — run 'cli sessions' to list ids");
167
- out = await ingestSession(id, { limit: parsed.opts.limit ? Number(parsed.opts.limit) : 0 });
194
+ out = await ingestSession(id, { limit: toNum(parsed.opts.limit, 0) ?? 0 });
168
195
  break;
169
196
  }
170
197
  case "sync-latest": {
171
198
  const parsed = parseOpts(args);
172
- out = await ingestLatest({ limit: parsed.opts.limit ? Number(parsed.opts.limit) : 0, directory: parsed.opts.dir });
199
+ out = await ingestLatest({
200
+ limit: toNum(parsed.opts.limit, 0) ?? 0,
201
+ directory: parsed.opts.dir ?? parsed.positional[0],
202
+ });
173
203
  break;
174
204
  }
175
205
  case "sync-logs": {
@@ -178,7 +208,7 @@ async function run(cmd, args) {
178
208
  break;
179
209
  }
180
210
  case "ingest-jsonl":
181
- out = await ingestJsonlFile(args[0]);
211
+ out = await ingestJsonlFile(requireArg(args[0], "ingest-jsonl <file>"));
182
212
  break;
183
213
  default:
184
214
  console.error(`Unknown command: ${cmd}`);
@@ -188,23 +218,39 @@ async function run(cmd, args) {
188
218
  }
189
219
  catch (e) {
190
220
  console.error(`Error: ${e.message}`);
221
+ closeDB();
191
222
  process.exit(1);
192
223
  }
193
224
  console.log(JSON.stringify(out, null, 2));
194
225
  }
226
+ /** Query text from positional words — empty input is an error, not a silent []. */
227
+ function queryArg(positional) {
228
+ const q = positional.join(" ").trim();
229
+ if (!q)
230
+ throw new Error("Missing query — usage: <command> <query>");
231
+ return q;
232
+ }
195
233
  function parseOpts(args) {
196
234
  const positional = [];
197
235
  const opts = {};
198
236
  for (let i = 0; i < args.length; i++) {
199
237
  const a = args[i];
200
238
  if (a.startsWith("--")) {
239
+ const eq = a.indexOf("=");
240
+ if (eq > 0) {
241
+ // --key=value: the value may legitimately start with "--".
242
+ opts[a.slice(2, eq)] = a.slice(eq + 1);
243
+ continue;
244
+ }
201
245
  const key = a.slice(2);
202
246
  const next = args[i + 1];
203
- if (next && !next.startsWith("--")) {
247
+ if (next !== undefined && !next.startsWith("--")) {
204
248
  opts[key] = next;
205
249
  i++;
206
250
  }
207
251
  else {
252
+ // Boolean flag. Previously the literal "true" was stored as the value,
253
+ // which then leaked into --content and --dir.
208
254
  opts[key] = "true";
209
255
  }
210
256
  }
@@ -214,6 +260,13 @@ function parseOpts(args) {
214
260
  }
215
261
  return { positional, opts };
216
262
  }
263
+ /** Parse a numeric CLI option; falls back to `fallback` for missing/garbage input. */
264
+ function toNum(raw, fallback) {
265
+ if (raw === undefined)
266
+ return fallback;
267
+ const n = Number(raw);
268
+ return Number.isFinite(n) ? n : fallback;
269
+ }
217
270
  main().catch((e) => {
218
271
  console.error(`Error: ${e.message}`);
219
272
  process.exit(1);
@@ -5,9 +5,9 @@ export declare function getDB(): DatabaseSync;
5
5
  /** SHA-256 hex of raw content — used for document dedup. */
6
6
  export declare function contentHash(content: string): string;
7
7
  export declare function newId(): string;
8
- /** Serialize a Float64Array embedding to a SQLite BLOB. */
8
+ /** Serialize a Float64Array embedding to a SQLite BLOB (view-safe). */
9
9
  export declare function packVector(v: Float64Array): Uint8Array;
10
- /** Deserialize a BLOB back into a Float64Array. */
10
+ /** Deserialize a BLOB back into a Float64Array. Returns null on absent/partial data. */
11
11
  export declare function unpackVector(blob: Uint8Array | null): Float64Array | null;
12
12
  export declare function nowMs(): number;
13
13
  export declare function closeDB(): void;
@@ -1 +1 @@
1
- {"version":3,"file":"database.d.ts","sourceRoot":"","sources":["../../src/db/database.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,YAAY,EAAE,MAAM,aAAa,CAAC;AAK3C,eAAO,MAAM,WAAW,QAEoB,CAAC;AAE7C,eAAO,MAAM,OAAO,QAAuC,CAAC;AAU5D,wBAAgB,KAAK,IAAI,YAAY,CAUpC;AAgFD,4DAA4D;AAC5D,wBAAgB,WAAW,CAAC,OAAO,EAAE,MAAM,GAAG,MAAM,CAEnD;AAED,wBAAgB,KAAK,IAAI,MAAM,CAE9B;AAED,2DAA2D;AAC3D,wBAAgB,UAAU,CAAC,CAAC,EAAE,YAAY,GAAG,UAAU,CAEtD;AAED,mDAAmD;AACnD,wBAAgB,YAAY,CAAC,IAAI,EAAE,UAAU,GAAG,IAAI,GAAG,YAAY,GAAG,IAAI,CAGzE;AAED,wBAAgB,KAAK,IAAI,MAAM,CAE9B;AAED,wBAAgB,OAAO,SAGtB;AAMD,wBAAgB,WAAW,IAAI,MAAM,CAKpC;AAED,wBAAgB,YAAY,CAAC,GAAG,EAAE,MAAM,EAAE,KAAK,EAAE,MAAM,QAQtD;AAED;;;GAGG;AACH,wBAAgB,SAAS,CAAC,GAAG,EAAE,YAAY,EAAE,KAAK,EAAE,MAAM,GAAG,IAAI,CAahE;AAED,wBAAgB,aAAa,IAAI,MAAM,CAKtC"}
1
+ {"version":3,"file":"database.d.ts","sourceRoot":"","sources":["../../src/db/database.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,YAAY,EAAE,MAAM,aAAa,CAAC;AAK3C,eAAO,MAAM,WAAW,QAEoB,CAAC;AAE7C,eAAO,MAAM,OAAO,QAAuC,CAAC;AAU5D,wBAAgB,KAAK,IAAI,YAAY,CAepC;AAyGD,4DAA4D;AAC5D,wBAAgB,WAAW,CAAC,OAAO,EAAE,MAAM,GAAG,MAAM,CAEnD;AAED,wBAAgB,KAAK,IAAI,MAAM,CAE9B;AAED,uEAAuE;AACvE,wBAAgB,UAAU,CAAC,CAAC,EAAE,YAAY,GAAG,UAAU,CAEtD;AAED,wFAAwF;AACxF,wBAAgB,YAAY,CAAC,IAAI,EAAE,UAAU,GAAG,IAAI,GAAG,YAAY,GAAG,IAAI,CAGzE;AAED,wBAAgB,KAAK,IAAI,MAAM,CAE9B;AAED,wBAAgB,OAAO,SAItB;AASD,wBAAgB,WAAW,IAAI,MAAM,CAQpC;AAED,wBAAgB,YAAY,CAAC,GAAG,EAAE,MAAM,EAAE,KAAK,EAAE,MAAM,QAStD;AAED;;;GAGG;AACH,wBAAgB,SAAS,CAAC,GAAG,EAAE,YAAY,EAAE,KAAK,EAAE,MAAM,GAAG,IAAI,CAahE;AAED,wBAAgB,aAAa,IAAI,MAAM,CAKtC"}
@@ -20,8 +20,13 @@ export function getDB() {
20
20
  db.exec("PRAGMA journal_mode = WAL;");
21
21
  db.exec("PRAGMA synchronous = NORMAL;");
22
22
  db.exec("PRAGMA foreign_keys = ON;");
23
- _db = db;
23
+ // Wait up to 5s for a concurrent writer (CLI + MCP server + auto-save plugin
24
+ // all share the same store) instead of failing with SQLITE_BUSY immediately.
25
+ db.exec("PRAGMA busy_timeout = 5000;");
26
+ // Migrate BEFORE caching the handle: a failed migration must surface and be
27
+ // retried on the next call, never leave a half-migrated connection cached.
24
28
  migrate(db);
29
+ _db = db;
25
30
  return db;
26
31
  }
27
32
  function columnExists(db, table, column) {
@@ -29,6 +34,25 @@ function columnExists(db, table, column) {
29
34
  return cols.some((c) => c.name === column);
30
35
  }
31
36
  function migrate(db) {
37
+ // One IMMEDIATE transaction for the whole schema step: two processes opening
38
+ // the same fresh store (CLI + MCP server + plugin) must not race and end up
39
+ // with half-applied DDL or a "duplicate column" failure.
40
+ db.exec("BEGIN IMMEDIATE;");
41
+ try {
42
+ createSchema(db);
43
+ db.exec("COMMIT;");
44
+ }
45
+ catch (e) {
46
+ try {
47
+ db.exec("ROLLBACK;");
48
+ }
49
+ catch {
50
+ /* transaction already closed */
51
+ }
52
+ throw e;
53
+ }
54
+ }
55
+ function createSchema(db) {
32
56
  db.exec(`
33
57
  CREATE TABLE IF NOT EXISTS documents (
34
58
  id TEXT PRIMARY KEY,
@@ -51,6 +75,7 @@ function migrate(db) {
51
75
  created_at INTEGER NOT NULL
52
76
  );
53
77
  CREATE INDEX IF NOT EXISTS idx_chunks_doc_id ON chunks(doc_id);
78
+ CREATE INDEX IF NOT EXISTS idx_documents_updated ON documents(updated_at DESC);
54
79
 
55
80
  CREATE TABLE IF NOT EXISTS memories (
56
81
  id TEXT PRIMARY KEY,
@@ -66,6 +91,7 @@ function migrate(db) {
66
91
  );
67
92
  CREATE INDEX IF NOT EXISTS idx_memories_type ON memories(type);
68
93
  CREATE INDEX IF NOT EXISTS idx_memories_prune ON memories(importance, recall_count, created_at);
94
+ CREATE INDEX IF NOT EXISTS idx_memories_updated ON memories(updated_at DESC);
69
95
 
70
96
  CREATE TABLE IF NOT EXISTS meta (
71
97
  key TEXT PRIMARY KEY,
@@ -84,17 +110,24 @@ function migrate(db) {
84
110
  const ftsRow = db
85
111
  .prepare("SELECT sql FROM sqlite_master WHERE type = 'table' AND name = 'chunks_fts'")
86
112
  .get();
87
- if (!ftsRow) {
88
- db.exec("CREATE VIRTUAL TABLE chunks_fts USING fts5(content, tokenize = 'trigram');");
89
- }
90
- else if (!ftsRow.sql.includes("trigram")) {
113
+ if (ftsRow) {
91
114
  // Existing store built with 'unicode61' — recreate with trigram and backfill.
92
- db.exec("DROP TABLE chunks_fts;");
93
- db.exec("CREATE VIRTUAL TABLE chunks_fts USING fts5(content, tokenize = 'trigram');");
94
- db.exec(`INSERT INTO chunks_fts(rowid, content)
95
- SELECT rowid, content FROM chunks`);
115
+ if (!ftsRow.sql.includes("trigram"))
116
+ rebuildFts(db);
117
+ }
118
+ else {
119
+ // First run with this schema (or a store whose FTS table was dropped):
120
+ // create AND backfill, otherwise existing chunks silently lose keyword search
121
+ // forever — the next boot sees "trigram" and would skip the rebuild.
122
+ rebuildFts(db);
96
123
  }
97
124
  }
125
+ /** Runs inside the migrate() transaction. */
126
+ function rebuildFts(db) {
127
+ db.exec("DROP TABLE IF EXISTS chunks_fts;");
128
+ db.exec("CREATE VIRTUAL TABLE chunks_fts USING fts5(content, tokenize = 'trigram');");
129
+ db.exec("INSERT INTO chunks_fts(rowid, content) SELECT rowid, content FROM chunks;");
130
+ }
98
131
  /** SHA-256 hex of raw content — used for document dedup. */
99
132
  export function contentHash(content) {
100
133
  return createHash("sha256").update(content, "utf8").digest("hex");
@@ -102,13 +135,13 @@ export function contentHash(content) {
102
135
  export function newId() {
103
136
  return randomUUID();
104
137
  }
105
- /** Serialize a Float64Array embedding to a SQLite BLOB. */
138
+ /** Serialize a Float64Array embedding to a SQLite BLOB (view-safe). */
106
139
  export function packVector(v) {
107
- return new Uint8Array(v.buffer.slice(0));
140
+ return new Uint8Array(v.buffer.slice(v.byteOffset, v.byteOffset + v.byteLength));
108
141
  }
109
- /** Deserialize a BLOB back into a Float64Array. */
142
+ /** Deserialize a BLOB back into a Float64Array. Returns null on absent/partial data. */
110
143
  export function unpackVector(blob) {
111
- if (!blob || blob.byteLength === 0)
144
+ if (!blob || blob.byteLength === 0 || blob.byteLength % 8 !== 0)
112
145
  return null;
113
146
  return new Float64Array(blob.buffer.slice(blob.byteOffset, blob.byteOffset + blob.byteLength));
114
147
  }
@@ -118,18 +151,25 @@ export function nowMs() {
118
151
  export function closeDB() {
119
152
  _db?.close();
120
153
  _db = null;
154
+ cachedDim = null;
121
155
  }
122
156
  /* ------------------------------------------------------------------ */
123
157
  /* Embedding dimension metadata */
124
158
  /* ------------------------------------------------------------------ */
159
+ /** Cached embed_dim: locked on first write, so it is read once per process. */
160
+ let cachedDim = null;
125
161
  export function getEmbedDim() {
126
- const r = getDB().prepare("SELECT value FROM meta WHERE key = 'embed_dim'").get();
127
- return r ? Number(r.value) : 0;
162
+ if (cachedDim === null) {
163
+ const r = getDB().prepare("SELECT value FROM meta WHERE key = 'embed_dim'").get();
164
+ cachedDim = r ? Number(r.value) : 0;
165
+ }
166
+ return cachedDim;
128
167
  }
129
168
  export function setEmbedMeta(dim, model) {
130
169
  const db = getDB();
131
170
  db.prepare("INSERT INTO meta(key, value) VALUES ('embed_dim', ?) ON CONFLICT(key) DO UPDATE SET value = excluded.value").run(String(dim));
132
171
  db.prepare("INSERT INTO meta(key, value) VALUES ('embed_model', ?) ON CONFLICT(key) DO UPDATE SET value = excluded.value").run(model);
172
+ cachedDim = dim;
133
173
  }
134
174
  /**
135
175
  * Lock the store to one embedding dimension. Call after producing a vector.
@@ -1 +1 @@
1
- {"version":3,"file":"database.js","sourceRoot":"","sources":["../../src/db/database.ts"],"names":[],"mappings":"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"}
1
+ {"version":3,"file":"database.js","sourceRoot":"","sources":["../../src/db/database.ts"],"names":[],"mappings":"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"}
package/dist/env.js CHANGED
@@ -6,38 +6,58 @@
6
6
  * config (MCP clients, CI, shell) always wins. Runs synchronously at import —
7
7
  * must be imported before any module that reads `process.env.*` at load time.
8
8
  * Supports `#` comments, `export KEY=` prefixes, and single/double-quoted values.
9
+ *
10
+ * Only this package's own knobs are loadable: an untrusted repo's `.env` must
11
+ * not be able to inject arbitrary process.env entries into the host session.
9
12
  */
10
13
  import * as fs from "fs";
11
14
  import * as path from "path";
15
+ /** Prefixes/names this server actually reads. */
16
+ const LOADABLE = /^(RAG_[A-Z0-9_]+|OPENCODE_[A-Z0-9_]+|EMBEDDING_[A-Z0-9_]+|SEARCH_MODE)$/;
17
+ const VALID_KEY = /^[A-Za-z_][A-Za-z0-9_]*$/;
18
+ /** Strip one layer of matching quotes plus any trailing `# comment`. */
19
+ function unquote(value) {
20
+ const q = value[0];
21
+ if (q === '"' || q === "'") {
22
+ const close = value.indexOf(q, 1);
23
+ if (close > 0) {
24
+ const rest = value.slice(close + 1).trim();
25
+ if (rest === "" || rest.startsWith("#"))
26
+ return value.slice(1, close);
27
+ }
28
+ }
29
+ const hash = value.indexOf(" #");
30
+ return hash >= 0 ? value.slice(0, hash).trimEnd() : value;
31
+ }
12
32
  export function loadDotenv(file) {
13
33
  let raw;
14
34
  try {
15
35
  raw = fs.readFileSync(file, "utf8");
16
36
  }
17
- catch {
37
+ catch (e) {
38
+ if (process.env.RAG_ENV_FILE) {
39
+ process.stderr.write(`env: cannot read RAG_ENV_FILE ${file} (${e.message})\n`);
40
+ }
18
41
  return;
19
42
  }
20
43
  for (const line of raw.split(/\r?\n/)) {
21
44
  let t = line.trim();
22
45
  if (!t || t.startsWith("#"))
23
46
  continue;
24
- if (t.startsWith("export "))
47
+ if (t.startsWith("export ") || t.startsWith("export\t"))
25
48
  t = t.slice(7).trimStart();
26
49
  const eq = t.indexOf("=");
27
50
  if (eq <= 0)
28
51
  continue;
29
52
  const key = t.slice(0, eq).trim();
30
- let value = t.slice(eq + 1).trim();
31
- // Strip trailing inline comments for unquoted values (e.g. `KEY=val # note`).
32
- if (!value.startsWith('"') && !value.startsWith("'")) {
33
- const hash = value.indexOf(" #");
34
- if (hash >= 0)
35
- value = value.slice(0, hash).trimEnd();
36
- }
37
- if (value.length >= 2 && ((value.startsWith('"') && value.endsWith('"')) || (value.startsWith("'") && value.endsWith("'")))) {
38
- value = value.slice(1, -1);
53
+ if (!VALID_KEY.test(key))
54
+ continue;
55
+ if (!LOADABLE.test(key)) {
56
+ process.stderr.write(`env: ignoring ${key} (not an mcp-rag-memory variable)\n`);
57
+ continue;
39
58
  }
40
- if (key && process.env[key] === undefined)
59
+ const value = unquote(t.slice(eq + 1).trim());
60
+ if (process.env[key] === undefined)
41
61
  process.env[key] = value;
42
62
  }
43
63
  }