@levalicious/server-memory 0.0.9 → 0.0.11
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 +41 -27
- package/dist/server.js +184 -212
- package/dist/tests/memory-server.test.js +442 -88
- package/package.json +5 -4
package/README.md
CHANGED
|
@@ -108,11 +108,12 @@ Example:
|
|
|
108
108
|
|
|
109
109
|
- **search_nodes**
|
|
110
110
|
- Search for nodes using a regex pattern
|
|
111
|
-
- Input:
|
|
112
|
-
|
|
113
|
-
-
|
|
114
|
-
-
|
|
115
|
-
-
|
|
111
|
+
- Input:
|
|
112
|
+
- `query` (string): Regex pattern to search
|
|
113
|
+
- `sortBy` (string, optional): Sort field ("mtime", "obsMtime", or "name")
|
|
114
|
+
- `sortDir` (string, optional): Sort direction ("asc" or "desc")
|
|
115
|
+
- `entityCursor` (number, optional), `relationCursor` (number, optional)
|
|
116
|
+
- Searches across entity names, types, and observation content
|
|
116
117
|
- Returns matching entities and their relations (paginated)
|
|
117
118
|
|
|
118
119
|
- **open_nodes_filtered**
|
|
@@ -132,9 +133,15 @@ Example:
|
|
|
132
133
|
- Silently skips non-existent nodes (paginated)
|
|
133
134
|
|
|
134
135
|
- **get_neighbors**
|
|
135
|
-
- Get neighboring entities connected to a specific entity within a given depth
|
|
136
|
-
- Input:
|
|
137
|
-
|
|
136
|
+
- Get names of neighboring entities connected to a specific entity within a given depth
|
|
137
|
+
- Input:
|
|
138
|
+
- `entityName` (string): The entity to find neighbors for
|
|
139
|
+
- `depth` (number, default: 1): Maximum traversal depth
|
|
140
|
+
- `sortBy` (string, optional): Sort field ("mtime", "obsMtime", or "name")
|
|
141
|
+
- `sortDir` (string, optional): Sort direction ("asc" or "desc")
|
|
142
|
+
- `cursor` (number, optional): Pagination cursor
|
|
143
|
+
- Returns neighbor names with timestamps (paginated)
|
|
144
|
+
- Use `open_nodes` to get full entity data for neighbors
|
|
138
145
|
|
|
139
146
|
- **find_path**
|
|
140
147
|
- Find a path between two entities in the knowledge graph
|
|
@@ -143,7 +150,11 @@ Example:
|
|
|
143
150
|
|
|
144
151
|
- **get_entities_by_type**
|
|
145
152
|
- Get all entities of a specific type
|
|
146
|
-
- Input:
|
|
153
|
+
- Input:
|
|
154
|
+
- `entityType` (string): Type to filter by
|
|
155
|
+
- `sortBy` (string, optional): Sort field ("mtime", "obsMtime", or "name")
|
|
156
|
+
- `sortDir` (string, optional): Sort direction ("asc" or "desc")
|
|
157
|
+
- `cursor` (number, optional)
|
|
147
158
|
- Returns all entities matching the specified type (paginated)
|
|
148
159
|
|
|
149
160
|
- **get_entity_types**
|
|
@@ -163,8 +174,11 @@ Example:
|
|
|
163
174
|
|
|
164
175
|
- **get_orphaned_entities**
|
|
165
176
|
- Get entities that have no relations (orphaned entities)
|
|
166
|
-
- Input:
|
|
167
|
-
|
|
177
|
+
- Input:
|
|
178
|
+
- `strict` (boolean, default: false): If true, returns entities not connected to 'Self' entity
|
|
179
|
+
- `sortBy` (string, optional): Sort field ("mtime", "obsMtime", or "name")
|
|
180
|
+
- `sortDir` (string, optional): Sort direction ("asc" or "desc")
|
|
181
|
+
- `cursor` (number, optional)
|
|
168
182
|
- Returns entities with no connections (paginated)
|
|
169
183
|
|
|
170
184
|
- **validate_graph**
|
|
@@ -172,22 +186,22 @@ Example:
|
|
|
172
186
|
- No input required
|
|
173
187
|
- Returns missing entities referenced in relations and observation limit violations
|
|
174
188
|
|
|
175
|
-
- **
|
|
176
|
-
-
|
|
177
|
-
- Input:
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
-
|
|
185
|
-
-
|
|
186
|
-
|
|
187
|
-
-
|
|
188
|
-
|
|
189
|
-
-
|
|
190
|
-
-
|
|
189
|
+
- **decode_timestamp**
|
|
190
|
+
- Decode a millisecond timestamp to human-readable UTC format
|
|
191
|
+
- Input:
|
|
192
|
+
- `timestamp` (number, optional): Millisecond timestamp to decode. If omitted, returns current time
|
|
193
|
+
- `relative` (boolean, optional): If true, include relative time (e.g., "3 days ago")
|
|
194
|
+
- Returns timestamp, ISO 8601 string, formatted UTC string, and optional relative time
|
|
195
|
+
- Useful for interpreting `mtime`/`obsMtime` values from entities
|
|
196
|
+
|
|
197
|
+
- **random_walk**
|
|
198
|
+
- Perform a random walk from a starting entity, following random relations
|
|
199
|
+
- Input:
|
|
200
|
+
- `start` (string): Name of the entity to start the walk from
|
|
201
|
+
- `depth` (number, default: 3): Number of hops to take
|
|
202
|
+
- `seed` (string, optional): Seed for reproducible walks
|
|
203
|
+
- Returns the terminal entity name and the path taken
|
|
204
|
+
- Useful for serendipitous exploration of the knowledge graph
|
|
191
205
|
|
|
192
206
|
- **sequentialthinking**
|
|
193
207
|
- Record a thought in the knowledge graph
|
package/dist/server.js
CHANGED
|
@@ -14,6 +14,44 @@ const DEFAULT_MEMORY_FILE_PATH = process.env.MEMORY_FILE_PATH
|
|
|
14
14
|
? process.env.MEMORY_FILE_PATH
|
|
15
15
|
: path.join(path.dirname(fileURLToPath(import.meta.url)), process.env.MEMORY_FILE_PATH)
|
|
16
16
|
: defaultMemoryPath;
|
|
17
|
+
/**
|
|
18
|
+
* Sort entities by the specified field and direction.
|
|
19
|
+
* Returns a new array (does not mutate input).
|
|
20
|
+
* If sortBy is undefined, returns the original array (no sorting - preserves insertion order).
|
|
21
|
+
*/
|
|
22
|
+
function sortEntities(entities, sortBy, sortDir) {
|
|
23
|
+
if (!sortBy)
|
|
24
|
+
return entities; // No sorting - preserve current behavior
|
|
25
|
+
const dir = sortDir ?? (sortBy === "name" ? "asc" : "desc");
|
|
26
|
+
const mult = dir === "asc" ? 1 : -1;
|
|
27
|
+
return [...entities].sort((a, b) => {
|
|
28
|
+
if (sortBy === "name") {
|
|
29
|
+
return mult * a.name.localeCompare(b.name);
|
|
30
|
+
}
|
|
31
|
+
// For timestamps, treat undefined as 0 (oldest)
|
|
32
|
+
const aVal = a[sortBy] ?? 0;
|
|
33
|
+
const bVal = b[sortBy] ?? 0;
|
|
34
|
+
return mult * (aVal - bVal);
|
|
35
|
+
});
|
|
36
|
+
}
|
|
37
|
+
/**
|
|
38
|
+
* Sort neighbors by the specified field and direction.
|
|
39
|
+
* If sortBy is undefined, returns the original array (no sorting).
|
|
40
|
+
*/
|
|
41
|
+
function sortNeighbors(neighbors, sortBy, sortDir) {
|
|
42
|
+
if (!sortBy)
|
|
43
|
+
return neighbors;
|
|
44
|
+
const dir = sortDir ?? (sortBy === "name" ? "asc" : "desc");
|
|
45
|
+
const mult = dir === "asc" ? 1 : -1;
|
|
46
|
+
return [...neighbors].sort((a, b) => {
|
|
47
|
+
if (sortBy === "name") {
|
|
48
|
+
return mult * a.name.localeCompare(b.name);
|
|
49
|
+
}
|
|
50
|
+
const aVal = a[sortBy] ?? 0;
|
|
51
|
+
const bVal = b[sortBy] ?? 0;
|
|
52
|
+
return mult * (aVal - bVal);
|
|
53
|
+
});
|
|
54
|
+
}
|
|
17
55
|
export const MAX_CHARS = 2048;
|
|
18
56
|
function paginateItems(items, cursor = 0, maxChars = MAX_CHARS) {
|
|
19
57
|
const result = [];
|
|
@@ -93,8 +131,6 @@ function paginateGraph(graph, entityCursor = 0, relationCursor = 0) {
|
|
|
93
131
|
}
|
|
94
132
|
// The KnowledgeGraphManager class contains all operations to interact with the knowledge graph
|
|
95
133
|
export class KnowledgeGraphManager {
|
|
96
|
-
bclCtr = 0;
|
|
97
|
-
bclTerm = "";
|
|
98
134
|
memoryFilePath;
|
|
99
135
|
constructor(memoryFilePath = DEFAULT_MEMORY_FILE_PATH) {
|
|
100
136
|
this.memoryFilePath = memoryFilePath;
|
|
@@ -254,7 +290,7 @@ export class KnowledgeGraphManager {
|
|
|
254
290
|
});
|
|
255
291
|
}
|
|
256
292
|
// Regex-based search function
|
|
257
|
-
async searchNodes(query) {
|
|
293
|
+
async searchNodes(query, sortBy, sortDir) {
|
|
258
294
|
const graph = await this.loadGraph();
|
|
259
295
|
let regex;
|
|
260
296
|
try {
|
|
@@ -272,7 +308,7 @@ export class KnowledgeGraphManager {
|
|
|
272
308
|
// Filter relations to only include those between filtered entities
|
|
273
309
|
const filteredRelations = graph.relations.filter(r => filteredEntityNames.has(r.from) && filteredEntityNames.has(r.to));
|
|
274
310
|
const filteredGraph = {
|
|
275
|
-
entities: filteredEntities,
|
|
311
|
+
entities: sortEntities(filteredEntities, sortBy, sortDir),
|
|
276
312
|
relations: filteredRelations,
|
|
277
313
|
};
|
|
278
314
|
return filteredGraph;
|
|
@@ -305,25 +341,21 @@ export class KnowledgeGraphManager {
|
|
|
305
341
|
};
|
|
306
342
|
return filteredGraph;
|
|
307
343
|
}
|
|
308
|
-
async getNeighbors(entityName, depth = 1,
|
|
344
|
+
async getNeighbors(entityName, depth = 1, sortBy, sortDir) {
|
|
309
345
|
const graph = await this.loadGraph();
|
|
310
346
|
const visited = new Set();
|
|
311
|
-
const
|
|
312
|
-
const resultRelations = new Map(); // Deduplicate relations
|
|
313
|
-
const relationKey = (r) => `${r.from}|${r.relationType}|${r.to}`;
|
|
347
|
+
const neighborNames = new Set();
|
|
314
348
|
const traverse = (currentName, currentDepth) => {
|
|
315
349
|
if (currentDepth > depth || visited.has(currentName))
|
|
316
350
|
return;
|
|
317
351
|
visited.add(currentName);
|
|
318
|
-
if (withEntities) {
|
|
319
|
-
const entity = graph.entities.find(e => e.name === currentName);
|
|
320
|
-
if (entity) {
|
|
321
|
-
resultEntities.set(currentName, entity);
|
|
322
|
-
}
|
|
323
|
-
}
|
|
324
352
|
// Find all relations involving this entity
|
|
325
353
|
const connectedRelations = graph.relations.filter(r => r.from === currentName || r.to === currentName);
|
|
326
|
-
|
|
354
|
+
// Collect neighbor names
|
|
355
|
+
connectedRelations.forEach(r => {
|
|
356
|
+
const neighborName = r.from === currentName ? r.to : r.from;
|
|
357
|
+
neighborNames.add(neighborName);
|
|
358
|
+
});
|
|
327
359
|
if (currentDepth < depth) {
|
|
328
360
|
// Traverse to connected entities
|
|
329
361
|
connectedRelations.forEach(r => {
|
|
@@ -333,10 +365,19 @@ export class KnowledgeGraphManager {
|
|
|
333
365
|
}
|
|
334
366
|
};
|
|
335
367
|
traverse(entityName, 0);
|
|
336
|
-
|
|
337
|
-
|
|
338
|
-
|
|
339
|
-
|
|
368
|
+
// Remove the starting entity from neighbors (it's not its own neighbor)
|
|
369
|
+
neighborNames.delete(entityName);
|
|
370
|
+
// Build neighbor objects with timestamps
|
|
371
|
+
const entityMap = new Map(graph.entities.map(e => [e.name, e]));
|
|
372
|
+
const neighbors = Array.from(neighborNames).map(name => {
|
|
373
|
+
const entity = entityMap.get(name);
|
|
374
|
+
return {
|
|
375
|
+
name,
|
|
376
|
+
mtime: entity?.mtime,
|
|
377
|
+
obsMtime: entity?.obsMtime,
|
|
378
|
+
};
|
|
379
|
+
});
|
|
380
|
+
return sortNeighbors(neighbors, sortBy, sortDir);
|
|
340
381
|
}
|
|
341
382
|
async findPath(fromEntity, toEntity, maxDepth = 5) {
|
|
342
383
|
const graph = await this.loadGraph();
|
|
@@ -358,9 +399,10 @@ export class KnowledgeGraphManager {
|
|
|
358
399
|
};
|
|
359
400
|
return dfs(fromEntity, toEntity, [], 0) || [];
|
|
360
401
|
}
|
|
361
|
-
async getEntitiesByType(entityType) {
|
|
402
|
+
async getEntitiesByType(entityType, sortBy, sortDir) {
|
|
362
403
|
const graph = await this.loadGraph();
|
|
363
|
-
|
|
404
|
+
const filtered = graph.entities.filter(e => e.entityType === entityType);
|
|
405
|
+
return sortEntities(filtered, sortBy, sortDir);
|
|
364
406
|
}
|
|
365
407
|
async getEntityTypes() {
|
|
366
408
|
const graph = await this.loadGraph();
|
|
@@ -383,7 +425,7 @@ export class KnowledgeGraphManager {
|
|
|
383
425
|
relationTypes: relationTypes.size
|
|
384
426
|
};
|
|
385
427
|
}
|
|
386
|
-
async getOrphanedEntities(strict = false) {
|
|
428
|
+
async getOrphanedEntities(strict = false, sortBy, sortDir) {
|
|
387
429
|
const graph = await this.loadGraph();
|
|
388
430
|
if (!strict) {
|
|
389
431
|
// Simple mode: entities with no relations at all
|
|
@@ -392,7 +434,8 @@ export class KnowledgeGraphManager {
|
|
|
392
434
|
connectedEntityNames.add(r.from);
|
|
393
435
|
connectedEntityNames.add(r.to);
|
|
394
436
|
});
|
|
395
|
-
|
|
437
|
+
const orphans = graph.entities.filter(e => !connectedEntityNames.has(e.name));
|
|
438
|
+
return sortEntities(orphans, sortBy, sortDir);
|
|
396
439
|
}
|
|
397
440
|
// Strict mode: entities not connected to "Self" (directly or indirectly)
|
|
398
441
|
// Build adjacency list (bidirectional)
|
|
@@ -420,7 +463,8 @@ export class KnowledgeGraphManager {
|
|
|
420
463
|
}
|
|
421
464
|
}
|
|
422
465
|
// Return entities not connected to Self (excluding Self itself if it exists)
|
|
423
|
-
|
|
466
|
+
const orphans = graph.entities.filter(e => !connectedToSelf.has(e.name));
|
|
467
|
+
return sortEntities(orphans, sortBy, sortDir);
|
|
424
468
|
}
|
|
425
469
|
async validateGraph() {
|
|
426
470
|
const graph = await this.loadGraph();
|
|
@@ -457,163 +501,96 @@ export class KnowledgeGraphManager {
|
|
|
457
501
|
observationViolations
|
|
458
502
|
};
|
|
459
503
|
}
|
|
460
|
-
|
|
461
|
-
|
|
462
|
-
|
|
463
|
-
|
|
464
|
-
|
|
465
|
-
|
|
466
|
-
|
|
467
|
-
|
|
468
|
-
let
|
|
469
|
-
|
|
470
|
-
|
|
471
|
-
|
|
472
|
-
|
|
473
|
-
|
|
474
|
-
|
|
475
|
-
|
|
476
|
-
if (mode === 0) {
|
|
477
|
-
t1 += b;
|
|
478
|
-
let size = t1.length + t0.length;
|
|
479
|
-
if (size > max_size)
|
|
480
|
-
max_size = size;
|
|
481
|
-
if (t1.slice(-4) === '1100') {
|
|
482
|
-
mode = 1;
|
|
483
|
-
t1 = t1.slice(0, -4);
|
|
484
|
-
}
|
|
485
|
-
else if (t1.slice(-5) === '11101') {
|
|
486
|
-
mode = 3;
|
|
487
|
-
t1 = t1.slice(0, -5);
|
|
488
|
-
}
|
|
489
|
-
}
|
|
490
|
-
else if (mode === 1) {
|
|
491
|
-
t2 += b;
|
|
492
|
-
if (b == '1') {
|
|
493
|
-
ctr += 1;
|
|
494
|
-
}
|
|
495
|
-
else if (b == '0') {
|
|
496
|
-
ctr -= 1;
|
|
497
|
-
t2 += t0[0];
|
|
498
|
-
t0 = t0.slice(1);
|
|
499
|
-
}
|
|
500
|
-
if (ctr === 0) {
|
|
501
|
-
mode = 2;
|
|
502
|
-
ctr = 1;
|
|
503
|
-
}
|
|
504
|
-
}
|
|
505
|
-
else if (mode === 2) {
|
|
506
|
-
if (b == '1') {
|
|
507
|
-
ctr += 1;
|
|
508
|
-
}
|
|
509
|
-
else if (b == '0') {
|
|
510
|
-
ctr -= 1;
|
|
511
|
-
t0 = t0.slice(1);
|
|
512
|
-
}
|
|
513
|
-
if (ctr === 0) {
|
|
514
|
-
t0 = t2 + t0;
|
|
515
|
-
t2 = '';
|
|
516
|
-
mode = 0;
|
|
517
|
-
ctr = 1;
|
|
518
|
-
stepCount += 1;
|
|
519
|
-
}
|
|
520
|
-
}
|
|
521
|
-
else if (mode === 3) {
|
|
522
|
-
t2 += b;
|
|
523
|
-
if (b == '1') {
|
|
524
|
-
ctr += 1;
|
|
525
|
-
}
|
|
526
|
-
else if (b == '0') {
|
|
527
|
-
ctr -= 1;
|
|
528
|
-
t2 += t0[0];
|
|
529
|
-
t0 = t0.slice(1);
|
|
530
|
-
}
|
|
531
|
-
if (ctr === 0) {
|
|
532
|
-
mode = 4;
|
|
533
|
-
ctr = 1;
|
|
534
|
-
}
|
|
504
|
+
async randomWalk(start, depth = 3, seed) {
|
|
505
|
+
const graph = await this.loadGraph();
|
|
506
|
+
// Verify start entity exists
|
|
507
|
+
const startEntity = graph.entities.find(e => e.name === start);
|
|
508
|
+
if (!startEntity) {
|
|
509
|
+
throw new Error(`Start entity not found: ${start}`);
|
|
510
|
+
}
|
|
511
|
+
// Create seeded RNG if seed provided, otherwise use crypto.randomBytes
|
|
512
|
+
let rngState = seed ? this.hashSeed(seed) : null;
|
|
513
|
+
const random = () => {
|
|
514
|
+
if (rngState !== null) {
|
|
515
|
+
// Simple seeded PRNG (xorshift32)
|
|
516
|
+
rngState ^= rngState << 13;
|
|
517
|
+
rngState ^= rngState >>> 17;
|
|
518
|
+
rngState ^= rngState << 5;
|
|
519
|
+
return (rngState >>> 0) / 0xFFFFFFFF;
|
|
535
520
|
}
|
|
536
|
-
else
|
|
537
|
-
|
|
538
|
-
if (b == '1') {
|
|
539
|
-
ctr += 1;
|
|
540
|
-
}
|
|
541
|
-
else if (b == '0') {
|
|
542
|
-
ctr -= 1;
|
|
543
|
-
t3 += t0[0];
|
|
544
|
-
t0 = t0.slice(1);
|
|
545
|
-
}
|
|
546
|
-
if (ctr === 0) {
|
|
547
|
-
mode = 5;
|
|
548
|
-
ctr = 1;
|
|
549
|
-
}
|
|
521
|
+
else {
|
|
522
|
+
return randomBytes(4).readUInt32BE() / 0xFFFFFFFF;
|
|
550
523
|
}
|
|
551
|
-
|
|
552
|
-
|
|
553
|
-
|
|
554
|
-
|
|
555
|
-
|
|
556
|
-
|
|
557
|
-
|
|
558
|
-
|
|
559
|
-
|
|
560
|
-
|
|
561
|
-
|
|
562
|
-
t0 = '11' + t2 + t4 + '1' + t3 + t4 + t0;
|
|
563
|
-
t2 = '';
|
|
564
|
-
t3 = '';
|
|
565
|
-
t4 = '';
|
|
566
|
-
mode = 0;
|
|
567
|
-
ctr = 1;
|
|
568
|
-
stepCount += 1;
|
|
569
|
-
}
|
|
524
|
+
};
|
|
525
|
+
const path = [start];
|
|
526
|
+
let current = start;
|
|
527
|
+
for (let i = 0; i < depth; i++) {
|
|
528
|
+
// Get unique neighbors (both directions)
|
|
529
|
+
const neighbors = new Set();
|
|
530
|
+
for (const rel of graph.relations) {
|
|
531
|
+
if (rel.from === current && rel.to !== current)
|
|
532
|
+
neighbors.add(rel.to);
|
|
533
|
+
if (rel.to === current && rel.from !== current)
|
|
534
|
+
neighbors.add(rel.from);
|
|
570
535
|
}
|
|
536
|
+
// Filter to only existing entities
|
|
537
|
+
const validNeighbors = Array.from(neighbors).filter(n => graph.entities.some(e => e.name === n));
|
|
538
|
+
if (validNeighbors.length === 0)
|
|
539
|
+
break; // Dead end
|
|
540
|
+
// Pick random neighbor (uniform over entities)
|
|
541
|
+
const idx = Math.floor(random() * validNeighbors.length);
|
|
542
|
+
current = validNeighbors[idx];
|
|
543
|
+
path.push(current);
|
|
571
544
|
}
|
|
572
|
-
|
|
573
|
-
return {
|
|
574
|
-
result: t1,
|
|
575
|
-
info: `${stepCount} steps, max size ${max_size}`,
|
|
576
|
-
halted,
|
|
577
|
-
errored: halted && mode != 0,
|
|
578
|
-
};
|
|
545
|
+
return { entity: current, path };
|
|
579
546
|
}
|
|
580
|
-
|
|
581
|
-
|
|
582
|
-
|
|
583
|
-
|
|
584
|
-
|
|
585
|
-
|
|
586
|
-
|
|
587
|
-
if (!validTerms.includes(term)) {
|
|
588
|
-
throw new Error(`Invalid BCL term: ${term}\nExpected one of: ${validTerms.join(", ")}`);
|
|
589
|
-
}
|
|
590
|
-
let processedTerm = 0;
|
|
591
|
-
if (term === "00" || term === "K")
|
|
592
|
-
processedTerm = 1;
|
|
593
|
-
else if (term === "01" || term === "S")
|
|
594
|
-
processedTerm = 2;
|
|
595
|
-
this.bclTerm += termset[processedTerm];
|
|
596
|
-
if (processedTerm === 0) {
|
|
597
|
-
if (this.bclCtr === 0)
|
|
598
|
-
this.bclCtr += 1;
|
|
599
|
-
this.bclCtr += 1;
|
|
600
|
-
}
|
|
601
|
-
else {
|
|
602
|
-
this.bclCtr -= 1;
|
|
603
|
-
}
|
|
604
|
-
if (this.bclCtr <= 0) {
|
|
605
|
-
const constructedProgram = this.bclTerm;
|
|
606
|
-
this.bclCtr = 0;
|
|
607
|
-
this.bclTerm = "";
|
|
608
|
-
return `Constructed Program: ${constructedProgram}`;
|
|
609
|
-
}
|
|
610
|
-
else {
|
|
611
|
-
return `Need ${this.bclCtr} more term(s) to complete the program.`;
|
|
547
|
+
hashSeed(seed) {
|
|
548
|
+
// Simple string hash to 32-bit integer
|
|
549
|
+
let hash = 0;
|
|
550
|
+
for (let i = 0; i < seed.length; i++) {
|
|
551
|
+
const char = seed.charCodeAt(i);
|
|
552
|
+
hash = ((hash << 5) - hash) + char;
|
|
553
|
+
hash = hash & hash; // Convert to 32-bit integer
|
|
612
554
|
}
|
|
555
|
+
return hash || 1; // Ensure non-zero for xorshift
|
|
613
556
|
}
|
|
614
|
-
|
|
615
|
-
|
|
616
|
-
|
|
557
|
+
decodeTimestamp(timestamp, relative = false) {
|
|
558
|
+
const ts = timestamp ?? Date.now();
|
|
559
|
+
const date = new Date(ts);
|
|
560
|
+
const result = {
|
|
561
|
+
timestamp: ts,
|
|
562
|
+
iso8601: date.toISOString(),
|
|
563
|
+
formatted: date.toUTCString(),
|
|
564
|
+
};
|
|
565
|
+
if (relative) {
|
|
566
|
+
const now = Date.now();
|
|
567
|
+
const diffMs = now - ts;
|
|
568
|
+
const diffSec = Math.abs(diffMs) / 1000;
|
|
569
|
+
const diffMin = diffSec / 60;
|
|
570
|
+
const diffHour = diffMin / 60;
|
|
571
|
+
const diffDay = diffHour / 24;
|
|
572
|
+
let relStr;
|
|
573
|
+
if (diffSec < 60) {
|
|
574
|
+
relStr = `${Math.floor(diffSec)} seconds`;
|
|
575
|
+
}
|
|
576
|
+
else if (diffMin < 60) {
|
|
577
|
+
relStr = `${Math.floor(diffMin)} minutes`;
|
|
578
|
+
}
|
|
579
|
+
else if (diffHour < 24) {
|
|
580
|
+
relStr = `${Math.floor(diffHour)} hours`;
|
|
581
|
+
}
|
|
582
|
+
else if (diffDay < 30) {
|
|
583
|
+
relStr = `${Math.floor(diffDay)} days`;
|
|
584
|
+
}
|
|
585
|
+
else if (diffDay < 365) {
|
|
586
|
+
relStr = `${Math.floor(diffDay / 30)} months`;
|
|
587
|
+
}
|
|
588
|
+
else {
|
|
589
|
+
relStr = `${Math.floor(diffDay / 365)} years`;
|
|
590
|
+
}
|
|
591
|
+
result.relative = diffMs >= 0 ? `${relStr} ago` : `in ${relStr}`;
|
|
592
|
+
}
|
|
593
|
+
return result;
|
|
617
594
|
}
|
|
618
595
|
async addThought(observations, previousCtxId) {
|
|
619
596
|
return this.withLock(async () => {
|
|
@@ -668,7 +645,7 @@ export function createServer(memoryFilePath) {
|
|
|
668
645
|
sizes: ["any"]
|
|
669
646
|
}
|
|
670
647
|
],
|
|
671
|
-
version: "0.0.
|
|
648
|
+
version: "0.0.11",
|
|
672
649
|
}, {
|
|
673
650
|
capabilities: {
|
|
674
651
|
tools: {},
|
|
@@ -821,6 +798,8 @@ export function createServer(memoryFilePath) {
|
|
|
821
798
|
type: "object",
|
|
822
799
|
properties: {
|
|
823
800
|
query: { type: "string", description: "Regex pattern to match against entity names, types, and observations." },
|
|
801
|
+
sortBy: { type: "string", enum: ["mtime", "obsMtime", "name"], description: "Sort field for entities. Omit for insertion order." },
|
|
802
|
+
sortDir: { type: "string", enum: ["asc", "desc"], description: "Sort direction. Default: desc for timestamps, asc for name." },
|
|
824
803
|
entityCursor: { type: "number", description: "Cursor for entity pagination (from previous response's nextCursor)" },
|
|
825
804
|
relationCursor: { type: "number", description: "Cursor for relation pagination" },
|
|
826
805
|
},
|
|
@@ -863,15 +842,15 @@ export function createServer(memoryFilePath) {
|
|
|
863
842
|
},
|
|
864
843
|
{
|
|
865
844
|
name: "get_neighbors",
|
|
866
|
-
description: "Get neighboring entities connected to a specific entity within a given depth. Results are paginated (max 512 chars).",
|
|
845
|
+
description: "Get names of neighboring entities connected to a specific entity within a given depth. Returns neighbor names with timestamps for sorting. Use open_nodes to get full entity data. Results are paginated (max 512 chars).",
|
|
867
846
|
inputSchema: {
|
|
868
847
|
type: "object",
|
|
869
848
|
properties: {
|
|
870
849
|
entityName: { type: "string", description: "The name of the entity to find neighbors for" },
|
|
871
|
-
depth: { type: "number", description: "Maximum depth to traverse (default:
|
|
872
|
-
|
|
873
|
-
|
|
874
|
-
|
|
850
|
+
depth: { type: "number", description: "Maximum depth to traverse (default: 1)", default: 1 },
|
|
851
|
+
sortBy: { type: "string", enum: ["mtime", "obsMtime", "name"], description: "Sort field for neighbors. Omit for arbitrary order." },
|
|
852
|
+
sortDir: { type: "string", enum: ["asc", "desc"], description: "Sort direction. Default: desc for timestamps, asc for name." },
|
|
853
|
+
cursor: { type: "number", description: "Cursor for pagination" },
|
|
875
854
|
},
|
|
876
855
|
required: ["entityName"],
|
|
877
856
|
},
|
|
@@ -897,6 +876,8 @@ export function createServer(memoryFilePath) {
|
|
|
897
876
|
type: "object",
|
|
898
877
|
properties: {
|
|
899
878
|
entityType: { type: "string", description: "The type of entities to retrieve" },
|
|
879
|
+
sortBy: { type: "string", enum: ["mtime", "obsMtime", "name"], description: "Sort field for entities. Omit for insertion order." },
|
|
880
|
+
sortDir: { type: "string", enum: ["asc", "desc"], description: "Sort direction. Default: desc for timestamps, asc for name." },
|
|
900
881
|
cursor: { type: "number", description: "Cursor for pagination" },
|
|
901
882
|
},
|
|
902
883
|
required: ["entityType"],
|
|
@@ -933,6 +914,8 @@ export function createServer(memoryFilePath) {
|
|
|
933
914
|
type: "object",
|
|
934
915
|
properties: {
|
|
935
916
|
strict: { type: "boolean", description: "If true, returns entities not connected to 'Self' (directly or indirectly). Default: false" },
|
|
917
|
+
sortBy: { type: "string", enum: ["mtime", "obsMtime", "name"], description: "Sort field for entities. Omit for insertion order." },
|
|
918
|
+
sortDir: { type: "string", enum: ["asc", "desc"], description: "Sort direction. Default: desc for timestamps, asc for name." },
|
|
936
919
|
cursor: { type: "number", description: "Cursor for pagination" },
|
|
937
920
|
},
|
|
938
921
|
},
|
|
@@ -946,37 +929,27 @@ export function createServer(memoryFilePath) {
|
|
|
946
929
|
},
|
|
947
930
|
},
|
|
948
931
|
{
|
|
949
|
-
name: "
|
|
950
|
-
description: "
|
|
932
|
+
name: "decode_timestamp",
|
|
933
|
+
description: "Decode a millisecond timestamp to human-readable UTC format. If no timestamp provided, returns the current time. Use this to interpret mtime/obsMtime values from entities.",
|
|
951
934
|
inputSchema: {
|
|
952
935
|
type: "object",
|
|
953
936
|
properties: {
|
|
954
|
-
|
|
955
|
-
|
|
937
|
+
timestamp: { type: "number", description: "Millisecond timestamp to decode. If omitted, returns current time." },
|
|
938
|
+
relative: { type: "boolean", description: "If true, include relative time (e.g., '3 days ago'). Default: false" },
|
|
956
939
|
},
|
|
957
|
-
required: ["program"],
|
|
958
940
|
},
|
|
959
941
|
},
|
|
960
942
|
{
|
|
961
|
-
name: "
|
|
962
|
-
description: "
|
|
943
|
+
name: "random_walk",
|
|
944
|
+
description: "Perform a random walk from a starting entity, following random relations. Returns the terminal entity name and the path taken. Useful for serendipitous exploration of the knowledge graph.",
|
|
963
945
|
inputSchema: {
|
|
964
946
|
type: "object",
|
|
965
947
|
properties: {
|
|
966
|
-
|
|
967
|
-
|
|
968
|
-
|
|
969
|
-
},
|
|
948
|
+
start: { type: "string", description: "Name of the entity to start the walk from." },
|
|
949
|
+
depth: { type: "number", description: "Number of steps to take. Default: 3" },
|
|
950
|
+
seed: { type: "string", description: "Optional seed for reproducible walks." },
|
|
970
951
|
},
|
|
971
|
-
required: ["
|
|
972
|
-
},
|
|
973
|
-
},
|
|
974
|
-
{
|
|
975
|
-
name: "clear_bcl_term",
|
|
976
|
-
description: "Clear the current BCL term being constructed and reset the constructor state",
|
|
977
|
-
inputSchema: {
|
|
978
|
-
type: "object",
|
|
979
|
-
properties: {},
|
|
952
|
+
required: ["start"],
|
|
980
953
|
},
|
|
981
954
|
},
|
|
982
955
|
{
|
|
@@ -1026,7 +999,7 @@ Use this to build chains of reasoning that persist in the graph. Each thought ca
|
|
|
1026
999
|
await knowledgeGraphManager.deleteRelations(args.relations);
|
|
1027
1000
|
return { content: [{ type: "text", text: "Relations deleted successfully" }] };
|
|
1028
1001
|
case "search_nodes": {
|
|
1029
|
-
const graph = await knowledgeGraphManager.searchNodes(args.query);
|
|
1002
|
+
const graph = await knowledgeGraphManager.searchNodes(args.query, args.sortBy, args.sortDir);
|
|
1030
1003
|
return { content: [{ type: "text", text: JSON.stringify(paginateGraph(graph, args.entityCursor ?? 0, args.relationCursor ?? 0)) }] };
|
|
1031
1004
|
}
|
|
1032
1005
|
case "open_nodes_filtered": {
|
|
@@ -1038,15 +1011,15 @@ Use this to build chains of reasoning that persist in the graph. Each thought ca
|
|
|
1038
1011
|
return { content: [{ type: "text", text: JSON.stringify(paginateGraph(graph, args.entityCursor ?? 0, args.relationCursor ?? 0)) }] };
|
|
1039
1012
|
}
|
|
1040
1013
|
case "get_neighbors": {
|
|
1041
|
-
const
|
|
1042
|
-
return { content: [{ type: "text", text: JSON.stringify(
|
|
1014
|
+
const neighbors = await knowledgeGraphManager.getNeighbors(args.entityName, args.depth ?? 1, args.sortBy, args.sortDir);
|
|
1015
|
+
return { content: [{ type: "text", text: JSON.stringify(paginateItems(neighbors, args.cursor ?? 0)) }] };
|
|
1043
1016
|
}
|
|
1044
1017
|
case "find_path": {
|
|
1045
1018
|
const path = await knowledgeGraphManager.findPath(args.fromEntity, args.toEntity, args.maxDepth);
|
|
1046
1019
|
return { content: [{ type: "text", text: JSON.stringify(paginateItems(path, args.cursor ?? 0)) }] };
|
|
1047
1020
|
}
|
|
1048
1021
|
case "get_entities_by_type": {
|
|
1049
|
-
const entities = await knowledgeGraphManager.getEntitiesByType(args.entityType);
|
|
1022
|
+
const entities = await knowledgeGraphManager.getEntitiesByType(args.entityType, args.sortBy, args.sortDir);
|
|
1050
1023
|
return { content: [{ type: "text", text: JSON.stringify(paginateItems(entities, args.cursor ?? 0)) }] };
|
|
1051
1024
|
}
|
|
1052
1025
|
case "get_entity_types":
|
|
@@ -1056,18 +1029,17 @@ Use this to build chains of reasoning that persist in the graph. Each thought ca
|
|
|
1056
1029
|
case "get_stats":
|
|
1057
1030
|
return { content: [{ type: "text", text: JSON.stringify(await knowledgeGraphManager.getStats(), null, 2) }] };
|
|
1058
1031
|
case "get_orphaned_entities": {
|
|
1059
|
-
const entities = await knowledgeGraphManager.getOrphanedEntities(args.strict ?? false);
|
|
1032
|
+
const entities = await knowledgeGraphManager.getOrphanedEntities(args.strict ?? false, args.sortBy, args.sortDir);
|
|
1060
1033
|
return { content: [{ type: "text", text: JSON.stringify(paginateItems(entities, args.cursor ?? 0)) }] };
|
|
1061
1034
|
}
|
|
1062
1035
|
case "validate_graph":
|
|
1063
1036
|
return { content: [{ type: "text", text: JSON.stringify(await knowledgeGraphManager.validateGraph(), null, 2) }] };
|
|
1064
|
-
case "
|
|
1065
|
-
return { content: [{ type: "text", text: JSON.stringify(
|
|
1066
|
-
case "
|
|
1067
|
-
|
|
1068
|
-
|
|
1069
|
-
|
|
1070
|
-
return { content: [{ type: "text", text: "BCL term constructor cleared successfully" }] };
|
|
1037
|
+
case "decode_timestamp":
|
|
1038
|
+
return { content: [{ type: "text", text: JSON.stringify(knowledgeGraphManager.decodeTimestamp(args.timestamp, args.relative ?? false)) }] };
|
|
1039
|
+
case "random_walk": {
|
|
1040
|
+
const result = await knowledgeGraphManager.randomWalk(args.start, args.depth ?? 3, args.seed);
|
|
1041
|
+
return { content: [{ type: "text", text: JSON.stringify(result) }] };
|
|
1042
|
+
}
|
|
1071
1043
|
case "sequentialthinking": {
|
|
1072
1044
|
const result = await knowledgeGraphManager.addThought(args.observations, args.previousCtxId);
|
|
1073
1045
|
return { content: [{ type: "text", text: JSON.stringify(result) }] };
|
|
@@ -283,64 +283,43 @@ describe('MCP Memory Server E2E Tests', () => {
|
|
|
283
283
|
]
|
|
284
284
|
});
|
|
285
285
|
});
|
|
286
|
-
it('should get neighbors at depth 0
|
|
286
|
+
it('should get immediate neighbors at depth 0', async () => {
|
|
287
287
|
const result = await callTool(client, 'get_neighbors', {
|
|
288
288
|
entityName: 'Root',
|
|
289
289
|
depth: 0
|
|
290
290
|
});
|
|
291
|
-
|
|
292
|
-
expect(result.
|
|
291
|
+
// depth 0 returns immediate neighbors only
|
|
292
|
+
expect(result.items).toHaveLength(2);
|
|
293
|
+
const names = result.items.map(n => n.name);
|
|
294
|
+
expect(names).toContain('Child1');
|
|
295
|
+
expect(names).toContain('Child2');
|
|
293
296
|
});
|
|
294
|
-
it('should get neighbors
|
|
295
|
-
|
|
296
|
-
|
|
297
|
-
|
|
298
|
-
|
|
299
|
-
|
|
300
|
-
|
|
301
|
-
|
|
302
|
-
|
|
303
|
-
|
|
304
|
-
|
|
305
|
-
allEntities.push(...result.entities.items);
|
|
306
|
-
entityCursor = result.entities.nextCursor;
|
|
307
|
-
}
|
|
308
|
-
expect(allEntities).toHaveLength(3);
|
|
309
|
-
expect(allEntities.map(e => e.name)).toContain('Root');
|
|
310
|
-
expect(allEntities.map(e => e.name)).toContain('Child1');
|
|
311
|
-
expect(allEntities.map(e => e.name)).toContain('Child2');
|
|
297
|
+
it('should get neighbors at depth 1 (includes neighbors of neighbors)', async () => {
|
|
298
|
+
const result = await callTool(client, 'get_neighbors', {
|
|
299
|
+
entityName: 'Root',
|
|
300
|
+
depth: 1
|
|
301
|
+
});
|
|
302
|
+
// depth 1: Child1, Child2 (immediate) + Grandchild (neighbor of Child1)
|
|
303
|
+
expect(result.items).toHaveLength(3);
|
|
304
|
+
const names = result.items.map(n => n.name);
|
|
305
|
+
expect(names).toContain('Child1');
|
|
306
|
+
expect(names).toContain('Child2');
|
|
307
|
+
expect(names).toContain('Grandchild');
|
|
312
308
|
});
|
|
313
309
|
it('should traverse to specified depth', async () => {
|
|
314
|
-
|
|
315
|
-
|
|
316
|
-
|
|
317
|
-
|
|
318
|
-
|
|
319
|
-
//
|
|
320
|
-
|
|
321
|
-
|
|
322
|
-
|
|
323
|
-
|
|
324
|
-
|
|
325
|
-
// Pass large cursor to skip when done, 0 to fetch
|
|
326
|
-
entityCursor: entityCursor !== null ? entityCursor : 999999,
|
|
327
|
-
relationCursor: relationCursor !== null ? relationCursor : 999999
|
|
328
|
-
});
|
|
329
|
-
// Collect entities if we still need them
|
|
330
|
-
if (entityCursor !== null) {
|
|
331
|
-
allEntities.push(...result.entities.items);
|
|
332
|
-
entityCursor = result.entities.nextCursor;
|
|
333
|
-
}
|
|
334
|
-
// Collect relations if we still need them
|
|
335
|
-
if (relationCursor !== null) {
|
|
336
|
-
allRelations.push(...result.relations.items);
|
|
337
|
-
relationCursor = result.relations.nextCursor;
|
|
338
|
-
}
|
|
339
|
-
}
|
|
340
|
-
expect(allEntities).toHaveLength(4); // All nodes
|
|
341
|
-
expect(allRelations).toHaveLength(3); // All relations
|
|
310
|
+
const result = await callTool(client, 'get_neighbors', {
|
|
311
|
+
entityName: 'Root',
|
|
312
|
+
depth: 2
|
|
313
|
+
});
|
|
314
|
+
// At depth 2 from Root: same as depth 1 since graph is small
|
|
315
|
+
// Child1, Child2, Grandchild (Root is excluded as starting point)
|
|
316
|
+
expect(result.items).toHaveLength(3);
|
|
317
|
+
const names = result.items.map(n => n.name);
|
|
318
|
+
expect(names).toContain('Child1');
|
|
319
|
+
expect(names).toContain('Child2');
|
|
320
|
+
expect(names).toContain('Grandchild');
|
|
342
321
|
});
|
|
343
|
-
it('should deduplicate
|
|
322
|
+
it('should deduplicate neighbors in traversal', async () => {
|
|
344
323
|
// Add a bidirectional relation
|
|
345
324
|
await callTool(client, 'create_relations', {
|
|
346
325
|
relations: [{ from: 'Child2', to: 'Root', relationType: 'child_of' }]
|
|
@@ -349,10 +328,10 @@ describe('MCP Memory Server E2E Tests', () => {
|
|
|
349
328
|
entityName: 'Root',
|
|
350
329
|
depth: 1
|
|
351
330
|
});
|
|
352
|
-
// Each
|
|
353
|
-
const
|
|
354
|
-
const
|
|
355
|
-
expect(
|
|
331
|
+
// Each neighbor should appear only once
|
|
332
|
+
const names = result.items.map(n => n.name);
|
|
333
|
+
const uniqueNames = [...new Set(names)];
|
|
334
|
+
expect(names.length).toBe(uniqueNames.length);
|
|
356
335
|
});
|
|
357
336
|
it('should find path between entities', async () => {
|
|
358
337
|
const result = await callTool(client, 'find_path', {
|
|
@@ -478,35 +457,6 @@ describe('MCP Memory Server E2E Tests', () => {
|
|
|
478
457
|
expect(result.missingEntities).toContain('Missing');
|
|
479
458
|
});
|
|
480
459
|
});
|
|
481
|
-
describe('BCL Evaluator', () => {
|
|
482
|
-
it('should evaluate K combinator (identity for first arg)', async () => {
|
|
483
|
-
// K = 00, evaluating K applied to two args should return first
|
|
484
|
-
// This is a simplified test - BCL semantics are complex
|
|
485
|
-
const result = await callTool(client, 'evaluate_bcl', {
|
|
486
|
-
program: '00',
|
|
487
|
-
maxSteps: 100
|
|
488
|
-
});
|
|
489
|
-
expect(result.halted).toBe(true);
|
|
490
|
-
});
|
|
491
|
-
it('should construct BCL terms incrementally', async () => {
|
|
492
|
-
let result = await callTool(client, 'add_bcl_term', { term: 'App' });
|
|
493
|
-
expect(result).toContain('more term');
|
|
494
|
-
result = await callTool(client, 'add_bcl_term', { term: 'K' });
|
|
495
|
-
expect(result).toContain('more term');
|
|
496
|
-
result = await callTool(client, 'add_bcl_term', { term: 'S' });
|
|
497
|
-
expect(result).toContain('Constructed Program');
|
|
498
|
-
});
|
|
499
|
-
it('should clear BCL constructor state', async () => {
|
|
500
|
-
await callTool(client, 'add_bcl_term', { term: 'App' });
|
|
501
|
-
await callTool(client, 'clear_bcl_term', {});
|
|
502
|
-
// After clearing, we should need to start fresh
|
|
503
|
-
const result = await callTool(client, 'add_bcl_term', { term: 'K' });
|
|
504
|
-
expect(result).toContain('Constructed Program');
|
|
505
|
-
});
|
|
506
|
-
it('should reject invalid BCL terms', async () => {
|
|
507
|
-
await expect(callTool(client, 'add_bcl_term', { term: 'invalid' })).rejects.toThrow(/Invalid BCL term/);
|
|
508
|
-
});
|
|
509
|
-
});
|
|
510
460
|
describe('Sequential Thinking', () => {
|
|
511
461
|
it('should create a thought and return ctxId', async () => {
|
|
512
462
|
const result = await callTool(client, 'sequentialthinking', {
|
|
@@ -524,13 +474,13 @@ describe('MCP Memory Server E2E Tests', () => {
|
|
|
524
474
|
previousCtxId: first.ctxId,
|
|
525
475
|
observations: ['Following up']
|
|
526
476
|
});
|
|
527
|
-
// Verify the chain via
|
|
477
|
+
// Verify the chain via neighbors
|
|
528
478
|
const neighbors = await callTool(client, 'get_neighbors', {
|
|
529
479
|
entityName: first.ctxId,
|
|
530
480
|
depth: 1
|
|
531
481
|
});
|
|
532
|
-
//
|
|
533
|
-
expect(neighbors.
|
|
482
|
+
// Second thought should be a neighbor of first
|
|
483
|
+
expect(neighbors.items.some(n => n.name === second.ctxId)).toBe(true);
|
|
534
484
|
});
|
|
535
485
|
it('should ignore invalid previousCtxId gracefully', async () => {
|
|
536
486
|
const result = await callTool(client, 'sequentialthinking', {
|
|
@@ -538,12 +488,12 @@ describe('MCP Memory Server E2E Tests', () => {
|
|
|
538
488
|
observations: ['Orphaned thought']
|
|
539
489
|
});
|
|
540
490
|
expect(result.ctxId).toMatch(/^[0-9a-f]{24}$/);
|
|
541
|
-
// Verify no relations were created
|
|
491
|
+
// Verify no neighbors (no valid relations were created)
|
|
542
492
|
const neighbors = await callTool(client, 'get_neighbors', {
|
|
543
493
|
entityName: result.ctxId,
|
|
544
494
|
depth: 1
|
|
545
495
|
});
|
|
546
|
-
expect(neighbors.
|
|
496
|
+
expect(neighbors.items).toHaveLength(0);
|
|
547
497
|
});
|
|
548
498
|
it('should enforce observation limits on thoughts', async () => {
|
|
549
499
|
await expect(callTool(client, 'sequentialthinking', {
|
|
@@ -562,4 +512,408 @@ describe('MCP Memory Server E2E Tests', () => {
|
|
|
562
512
|
expect(thought.obsMtime).toBeDefined();
|
|
563
513
|
});
|
|
564
514
|
});
|
|
515
|
+
describe('Timestamp Decoding', () => {
|
|
516
|
+
it('should decode a specific timestamp', async () => {
|
|
517
|
+
const result = await callTool(client, 'decode_timestamp', {
|
|
518
|
+
timestamp: 1735200000000 // Known timestamp
|
|
519
|
+
});
|
|
520
|
+
expect(result.timestamp).toBe(1735200000000);
|
|
521
|
+
expect(result.iso8601).toBe('2024-12-26T08:00:00.000Z');
|
|
522
|
+
expect(result.formatted).toContain('2024');
|
|
523
|
+
});
|
|
524
|
+
it('should return current time when no timestamp provided', async () => {
|
|
525
|
+
const before = Date.now();
|
|
526
|
+
const result = await callTool(client, 'decode_timestamp', {});
|
|
527
|
+
const after = Date.now();
|
|
528
|
+
expect(result.timestamp).toBeGreaterThanOrEqual(before);
|
|
529
|
+
expect(result.timestamp).toBeLessThanOrEqual(after);
|
|
530
|
+
});
|
|
531
|
+
it('should include relative time when requested', async () => {
|
|
532
|
+
const oneHourAgo = Date.now() - 3600000;
|
|
533
|
+
const result = await callTool(client, 'decode_timestamp', {
|
|
534
|
+
timestamp: oneHourAgo,
|
|
535
|
+
relative: true
|
|
536
|
+
});
|
|
537
|
+
expect(result.relative).toContain('hour');
|
|
538
|
+
expect(result.relative).toContain('ago');
|
|
539
|
+
});
|
|
540
|
+
it('should handle future timestamps', async () => {
|
|
541
|
+
const oneHourFromNow = Date.now() + 3600000;
|
|
542
|
+
const result = await callTool(client, 'decode_timestamp', {
|
|
543
|
+
timestamp: oneHourFromNow,
|
|
544
|
+
relative: true
|
|
545
|
+
});
|
|
546
|
+
expect(result.relative).toContain('in');
|
|
547
|
+
});
|
|
548
|
+
});
|
|
549
|
+
describe('Random Walk', () => {
|
|
550
|
+
beforeEach(async () => {
|
|
551
|
+
// Create a small graph for walking
|
|
552
|
+
await callTool(client, 'create_entities', {
|
|
553
|
+
entities: [
|
|
554
|
+
{ name: 'Center', entityType: 'Node', observations: ['Hub node'] },
|
|
555
|
+
{ name: 'North', entityType: 'Node', observations: ['North node'] },
|
|
556
|
+
{ name: 'South', entityType: 'Node', observations: ['South node'] },
|
|
557
|
+
{ name: 'East', entityType: 'Node', observations: ['East node'] },
|
|
558
|
+
{ name: 'Isolated', entityType: 'Node', observations: ['No connections'] },
|
|
559
|
+
]
|
|
560
|
+
});
|
|
561
|
+
await callTool(client, 'create_relations', {
|
|
562
|
+
relations: [
|
|
563
|
+
{ from: 'Center', to: 'North', relationType: 'connects' },
|
|
564
|
+
{ from: 'Center', to: 'South', relationType: 'connects' },
|
|
565
|
+
{ from: 'Center', to: 'East', relationType: 'connects' },
|
|
566
|
+
{ from: 'North', to: 'South', relationType: 'connects' },
|
|
567
|
+
]
|
|
568
|
+
});
|
|
569
|
+
});
|
|
570
|
+
it('should perform a walk and return path', async () => {
|
|
571
|
+
const result = await callTool(client, 'random_walk', {
|
|
572
|
+
start: 'Center',
|
|
573
|
+
depth: 2
|
|
574
|
+
});
|
|
575
|
+
expect(result.path[0]).toBe('Center');
|
|
576
|
+
expect(result.path.length).toBeGreaterThanOrEqual(1);
|
|
577
|
+
expect(result.path.length).toBeLessThanOrEqual(3);
|
|
578
|
+
expect(result.entity).toBe(result.path[result.path.length - 1]);
|
|
579
|
+
});
|
|
580
|
+
it('should terminate early at dead ends', async () => {
|
|
581
|
+
const result = await callTool(client, 'random_walk', {
|
|
582
|
+
start: 'Isolated',
|
|
583
|
+
depth: 5
|
|
584
|
+
});
|
|
585
|
+
expect(result.path).toEqual(['Isolated']);
|
|
586
|
+
expect(result.entity).toBe('Isolated');
|
|
587
|
+
});
|
|
588
|
+
it('should produce reproducible walks with same seed', async () => {
|
|
589
|
+
const result1 = await callTool(client, 'random_walk', {
|
|
590
|
+
start: 'Center',
|
|
591
|
+
depth: 3,
|
|
592
|
+
seed: 'test-seed-123'
|
|
593
|
+
});
|
|
594
|
+
const result2 = await callTool(client, 'random_walk', {
|
|
595
|
+
start: 'Center',
|
|
596
|
+
depth: 3,
|
|
597
|
+
seed: 'test-seed-123'
|
|
598
|
+
});
|
|
599
|
+
expect(result1.path).toEqual(result2.path);
|
|
600
|
+
});
|
|
601
|
+
it('should throw on non-existent start entity', async () => {
|
|
602
|
+
await expect(callTool(client, 'random_walk', { start: 'NonExistent', depth: 2 })).rejects.toThrow(/not found/);
|
|
603
|
+
});
|
|
604
|
+
});
|
|
605
|
+
describe('Sorting', () => {
|
|
606
|
+
// Helper to create entities with controlled timestamps
|
|
607
|
+
async function createEntitiesWithDelay(entities) {
|
|
608
|
+
for (const entity of entities) {
|
|
609
|
+
await callTool(client, 'create_entities', { entities: [entity] });
|
|
610
|
+
// Small delay to ensure distinct mtime values
|
|
611
|
+
await new Promise(resolve => setTimeout(resolve, 10));
|
|
612
|
+
}
|
|
613
|
+
}
|
|
614
|
+
describe('search_nodes sorting', () => {
|
|
615
|
+
beforeEach(async () => {
|
|
616
|
+
// Create entities with distinct timestamps
|
|
617
|
+
await createEntitiesWithDelay([
|
|
618
|
+
{ name: 'Alpha', entityType: 'Letter', observations: ['First letter'] },
|
|
619
|
+
{ name: 'Beta', entityType: 'Letter', observations: ['Second letter'] },
|
|
620
|
+
{ name: 'Gamma', entityType: 'Letter', observations: ['Third letter'] }
|
|
621
|
+
]);
|
|
622
|
+
});
|
|
623
|
+
it('should preserve insertion order when sortBy is omitted', async () => {
|
|
624
|
+
const result = await callTool(client, 'search_nodes', {
|
|
625
|
+
query: 'Letter'
|
|
626
|
+
});
|
|
627
|
+
const names = result.entities.items.map(e => e.name);
|
|
628
|
+
expect(names).toEqual(['Alpha', 'Beta', 'Gamma']);
|
|
629
|
+
});
|
|
630
|
+
it('should sort by name ascending', async () => {
|
|
631
|
+
const result = await callTool(client, 'search_nodes', {
|
|
632
|
+
query: 'Letter',
|
|
633
|
+
sortBy: 'name',
|
|
634
|
+
sortDir: 'asc'
|
|
635
|
+
});
|
|
636
|
+
const names = result.entities.items.map(e => e.name);
|
|
637
|
+
expect(names).toEqual(['Alpha', 'Beta', 'Gamma']);
|
|
638
|
+
});
|
|
639
|
+
it('should sort by name descending', async () => {
|
|
640
|
+
const result = await callTool(client, 'search_nodes', {
|
|
641
|
+
query: 'Letter',
|
|
642
|
+
sortBy: 'name',
|
|
643
|
+
sortDir: 'desc'
|
|
644
|
+
});
|
|
645
|
+
const names = result.entities.items.map(e => e.name);
|
|
646
|
+
expect(names).toEqual(['Gamma', 'Beta', 'Alpha']);
|
|
647
|
+
});
|
|
648
|
+
it('should sort by mtime descending by default', async () => {
|
|
649
|
+
const result = await callTool(client, 'search_nodes', {
|
|
650
|
+
query: 'Letter',
|
|
651
|
+
sortBy: 'mtime'
|
|
652
|
+
});
|
|
653
|
+
const names = result.entities.items.map(e => e.name);
|
|
654
|
+
// Gamma was created last, so should be first when sorted desc
|
|
655
|
+
expect(names).toEqual(['Gamma', 'Beta', 'Alpha']);
|
|
656
|
+
});
|
|
657
|
+
it('should sort by mtime ascending when specified', async () => {
|
|
658
|
+
const result = await callTool(client, 'search_nodes', {
|
|
659
|
+
query: 'Letter',
|
|
660
|
+
sortBy: 'mtime',
|
|
661
|
+
sortDir: 'asc'
|
|
662
|
+
});
|
|
663
|
+
const names = result.entities.items.map(e => e.name);
|
|
664
|
+
// Alpha was created first, so should be first when sorted asc
|
|
665
|
+
expect(names).toEqual(['Alpha', 'Beta', 'Gamma']);
|
|
666
|
+
});
|
|
667
|
+
it('should sort by obsMtime', async () => {
|
|
668
|
+
// Update observation on Alpha to make it have most recent obsMtime
|
|
669
|
+
await callTool(client, 'delete_observations', {
|
|
670
|
+
deletions: [{ entityName: 'Alpha', observations: ['First letter'] }]
|
|
671
|
+
});
|
|
672
|
+
await callTool(client, 'add_observations', {
|
|
673
|
+
observations: [{ entityName: 'Alpha', contents: ['Updated'] }]
|
|
674
|
+
});
|
|
675
|
+
const result = await callTool(client, 'search_nodes', {
|
|
676
|
+
query: 'Letter|Updated',
|
|
677
|
+
sortBy: 'obsMtime'
|
|
678
|
+
});
|
|
679
|
+
const names = result.entities.items.map(e => e.name);
|
|
680
|
+
// Alpha should be first because its obsMtime was just updated
|
|
681
|
+
expect(names[0]).toBe('Alpha');
|
|
682
|
+
});
|
|
683
|
+
});
|
|
684
|
+
describe('get_entities_by_type sorting', () => {
|
|
685
|
+
beforeEach(async () => {
|
|
686
|
+
await createEntitiesWithDelay([
|
|
687
|
+
{ name: 'Zebra', entityType: 'Animal', observations: ['Striped'] },
|
|
688
|
+
{ name: 'Aardvark', entityType: 'Animal', observations: ['Nocturnal'] },
|
|
689
|
+
{ name: 'Monkey', entityType: 'Animal', observations: ['Clever'] }
|
|
690
|
+
]);
|
|
691
|
+
});
|
|
692
|
+
it('should preserve insertion order when sortBy is omitted', async () => {
|
|
693
|
+
const result = await callTool(client, 'get_entities_by_type', {
|
|
694
|
+
entityType: 'Animal'
|
|
695
|
+
});
|
|
696
|
+
const names = result.items.map(e => e.name);
|
|
697
|
+
expect(names).toEqual(['Zebra', 'Aardvark', 'Monkey']);
|
|
698
|
+
});
|
|
699
|
+
it('should sort by name ascending (default for name)', async () => {
|
|
700
|
+
const result = await callTool(client, 'get_entities_by_type', {
|
|
701
|
+
entityType: 'Animal',
|
|
702
|
+
sortBy: 'name'
|
|
703
|
+
});
|
|
704
|
+
const names = result.items.map(e => e.name);
|
|
705
|
+
expect(names).toEqual(['Aardvark', 'Monkey', 'Zebra']);
|
|
706
|
+
});
|
|
707
|
+
it('should sort by name descending', async () => {
|
|
708
|
+
const result = await callTool(client, 'get_entities_by_type', {
|
|
709
|
+
entityType: 'Animal',
|
|
710
|
+
sortBy: 'name',
|
|
711
|
+
sortDir: 'desc'
|
|
712
|
+
});
|
|
713
|
+
const names = result.items.map(e => e.name);
|
|
714
|
+
expect(names).toEqual(['Zebra', 'Monkey', 'Aardvark']);
|
|
715
|
+
});
|
|
716
|
+
it('should sort by mtime descending (default for mtime)', async () => {
|
|
717
|
+
const result = await callTool(client, 'get_entities_by_type', {
|
|
718
|
+
entityType: 'Animal',
|
|
719
|
+
sortBy: 'mtime'
|
|
720
|
+
});
|
|
721
|
+
const names = result.items.map(e => e.name);
|
|
722
|
+
// Monkey was created last
|
|
723
|
+
expect(names).toEqual(['Monkey', 'Aardvark', 'Zebra']);
|
|
724
|
+
});
|
|
725
|
+
it('should sort by mtime ascending', async () => {
|
|
726
|
+
const result = await callTool(client, 'get_entities_by_type', {
|
|
727
|
+
entityType: 'Animal',
|
|
728
|
+
sortBy: 'mtime',
|
|
729
|
+
sortDir: 'asc'
|
|
730
|
+
});
|
|
731
|
+
const names = result.items.map(e => e.name);
|
|
732
|
+
// Zebra was created first
|
|
733
|
+
expect(names).toEqual(['Zebra', 'Aardvark', 'Monkey']);
|
|
734
|
+
});
|
|
735
|
+
});
|
|
736
|
+
describe('get_orphaned_entities sorting', () => {
|
|
737
|
+
beforeEach(async () => {
|
|
738
|
+
// Create orphaned entities (no relations)
|
|
739
|
+
await createEntitiesWithDelay([
|
|
740
|
+
{ name: 'Orphan_Z', entityType: 'Orphan', observations: ['Alone'] },
|
|
741
|
+
{ name: 'Orphan_A', entityType: 'Orphan', observations: ['Solo'] },
|
|
742
|
+
{ name: 'Orphan_M', entityType: 'Orphan', observations: ['Isolated'] }
|
|
743
|
+
]);
|
|
744
|
+
});
|
|
745
|
+
it('should preserve insertion order when sortBy is omitted', async () => {
|
|
746
|
+
const result = await callTool(client, 'get_orphaned_entities', {});
|
|
747
|
+
const names = result.items.map(e => e.name);
|
|
748
|
+
expect(names).toEqual(['Orphan_Z', 'Orphan_A', 'Orphan_M']);
|
|
749
|
+
});
|
|
750
|
+
it('should sort by name ascending', async () => {
|
|
751
|
+
const result = await callTool(client, 'get_orphaned_entities', {
|
|
752
|
+
sortBy: 'name'
|
|
753
|
+
});
|
|
754
|
+
const names = result.items.map(e => e.name);
|
|
755
|
+
expect(names).toEqual(['Orphan_A', 'Orphan_M', 'Orphan_Z']);
|
|
756
|
+
});
|
|
757
|
+
it('should sort by name descending', async () => {
|
|
758
|
+
const result = await callTool(client, 'get_orphaned_entities', {
|
|
759
|
+
sortBy: 'name',
|
|
760
|
+
sortDir: 'desc'
|
|
761
|
+
});
|
|
762
|
+
const names = result.items.map(e => e.name);
|
|
763
|
+
expect(names).toEqual(['Orphan_Z', 'Orphan_M', 'Orphan_A']);
|
|
764
|
+
});
|
|
765
|
+
it('should sort by mtime descending (default)', async () => {
|
|
766
|
+
const result = await callTool(client, 'get_orphaned_entities', {
|
|
767
|
+
sortBy: 'mtime'
|
|
768
|
+
});
|
|
769
|
+
const names = result.items.map(e => e.name);
|
|
770
|
+
// Orphan_M was created last
|
|
771
|
+
expect(names).toEqual(['Orphan_M', 'Orphan_A', 'Orphan_Z']);
|
|
772
|
+
});
|
|
773
|
+
it('should work with strict mode and sorting', async () => {
|
|
774
|
+
// Create Self and connect one orphan to it
|
|
775
|
+
await callTool(client, 'create_entities', {
|
|
776
|
+
entities: [{ name: 'Self', entityType: 'Agent', observations: [] }]
|
|
777
|
+
});
|
|
778
|
+
await callTool(client, 'create_relations', {
|
|
779
|
+
relations: [{ from: 'Self', to: 'Orphan_A', relationType: 'knows' }]
|
|
780
|
+
});
|
|
781
|
+
const result = await callTool(client, 'get_orphaned_entities', {
|
|
782
|
+
strict: true,
|
|
783
|
+
sortBy: 'name'
|
|
784
|
+
});
|
|
785
|
+
const names = result.items.map(e => e.name);
|
|
786
|
+
// Orphan_A is now connected to Self, so only M and Z are orphaned
|
|
787
|
+
expect(names).toEqual(['Orphan_M', 'Orphan_Z']);
|
|
788
|
+
});
|
|
789
|
+
});
|
|
790
|
+
describe('get_neighbors sorting', () => {
|
|
791
|
+
beforeEach(async () => {
|
|
792
|
+
// Create a hub with neighbors created at different times
|
|
793
|
+
await callTool(client, 'create_entities', {
|
|
794
|
+
entities: [{ name: 'Hub', entityType: 'Center', observations: [] }]
|
|
795
|
+
});
|
|
796
|
+
await createEntitiesWithDelay([
|
|
797
|
+
{ name: 'Neighbor_Z', entityType: 'Node', observations: ['First'] },
|
|
798
|
+
{ name: 'Neighbor_A', entityType: 'Node', observations: ['Second'] },
|
|
799
|
+
{ name: 'Neighbor_M', entityType: 'Node', observations: ['Third'] }
|
|
800
|
+
]);
|
|
801
|
+
// Connect all to Hub
|
|
802
|
+
await callTool(client, 'create_relations', {
|
|
803
|
+
relations: [
|
|
804
|
+
{ from: 'Hub', to: 'Neighbor_Z', relationType: 'connects' },
|
|
805
|
+
{ from: 'Hub', to: 'Neighbor_A', relationType: 'connects' },
|
|
806
|
+
{ from: 'Hub', to: 'Neighbor_M', relationType: 'connects' }
|
|
807
|
+
]
|
|
808
|
+
});
|
|
809
|
+
});
|
|
810
|
+
it('should return unsorted neighbors when sortBy is omitted', async () => {
|
|
811
|
+
const result = await callTool(client, 'get_neighbors', {
|
|
812
|
+
entityName: 'Hub'
|
|
813
|
+
});
|
|
814
|
+
expect(result.items).toHaveLength(3);
|
|
815
|
+
// Just verify all neighbors are present
|
|
816
|
+
const names = result.items.map(n => n.name);
|
|
817
|
+
expect(names).toContain('Neighbor_Z');
|
|
818
|
+
expect(names).toContain('Neighbor_A');
|
|
819
|
+
expect(names).toContain('Neighbor_M');
|
|
820
|
+
});
|
|
821
|
+
it('should sort neighbors by name ascending', async () => {
|
|
822
|
+
const result = await callTool(client, 'get_neighbors', {
|
|
823
|
+
entityName: 'Hub',
|
|
824
|
+
sortBy: 'name'
|
|
825
|
+
});
|
|
826
|
+
const names = result.items.map(n => n.name);
|
|
827
|
+
expect(names).toEqual(['Neighbor_A', 'Neighbor_M', 'Neighbor_Z']);
|
|
828
|
+
});
|
|
829
|
+
it('should sort neighbors by name descending', async () => {
|
|
830
|
+
const result = await callTool(client, 'get_neighbors', {
|
|
831
|
+
entityName: 'Hub',
|
|
832
|
+
sortBy: 'name',
|
|
833
|
+
sortDir: 'desc'
|
|
834
|
+
});
|
|
835
|
+
const names = result.items.map(n => n.name);
|
|
836
|
+
expect(names).toEqual(['Neighbor_Z', 'Neighbor_M', 'Neighbor_A']);
|
|
837
|
+
});
|
|
838
|
+
it('should sort neighbors by mtime descending (default)', async () => {
|
|
839
|
+
const result = await callTool(client, 'get_neighbors', {
|
|
840
|
+
entityName: 'Hub',
|
|
841
|
+
sortBy: 'mtime'
|
|
842
|
+
});
|
|
843
|
+
const names = result.items.map(n => n.name);
|
|
844
|
+
// Neighbor_M was created last
|
|
845
|
+
expect(names).toEqual(['Neighbor_M', 'Neighbor_A', 'Neighbor_Z']);
|
|
846
|
+
});
|
|
847
|
+
it('should sort neighbors by mtime ascending', async () => {
|
|
848
|
+
const result = await callTool(client, 'get_neighbors', {
|
|
849
|
+
entityName: 'Hub',
|
|
850
|
+
sortBy: 'mtime',
|
|
851
|
+
sortDir: 'asc'
|
|
852
|
+
});
|
|
853
|
+
const names = result.items.map(n => n.name);
|
|
854
|
+
// Neighbor_Z was created first
|
|
855
|
+
expect(names).toEqual(['Neighbor_Z', 'Neighbor_A', 'Neighbor_M']);
|
|
856
|
+
});
|
|
857
|
+
it('should include mtime and obsMtime in neighbor objects', async () => {
|
|
858
|
+
const result = await callTool(client, 'get_neighbors', {
|
|
859
|
+
entityName: 'Hub',
|
|
860
|
+
sortBy: 'name'
|
|
861
|
+
});
|
|
862
|
+
// Each neighbor should have timestamp fields
|
|
863
|
+
for (const neighbor of result.items) {
|
|
864
|
+
expect(neighbor.mtime).toBeDefined();
|
|
865
|
+
expect(neighbor.obsMtime).toBeDefined();
|
|
866
|
+
expect(typeof neighbor.mtime).toBe('number');
|
|
867
|
+
expect(typeof neighbor.obsMtime).toBe('number');
|
|
868
|
+
}
|
|
869
|
+
});
|
|
870
|
+
it('should sort by obsMtime after observation update', async () => {
|
|
871
|
+
// Update observation on Neighbor_Z to make it have most recent obsMtime
|
|
872
|
+
await callTool(client, 'delete_observations', {
|
|
873
|
+
deletions: [{ entityName: 'Neighbor_Z', observations: ['First'] }]
|
|
874
|
+
});
|
|
875
|
+
await callTool(client, 'add_observations', {
|
|
876
|
+
observations: [{ entityName: 'Neighbor_Z', contents: ['Updated recently'] }]
|
|
877
|
+
});
|
|
878
|
+
const result = await callTool(client, 'get_neighbors', {
|
|
879
|
+
entityName: 'Hub',
|
|
880
|
+
sortBy: 'obsMtime'
|
|
881
|
+
});
|
|
882
|
+
const names = result.items.map(n => n.name);
|
|
883
|
+
// Neighbor_Z should be first because its obsMtime was just updated
|
|
884
|
+
expect(names[0]).toBe('Neighbor_Z');
|
|
885
|
+
});
|
|
886
|
+
});
|
|
887
|
+
describe('sorting with pagination', () => {
|
|
888
|
+
it('should maintain sort order across paginated results', async () => {
|
|
889
|
+
// Create many entities to force pagination
|
|
890
|
+
const entities = [];
|
|
891
|
+
for (let i = 0; i < 20; i++) {
|
|
892
|
+
entities.push({
|
|
893
|
+
name: `Entity_${String(i).padStart(2, '0')}`,
|
|
894
|
+
entityType: 'Numbered',
|
|
895
|
+
observations: [`Number ${i}`]
|
|
896
|
+
});
|
|
897
|
+
}
|
|
898
|
+
await callTool(client, 'create_entities', { entities });
|
|
899
|
+
// Fetch all pages sorted by name descending
|
|
900
|
+
const allEntities = [];
|
|
901
|
+
let entityCursor = 0;
|
|
902
|
+
while (entityCursor !== null) {
|
|
903
|
+
const result = await callTool(client, 'search_nodes', {
|
|
904
|
+
query: 'Numbered',
|
|
905
|
+
sortBy: 'name',
|
|
906
|
+
sortDir: 'desc',
|
|
907
|
+
entityCursor
|
|
908
|
+
});
|
|
909
|
+
allEntities.push(...result.entities.items);
|
|
910
|
+
entityCursor = result.entities.nextCursor;
|
|
911
|
+
}
|
|
912
|
+
// Verify all entities are in descending order
|
|
913
|
+
const names = allEntities.map(e => e.name);
|
|
914
|
+
const sortedNames = [...names].sort().reverse();
|
|
915
|
+
expect(names).toEqual(sortedNames);
|
|
916
|
+
});
|
|
917
|
+
});
|
|
918
|
+
});
|
|
565
919
|
});
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@levalicious/server-memory",
|
|
3
|
-
"version": "0.0.
|
|
3
|
+
"version": "0.0.11",
|
|
4
4
|
"description": "MCP server for enabling memory for Claude through a knowledge graph",
|
|
5
5
|
"license": "MIT",
|
|
6
6
|
"author": "Levalicious",
|
|
@@ -15,18 +15,19 @@
|
|
|
15
15
|
],
|
|
16
16
|
"scripts": {
|
|
17
17
|
"build": "tsc && shx chmod +x dist/*.js",
|
|
18
|
-
"prepare": "npm run build",
|
|
18
|
+
"prepare": "husky && npm run build",
|
|
19
19
|
"watch": "tsc --watch",
|
|
20
20
|
"test": "NODE_OPTIONS='--experimental-vm-modules' jest"
|
|
21
21
|
},
|
|
22
22
|
"dependencies": {
|
|
23
|
-
"@modelcontextprotocol/sdk": "1.
|
|
23
|
+
"@modelcontextprotocol/sdk": "1.25.1",
|
|
24
24
|
"proper-lockfile": "^4.1.2"
|
|
25
25
|
},
|
|
26
26
|
"devDependencies": {
|
|
27
27
|
"@types/jest": "^30.0.0",
|
|
28
|
-
"@types/node": "^
|
|
28
|
+
"@types/node": "^25",
|
|
29
29
|
"@types/proper-lockfile": "^4.1.4",
|
|
30
|
+
"husky": "^9.1.7",
|
|
30
31
|
"jest": "^30.2.0",
|
|
31
32
|
"shx": "^0.4.0",
|
|
32
33
|
"ts-jest": "^29.4.5",
|