rag-memory-epf-mcp 5.0.0 → 5.2.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/README.md +2 -2
- package/dist/index.d.ts +4 -0
- package/dist/index.js +58 -12
- package/dist/src/observations/date-prefix.d.ts +26 -0
- package/dist/src/observations/date-prefix.js +89 -0
- package/dist/src/tools/knowledge-graph-tools.js +5 -0
- package/dist/src/tools/rag-tools.js +1 -0
- package/docs/UPDATING.md +35 -0
- package/package.json +3 -3
package/README.md
CHANGED
|
@@ -64,7 +64,7 @@ Place this `.mcp.json` in each project folder with its own `DB_FILE_PATH`. Each
|
|
|
64
64
|
### Document Pipeline (9)
|
|
65
65
|
| Tool | Description | Annotation |
|
|
66
66
|
|------|------------|------------|
|
|
67
|
-
| `storeDocument` | Store documents with metadata | idempotent |
|
|
67
|
+
| `storeDocument` | Store documents with metadata. Replacing an existing document reports what it destroyed: `{ replaced, deletedChunks }` | idempotent |
|
|
68
68
|
| `chunkDocument` | Create text chunks with configurable parameters | — |
|
|
69
69
|
| `embedChunks` | Generate 1024-dim embeddings + auto-link entities | idempotent |
|
|
70
70
|
| `embedAllEntities` | Batch embed all entities (32 parallel) | idempotent |
|
|
@@ -72,7 +72,7 @@ Place this `.mcp.json` in each project folder with its own `DB_FILE_PATH`. Each
|
|
|
72
72
|
| `linkEntitiesToDocument` | Link entities to chunks where they actually appear (text-matched) | idempotent |
|
|
73
73
|
| `deleteDocuments` | Remove documents and associated data | destructive |
|
|
74
74
|
| `listDocuments` | View all stored documents | readOnly |
|
|
75
|
-
| `syncDocumentFromFile` | One-call server-side sync: reads file + delete/store/chunk/embed/link, content stays off model context. Atomic (embed-first transaction swap) + `content_hash` dedup (skips unchanged files) | idempotent |
|
|
75
|
+
| `syncDocumentFromFile` | One-call server-side sync: reads file + delete/store/chunk/embed/link, content stays off model context. Atomic (embed-first transaction swap) + `content_hash` dedup (skips unchanged files). `excludePattern` strips regions before indexing, and the hash follows the stripped text so changing the pattern re-indexes | idempotent |
|
|
76
76
|
|
|
77
77
|
### Search & Retrieval (9)
|
|
78
78
|
| Tool | Description | Annotation |
|
package/dist/index.d.ts
CHANGED
|
@@ -70,6 +70,7 @@ export declare class RAGKnowledgeGraphManager {
|
|
|
70
70
|
currentProfileId: number;
|
|
71
71
|
grandfatherAllowed: boolean;
|
|
72
72
|
coordinator: BackfillCoordinator | null;
|
|
73
|
+
readonly calendarTimeZone: string;
|
|
73
74
|
private embeddingCache;
|
|
74
75
|
private readonly EMBEDDING_CACHE_MAX;
|
|
75
76
|
private dictionaryCache;
|
|
@@ -227,6 +228,7 @@ export declare class RAGKnowledgeGraphManager {
|
|
|
227
228
|
syncDocumentFromFile(filePath: string, documentId: string, options?: {
|
|
228
229
|
metadata?: Record<string, any>;
|
|
229
230
|
content?: string;
|
|
231
|
+
excludePattern?: string | string[];
|
|
230
232
|
entityNames?: string[];
|
|
231
233
|
chunkParams?: {
|
|
232
234
|
maxTokens?: number;
|
|
@@ -252,6 +254,8 @@ export declare class RAGKnowledgeGraphManager {
|
|
|
252
254
|
storeDocument(id: string, content: string, metadata?: Record<string, any>): Promise<{
|
|
253
255
|
id: string;
|
|
254
256
|
stored: boolean;
|
|
257
|
+
replaced: boolean;
|
|
258
|
+
deletedChunks: number;
|
|
255
259
|
}>;
|
|
256
260
|
chunkDocument(documentId: string, options?: {
|
|
257
261
|
maxTokens?: number;
|
package/dist/index.js
CHANGED
|
@@ -32,6 +32,7 @@ import { migrations } from './src/migrations/migrations.js';
|
|
|
32
32
|
import { EmbeddingGate, GateNotReadyError, GateDisabledError, TerminalConfigError } from './src/embeddingGate.js';
|
|
33
33
|
import { resolveModelCacheDir, preflightCacheDir, artifactKey, ModelDownloadLock, handleLoaderFailure } from './src/modelCache.js';
|
|
34
34
|
import { BackfillCoordinator } from './src/backfillCoordinator.js';
|
|
35
|
+
import { calendarDate, resolveCalendarTimeZone, stampDatePrefix, stripDatePrefix } from './src/observations/date-prefix.js';
|
|
35
36
|
import os from 'node:os';
|
|
36
37
|
import { createHash } from 'crypto';
|
|
37
38
|
import { createRequire } from 'module';
|
|
@@ -146,6 +147,28 @@ function safeRowid(value) {
|
|
|
146
147
|
}
|
|
147
148
|
return n;
|
|
148
149
|
}
|
|
150
|
+
// Remove regions the caller does not want indexed. Compiled with `s` because the intended use is
|
|
151
|
+
// spanning a marked block (`<!-- SECRET -->…<!-- /SECRET -->`) and JS has no inline (?s) flag —
|
|
152
|
+
// without it every such pattern would silently match nothing.
|
|
153
|
+
// A malformed pattern throws rather than degrading to "no exclusion": indexing is a disclosure
|
|
154
|
+
// path, so believing you excluded something you did not is worse than a failed sync.
|
|
155
|
+
function applyExcludePatterns(text, pattern) {
|
|
156
|
+
if (pattern === undefined)
|
|
157
|
+
return text;
|
|
158
|
+
const patterns = Array.isArray(pattern) ? pattern : [pattern];
|
|
159
|
+
let out = text;
|
|
160
|
+
for (const p of patterns) {
|
|
161
|
+
let re;
|
|
162
|
+
try {
|
|
163
|
+
re = new RegExp(p, 'gs');
|
|
164
|
+
}
|
|
165
|
+
catch (e) {
|
|
166
|
+
throw new Error(`excludePattern is not a valid regular expression: ${JSON.stringify(p)} (${e.message})`);
|
|
167
|
+
}
|
|
168
|
+
out = out.replace(re, '');
|
|
169
|
+
}
|
|
170
|
+
return out;
|
|
171
|
+
}
|
|
149
172
|
// Enhanced RAG-enabled Knowledge Graph Manager
|
|
150
173
|
export class RAGKnowledgeGraphManager {
|
|
151
174
|
db = null;
|
|
@@ -157,6 +180,9 @@ export class RAGKnowledgeGraphManager {
|
|
|
157
180
|
// default model config, or with the explicit trust opt-in (spec §6b guard).
|
|
158
181
|
grandfatherAllowed = IS_DEFAULT_MODEL_CONFIG || process.env.RAG_MEMORY_TRUST_LEGACY_VECTORS === '1';
|
|
159
182
|
coordinator = null;
|
|
183
|
+
// The calendar that date-only human labels are written in. Resolved once, here, so an invalid
|
|
184
|
+
// zone fails at construction instead of quietly writing wrong days for weeks.
|
|
185
|
+
calendarTimeZone = resolveCalendarTimeZone(process.env.RAG_MEMORY_CALENDAR_TZ);
|
|
160
186
|
embeddingCache = new Map();
|
|
161
187
|
EMBEDDING_CACHE_MAX = 500;
|
|
162
188
|
dictionaryCache = null;
|
|
@@ -552,11 +578,12 @@ export class RAGKnowledgeGraphManager {
|
|
|
552
578
|
}
|
|
553
579
|
// === ORIGINAL MCP FUNCTIONALITY ===
|
|
554
580
|
_timestampObservation(obs) {
|
|
555
|
-
//
|
|
556
|
-
|
|
557
|
-
|
|
558
|
-
|
|
559
|
-
|
|
581
|
+
// Stamp and dedup share one parser (src/observations/date-prefix.ts). They used to carry
|
|
582
|
+
// separate regexes and disagreed about "[2026-08-11 session16] ...": stamping treated it as
|
|
583
|
+
// undated and prepended a second date, while dedup could not strip it — so the same sentence
|
|
584
|
+
// written in two sessions became two observations. Measured on a live database: 82 rows with
|
|
585
|
+
// two dates, 29 with a day earlier than the day they were written.
|
|
586
|
+
return stampDatePrefix(obs, this.calendarTimeZone);
|
|
560
587
|
}
|
|
561
588
|
async createEntities(entities) {
|
|
562
589
|
if (!this.db)
|
|
@@ -568,7 +595,7 @@ export class RAGKnowledgeGraphManager {
|
|
|
568
595
|
INSERT OR IGNORE INTO entities (id, name, entityType, observations, metadata)
|
|
569
596
|
VALUES (?, ?, ?, '[]', ?)
|
|
570
597
|
`);
|
|
571
|
-
const stripDate =
|
|
598
|
+
const stripDate = stripDatePrefix; // shared with the stamp path — see date-prefix.ts
|
|
572
599
|
for (const entity of entities) {
|
|
573
600
|
const entityId = `entity_${entity.name.toLowerCase().replace(/[^\p{L}\p{N}]/gu, '_')}`;
|
|
574
601
|
const ts = new Date().toISOString();
|
|
@@ -678,7 +705,7 @@ export class RAGKnowledgeGraphManager {
|
|
|
678
705
|
// dedup 기준은 v3.6 과 같다: 날짜 prefix 를 뗀 본문이 active 에 이미 있으면
|
|
679
706
|
// 새 revision 을 만들지 않는다. 다만 v13 에서는 같은 사실이 다른 출처에서 다시
|
|
680
707
|
// 온 것이므로 그 revision 에 source link 를 더한다(spec §8.3 T13).
|
|
681
|
-
const stripDate =
|
|
708
|
+
const stripDate = stripDatePrefix; // shared with the stamp path — see date-prefix.ts
|
|
682
709
|
const activeRows = this.db.prepare(`SELECT observation_id, content FROM entity_observations
|
|
683
710
|
WHERE entity_id = ? AND status = 'active'`).all(entityId);
|
|
684
711
|
const activeByBare = new Map(activeRows.map(r => [stripDate(r.content), r.observation_id]));
|
|
@@ -1935,9 +1962,16 @@ export class RAGKnowledgeGraphManager {
|
|
|
1935
1962
|
const zero = { reusedChunks: 0, newlyEmbeddedChunks: 0, queuedChunks: 0, deletedChunks: 0, chunkerTransitioned: false };
|
|
1936
1963
|
for (let attempt = 1; attempt <= 3; attempt++) {
|
|
1937
1964
|
// r6-3: CAS 재시작 = 처음부터 — 파일 읽기·hash·metadata 도 attempt 안에서 재계산한다.
|
|
1938
|
-
|
|
1965
|
+
// Strip excluded regions before anything else looks at the text. Everything downstream —
|
|
1966
|
+
// content_hash, bytes, chunking — then describes what was actually indexed, so changing the
|
|
1967
|
+
// pattern alone still invalidates the dedup gate below. Hashing the raw file instead would
|
|
1968
|
+
// report `unchanged` for a different exclusion, which is the silent-wrong case.
|
|
1969
|
+
const content = applyExcludePatterns(options.content !== undefined ? options.content : fsSync.readFileSync(filePath, 'utf-8'), options.excludePattern);
|
|
1939
1970
|
const bytes = Buffer.byteLength(content, 'utf-8');
|
|
1940
|
-
|
|
1971
|
+
// Same calendar as the observation prefix. Deciding this explicitly rather than leaving it
|
|
1972
|
+
// on UTC: both are date-only labels a person reads, and "observations in Seoul days but
|
|
1973
|
+
// documents in UTC days" is not a distinction anyone could explain later.
|
|
1974
|
+
const today = calendarDate(new Date(), this.calendarTimeZone);
|
|
1941
1975
|
const contentHash = shaHex(content);
|
|
1942
1976
|
// spec §5.1: content_hash 는 system-owned — user metadata 뒤에 쓴다 (r1: spread 가 덮어쓸 수 있었다).
|
|
1943
1977
|
const metadata = { source: filePath, updated: today, ...(options.metadata || {}), content_hash: contentHash };
|
|
@@ -2104,15 +2138,18 @@ export class RAGKnowledgeGraphManager {
|
|
|
2104
2138
|
if (!this.db)
|
|
2105
2139
|
throw new Error('Database not initialized');
|
|
2106
2140
|
console.error(`📄 Storing document: ${id}`);
|
|
2141
|
+
// Decide `replaced` from the document row, not from the chunk count: a document stored but
|
|
2142
|
+
// never chunked still gets overwritten here, and reporting that as a fresh write would be a lie.
|
|
2143
|
+
const existed = this.db.prepare(`SELECT 1 FROM documents WHERE id = ?`).get(id) !== undefined;
|
|
2107
2144
|
// Clean up existing document
|
|
2108
|
-
await this.cleanupDocument(id);
|
|
2145
|
+
const cleaned = await this.cleanupDocument(id);
|
|
2109
2146
|
// Store document
|
|
2110
2147
|
this.db.prepare(`
|
|
2111
2148
|
INSERT OR REPLACE INTO documents (id, content, metadata)
|
|
2112
2149
|
VALUES (?, ?, ?)
|
|
2113
2150
|
`).run(id, content, JSON.stringify(metadata));
|
|
2114
2151
|
console.error(`✅ Document stored: ${id}`);
|
|
2115
|
-
return { id, stored: true };
|
|
2152
|
+
return { id, stored: true, replaced: existed, deletedChunks: cleaned.deletedChunks };
|
|
2116
2153
|
}
|
|
2117
2154
|
async chunkDocument(documentId, options = {}) {
|
|
2118
2155
|
if (!this.db)
|
|
@@ -2430,9 +2467,13 @@ export class RAGKnowledgeGraphManager {
|
|
|
2430
2467
|
console.error(`✅ Entities linked: ${linkedCount} entities linked to document`);
|
|
2431
2468
|
return { documentId, linkedEntities: linkedCount };
|
|
2432
2469
|
}
|
|
2470
|
+
// Report what was destroyed. The counts were already computed here and thrown away, so a caller
|
|
2471
|
+
// that replaces a document could not tell from the return value that anything was deleted
|
|
2472
|
+
// (2026-08-05 field report from a deployed project: "{stored:true} came back and I did not know
|
|
2473
|
+
// what I had just wiped"). Silent destruction is the defect; the numbers are free.
|
|
2433
2474
|
async cleanupDocument(documentId) {
|
|
2434
2475
|
if (!this.db)
|
|
2435
|
-
return;
|
|
2476
|
+
return { deletedChunks: 0, deletedAssociations: 0, deletedVectors: 0 };
|
|
2436
2477
|
console.error(`🧹 Cleaning up document: ${documentId}`);
|
|
2437
2478
|
// Get existing chunks
|
|
2438
2479
|
const existingChunks = this.db.prepare(`
|
|
@@ -2460,6 +2501,7 @@ export class RAGKnowledgeGraphManager {
|
|
|
2460
2501
|
console.error(` ├─ Deleted ${deletedVectors} vector embeddings`);
|
|
2461
2502
|
console.error(` └─ Deleted ${metadata.changes} chunk metadata records`);
|
|
2462
2503
|
}
|
|
2504
|
+
return { deletedChunks: existingChunks.length, deletedAssociations, deletedVectors };
|
|
2463
2505
|
}
|
|
2464
2506
|
async deleteDocument(documentId) {
|
|
2465
2507
|
if (!this.db)
|
|
@@ -3418,6 +3460,9 @@ export class RAGKnowledgeGraphManager {
|
|
|
3418
3460
|
server: {
|
|
3419
3461
|
version: PKG_VERSION,
|
|
3420
3462
|
node: process.versions.node,
|
|
3463
|
+
// Which calendar produced the date-only labels in this database. Surfaced because a
|
|
3464
|
+
// wrong value is otherwise invisible: the labels look plausible either way.
|
|
3465
|
+
calendar_timezone: this.calendarTimeZone,
|
|
3421
3466
|
embeddings_mode: this.embeddingsMode,
|
|
3422
3467
|
model: `${EMBEDDING_MODEL}@${MODEL_REVISION}`,
|
|
3423
3468
|
model_state: gs.state,
|
|
@@ -3839,6 +3884,7 @@ server.setRequestHandler(CallToolRequestSchema, async (request) => {
|
|
|
3839
3884
|
return { content: [{ type: "text", text: JSON.stringify(await ragKgManager.syncDocumentFromFile(validatedArgs.path, validatedArgs.documentId, {
|
|
3840
3885
|
metadata: validatedArgs.metadata,
|
|
3841
3886
|
content: validatedArgs.content,
|
|
3887
|
+
excludePattern: validatedArgs.excludePattern,
|
|
3842
3888
|
entityNames: validatedArgs.entityNames,
|
|
3843
3889
|
chunkParams: validatedArgs.chunkParams,
|
|
3844
3890
|
}), null, 2) }] };
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
/** The calendar day of `instant` in `timeZone`, as YYYY-MM-DD. */
|
|
2
|
+
export declare function calendarDate(instant: Date, timeZone: string): string;
|
|
3
|
+
/**
|
|
4
|
+
* Resolve RAG_MEMORY_CALENDAR_TZ. Unset or blank means UTC. An unrecognised zone throws at
|
|
5
|
+
* boot rather than silently falling back — a wrong-but-quiet calendar writes wrong labels for
|
|
6
|
+
* as long as nobody looks, which is the failure this whole module exists to end.
|
|
7
|
+
*/
|
|
8
|
+
export declare function resolveCalendarTimeZone(raw: string | undefined | null): string;
|
|
9
|
+
export interface DatePrefix {
|
|
10
|
+
/** YYYY-MM-DD, already validated as a real calendar day. */
|
|
11
|
+
date: string;
|
|
12
|
+
/** Text between the date and the closing bracket, e.g. a session marker. */
|
|
13
|
+
annotation: string | null;
|
|
14
|
+
/** The matched prefix including the brackets, without trailing whitespace. */
|
|
15
|
+
matched: string;
|
|
16
|
+
}
|
|
17
|
+
/** Parse a leading date prefix, or null when the text does not start with one. */
|
|
18
|
+
export declare function parseDatePrefix(content: string): DatePrefix | null;
|
|
19
|
+
/**
|
|
20
|
+
* The dedup key: the text with its date prefix removed. The annotation comes off with the date
|
|
21
|
+
* because it records *when and in which session the line was written*, not what the line claims
|
|
22
|
+
* — the same sentence written in two sessions is one fact, and dedup has to see that.
|
|
23
|
+
*/
|
|
24
|
+
export declare function stripDatePrefix(content: string): string;
|
|
25
|
+
/** Prepend today's calendar day unless the text already carries a valid date prefix. */
|
|
26
|
+
export declare function stampDatePrefix(content: string, timeZone: string, now?: Date): string;
|
|
@@ -0,0 +1,89 @@
|
|
|
1
|
+
// One place that decides what a leading "[date]" on an observation means.
|
|
2
|
+
//
|
|
3
|
+
// Why this is a module and not two regexes inline: the stamp path and the dedup path each had
|
|
4
|
+
// their own copy of the pattern, and they disagreed. Stamping asked "does this already start
|
|
5
|
+
// with a date?" while dedup asked "what is this text without its date?", so widening one without
|
|
6
|
+
// the other let the same sentence be stored twice under two session markers. Both now call in
|
|
7
|
+
// here.
|
|
8
|
+
//
|
|
9
|
+
// Why an explicit timezone instead of the process one: a date-only label has no meaning until
|
|
10
|
+
// you say which calendar produced it. The same database is reached from a laptop, from CI and
|
|
11
|
+
// from another country; deriving the day from the ambient TZ makes the stored string mean
|
|
12
|
+
// something different on each. The default stays UTC so the product does not inherit whichever
|
|
13
|
+
// zone its first author happened to sit in — a deployment that wants local days says so.
|
|
14
|
+
/** The calendar day of `instant` in `timeZone`, as YYYY-MM-DD. */
|
|
15
|
+
export function calendarDate(instant, timeZone) {
|
|
16
|
+
const parts = new Intl.DateTimeFormat('en-US', {
|
|
17
|
+
timeZone,
|
|
18
|
+
year: 'numeric',
|
|
19
|
+
month: '2-digit',
|
|
20
|
+
day: '2-digit',
|
|
21
|
+
}).formatToParts(instant);
|
|
22
|
+
const part = (type) => {
|
|
23
|
+
const found = parts.find(p => p.type === type);
|
|
24
|
+
if (!found)
|
|
25
|
+
throw new Error(`calendarDate: missing ${type} for timeZone ${timeZone}`);
|
|
26
|
+
return found.value;
|
|
27
|
+
};
|
|
28
|
+
return `${part('year')}-${part('month')}-${part('day')}`;
|
|
29
|
+
}
|
|
30
|
+
/**
|
|
31
|
+
* Resolve RAG_MEMORY_CALENDAR_TZ. Unset or blank means UTC. An unrecognised zone throws at
|
|
32
|
+
* boot rather than silently falling back — a wrong-but-quiet calendar writes wrong labels for
|
|
33
|
+
* as long as nobody looks, which is the failure this whole module exists to end.
|
|
34
|
+
*/
|
|
35
|
+
export function resolveCalendarTimeZone(raw) {
|
|
36
|
+
const value = (raw ?? '').trim();
|
|
37
|
+
if (!value)
|
|
38
|
+
return 'UTC';
|
|
39
|
+
try {
|
|
40
|
+
new Intl.DateTimeFormat('en-US', { timeZone: value });
|
|
41
|
+
}
|
|
42
|
+
catch {
|
|
43
|
+
throw new Error(`RAG_MEMORY_CALENDAR_TZ is not a recognised IANA time zone: ${JSON.stringify(value)}. ` +
|
|
44
|
+
`Use a name like "Asia/Seoul", or leave it unset for UTC.`);
|
|
45
|
+
}
|
|
46
|
+
return value;
|
|
47
|
+
}
|
|
48
|
+
// The annotation may not contain a newline or a bracket: an unclosed or multi-line "[2026-…"
|
|
49
|
+
// is prose that happens to start with a digit, not a prefix. Requiring the closing bracket is
|
|
50
|
+
// what keeps "[2026-08-11 unclosed" and "[2026-08-111]" out.
|
|
51
|
+
const PREFIX_RE = /^\[(\d{4})-(\d{2})-(\d{2})(?:[ \t]([^\]\n]*))?\]/;
|
|
52
|
+
function isRealCalendarDay(year, month, day) {
|
|
53
|
+
const asUtc = new Date(Date.UTC(year, month - 1, day));
|
|
54
|
+
return asUtc.getUTCFullYear() === year
|
|
55
|
+
&& asUtc.getUTCMonth() === month - 1
|
|
56
|
+
&& asUtc.getUTCDate() === day;
|
|
57
|
+
}
|
|
58
|
+
/** Parse a leading date prefix, or null when the text does not start with one. */
|
|
59
|
+
export function parseDatePrefix(content) {
|
|
60
|
+
const m = PREFIX_RE.exec(content);
|
|
61
|
+
if (!m)
|
|
62
|
+
return null;
|
|
63
|
+
const [matched, year, month, day, annotation] = m;
|
|
64
|
+
if (!isRealCalendarDay(Number(year), Number(month), Number(day)))
|
|
65
|
+
return null;
|
|
66
|
+
const trimmed = annotation === undefined ? null : annotation.trim();
|
|
67
|
+
return {
|
|
68
|
+
date: `${year}-${month}-${day}`,
|
|
69
|
+
annotation: trimmed === '' ? null : trimmed,
|
|
70
|
+
matched,
|
|
71
|
+
};
|
|
72
|
+
}
|
|
73
|
+
/**
|
|
74
|
+
* The dedup key: the text with its date prefix removed. The annotation comes off with the date
|
|
75
|
+
* because it records *when and in which session the line was written*, not what the line claims
|
|
76
|
+
* — the same sentence written in two sessions is one fact, and dedup has to see that.
|
|
77
|
+
*/
|
|
78
|
+
export function stripDatePrefix(content) {
|
|
79
|
+
const prefix = parseDatePrefix(content);
|
|
80
|
+
if (!prefix)
|
|
81
|
+
return content;
|
|
82
|
+
return content.slice(prefix.matched.length).replace(/^\s+/, '');
|
|
83
|
+
}
|
|
84
|
+
/** Prepend today's calendar day unless the text already carries a valid date prefix. */
|
|
85
|
+
export function stampDatePrefix(content, timeZone, now = new Date()) {
|
|
86
|
+
if (parseDatePrefix(content))
|
|
87
|
+
return content;
|
|
88
|
+
return `[${calendarDate(now, timeZone)}] ${content}`;
|
|
89
|
+
}
|
|
@@ -200,6 +200,11 @@ Observations provide the factual foundation that supports entity existence and p
|
|
|
200
200
|
- (!important!) **Entity must exist** - this tool only adds to existing entities
|
|
201
201
|
- (!important!) Duplicates are filtered - repeating existing text with a **new** sources entry
|
|
202
202
|
adds evidence to the existing revision and returns null for that position
|
|
203
|
+
- (!important!) A \`[YYYY-MM-DD]\` prefix is prepended unless the text already starts with a valid
|
|
204
|
+
one. The day comes from \`RAG_MEMORY_CALENDAR_TZ\` (default UTC) - see \`calendar_timezone\` in
|
|
205
|
+
getKnowledgeGraphStats. Text may open with \`[YYYY-MM-DD note]\`; the note is treated as recording
|
|
206
|
+
metadata and is ignored when deduplicating, so the same sentence filed under two session markers
|
|
207
|
+
stays one observation
|
|
203
208
|
- (!important!) Observations are cumulative. **This tool never replaces anything** - use
|
|
204
209
|
correctObservation to supersede and retractObservation to withdraw
|
|
205
210
|
- (!important!) **Be specific and factual** - observations should be verifiable statements
|
|
@@ -594,6 +594,7 @@ const syncDocumentFromFileSchema = {
|
|
|
594
594
|
documentId: z.string().describe('RAG document ID to (re)create from the file'),
|
|
595
595
|
metadata: z.record(z.any()).optional().describe('Metadata merged into the stored document'),
|
|
596
596
|
content: z.string().optional().describe('Optional content override; stored instead of reading the file'),
|
|
597
|
+
excludePattern: z.union([z.string(), z.array(z.string())]).optional().describe('Regular expression(s) whose matches are stripped before indexing. Applied to the file (or to `content`) first, so hash, byte count and chunking all describe what was actually indexed. Compiled with the dotAll flag, so a pattern may span lines to drop a marked block. An invalid expression fails the call rather than indexing the whole file.'),
|
|
597
598
|
entityNames: z.array(z.string()).optional().describe('Optional entities to explicitly link'),
|
|
598
599
|
chunkParams: z.record(z.any()).optional().describe('Optional chunking parameters { maxTokens }. overlap: omit or 0 only (rejected otherwise since v5.0.0)'),
|
|
599
600
|
};
|
package/docs/UPDATING.md
CHANGED
|
@@ -108,6 +108,41 @@ path and holder pid (e.g. `.download-<key>.lock`). Verify the holder process
|
|
|
108
108
|
is genuinely gone or hung (`ps -p <pid>`), then remove the lock file manually;
|
|
109
109
|
the next start becomes a clean download owner.
|
|
110
110
|
|
|
111
|
+
## v5.1.0 (schema v14, unchanged): destructive-replace reporting + `excludePattern`
|
|
112
|
+
|
|
113
|
+
**What changes on upgrade**: nothing you have to do. No migration, no schema
|
|
114
|
+
change, no re-embedding. Both changes are additive — existing calls keep their
|
|
115
|
+
arguments and keep working, and the new response fields are extra keys.
|
|
116
|
+
|
|
117
|
+
**`storeDocument` now says what it destroyed.** It has always deleted the
|
|
118
|
+
previous document's chunks, vectors and entity links before writing, but the
|
|
119
|
+
response was `{ id, stored: true }`, so a caller replacing a document could not
|
|
120
|
+
tell from the return value that anything was removed. It now returns
|
|
121
|
+
`{ id, stored, replaced, deletedChunks }`, matching what `syncDocumentFromFile`
|
|
122
|
+
already reported. `replaced` is decided by the document row, not the chunk
|
|
123
|
+
count — a document that was stored but never chunked still gets overwritten,
|
|
124
|
+
and reporting that as a fresh write would be wrong.
|
|
125
|
+
|
|
126
|
+
**`syncDocumentFromFile` accepts `excludePattern`** (string or array of
|
|
127
|
+
strings): regions matching these regular expressions are stripped before
|
|
128
|
+
indexing. Previously the only way to leave part of a file out was to read it
|
|
129
|
+
yourself and pass the whole edited text through `content`, which defeats the
|
|
130
|
+
point of a tool that reads server-side to keep content off the model context.
|
|
131
|
+
|
|
132
|
+
Three properties worth knowing:
|
|
133
|
+
|
|
134
|
+
1. **The exclusion happens first**, before hashing and chunking, so
|
|
135
|
+
`content_hash`, the reported `bytes` and the chunk boundaries all describe
|
|
136
|
+
what was actually indexed. Changing only the pattern therefore invalidates
|
|
137
|
+
the dedup gate and re-indexes; it does not silently return `unchanged`.
|
|
138
|
+
2. **Patterns are compiled with the dotAll flag**, so one pattern can span
|
|
139
|
+
lines to drop a marked block (`<!-- SECRET -->[\s\S]*?<!-- /SECRET -->`).
|
|
140
|
+
JavaScript has no inline `(?s)`, so without this every such pattern would
|
|
141
|
+
quietly match nothing.
|
|
142
|
+
3. **An invalid expression fails the call.** Degrading to "no exclusion" would
|
|
143
|
+
index the whole file while the caller believes it was filtered, and an index
|
|
144
|
+
is a disclosure path — a failed sync is the safer error.
|
|
145
|
+
|
|
111
146
|
## v5.0.0 (schema v14): chunker c1 + vector reuse
|
|
112
147
|
|
|
113
148
|
**Breaking**: `chunkParams.overlap` is rejected on BOTH public paths
|
package/package.json
CHANGED
|
@@ -1,10 +1,10 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "rag-memory-epf-mcp",
|
|
3
|
-
"version": "5.
|
|
3
|
+
"version": "5.2.0",
|
|
4
4
|
"engines": {
|
|
5
5
|
"node": ">=24"
|
|
6
6
|
},
|
|
7
|
-
"description": "Project-local RAG memory MCP server
|
|
7
|
+
"description": "Project-local RAG memory MCP server — knowledge graph + multilingual vector + FTS5 in a single SQLite file. Per-project isolation, 38 MCP tools, codepoint-safe chunking (Korean/CJK/emoji).",
|
|
8
8
|
"keywords": [
|
|
9
9
|
"mcp",
|
|
10
10
|
"model-context-protocol",
|
|
@@ -45,7 +45,7 @@
|
|
|
45
45
|
"prepare": "npm run build",
|
|
46
46
|
"watch": "tsc --watch",
|
|
47
47
|
"verify:invariants": "node test/chunk-invariants.test.mjs",
|
|
48
|
-
"verify:engine": "node test/engine-smoke.test.mjs && node test/launch-smoke.test.mjs && node test/sync-atomicity.test.mjs && node test/dedup.test.mjs && node test/search-degradation.test.mjs && node test/entity-embed-cap.test.mjs && node test/migration12.test.mjs && node test/model-cache.test.mjs && node test/embedding-gate.test.mjs && node test/lazy-boot.test.mjs && node test/reconciliation.test.mjs && node test/backfill.test.mjs && node test/fts-query.test.mjs && node test/search-contracts.test.mjs && node test/tool-contracts.test.mjs && node test/bounded-exit.test.mjs && node test/observation-schema.test.mjs && node test/observation-migration.test.mjs && node test/observation-lifecycle.test.mjs && node test/observation-contracts.test.mjs && node test/observation-search.test.mjs && node test/observation-cascade.test.mjs && node test/observation-realdata.test.mjs && node test/chunker-c.test.mjs && node test/migration14.test.mjs && node test/chunk-params-validation.test.mjs && node test/vector-reuse.test.mjs && node test/entity-range-linking.test.mjs && node test/stats-chunking.test.mjs && node test/migration14-realdata.test.mjs && node test/migration14-realdata-sync.test.mjs && node test/search-summaries-off.test.mjs",
|
|
48
|
+
"verify:engine": "node test/engine-smoke.test.mjs && node test/launch-smoke.test.mjs && node test/sync-atomicity.test.mjs && node test/dedup.test.mjs && node test/search-degradation.test.mjs && node test/entity-embed-cap.test.mjs && node test/migration12.test.mjs && node test/model-cache.test.mjs && node test/embedding-gate.test.mjs && node test/lazy-boot.test.mjs && node test/reconciliation.test.mjs && node test/backfill.test.mjs && node test/fts-query.test.mjs && node test/search-contracts.test.mjs && node test/tool-contracts.test.mjs && node test/bounded-exit.test.mjs && node test/observation-schema.test.mjs && node test/observation-migration.test.mjs && node test/observation-lifecycle.test.mjs && node test/observation-contracts.test.mjs && node test/observation-search.test.mjs && node test/observation-cascade.test.mjs && node test/observation-realdata.test.mjs && node test/chunker-c.test.mjs && node test/migration14.test.mjs && node test/chunk-params-validation.test.mjs && node test/vector-reuse.test.mjs && node test/entity-range-linking.test.mjs && node test/stats-chunking.test.mjs && node test/migration14-realdata.test.mjs && node test/migration14-realdata-sync.test.mjs && node test/search-summaries-off.test.mjs && node test/document-return-contracts.test.mjs && node test/observation-date-prefix.test.mjs",
|
|
49
49
|
"test": "npm run build && npm run verify:invariants && npm run verify:engine",
|
|
50
50
|
"prepublishOnly": "npm run build && npm run verify:invariants && npm run verify:engine"
|
|
51
51
|
},
|