rag-memory-epf-mcp 3.3.4 → 3.3.6
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +35 -5
- package/dist/index.js +20 -78
- package/dist/src/chunkText.d.ts +10 -0
- package/dist/src/chunkText.js +104 -0
- package/dist/src/migrations/migrations.js +50 -0
- package/package.json +24 -3
package/README.md
CHANGED
|
@@ -13,7 +13,8 @@ A **project-local RAG memory** MCP server — knowledge graph + multilingual vec
|
|
|
13
13
|
- **3-signal hybrid search** — vector similarity (bge-m3, 1024-dim) + FTS5 BM25 keyword matching + knowledge graph re-ranking, combined via Reciprocal Rank Fusion
|
|
14
14
|
- **100+ languages** — Korean, Chinese, Japanese, Arabic, and more. Cross-lingual search works out of the box.
|
|
15
15
|
- **Graph-aware scoring** — per-entity geometric decay (0.5^i) with hard cap prevents any single document from dominating results
|
|
16
|
-
- **
|
|
16
|
+
- **30 MCP tools** — knowledge graph CRUD, document pipeline, hybrid search, multi-hop traversal, graph analytics (centrality / community detection / structure), export/import, temporal queries
|
|
17
|
+
- **Codepoint-safe chunking** — chunk offsets are Unicode codepoints, language-neutral across SQL `substr`, Python slicing, and JS `[...str]` iteration. Korean/CJK/emoji documents stay aligned. Verified by a publish-time invariant test.
|
|
17
18
|
- **SQLite optimized** — WAL mode, 32MB cache, 256MB mmap, FTS5 triggers, 7 indexes
|
|
18
19
|
- **MCP SDK 1.27.1** — Tool Annotations (readOnly/destructive/idempotent), latest protocol 2025-11-25
|
|
19
20
|
|
|
@@ -35,7 +36,7 @@ A **project-local RAG memory** MCP server — knowledge graph + multilingual vec
|
|
|
35
36
|
|
|
36
37
|
Place this `.mcp.json` in each project folder with its own `DB_FILE_PATH`. Each project maintains completely isolated memory.
|
|
37
38
|
|
|
38
|
-
## Tools (
|
|
39
|
+
## Tools (30)
|
|
39
40
|
|
|
40
41
|
### Knowledge Graph (7)
|
|
41
42
|
| Tool | Description | Annotation |
|
|
@@ -80,6 +81,13 @@ Place this `.mcp.json` in each project folder with its own `DB_FILE_PATH`. Each
|
|
|
80
81
|
| `runMigrations` | Apply pending migrations | idempotent |
|
|
81
82
|
| `rollbackMigration` | Revert to a previous schema version | destructive |
|
|
82
83
|
|
|
84
|
+
### Graph Analytics (3)
|
|
85
|
+
| Tool | Description | Annotation |
|
|
86
|
+
|------|------------|------------|
|
|
87
|
+
| `getGraphMetrics` | Per-entity centrality (degree, betweenness, closeness, pagerank) | readOnly |
|
|
88
|
+
| `detectCommunities` | Louvain community detection + modularity score | readOnly |
|
|
89
|
+
| `analyzeGraphStructure` | Density, connected components, clustering coefficient | readOnly |
|
|
90
|
+
|
|
83
91
|
## Document Processing Pipeline
|
|
84
92
|
|
|
85
93
|
```
|
|
@@ -112,7 +120,7 @@ storeDocument(id, content, metadata)
|
|
|
112
120
|
│ │ ├── chunks (sqlite-vec, 1024-dim) │ │
|
|
113
121
|
│ │ ├── entity_embeddings (sqlite-vec) │ │
|
|
114
122
|
│ │ ├── entities_fts + chunks_fts (FTS5) │ │
|
|
115
|
-
│ │ └──
|
|
123
|
+
│ │ └── 11 migrations (auto-applied) │ │
|
|
116
124
|
│ └────────────────────────────────────────┘ │
|
|
117
125
|
│ │
|
|
118
126
|
│ bge-m3 (ONNX, 100+ langs) │
|
|
@@ -128,6 +136,24 @@ storeDocument(id, content, metadata)
|
|
|
128
136
|
|
|
129
137
|
## Changelog
|
|
130
138
|
|
|
139
|
+
### v3.3.6
|
|
140
|
+
- **Publish-time invariant test** — `npm run verify:invariants` (wired as `prepublishOnly`) catches `chunkText` offset regressions before they ship. Tests ASCII / Korean / emoji-heavy / mixed CJK + supplementary plane / pure supplementary inputs against the codepoint-slice contract.
|
|
141
|
+
- **`chunkText` extracted to `src/chunkText.ts`** — algorithm now testable in isolation. The class method is a thin wrapper. No user-facing API change.
|
|
142
|
+
- **README accuracy** — tool count corrected to 30, migration count to 11, Graph Analytics tools surfaced.
|
|
143
|
+
|
|
144
|
+
### v3.3.5
|
|
145
|
+
- **Fix: chunk offsets stored as JS UTF-16 units instead of Unicode codepoints** — Korean/CJK/emoji documents had `start_pos`/`end_pos` that disagreed with SQL `substr` and Python slicing for any chunk crossing a supplementary character. `chunkText` now maintains parallel UTF-16 + codepoint cursors and reports codepoint offsets.
|
|
146
|
+
- **Migration v11** — converts existing `chunk_metadata.start_pos`/`end_pos` from UTF-16 units to codepoints by re-locating each chunk via `indexOf` and counting codepoints.
|
|
147
|
+
|
|
148
|
+
### v3.3.4
|
|
149
|
+
- **Migration version 9 → 10 jump + `ALTER TABLE` idempotency guards** — some databases from early v3.x experiments (Ollama dimension swap) had recorded a migration at version 9, causing the new v9 migration to silently no-op. Bumped to version 10 and added `PRAGMA table_info` guards so the column-add is safe to re-run.
|
|
150
|
+
|
|
151
|
+
### v3.3.3
|
|
152
|
+
- **Separate token-space and char-space offsets in `chunk_metadata`** — added `start_token`/`end_token` columns. Existing `start_pos`/`end_pos` are reinterpreted as character offsets into `documents.content`. Backfill migration recomputes char offsets via `indexOf` with a per-document cursor; misses leave NULL so callers can re-chunk to repair.
|
|
153
|
+
|
|
154
|
+
### v3.3.0
|
|
155
|
+
- **Graph analytics (graphology)** — three new MCP tools: `getGraphMetrics` (degree / betweenness / closeness / pagerank), `detectCommunities` (Louvain + modularity), `analyzeGraphStructure` (density / components / clustering). 27 → 30 tools.
|
|
156
|
+
|
|
131
157
|
### v3.2.1
|
|
132
158
|
- **Fix: `autoLinkEntities` silent failure** — was JOINing a non-existent `observations` table (observations are stored as JSON array column in `entities`). Changed to direct column select + `JSON.parse()`.
|
|
133
159
|
|
|
@@ -148,11 +174,11 @@ storeDocument(id, content, metadata)
|
|
|
148
174
|
- **Multi-hop graph traversal** — `getNeighbors` tool with `WITH RECURSIVE` CTE, depth 1-5, cycle detection, bidirectional
|
|
149
175
|
- **Embedding LRU cache** — 500-entry in-memory cache, skips redundant re-computation
|
|
150
176
|
- **Configurable model** — `EMBEDDING_MODEL` env var to use alternative embedding models
|
|
151
|
-
- 27 tools total
|
|
177
|
+
- 27 tools total at this version (30 as of v3.3.0+)
|
|
152
178
|
|
|
153
179
|
### v1.8.0
|
|
154
180
|
- **MCP SDK 1.27.1** — protocol 2025-11-25, security fix GHSA-345p-7cg4-v4c7 (CVSS 7.1)
|
|
155
|
-
- **Tool Annotations** — all
|
|
181
|
+
- **Tool Annotations** — all tools annotated (readOnlyHint, destructiveHint, idempotentHint)
|
|
156
182
|
- **SIGTERM graceful shutdown** — clean exit without ONNX mutex crash
|
|
157
183
|
|
|
158
184
|
### v1.7.0
|
|
@@ -188,8 +214,12 @@ git clone https://github.com/bripin123/rag-memory-epf-mcp.git
|
|
|
188
214
|
cd rag-memory-epf-mcp
|
|
189
215
|
npm install
|
|
190
216
|
npm run build
|
|
217
|
+
npm test # build + invariant verification
|
|
218
|
+
npm run verify:invariants # standalone invariant test (assumes dist/ built)
|
|
191
219
|
```
|
|
192
220
|
|
|
221
|
+
`npm publish` automatically runs `prepublishOnly` (`build` + `verify:invariants`); a chunk-offset regression blocks the publish at the source.
|
|
222
|
+
|
|
193
223
|
## License
|
|
194
224
|
|
|
195
225
|
MIT License. See [LICENSE](LICENSE).
|
package/dist/index.js
CHANGED
|
@@ -21,6 +21,8 @@ import modularity from 'graphology-metrics/graph/modularity.js';
|
|
|
21
21
|
import { getAllMCPTools, validateToolArgs, getSystemInfo } from './src/tools/tool-registry.js';
|
|
22
22
|
// Import migration system
|
|
23
23
|
import { MigrationManager } from './src/migrations/migration-manager.js';
|
|
24
|
+
// Import chunk text algorithm (extracted for publish-time invariant testing)
|
|
25
|
+
import { chunkText as splitTextIntoChunks } from './src/chunkText.js';
|
|
24
26
|
import { migrations } from './src/migrations/migrations.js';
|
|
25
27
|
import { createHash } from 'crypto';
|
|
26
28
|
import { createRequire } from 'module';
|
|
@@ -45,34 +47,7 @@ const EMBEDDING_MODEL = process.env.EMBEDDING_MODEL || 'Xenova/bge-m3';
|
|
|
45
47
|
// When a chunk is not at the document head/tail, any partial sequence at that
|
|
46
48
|
// edge belongs to an adjacent chunk and must be removed so TextDecoder does
|
|
47
49
|
// not emit U+FFFD. Pass trimHead/trimTail=false to preserve head/tail bytes.
|
|
48
|
-
|
|
49
|
-
let start = 0;
|
|
50
|
-
let end = bytes.length;
|
|
51
|
-
if (trimHead) {
|
|
52
|
-
while (start < end && (bytes[start] & 0xC0) === 0x80)
|
|
53
|
-
start++;
|
|
54
|
-
}
|
|
55
|
-
if (trimTail) {
|
|
56
|
-
let i = end - 1;
|
|
57
|
-
while (i >= start && (bytes[i] & 0xC0) === 0x80)
|
|
58
|
-
i--;
|
|
59
|
-
if (i >= start) {
|
|
60
|
-
const lead = bytes[i];
|
|
61
|
-
let needed = 1;
|
|
62
|
-
if ((lead & 0x80) === 0)
|
|
63
|
-
needed = 1;
|
|
64
|
-
else if ((lead & 0xE0) === 0xC0)
|
|
65
|
-
needed = 2;
|
|
66
|
-
else if ((lead & 0xF0) === 0xE0)
|
|
67
|
-
needed = 3;
|
|
68
|
-
else if ((lead & 0xF8) === 0xF0)
|
|
69
|
-
needed = 4;
|
|
70
|
-
if (end - i < needed)
|
|
71
|
-
end = i;
|
|
72
|
-
}
|
|
73
|
-
}
|
|
74
|
-
return bytes.subarray(start, end);
|
|
75
|
-
}
|
|
50
|
+
// (Implementation moved to src/chunkText.ts for testability.)
|
|
76
51
|
function safeRowid(value) {
|
|
77
52
|
const n = Number(value);
|
|
78
53
|
if (!Number.isInteger(n) || n < 0) {
|
|
@@ -1139,59 +1114,26 @@ class RAGKnowledgeGraphManager {
|
|
|
1139
1114
|
//
|
|
1140
1115
|
// Each chunk records both token-space offsets (start_token/end_token from the
|
|
1141
1116
|
// BPE encoder loop) and char-space offsets (start_pos/end_pos into the original
|
|
1142
|
-
// text). Char offsets are
|
|
1143
|
-
//
|
|
1144
|
-
//
|
|
1117
|
+
// text). Char offsets are Unicode codepoint counts — language-neutral, so SQL
|
|
1118
|
+
// substr, Python str slicing, and JS [...str] iteration all line up. JS's
|
|
1119
|
+
// native UTF-16 indexing differs for supplementary characters (emoji, rare
|
|
1120
|
+
// CJK), so the function maintains parallel UTF-16 and codepoint cursors and
|
|
1121
|
+
// reports codepoint offsets. On a coincidental indexOf miss the char offsets
|
|
1122
|
+
// are NULL.
|
|
1145
1123
|
chunkText(text, maxTokens = 800, overlap = 160) {
|
|
1146
1124
|
if (!this.encoding)
|
|
1147
1125
|
throw new Error('Tokenizer not initialized');
|
|
1148
|
-
const
|
|
1149
|
-
|
|
1150
|
-
|
|
1151
|
-
|
|
1152
|
-
|
|
1153
|
-
|
|
1154
|
-
|
|
1155
|
-
|
|
1156
|
-
|
|
1157
|
-
|
|
1158
|
-
|
|
1159
|
-
// trimIncompleteUtf8 leaves the leading bytes intact when isFirst is true.
|
|
1160
|
-
let startPos;
|
|
1161
|
-
let endPos;
|
|
1162
|
-
if (isFirst) {
|
|
1163
|
-
startPos = 0;
|
|
1164
|
-
endPos = chunkText.length;
|
|
1165
|
-
charCursor = 0;
|
|
1166
|
-
}
|
|
1167
|
-
else if (chunkText.length === 0) {
|
|
1168
|
-
startPos = null;
|
|
1169
|
-
endPos = null;
|
|
1170
|
-
}
|
|
1171
|
-
else {
|
|
1172
|
-
const idx = text.indexOf(chunkText, charCursor);
|
|
1173
|
-
if (idx >= 0) {
|
|
1174
|
-
startPos = idx;
|
|
1175
|
-
endPos = idx + chunkText.length;
|
|
1176
|
-
charCursor = idx;
|
|
1177
|
-
}
|
|
1178
|
-
else {
|
|
1179
|
-
startPos = null;
|
|
1180
|
-
endPos = null;
|
|
1181
|
-
}
|
|
1182
|
-
}
|
|
1183
|
-
chunks.push({
|
|
1184
|
-
id: '',
|
|
1185
|
-
document_id: '',
|
|
1186
|
-
chunk_index: chunks.length,
|
|
1187
|
-
text: chunkText,
|
|
1188
|
-
start_pos: startPos,
|
|
1189
|
-
end_pos: endPos,
|
|
1190
|
-
start_token: i,
|
|
1191
|
-
end_token: i + chunkTokens.length
|
|
1192
|
-
});
|
|
1193
|
-
}
|
|
1194
|
-
return chunks;
|
|
1126
|
+
const segments = splitTextIntoChunks(text, this.encoding, maxTokens, overlap);
|
|
1127
|
+
return segments.map((seg, idx) => ({
|
|
1128
|
+
id: '',
|
|
1129
|
+
document_id: '',
|
|
1130
|
+
chunk_index: idx,
|
|
1131
|
+
text: seg.text,
|
|
1132
|
+
start_pos: seg.start_pos,
|
|
1133
|
+
end_pos: seg.end_pos,
|
|
1134
|
+
start_token: seg.start_token,
|
|
1135
|
+
end_token: seg.end_token
|
|
1136
|
+
}));
|
|
1195
1137
|
}
|
|
1196
1138
|
// Generate embeddings using sentence transformers
|
|
1197
1139
|
// isQuery: true for search queries (adds instruction prefix), false for documents/entities
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
import type { Tiktoken } from 'tiktoken';
|
|
2
|
+
export interface ChunkSegment {
|
|
3
|
+
text: string;
|
|
4
|
+
start_pos: number | null;
|
|
5
|
+
end_pos: number | null;
|
|
6
|
+
start_token: number;
|
|
7
|
+
end_token: number;
|
|
8
|
+
}
|
|
9
|
+
export declare function trimIncompleteUtf8(bytes: Uint8Array, trimHead: boolean, trimTail: boolean): Uint8Array;
|
|
10
|
+
export declare function chunkText(text: string, encoding: Tiktoken, maxTokens?: number, overlap?: number): ChunkSegment[];
|
|
@@ -0,0 +1,104 @@
|
|
|
1
|
+
// Tokenize and chunk text using a BPE encoder while reporting both token-space
|
|
2
|
+
// and char-space (Unicode codepoint) offsets back into the original string.
|
|
3
|
+
//
|
|
4
|
+
// BPE tokenizers (cl100k_base) split multi-byte UTF-8 sequences across tokens.
|
|
5
|
+
// Slicing token arrays at arbitrary boundaries can leave incomplete UTF-8
|
|
6
|
+
// prefix/suffix bytes, which TextDecoder replaces with U+FFFD (�). We trim the
|
|
7
|
+
// incomplete sequences at chunk boundaries; overlap covers the removed bytes.
|
|
8
|
+
//
|
|
9
|
+
// Each chunk records both token-space offsets (start_token/end_token from the
|
|
10
|
+
// BPE encoder loop) and char-space offsets (start_pos/end_pos into the original
|
|
11
|
+
// text). Char offsets are Unicode codepoint counts — language-neutral, so SQL
|
|
12
|
+
// substr, Python str slicing, and JS [...str] iteration all line up. JS's
|
|
13
|
+
// native UTF-16 indexing differs for supplementary characters (emoji, rare CJK),
|
|
14
|
+
// so the function maintains parallel UTF-16 and codepoint cursors and reports
|
|
15
|
+
// codepoint offsets. On a coincidental indexOf miss the char offsets are NULL.
|
|
16
|
+
//
|
|
17
|
+
// Extracted to a standalone module so publish-time invariant tests can exercise
|
|
18
|
+
// the algorithm directly without booting the full RAG-Memory stack.
|
|
19
|
+
// trimIncompleteUtf8: strip incomplete UTF-8 sequences from the head/tail of a
|
|
20
|
+
// byte buffer produced by decoding an arbitrary token slice. A multi-byte
|
|
21
|
+
// codepoint that begins or ends on the cut edge belongs to an adjacent chunk
|
|
22
|
+
// and must be removed so TextDecoder does not emit U+FFFD. Pass
|
|
23
|
+
// trimHead/trimTail=false to preserve head/tail bytes (first/last chunks).
|
|
24
|
+
export function trimIncompleteUtf8(bytes, trimHead, trimTail) {
|
|
25
|
+
let start = 0;
|
|
26
|
+
let end = bytes.length;
|
|
27
|
+
if (trimHead) {
|
|
28
|
+
while (start < end && (bytes[start] & 0xC0) === 0x80)
|
|
29
|
+
start++;
|
|
30
|
+
}
|
|
31
|
+
if (trimTail) {
|
|
32
|
+
let i = end - 1;
|
|
33
|
+
while (i >= start && (bytes[i] & 0xC0) === 0x80)
|
|
34
|
+
i--;
|
|
35
|
+
if (i >= start) {
|
|
36
|
+
const lead = bytes[i];
|
|
37
|
+
let needed = 1;
|
|
38
|
+
if ((lead & 0x80) === 0)
|
|
39
|
+
needed = 1;
|
|
40
|
+
else if ((lead & 0xE0) === 0xC0)
|
|
41
|
+
needed = 2;
|
|
42
|
+
else if ((lead & 0xF0) === 0xE0)
|
|
43
|
+
needed = 3;
|
|
44
|
+
else if ((lead & 0xF8) === 0xF0)
|
|
45
|
+
needed = 4;
|
|
46
|
+
if (end - i < needed)
|
|
47
|
+
end = i;
|
|
48
|
+
}
|
|
49
|
+
}
|
|
50
|
+
return bytes.subarray(start, end);
|
|
51
|
+
}
|
|
52
|
+
export function chunkText(text, encoding, maxTokens = 800, overlap = 160) {
|
|
53
|
+
const tokens = encoding.encode(text);
|
|
54
|
+
const segments = [];
|
|
55
|
+
let utf16Cursor = 0;
|
|
56
|
+
let cpCursor = 0;
|
|
57
|
+
for (let i = 0; i < tokens.length; i += maxTokens - overlap) {
|
|
58
|
+
const chunkTokens = tokens.slice(i, i + maxTokens);
|
|
59
|
+
const decodedBytes = encoding.decode(chunkTokens);
|
|
60
|
+
const isFirst = i === 0;
|
|
61
|
+
const isLast = i + chunkTokens.length >= tokens.length;
|
|
62
|
+
const safeBytes = trimIncompleteUtf8(decodedBytes, !isFirst, !isLast);
|
|
63
|
+
const chunkTextStr = new TextDecoder('utf-8').decode(safeBytes);
|
|
64
|
+
let startPos;
|
|
65
|
+
let endPos;
|
|
66
|
+
if (isFirst) {
|
|
67
|
+
startPos = 0;
|
|
68
|
+
endPos = [...chunkTextStr].length;
|
|
69
|
+
utf16Cursor = 0;
|
|
70
|
+
cpCursor = 0;
|
|
71
|
+
}
|
|
72
|
+
else if (chunkTextStr.length === 0) {
|
|
73
|
+
startPos = null;
|
|
74
|
+
endPos = null;
|
|
75
|
+
}
|
|
76
|
+
else {
|
|
77
|
+
const utfIdx = text.indexOf(chunkTextStr, utf16Cursor);
|
|
78
|
+
if (utfIdx >= 0) {
|
|
79
|
+
// Advance cpCursor by codepoints between the previous cursor and the
|
|
80
|
+
// new chunk's start (handles overlap by anchoring at the previous
|
|
81
|
+
// chunk's start, not its end).
|
|
82
|
+
if (utfIdx > utf16Cursor) {
|
|
83
|
+
cpCursor += [...text.slice(utf16Cursor, utfIdx)].length;
|
|
84
|
+
utf16Cursor = utfIdx;
|
|
85
|
+
}
|
|
86
|
+
const cpLen = [...chunkTextStr].length;
|
|
87
|
+
startPos = cpCursor;
|
|
88
|
+
endPos = cpCursor + cpLen;
|
|
89
|
+
}
|
|
90
|
+
else {
|
|
91
|
+
startPos = null;
|
|
92
|
+
endPos = null;
|
|
93
|
+
}
|
|
94
|
+
}
|
|
95
|
+
segments.push({
|
|
96
|
+
text: chunkTextStr,
|
|
97
|
+
start_pos: startPos,
|
|
98
|
+
end_pos: endPos,
|
|
99
|
+
start_token: i,
|
|
100
|
+
end_token: i + chunkTokens.length
|
|
101
|
+
});
|
|
102
|
+
}
|
|
103
|
+
return segments;
|
|
104
|
+
}
|
|
@@ -520,5 +520,55 @@ export const migrations = [
|
|
|
520
520
|
// start_token/end_token columns remain (no DROP COLUMN); they will be
|
|
521
521
|
// ignored by older code.
|
|
522
522
|
}
|
|
523
|
+
},
|
|
524
|
+
// Migration 11: Convert chunk_metadata.start_pos/end_pos from JS UTF-16 code
|
|
525
|
+
// unit indices to Unicode codepoint indices. v3.3.4 stored offsets in JS's
|
|
526
|
+
// native UTF-16 unit space, which mismatches SQL substr/length and Python
|
|
527
|
+
// string indexing for any document containing supplementary characters
|
|
528
|
+
// (emoji, rare CJK). Codepoints are language-neutral. Walks each document's
|
|
529
|
+
// chunks in chunk_index order, locating each chunk in the source via UTF-16
|
|
530
|
+
// indexOf and counting codepoints between cursor positions to derive the
|
|
531
|
+
// codepoint offsets. Idempotent — chunks where indexOf misses keep their
|
|
532
|
+
// existing values.
|
|
533
|
+
{
|
|
534
|
+
version: 11,
|
|
535
|
+
description: 'Convert chunk_metadata.start_pos/end_pos to Unicode codepoint indices',
|
|
536
|
+
up: (db) => {
|
|
537
|
+
const docRows = db.prepare(`SELECT DISTINCT document_id FROM chunk_metadata
|
|
538
|
+
WHERE chunk_type='document' AND document_id IS NOT NULL
|
|
539
|
+
AND start_pos IS NOT NULL AND end_pos IS NOT NULL`).all();
|
|
540
|
+
const getContent = db.prepare(`SELECT content FROM documents WHERE id = ?`);
|
|
541
|
+
const getChunks = db.prepare(`SELECT rowid, text FROM chunk_metadata
|
|
542
|
+
WHERE document_id=? AND chunk_type='document'
|
|
543
|
+
AND start_pos IS NOT NULL AND end_pos IS NOT NULL
|
|
544
|
+
ORDER BY chunk_index ASC`);
|
|
545
|
+
const upd = db.prepare(`UPDATE chunk_metadata SET start_pos=?, end_pos=? WHERE rowid=?`);
|
|
546
|
+
for (const { document_id } of docRows) {
|
|
547
|
+
const doc = getContent.get(document_id);
|
|
548
|
+
if (!doc)
|
|
549
|
+
continue;
|
|
550
|
+
const content = doc.content;
|
|
551
|
+
const chunks = getChunks.all(document_id);
|
|
552
|
+
let utf16Cursor = 0;
|
|
553
|
+
let cpCursor = 0;
|
|
554
|
+
for (const c of chunks) {
|
|
555
|
+
if (!c.text)
|
|
556
|
+
continue;
|
|
557
|
+
const utfIdx = content.indexOf(c.text, utf16Cursor);
|
|
558
|
+
if (utfIdx < 0)
|
|
559
|
+
continue; // miss: leave existing
|
|
560
|
+
if (utfIdx > utf16Cursor) {
|
|
561
|
+
cpCursor += [...content.slice(utf16Cursor, utfIdx)].length;
|
|
562
|
+
utf16Cursor = utfIdx;
|
|
563
|
+
}
|
|
564
|
+
const cpLen = [...c.text].length;
|
|
565
|
+
upd.run(cpCursor, cpCursor + cpLen, c.rowid);
|
|
566
|
+
}
|
|
567
|
+
}
|
|
568
|
+
},
|
|
569
|
+
down: (_db) => {
|
|
570
|
+
// No clean reversal: would need the original document content to recompute
|
|
571
|
+
// UTF-16 indices, and we never recorded which chunks were touched. No-op.
|
|
572
|
+
}
|
|
523
573
|
}
|
|
524
574
|
];
|
package/package.json
CHANGED
|
@@ -1,7 +1,25 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "rag-memory-epf-mcp",
|
|
3
|
-
"version": "3.3.
|
|
4
|
-
"description": "
|
|
3
|
+
"version": "3.3.6",
|
|
4
|
+
"description": "Project-local RAG memory MCP server — knowledge graph + multilingual vector + FTS5 in a single SQLite file. Per-project isolation, 30 MCP tools, codepoint-safe chunking (Korean/CJK/emoji).",
|
|
5
|
+
"keywords": [
|
|
6
|
+
"mcp",
|
|
7
|
+
"model-context-protocol",
|
|
8
|
+
"rag",
|
|
9
|
+
"knowledge-graph",
|
|
10
|
+
"vector-search",
|
|
11
|
+
"fts5",
|
|
12
|
+
"sqlite",
|
|
13
|
+
"embeddings",
|
|
14
|
+
"bge-m3",
|
|
15
|
+
"multilingual",
|
|
16
|
+
"korean",
|
|
17
|
+
"claude-code",
|
|
18
|
+
"gemini-cli",
|
|
19
|
+
"codex-cli",
|
|
20
|
+
"memory",
|
|
21
|
+
"agent-memory"
|
|
22
|
+
],
|
|
5
23
|
"license": "MIT",
|
|
6
24
|
"author": "bripin123",
|
|
7
25
|
"homepage": "https://github.com/bripin123/rag-memory-epf-mcp",
|
|
@@ -21,7 +39,10 @@
|
|
|
21
39
|
"scripts": {
|
|
22
40
|
"build": "tsc && shx chmod +x dist/*.js",
|
|
23
41
|
"prepare": "npm run build",
|
|
24
|
-
"watch": "tsc --watch"
|
|
42
|
+
"watch": "tsc --watch",
|
|
43
|
+
"verify:invariants": "node test/chunk-invariants.test.mjs",
|
|
44
|
+
"test": "npm run build && npm run verify:invariants",
|
|
45
|
+
"prepublishOnly": "npm run build && npm run verify:invariants"
|
|
25
46
|
},
|
|
26
47
|
"dependencies": {
|
|
27
48
|
"@huggingface/transformers": "^3.5.1",
|