mcp-rag-memory 2.4.1 → 2.7.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.
Files changed (40) hide show
  1. package/.env.example +3 -1
  2. package/LICENSE +21 -0
  3. package/README.md +151 -16
  4. package/dist/cli.js +102 -34
  5. package/dist/db/database.d.ts +10 -2
  6. package/dist/db/database.d.ts.map +1 -1
  7. package/dist/db/database.js +119 -18
  8. package/dist/db/database.js.map +1 -1
  9. package/dist/env.js +32 -12
  10. package/dist/mcp/rag-server.js +140 -39
  11. package/dist/mcp/rag-server.js.map +1 -1
  12. package/dist/memory/memory.d.ts +5 -1
  13. package/dist/memory/memory.d.ts.map +1 -1
  14. package/dist/memory/memory.js +245 -101
  15. package/dist/memory/memory.js.map +1 -1
  16. package/dist/rag/chunker.d.ts +7 -0
  17. package/dist/rag/chunker.d.ts.map +1 -1
  18. package/dist/rag/chunker.js +119 -11
  19. package/dist/rag/chunker.js.map +1 -1
  20. package/dist/rag/embeddings.d.ts +25 -0
  21. package/dist/rag/embeddings.d.ts.map +1 -1
  22. package/dist/rag/embeddings.js +56 -4
  23. package/dist/rag/embeddings.js.map +1 -1
  24. package/dist/rag/pipeline.d.ts +19 -2
  25. package/dist/rag/pipeline.d.ts.map +1 -1
  26. package/dist/rag/pipeline.js +212 -39
  27. package/dist/rag/pipeline.js.map +1 -1
  28. package/dist/rag/reindex.d.ts +27 -0
  29. package/dist/rag/reindex.d.ts.map +1 -0
  30. package/dist/rag/reindex.js +138 -0
  31. package/dist/rag/reindex.js.map +1 -0
  32. package/dist/rag/vector-search.d.ts +26 -1
  33. package/dist/rag/vector-search.d.ts.map +1 -1
  34. package/dist/rag/vector-search.js +207 -65
  35. package/dist/rag/vector-search.js.map +1 -1
  36. package/dist/session/transcript.d.ts +15 -5
  37. package/dist/session/transcript.d.ts.map +1 -1
  38. package/dist/session/transcript.js +152 -25
  39. package/dist/session/transcript.js.map +1 -1
  40. package/package.json +6 -3
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
@@ -18,16 +18,20 @@ context **across sessions**.
18
18
  - **Context Management** — `remember` facts/decisions/preferences, `recall` them later (score-decayed by staleness), rate by importance, consolidate duplicates, filter by type/tag.
19
19
  - **Zero external APIs by default** — local hashing-embedder (1024-dim), built-in `node:sqlite`. Works fully offline. Optional `EMBEDDING_PROVIDER=transformers` for a higher-quality ONNX model.
20
20
  - **Safe ingestion** — content-hash deduplication, file size guard (`RAG_MAX_FILE_MB`) and an opt-in path allowlist (`RAG_ALLOWED_DIRS`).
21
- - **MCP server** — runs on stdio, 19 tools.
21
+ - **MCP server** — runs on stdio, 20 tools.
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**:
@@ -171,24 +190,25 @@ the global config to apply to every project.
171
190
 
172
191
  Restart opencode after any config change.
173
192
 
174
- ## MCP tools (19)
193
+ ## MCP tools (20)
175
194
 
176
195
  | Group | Tool | Purpose |
177
196
  |-------|------|---------|
178
- | System | `system_stats` | DB dir, embedding backend, doc/memory counts |
197
+ | System | `system_stats` | DB dir, embedding backend (incl. what the vector leg measures), store state, doc/memory counts |
198
+ | Maintenance | `rag_reindex` | Re-embed every stored vector for the current model, migrating the store in place |
179
199
  | RAG ingest | `rag_ingest_text` | Store text as a knowledge document (dedup-aware) |
180
200
  | | `rag_ingest_file` | Store a file (`content_type` auto-detected) |
181
201
  | | `rag_ingest_dir` | Recursively ingest source files |
182
- | RAG query | `rag_search` | Hybrid (BM25 + vector) search, ranked chunks + scores |
202
+ | RAG query | `rag_search` | Hybrid (BM25 + vector) search, ranked chunks + scores; `explain: true` adds per-leg scores |
183
203
  | | `rag_retrieve` | Ready-to-inject context block with token count |
184
- | RAG docs | `rag_list_documents`, `rag_document_stats` | Inventory |
204
+ | RAG docs | `rag_list_documents`, `rag_document_stats` | Inventory (`rag_list_documents` is paginated: `limit` 1–1000, `offset`) |
185
205
  | | `rag_delete_document` | Remove a document + chunks + FTS rows |
186
206
  | Session sync | `rag_sync_session` | Ingest an opencode session's user inputs (or the latest in a workspace) |
187
207
  | Memory | `memory_remember` | Save long-term memory (type/importance/tags) |
188
208
  | | `memory_recall` | Semantic memory search (decay + recall count) |
189
209
  | | `memory_context` | Compact context block from memories for prompts |
190
210
  | | `memory_list`, `memory_get`, `memory_update`, `memory_forget` | CRUD |
191
- | | `memory_consolidate` | Dedupe near-identical + promote hot memories |
211
+ | | `memory_consolidate` | Dedupe near-identical (cosine **and** word overlap) + promote hot memories |
192
212
  | | `memory_stats` | Counts, tokens, avg importance, by type |
193
213
 
194
214
  ## Run in other AI assistants
@@ -250,9 +270,45 @@ first winning: the MCP client's `env` block → your shell environment → a
250
270
  `.env` and edit, or point at any file via `RAG_ENV_FILE`. The store defaults to
251
271
  `.rag-data/` in the working directory.
252
272
 
273
+ ## Changing the embedding model
274
+
275
+ The store refuses to mix vector spaces: if `EMBEDDING_MODEL` changes, searching
276
+ with new vectors against old ones would silently blend two spaces and return
277
+ noise. `ensureDim` catches the mismatch and tells you to point `RAG_DB_DIR` at a
278
+ fresh directory and re-ingest — correct, but for a memory store holding months of
279
+ history that is a manual rebuild rather than a migration.
280
+
281
+ `reindex` does the migration in place. It re-embeds every chunk and memory for the
282
+ currently configured provider/model, then rewrites the store metadata.
283
+
284
+ ```bash
285
+ # 1. install the optional provider
286
+ npm i @huggingface/transformers
287
+
288
+ # 2. migrate the existing store (documents' text is the source of truth)
289
+ EMBEDDING_PROVIDER=transformers \
290
+ EMBEDDING_MODEL=Xenova/all-MiniLM-L6-v2 \
291
+ npm run cli -- reindex --batch 32
292
+
293
+ # 3. bake the provider into .env so the server uses it from now on
294
+ ```
295
+
296
+ Or over MCP: the `rag_reindex` tool takes the same `batch_size`.
297
+
298
+ **How interruption behaves.** A half-migrated store mixes old and new vectors, so
299
+ `reindex` marks the store `reindexing` for its whole duration and every search,
300
+ recall and ingest refuses to run while that flag is set. This is deliberate: the
301
+ failure mode of a partial migration is confident nonsense, and nothing about the
302
+ results would reveal it. If the process dies mid-run, just run `reindex` again —
303
+ the whole store is rewritten from text, so resuming is identical to starting over.
304
+ `system_stats` reports `storeState` and the remediation if you hit this.
305
+
306
+ Re-running is always safe: embedding is deterministic per model, so `reindex` is
307
+ idempotent. On an empty store it is a no-op that just records the model.
308
+
253
309
  ## Custom slash commands
254
310
 
255
- The 19 MCP tools are called by the AI automatically — but you can also
311
+ The 20 MCP tools are called by the AI automatically — but you can also
256
312
  trigger them directly with custom commands. Sources (same name, project
257
313
  wins):
258
314
 
@@ -279,10 +335,13 @@ npm run cli -- recall "deployment schedule"
279
335
  npm run cli -- ingest-dir ./src/rag
280
336
  npm run cli -- search "vector similarity"
281
337
  npm run cli -- search "auth bug" --source opencode-db # filter by source or doc-id
338
+ npm run cli -- search "auth bug" --explain # per-leg vector/keyword/RRF scores
339
+ npm run cli -- docs --limit 20 --offset 20 # paginated inventory
282
340
  npm run cli -- mem-context "database"
283
341
  npm run cli -- sessions # list recent opencode sessions
284
342
  npm run cli -- sync-session ses_123 # ingest one session's inputs
285
343
  npm run cli -- sync-latest # ingest the most recent session
344
+ npm run cli -- reindex --batch 32 # re-embed all vectors for the current model
286
345
  npm run clean-db -- --force # wipe ALL documents + memories (destructive)
287
346
  ```
288
347
 
@@ -339,14 +398,74 @@ The plugin needs an opencode restart to take effect.
339
398
  ## Performance & limits
340
399
 
341
400
  - **Search is brute-force cosine / FTS5 over every chunk**: `O(N)` per query
342
- (exact, deterministic — good for a personal store of thousands of chunks).
343
- ANN / HNSW indexing is planned scope (v3) for 100k+ chunks.
401
+ (exact, deterministic — good for a personal store of thousands of chunks).
402
+ ANN / HNSW indexing is planned scope (v3) for 100k+ chunks.
403
+ - **The vector cache holds every vector in RAM**, uncapped and complete. Truncating
404
+ it would silently change which documents are findable, so the cache stays whole
405
+ and the server warns instead: 8 KB per row at the default 1024 dims, so 100k
406
+ chunks is roughly 800 MB. Tune the warning threshold with
407
+ `RAG_VECTOR_CACHE_WARN_MB` (default 256). Beyond a few hundred thousand chunks,
408
+ expect to prune rather than scale.
344
409
  - Keyword leg uses the FTS5 **trigram** tokenizer, so CJK text and
345
410
  Indonesian-style substring/inflection matching work without extra config.
346
411
  Consequence: keyword terms of **≤ 2 characters are skipped** (trigram needs
347
412
  3); the vector leg still covers them.
348
413
  - Vector embeddings are local hash-based (no external APIs, ~0 cost, offline).
349
- They are tuned for short-phrase similarity, not full-document semantics.
414
+ They are tuned for short-phrase similarity, not full-document semantics.
415
+
416
+ ### What the vector leg actually measures
417
+
418
+ This matters more than it sounds, so the server states it explicitly in
419
+ `system_stats` (`embedding.vectorLeg`) rather than calling everything "semantic".
420
+
421
+ | Provider | `vectorLeg` | Meaning |
422
+ |---|---|---|
423
+ | `local` (default) | `lexical` | Hashes character 1/2/3-grams into 1024 buckets. Scores **lexical and sub-word overlap**, not meaning. |
424
+ | `transformers` | `semantic` | Real sentence embeddings from `EMBEDDING_MODEL`. Captures meaning. |
425
+
426
+ Consequence for the default: `"cara fix login"` and `"masalah autentikasi"`
427
+ share almost no n-grams, so the **vector** leg scores near zero even though they
428
+ mean the same thing. Retrieval still works because the **FTS5 trigram** leg
429
+ carries the query, and because the local embedder does respond to shared substrings
430
+ and character shapes. What you should not expect from the default is
431
+ paraphrase-level recall — restating a concept with different vocabulary is the
432
+ known blind spot.
433
+
434
+ To get true semantic recall, install the optional dependency and migrate in place:
435
+
436
+ ```bash
437
+ npm i @huggingface/transformers
438
+ EMBEDDING_PROVIDER=transformers EMBEDDING_MODEL=Xenova/all-MiniLM-L6-v2 npm run cli -- reindex
439
+ ```
440
+
441
+ `reindex` rewrites every stored vector for the new model. See
442
+ [Changing the embedding model](#changing-the-embedding-model).
443
+
444
+ ### Debugging a bad search
445
+
446
+ `rag_search` accepts `explain: true` (CLI: `--explain`), which attaches the
447
+ per-leg diagnostics to every hit:
448
+
449
+ ```jsonc
450
+ {
451
+ "score": 0.71, // final relevance, 0..1 — what min_score filters on
452
+ "legs": ["vector", "keyword"],
453
+ "explain": {
454
+ "vectorScore": 0.63, // cosine similarity, -1..1
455
+ "keywordScore": 1.0, // BM25 normalised against the best hit, 0..1
456
+ "rrfScore": 0.0246, // fused contribution that decided the order
457
+ "vectorRank": 0,
458
+ "keywordRank": 2
459
+ }
460
+ }
461
+ ```
462
+
463
+ | Symptom | Likely cause |
464
+ |---|---|
465
+ | `legs: ["keyword"]`, no `vectorScore` | Only the trigram leg matched. Expected for paraphrase queries under the `local` provider. |
466
+ | `legs: ["vector"]` with a low `vectorScore` | Lexical overlap only. Rephrase, or switch provider. |
467
+ | `keywordScore` near 0 for every hit | The query terms are absent from the corpus, or under 3 characters. |
468
+ | No hits at all | Lower `min_score`, or set `SEARCH_MODE=keyword` to isolate which leg is failing. |
350
469
 
351
470
  ## Architecture
352
471
 
@@ -361,7 +480,7 @@ src/
361
480
  │ └── pipeline.ts async ingest/search/retrieve, dedup, guards, doc mgmt
362
481
  ├── session/transcript.ts read opencode sessions (global+local DB), ingest inputs
363
482
  ├── memory/memory.ts remember / recall / consolidate + decay + prune
364
- ├── mcp/rag-server.ts MCP server (19 tools, stdio, npm bin)
483
+ ├── mcp/rag-server.ts MCP server (20 tools, stdio, npm bin)
365
484
  ├── cli.ts CLI playground (incl. sync-session / sync-latest / sync-logs)
366
485
  └── test/test-all.ts full test suite (unit + MCP round-trip)
367
486
  ```
@@ -379,14 +498,25 @@ Data lives in `.rag-data/rag.sqlite` (git-ignored).
379
498
  | `RAG_MAX_FILE_MB` | `10` | Reject files larger than this |
380
499
  | `RAG_ALLOWED_DIRS` | *(unset = anywhere)* | Semicolon/pipe/comma-separated allowed ingest roots |
381
500
  | `RAG_MEMORY_HALF_LIFE_DAYS` | `14` | Recall score decay half-life |
501
+ | `RAG_DEDUP_MAX_VECTORS` | `500` | Above this many memories `consolidate` skips near-duplicate detection and reports `skippedDedup`. Raise it on a dedicated server (the pass is O(n²)) |
502
+ | `RAG_VECTOR_CACHE_WARN_MB` | `256` | Warn when the in-memory vector cache exceeds this size |
382
503
  | `RAG_PRUNE` | `0` | Set `1` to allow `consolidate` to delete non-essential memories |
383
504
  | `RAG_PRUNE_IMPORTANCE` | `0.2` | Delete memories below this importance |
384
505
  | `RAG_PRUNE_AGE_DAYS` | `90` | ...and older than this (never-recalled only) |
385
506
  | `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 |
507
+ | `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) |
508
+ | `RAG_SESSION_MAX_MSGS` | `500` | Cap on user messages read from one session |
387
509
  | `RAG_SESSION_CONDENSE` | `1` | Set `0` to store session inputs verbatim |
388
510
  | `RAG_SESSION_CONDENSE_MIN_CHARS` | `120` | Inputs at/below this length are saved whole |
389
511
  | `RAG_SESSION_CONDENSE_RATIO` | `0.35` | Fraction of long-input length to keep as key points |
512
+ | `RAG_ENV_FILE` | `<cwd>/.env` | Env file to load at boot |
513
+
514
+ A `.env` file can only set this server's own variables (`RAG_*`,
515
+ `OPENCODE_*`, `EMBEDDING_*`, `SEARCH_MODE`); anything else in it is
516
+ ignored with a warning on stderr, and a real environment variable always
517
+ wins over the file. The loader runs from `dist/index.js` and
518
+ `dist/cli.js` only — if you invoke `src/mcp/rag-server.ts` directly
519
+ (TSX/`npx tsx`), import `src/env.js` yourself.
390
520
 
391
521
  ## Testing
392
522
 
@@ -489,8 +619,13 @@ instant.
489
619
  ### Opencode feels sluggish / UI lag after adding plugins
490
620
 
491
621
  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.
622
+ can slow down the TUI. This repo ships exactly one plugin
623
+ (`.opencode/plugin/session-logger.ts`) and it is debounced to at most one
624
+ `npx … sync-latest` per 60s, spawned detached with `stdio: "ignore"` and
625
+ `shell: false`. If it still costs too much, set `RAG_AUTOSYNC=0` (keeps the
626
+ plugin, disables the auto-sync) or drop the `plugin` entry from
627
+ `opencode.json` — the MCP server and its 20 tools keep working, you just have
628
+ to call `rag_sync_session` yourself.
494
629
 
495
630
  ---
496
631
 
package/dist/cli.js CHANGED
@@ -24,6 +24,7 @@
24
24
  * memory-stats
25
25
  * mem-context <topic>
26
26
  * stats Everything
27
+ * reindex [--batch N] Re-embed all vectors for the current model
27
28
  * sessions List opencode sessions (recent first)
28
29
  * sync-session <sessionId> [--limit N] Ingest a session's user inputs to RAG
29
30
  * sync-latest [--limit N] [--dir PATH] Ingest the most recent session (workspace-aware)
@@ -34,6 +35,7 @@ import "./env.js";
34
35
  import { ingestText, ingestFile, ingestDirectory, searchDocs, retrieve, listDocuments, deleteDocument, documentStats } from "./rag/pipeline.js";
35
36
  import { remember, recall, listMemories, getMemory, updateMemory, forget, consolidate, memoryStats, contextPrompt, MEMORY_TYPES } from "./memory/memory.js";
36
37
  import { ingestSession, ingestJsonlFile, ingestLogDir, ingestLatest, listOpenCodeSessions } from "./session/transcript.js";
38
+ import { reindex } from "./rag/reindex.js";
37
39
  import { closeDB, STORAGE_DIR } from "./db/database.js";
38
40
  import * as fs from "fs";
39
41
  async function main() {
@@ -47,100 +49,125 @@ async function main() {
47
49
  closeDB();
48
50
  }
49
51
  function helpText() {
50
- return `Commands: ingest-text|ingest-file|ingest-dir|search|retrieve|docs|doc-stats|rm-doc|remember|recall|memory-list|memory-get|memory-update|forget|consolidate|memory-stats|mem-context|stats|sessions|sync-session|sync-latest|sync-logs|ingest-jsonl`;
52
+ return `Commands: ingest-text|ingest-file|ingest-dir|search|retrieve|docs|doc-stats|rm-doc|remember|recall|memory-list|memory-get|memory-update|forget|consolidate|memory-stats|mem-context|stats|reindex|sessions|sync-session|sync-latest|sync-logs|ingest-jsonl`;
51
53
  }
52
54
  function assertMemoryType(type) {
53
55
  if (!MEMORY_TYPES.includes(type)) {
54
56
  throw new Error(`Invalid memory type "${type}". Valid: ${MEMORY_TYPES.join(", ")}`);
55
57
  }
56
58
  }
59
+ function requireArg(value, usage) {
60
+ if (!value || value.startsWith("--"))
61
+ throw new Error(`Missing argument — usage: ${usage}`);
62
+ return value;
63
+ }
57
64
  async function run(cmd, args) {
58
65
  let out;
59
66
  try {
60
67
  switch (cmd) {
61
68
  case "ingest-text": {
62
- const [title, fileOrDash] = args;
69
+ const parsed = parseOpts(args);
70
+ const [title, fileOrDash] = parsed.positional;
71
+ if (!title || !fileOrDash)
72
+ throw new Error("Missing argument — usage: ingest-text <title> <file|->");
63
73
  const content = fileOrDash === "-" ? fs.readFileSync(0, "utf-8") : fs.readFileSync(fileOrDash, "utf-8");
64
- out = await ingestText(content, title ?? fileOrDash ?? "untitled");
74
+ out = await ingestText(content, title);
65
75
  break;
66
76
  }
67
77
  case "ingest-file":
68
- out = await ingestFile(args[0]);
78
+ out = await ingestFile(requireArg(args[0], "ingest-file <path>"));
69
79
  break;
70
80
  case "ingest-dir":
71
- out = await ingestDirectory(args[0], { recursive: true });
81
+ out = await ingestDirectory(requireArg(args[0], "ingest-dir <dir>"), { recursive: true });
72
82
  break;
73
83
  case "search": {
74
84
  const parsed = parseOpts(args);
75
- out = await searchDocs(parsed.positional.join(" "), undefined, undefined, {
85
+ out = await searchDocs(queryArg(parsed.positional), toNum(parsed.opts["top-k"], 10), toNum(parsed.opts["min-score"], 0.08), {
76
86
  docId: parsed.opts["doc-id"],
77
87
  source: parsed.opts.source,
78
- });
88
+ }, parsed.opts.explain !== undefined);
79
89
  break;
80
90
  }
81
91
  case "retrieve": {
82
92
  const parsed = parseOpts(args);
83
- out = await retrieve(parsed.positional.join(" "), undefined, undefined, {
93
+ out = await retrieve(queryArg(parsed.positional), toNum(parsed.opts["top-k"], 6), toNum(parsed.opts["min-score"], 0.08), {
84
94
  docId: parsed.opts["doc-id"],
85
95
  source: parsed.opts.source,
86
96
  });
87
97
  break;
88
98
  }
89
- case "docs":
90
- out = listDocuments();
99
+ case "docs": {
100
+ const parsed = parseOpts(args);
101
+ out = listDocuments({ limit: toNum(parsed.opts.limit, undefined), offset: toNum(parsed.opts.offset, undefined) });
91
102
  break;
103
+ }
92
104
  case "doc-stats":
93
105
  out = documentStats();
94
106
  break;
95
107
  case "rm-doc":
96
- out = deleteDocument(args[0]);
108
+ out = deleteDocument(requireArg(args[0], "rm-doc <doc_id>"));
97
109
  break;
98
110
  case "remember": {
99
111
  const parsed = parseOpts(args);
112
+ const content = parsed.positional.join(" ");
113
+ if (!content)
114
+ throw new Error("Missing argument — usage: remember <content> [--type fact] [--importance 0.5] [--tag x]");
100
115
  const type = parsed.opts.type ?? "fact";
101
116
  assertMemoryType(type);
102
117
  out = await remember({
103
- content: parsed.positional.join(" "),
118
+ content,
104
119
  type,
105
- importance: parsed.opts.importance !== undefined ? Number(parsed.opts.importance) : 0.5,
120
+ importance: toNum(parsed.opts.importance, 0.5),
106
121
  tags: parsed.opts.tag ? [parsed.opts.tag] : undefined,
107
122
  });
108
123
  break;
109
124
  }
110
- case "recall":
111
- out = await recall(args.join(" "));
125
+ case "recall": {
126
+ const parsed = parseOpts(args);
127
+ out = await recall(queryArg(parsed.positional), toNum(parsed.opts["top-k"], 8), toNum(parsed.opts["min-score"], 0.1));
112
128
  break;
129
+ }
113
130
  case "memory-list": {
114
131
  const parsed = parseOpts(args);
132
+ const type = parsed.opts.type;
133
+ if (type !== undefined)
134
+ assertMemoryType(type);
115
135
  out = listMemories({
116
- type: parsed.opts.type,
136
+ type,
117
137
  tag: parsed.opts.tag,
118
- minImportance: parsed.opts["min-importance"] !== undefined ? Number(parsed.opts["min-importance"]) : undefined,
138
+ minImportance: toNum(parsed.opts["min-importance"], undefined),
139
+ limit: toNum(parsed.opts.limit, undefined),
119
140
  });
120
141
  break;
121
142
  }
122
143
  case "memory-get":
123
- out = getMemory(args[0]);
144
+ out = getMemory(requireArg(args[0], "memory-get <id>"));
124
145
  break;
125
146
  case "memory-update": {
126
147
  const [id, ...restArgs] = args;
127
148
  const parsed = parseOpts(restArgs);
128
- const type = parsed.opts.type ?? "fact";
129
- assertMemoryType(type);
149
+ if (!id)
150
+ throw new Error("Missing argument — usage: memory-update <id> [--content ...] [--importance 0.8] [--type fact] [--tag x]");
151
+ const type = parsed.opts.type;
152
+ if (type !== undefined)
153
+ assertMemoryType(type);
154
+ // --content=... (or --content <text>) sets the body; leftover positional
155
+ // words are the body too. The old `!== "true"` check silently discarded
156
+ // any content that was literally the word "true".
157
+ const content = parsed.opts.content ?? (parsed.positional.length ? parsed.positional.join(" ") : undefined);
158
+ if (content === undefined && type === undefined && parsed.opts.importance === undefined && parsed.opts.tag === undefined) {
159
+ throw new Error("Nothing to update — pass --content, --type, --importance and/or --tag");
160
+ }
130
161
  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,
162
+ content,
163
+ importance: toNum(parsed.opts.importance, undefined),
137
164
  type,
138
165
  tags: parsed.opts.tag ? [parsed.opts.tag] : undefined,
139
166
  });
140
167
  break;
141
168
  }
142
169
  case "forget":
143
- out = forget(args[0]);
170
+ out = forget(requireArg(args[0], "forget <id>"));
144
171
  break;
145
172
  case "consolidate":
146
173
  out = await consolidate();
@@ -148,15 +175,30 @@ async function run(cmd, args) {
148
175
  case "memory-stats":
149
176
  out = memoryStats();
150
177
  break;
151
- case "mem-context":
152
- out = await contextPrompt(args.join(" "));
178
+ case "mem-context": {
179
+ const parsed = parseOpts(args);
180
+ out = await contextPrompt(queryArg(parsed.positional), toNum(parsed.opts["top-k"], 6));
153
181
  break;
182
+ }
154
183
  case "stats":
155
184
  out = { documents: documentStats(), memories: memoryStats(), dbDir: STORAGE_DIR };
156
185
  break;
186
+ case "reindex": {
187
+ const parsed = parseOpts(args);
188
+ out = await reindex({
189
+ batchSize: toNum(parsed.opts.batch, undefined),
190
+ onProgress: (p) => {
191
+ if (p.total > 0) {
192
+ process.stderr.write(`reindex ${p.phase}: ${p.done}/${p.total}\r`);
193
+ }
194
+ },
195
+ });
196
+ process.stderr.write("\n");
197
+ break;
198
+ }
157
199
  case "sessions": {
158
200
  const parsed = parseOpts(args);
159
- out = listOpenCodeSessions(parsed.opts.limit ? Number(parsed.opts.limit) : 15);
201
+ out = listOpenCodeSessions(toNum(parsed.opts.limit, 15) ?? 15, { directory: parsed.opts.dir });
160
202
  break;
161
203
  }
162
204
  case "sync-session": {
@@ -164,12 +206,15 @@ async function run(cmd, args) {
164
206
  const parsed = parseOpts(restArgs);
165
207
  if (!id)
166
208
  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 });
209
+ out = await ingestSession(id, { limit: toNum(parsed.opts.limit, 0) ?? 0 });
168
210
  break;
169
211
  }
170
212
  case "sync-latest": {
171
213
  const parsed = parseOpts(args);
172
- out = await ingestLatest({ limit: parsed.opts.limit ? Number(parsed.opts.limit) : 0, directory: parsed.opts.dir });
214
+ out = await ingestLatest({
215
+ limit: toNum(parsed.opts.limit, 0) ?? 0,
216
+ directory: parsed.opts.dir ?? parsed.positional[0],
217
+ });
173
218
  break;
174
219
  }
175
220
  case "sync-logs": {
@@ -178,7 +223,7 @@ async function run(cmd, args) {
178
223
  break;
179
224
  }
180
225
  case "ingest-jsonl":
181
- out = await ingestJsonlFile(args[0]);
226
+ out = await ingestJsonlFile(requireArg(args[0], "ingest-jsonl <file>"));
182
227
  break;
183
228
  default:
184
229
  console.error(`Unknown command: ${cmd}`);
@@ -188,23 +233,39 @@ async function run(cmd, args) {
188
233
  }
189
234
  catch (e) {
190
235
  console.error(`Error: ${e.message}`);
236
+ closeDB();
191
237
  process.exit(1);
192
238
  }
193
239
  console.log(JSON.stringify(out, null, 2));
194
240
  }
241
+ /** Query text from positional words — empty input is an error, not a silent []. */
242
+ function queryArg(positional) {
243
+ const q = positional.join(" ").trim();
244
+ if (!q)
245
+ throw new Error("Missing query — usage: <command> <query>");
246
+ return q;
247
+ }
195
248
  function parseOpts(args) {
196
249
  const positional = [];
197
250
  const opts = {};
198
251
  for (let i = 0; i < args.length; i++) {
199
252
  const a = args[i];
200
253
  if (a.startsWith("--")) {
254
+ const eq = a.indexOf("=");
255
+ if (eq > 0) {
256
+ // --key=value: the value may legitimately start with "--".
257
+ opts[a.slice(2, eq)] = a.slice(eq + 1);
258
+ continue;
259
+ }
201
260
  const key = a.slice(2);
202
261
  const next = args[i + 1];
203
- if (next && !next.startsWith("--")) {
262
+ if (next !== undefined && !next.startsWith("--")) {
204
263
  opts[key] = next;
205
264
  i++;
206
265
  }
207
266
  else {
267
+ // Boolean flag. Previously the literal "true" was stored as the value,
268
+ // which then leaked into --content and --dir.
208
269
  opts[key] = "true";
209
270
  }
210
271
  }
@@ -214,6 +275,13 @@ function parseOpts(args) {
214
275
  }
215
276
  return { positional, opts };
216
277
  }
278
+ /** Parse a numeric CLI option; falls back to `fallback` for missing/garbage input. */
279
+ function toNum(raw, fallback) {
280
+ if (raw === undefined)
281
+ return fallback;
282
+ const n = Number(raw);
283
+ return Number.isFinite(n) ? n : fallback;
284
+ }
217
285
  main().catch((e) => {
218
286
  console.error(`Error: ${e.message}`);
219
287
  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;
@@ -19,4 +19,12 @@ export declare function setEmbedMeta(dim: number, model: string): void;
19
19
  */
20
20
  export declare function ensureDim(vec: Float64Array, model: string): void;
21
21
  export declare function getEmbedModel(): string;
22
+ /**
23
+ * `embed_state` is '' (or 'ready') for a coherent store and 'reindexing' while a
24
+ * vector-space migration is in flight.
25
+ */
26
+ export declare function getEmbedState(): string;
27
+ export declare function setEmbedState(state: string): void;
28
+ /** Throws when the store is mid-reindex. Called from ensureDim, the shared gate. */
29
+ export declare function assertEmbedStateReady(): void;
22
30
  //# sourceMappingURL=database.d.ts.map
@@ -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,CA0BpC;AA+GD,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,CAWpC;AAED,wBAAgB,YAAY,CAAC,GAAG,EAAE,MAAM,EAAE,KAAK,EAAE,MAAM,QAWtD;AAED;;;GAGG;AACH,wBAAgB,SAAS,CAAC,GAAG,EAAE,YAAY,EAAE,KAAK,EAAE,MAAM,GAAG,IAAI,CA6BhE;AAED,wBAAgB,aAAa,IAAI,MAAM,CAKtC;AAED;;;GAGG;AACH,wBAAgB,aAAa,IAAI,MAAM,CAKtC;AAED,wBAAgB,aAAa,CAAC,KAAK,EAAE,MAAM,GAAG,IAAI,CAIjD;AAED,oFAAoF;AACpF,wBAAgB,qBAAqB,IAAI,IAAI,CAS5C"}