@levalicious/server-memory 0.0.16 → 0.0.18

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/binding.gyp CHANGED
@@ -10,7 +10,16 @@
10
10
  "native"
11
11
  ],
12
12
  "cflags": ["-std=c11", "-Wall", "-Wextra", "-O2"],
13
- "defines": ["_GNU_SOURCE"]
13
+ "conditions": [
14
+ ["OS=='linux'", {
15
+ "defines": ["_GNU_SOURCE"]
16
+ }],
17
+ ["OS=='mac'", {
18
+ "xcode_settings": {
19
+ "OTHER_CFLAGS": ["-std=c11", "-Wall", "-Wextra", "-O2"]
20
+ }
21
+ }]
22
+ ]
14
23
  }
15
24
  ]
16
25
  }
package/dist/server.js CHANGED
@@ -953,7 +953,7 @@ export function createServer(memoryFilePath) {
953
953
  sizes: ["any"]
954
954
  }
955
955
  ],
956
- version: "0.0.16",
956
+ version: "0.0.18",
957
957
  }, {
958
958
  capabilities: {
959
959
  tools: {},
@@ -1276,7 +1276,7 @@ Use this to build chains of reasoning that persist in the graph. Each thought ca
1276
1276
  },
1277
1277
  {
1278
1278
  name: "kb_load",
1279
- description: `Load a plaintext document into the knowledge graph. Chunks the text into entities connected by a doubly-linked chain, runs sentence TextRank to identify the most important sentences, and creates an index entity that links directly to the chunks containing those sentences.
1279
+ description: `Load a plaintext document into the knowledge graph. Chunks the text into entities connected by a doubly-linked chain, runs sentence TextRank to identify the most important sentences, and creates an index entity per key phrase that links directly to the chunk containing that sentence.
1280
1280
 
1281
1281
  The file MUST be plaintext (.txt, .tex, .md, source code, etc.). For PDFs, use pdftotext first. For other binary formats, convert to text before calling this tool.`,
1282
1282
  inputSchema: {
@@ -317,28 +317,18 @@ export function loadDocument(text, title, st, topK = 15) {
317
317
  seenChunks.add(chunk.id);
318
318
  highlights.push({ chunk, sentence, score });
319
319
  }
320
- // 6. Build index observations (compressed sentence previews)
321
- const indexId = `${title}__index`;
322
- const indexObs = [];
323
- let current = '';
324
- for (const { sentence } of highlights) {
325
- const preview = sentence.text.length > 60
326
- ? sentence.text.slice(0, 57) + '...'
327
- : sentence.text;
328
- const candidate = current ? current + ' | ' + preview : preview;
329
- if (candidate.length <= MAX_OBS_LENGTH) {
330
- current = candidate;
331
- }
332
- else {
333
- if (current)
334
- indexObs.push(current);
335
- if (indexObs.length >= MAX_OBS_PER_ENTITY)
336
- break;
337
- current = preview.length <= MAX_OBS_LENGTH ? preview : preview.slice(0, MAX_OBS_LENGTH);
338
- }
320
+ // 6. Build index entities one per highlighted phrase
321
+ // Each DocumentIndex entity holds a single extracted key phrase as its
322
+ // observation, and links to the chunk that contains it. This gives the
323
+ // walker discrete semantic entry points into the document chain.
324
+ const indexEntities = [];
325
+ for (const { chunk, sentence } of highlights) {
326
+ const phrase = sentence.text.length <= MAX_OBS_LENGTH
327
+ ? sentence.text
328
+ : sentence.text.slice(0, MAX_OBS_LENGTH - 3) + '...';
329
+ const indexId = `${title}__idx_${indexEntities.length}`;
330
+ indexEntities.push({ id: indexId, phrase, chunk });
339
331
  }
340
- if (current && indexObs.length < MAX_OBS_PER_ENTITY)
341
- indexObs.push(current);
342
332
  // ─── Assemble entities ──────────────────────────────────────────
343
333
  const entities = [];
344
334
  const relations = [];
@@ -352,12 +342,14 @@ export function loadDocument(text, title, st, topK = 15) {
352
342
  observations: chunk.observations.map(o => o.text),
353
343
  });
354
344
  }
355
- // Index entity
356
- entities.push({
357
- name: indexId,
358
- entityType: 'DocumentIndex',
359
- observations: indexObs,
360
- });
345
+ // Index entities — one per key phrase
346
+ for (const idx of indexEntities) {
347
+ entities.push({
348
+ name: idx.id,
349
+ entityType: 'DocumentIndex',
350
+ observations: [idx.phrase],
351
+ });
352
+ }
361
353
  // ─── Assemble relations ─────────────────────────────────────────
362
354
  // Document → chain endpoints
363
355
  if (chunks.length > 0) {
@@ -373,13 +365,15 @@ export function loadDocument(text, title, st, topK = 15) {
373
365
  relations.push({ from: chunks[i].id, to: chunks[i + 1].id, relationType: 'follows' });
374
366
  relations.push({ from: chunks[i + 1].id, to: chunks[i].id, relationType: 'preceded_by' });
375
367
  }
376
- // Document → index
377
- relations.push({ from: title, to: indexId, relationType: 'has_index' });
378
- relations.push({ from: indexId, to: title, relationType: 'indexes' });
368
+ // Document → index entries
369
+ for (const idx of indexEntities) {
370
+ relations.push({ from: title, to: idx.id, relationType: 'has_index' });
371
+ relations.push({ from: idx.id, to: title, relationType: 'indexes' });
372
+ }
379
373
  // Index → highlighted chunks
380
- for (const { chunk } of highlights) {
381
- relations.push({ from: indexId, to: chunk.id, relationType: 'highlights' });
382
- relations.push({ from: chunk.id, to: indexId, relationType: 'highlighted_by' });
374
+ for (const idx of indexEntities) {
375
+ relations.push({ from: idx.id, to: idx.chunk.id, relationType: 'highlights' });
376
+ relations.push({ from: idx.chunk.id, to: idx.id, relationType: 'highlighted_by' });
383
377
  }
384
378
  return {
385
379
  entities,
@@ -1068,10 +1068,14 @@ describe('MCP Memory Server E2E Tests', () => {
1068
1068
  const docResult = await callTool(client, 'open_nodes', { names: ['test-doc'] });
1069
1069
  expect(docResult.entities.items).toHaveLength(1);
1070
1070
  expect(docResult.entities.items[0].entityType).toBe('Document');
1071
- // Check index entity
1072
- const indexResult = await callTool(client, 'open_nodes', { names: ['test-doc__index'] });
1073
- expect(indexResult.entities.items).toHaveLength(1);
1074
- expect(indexResult.entities.items[0].entityType).toBe('DocumentIndex');
1071
+ // Check index entities (one per key phrase)
1072
+ const indexEntities = await callTool(client, 'get_entities_by_type', { entityType: 'DocumentIndex' });
1073
+ expect(indexEntities.items.length).toBeGreaterThan(0);
1074
+ for (const idx of indexEntities.items) {
1075
+ expect(idx.entityType).toBe('DocumentIndex');
1076
+ expect(idx.observations.length).toBe(1);
1077
+ expect(idx.observations[0].length).toBeLessThanOrEqual(140);
1078
+ }
1075
1079
  // Check TextChunk entities exist via type query
1076
1080
  const chunks = await callTool(client, 'get_entities_by_type', { entityType: 'TextChunk' });
1077
1081
  expect(chunks.items.length).toBeGreaterThan(0);
@@ -1103,8 +1107,11 @@ describe('MCP Memory Server E2E Tests', () => {
1103
1107
  await fs.writeFile(docFile, sentences.join(' '));
1104
1108
  const result = await callTool(client, 'kb_load', { filePath: docFile });
1105
1109
  expect(result.stats.indexHighlights).toBeGreaterThan(0);
1106
- // Open the index and verify it has highlight relations
1107
- const indexResult = await callTool(client, 'open_nodes', { names: ['test-doc__index'] });
1110
+ // Find index entities and verify they have highlight relations
1111
+ const indexEntities = await callTool(client, 'get_entities_by_type', { entityType: 'DocumentIndex' });
1112
+ expect(indexEntities.items.length).toBeGreaterThan(0);
1113
+ const indexNames = indexEntities.items.map(e => e.name);
1114
+ const indexResult = await callTool(client, 'open_nodes', { names: indexNames });
1108
1115
  const highlightRels = indexResult.relations.items.filter(r => r.relationType === 'highlights');
1109
1116
  expect(highlightRels.length).toBeGreaterThan(0);
1110
1117
  });
@@ -1123,7 +1130,7 @@ describe('MCP Memory Server E2E Tests', () => {
1123
1130
  await callTool(client, 'kb_load', { filePath: docFile });
1124
1131
  // Second load with different content but same title — Document entity
1125
1132
  // already exists with entityType 'Document' and no observations,
1126
- // so it gets silently skipped. But the index entity already exists
1133
+ // so it gets silently skipped. But the index entities already exist
1127
1134
  // with different observations, so it should error.
1128
1135
  await fs.writeFile(docFile, 'Completely different content for dedup testing now.');
1129
1136
  await expect(callTool(client, 'kb_load', { filePath: docFile })).rejects.toThrow(/already exists/);
@@ -52,10 +52,20 @@ static int memfile_remap(memfile_t *mf, size_t new_size) {
52
52
  return -1;
53
53
  }
54
54
 
55
+ #ifdef __linux__
55
56
  void *new_base = mremap(mf->mmap_base, mf->mmap_size, new_size, MREMAP_MAYMOVE);
56
57
  if (new_base == MAP_FAILED) {
57
58
  return -1;
58
59
  }
60
+ #else
61
+ /* Portable fallback: unmap old region, map new size */
62
+ munmap(mf->mmap_base, mf->mmap_size);
63
+ void *new_base = mmap(NULL, new_size, PROT_READ | PROT_WRITE,
64
+ MAP_SHARED, mf->fd, 0);
65
+ if (new_base == MAP_FAILED) {
66
+ return -1;
67
+ }
68
+ #endif
59
69
 
60
70
  mf->mmap_base = new_base;
61
71
  mf->mmap_size = new_size;
@@ -239,8 +249,15 @@ int memfile_refresh(memfile_t *mf) {
239
249
  if (actual_size <= mf->mmap_size) return 0; /* No growth detected */
240
250
 
241
251
  /* File grew — remap to cover the new size */
252
+ #ifdef __linux__
242
253
  void *new_base = mremap(mf->mmap_base, mf->mmap_size, actual_size, MREMAP_MAYMOVE);
243
254
  if (new_base == MAP_FAILED) return -1;
255
+ #else
256
+ munmap(mf->mmap_base, mf->mmap_size);
257
+ void *new_base = mmap(NULL, actual_size, PROT_READ | PROT_WRITE,
258
+ MAP_SHARED, mf->fd, 0);
259
+ if (new_base == MAP_FAILED) return -1;
260
+ #endif
244
261
 
245
262
  mf->mmap_base = new_base;
246
263
  mf->mmap_size = actual_size;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@levalicious/server-memory",
3
- "version": "0.0.16",
3
+ "version": "0.0.18",
4
4
  "description": "MCP server for enabling memory for Claude through a knowledge graph",
5
5
  "license": "MIT",
6
6
  "author": "Levalicious",
@@ -26,7 +26,7 @@
26
26
  "test": "NODE_OPTIONS='--experimental-vm-modules' jest"
27
27
  },
28
28
  "dependencies": {
29
- "@modelcontextprotocol/sdk": "1.26.0"
29
+ "@modelcontextprotocol/sdk": "1.27.1"
30
30
  },
31
31
  "devDependencies": {
32
32
  "@types/jest": "^30.0.0",