@levalicious/server-memory 0.0.17 → 0.0.19

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/dist/server.js CHANGED
@@ -953,7 +953,7 @@ export function createServer(memoryFilePath) {
953
953
  sizes: ["any"]
954
954
  }
955
955
  ],
956
- version: "0.0.17",
956
+ version: "0.0.19",
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,17 @@ 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
+ }
353
+ // Index hub — empty structural node linking Document to index entries
354
+ const indexHubId = `${title}__index`;
355
+ entities.push({ name: indexHubId, entityType: 'DocumentIndex', observations: [] });
361
356
  // ─── Assemble relations ─────────────────────────────────────────
362
357
  // Document → chain endpoints
363
358
  if (chunks.length > 0) {
@@ -373,13 +368,18 @@ export function loadDocument(text, title, st, topK = 15) {
373
368
  relations.push({ from: chunks[i].id, to: chunks[i + 1].id, relationType: 'follows' });
374
369
  relations.push({ from: chunks[i + 1].id, to: chunks[i].id, relationType: 'preceded_by' });
375
370
  }
376
- // Document → index
377
- relations.push({ from: title, to: indexId, relationType: 'has_index' });
378
- relations.push({ from: indexId, to: title, relationType: 'indexes' });
379
- // 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' });
371
+ // Document → index hub
372
+ relations.push({ from: title, to: indexHubId, relationType: 'has_index' });
373
+ relations.push({ from: indexHubId, to: title, relationType: 'indexes' });
374
+ // Index hub index entries
375
+ for (const idx of indexEntities) {
376
+ relations.push({ from: indexHubId, to: idx.id, relationType: 'contains' });
377
+ relations.push({ from: idx.id, to: indexHubId, relationType: 'contained_in' });
378
+ }
379
+ // Index entries → highlighted chunks
380
+ for (const idx of indexEntities) {
381
+ relations.push({ from: idx.id, to: idx.chunk.id, relationType: 'highlights' });
382
+ relations.push({ from: idx.chunk.id, to: idx.id, relationType: 'highlighted_by' });
383
383
  }
384
384
  return {
385
385
  entities,
@@ -1068,10 +1068,17 @@ 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 hub (0 observations) + entries (1 observation each)
1072
+ const indexEntities = await callTool(client, 'get_entities_by_type', { entityType: 'DocumentIndex' });
1073
+ expect(indexEntities.items.length).toBeGreaterThan(1);
1074
+ const hub = indexEntities.items.filter(e => e.observations.length === 0);
1075
+ const entries = indexEntities.items.filter(e => e.observations.length > 0);
1076
+ expect(hub).toHaveLength(1);
1077
+ expect(entries.length).toBeGreaterThan(0);
1078
+ for (const entry of entries) {
1079
+ expect(entry.observations.length).toBe(1);
1080
+ expect(entry.observations[0].length).toBeLessThanOrEqual(140);
1081
+ }
1075
1082
  // Check TextChunk entities exist via type query
1076
1083
  const chunks = await callTool(client, 'get_entities_by_type', { entityType: 'TextChunk' });
1077
1084
  expect(chunks.items.length).toBeGreaterThan(0);
@@ -1103,8 +1110,11 @@ describe('MCP Memory Server E2E Tests', () => {
1103
1110
  await fs.writeFile(docFile, sentences.join(' '));
1104
1111
  const result = await callTool(client, 'kb_load', { filePath: docFile });
1105
1112
  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'] });
1113
+ // Find index entities and verify they have highlight relations
1114
+ const indexEntities = await callTool(client, 'get_entities_by_type', { entityType: 'DocumentIndex' });
1115
+ expect(indexEntities.items.length).toBeGreaterThan(0);
1116
+ const indexNames = indexEntities.items.map(e => e.name);
1117
+ const indexResult = await callTool(client, 'open_nodes', { names: indexNames });
1108
1118
  const highlightRels = indexResult.relations.items.filter(r => r.relationType === 'highlights');
1109
1119
  expect(highlightRels.length).toBeGreaterThan(0);
1110
1120
  });
@@ -1123,7 +1133,7 @@ describe('MCP Memory Server E2E Tests', () => {
1123
1133
  await callTool(client, 'kb_load', { filePath: docFile });
1124
1134
  // Second load with different content but same title — Document entity
1125
1135
  // already exists with entityType 'Document' and no observations,
1126
- // so it gets silently skipped. But the index entity already exists
1136
+ // so it gets silently skipped. But the index entities already exist
1127
1137
  // with different observations, so it should error.
1128
1138
  await fs.writeFile(docFile, 'Completely different content for dedup testing now.');
1129
1139
  await expect(callTool(client, 'kb_load', { filePath: docFile })).rejects.toThrow(/already exists/);
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@levalicious/server-memory",
3
- "version": "0.0.17",
3
+ "version": "0.0.19",
4
4
  "description": "MCP server for enabling memory for Claude through a knowledge graph",
5
5
  "license": "MIT",
6
6
  "author": "Levalicious",