@levalicious/server-memory 0.0.10 → 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 +100 -180
- package/dist/tests/memory-server.test.js +90 -29
- package/package.json +3 -2
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
|
@@ -131,8 +131,6 @@ function paginateGraph(graph, entityCursor = 0, relationCursor = 0) {
|
|
|
131
131
|
}
|
|
132
132
|
// The KnowledgeGraphManager class contains all operations to interact with the knowledge graph
|
|
133
133
|
export class KnowledgeGraphManager {
|
|
134
|
-
bclCtr = 0;
|
|
135
|
-
bclTerm = "";
|
|
136
134
|
memoryFilePath;
|
|
137
135
|
constructor(memoryFilePath = DEFAULT_MEMORY_FILE_PATH) {
|
|
138
136
|
this.memoryFilePath = memoryFilePath;
|
|
@@ -503,163 +501,96 @@ export class KnowledgeGraphManager {
|
|
|
503
501
|
observationViolations
|
|
504
502
|
};
|
|
505
503
|
}
|
|
506
|
-
|
|
507
|
-
|
|
508
|
-
|
|
509
|
-
|
|
510
|
-
|
|
511
|
-
|
|
512
|
-
|
|
513
|
-
|
|
514
|
-
let
|
|
515
|
-
|
|
516
|
-
|
|
517
|
-
|
|
518
|
-
|
|
519
|
-
|
|
520
|
-
|
|
521
|
-
|
|
522
|
-
if (mode === 0) {
|
|
523
|
-
t1 += b;
|
|
524
|
-
let size = t1.length + t0.length;
|
|
525
|
-
if (size > max_size)
|
|
526
|
-
max_size = size;
|
|
527
|
-
if (t1.slice(-4) === '1100') {
|
|
528
|
-
mode = 1;
|
|
529
|
-
t1 = t1.slice(0, -4);
|
|
530
|
-
}
|
|
531
|
-
else if (t1.slice(-5) === '11101') {
|
|
532
|
-
mode = 3;
|
|
533
|
-
t1 = t1.slice(0, -5);
|
|
534
|
-
}
|
|
535
|
-
}
|
|
536
|
-
else if (mode === 1) {
|
|
537
|
-
t2 += b;
|
|
538
|
-
if (b == '1') {
|
|
539
|
-
ctr += 1;
|
|
540
|
-
}
|
|
541
|
-
else if (b == '0') {
|
|
542
|
-
ctr -= 1;
|
|
543
|
-
t2 += t0[0];
|
|
544
|
-
t0 = t0.slice(1);
|
|
545
|
-
}
|
|
546
|
-
if (ctr === 0) {
|
|
547
|
-
mode = 2;
|
|
548
|
-
ctr = 1;
|
|
549
|
-
}
|
|
550
|
-
}
|
|
551
|
-
else if (mode === 2) {
|
|
552
|
-
if (b == '1') {
|
|
553
|
-
ctr += 1;
|
|
554
|
-
}
|
|
555
|
-
else if (b == '0') {
|
|
556
|
-
ctr -= 1;
|
|
557
|
-
t0 = t0.slice(1);
|
|
558
|
-
}
|
|
559
|
-
if (ctr === 0) {
|
|
560
|
-
t0 = t2 + t0;
|
|
561
|
-
t2 = '';
|
|
562
|
-
mode = 0;
|
|
563
|
-
ctr = 1;
|
|
564
|
-
stepCount += 1;
|
|
565
|
-
}
|
|
566
|
-
}
|
|
567
|
-
else if (mode === 3) {
|
|
568
|
-
t2 += b;
|
|
569
|
-
if (b == '1') {
|
|
570
|
-
ctr += 1;
|
|
571
|
-
}
|
|
572
|
-
else if (b == '0') {
|
|
573
|
-
ctr -= 1;
|
|
574
|
-
t2 += t0[0];
|
|
575
|
-
t0 = t0.slice(1);
|
|
576
|
-
}
|
|
577
|
-
if (ctr === 0) {
|
|
578
|
-
mode = 4;
|
|
579
|
-
ctr = 1;
|
|
580
|
-
}
|
|
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;
|
|
581
520
|
}
|
|
582
|
-
else
|
|
583
|
-
|
|
584
|
-
if (b == '1') {
|
|
585
|
-
ctr += 1;
|
|
586
|
-
}
|
|
587
|
-
else if (b == '0') {
|
|
588
|
-
ctr -= 1;
|
|
589
|
-
t3 += t0[0];
|
|
590
|
-
t0 = t0.slice(1);
|
|
591
|
-
}
|
|
592
|
-
if (ctr === 0) {
|
|
593
|
-
mode = 5;
|
|
594
|
-
ctr = 1;
|
|
595
|
-
}
|
|
521
|
+
else {
|
|
522
|
+
return randomBytes(4).readUInt32BE() / 0xFFFFFFFF;
|
|
596
523
|
}
|
|
597
|
-
|
|
598
|
-
|
|
599
|
-
|
|
600
|
-
|
|
601
|
-
|
|
602
|
-
|
|
603
|
-
|
|
604
|
-
|
|
605
|
-
|
|
606
|
-
|
|
607
|
-
|
|
608
|
-
t0 = '11' + t2 + t4 + '1' + t3 + t4 + t0;
|
|
609
|
-
t2 = '';
|
|
610
|
-
t3 = '';
|
|
611
|
-
t4 = '';
|
|
612
|
-
mode = 0;
|
|
613
|
-
ctr = 1;
|
|
614
|
-
stepCount += 1;
|
|
615
|
-
}
|
|
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);
|
|
616
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);
|
|
617
544
|
}
|
|
618
|
-
|
|
619
|
-
return {
|
|
620
|
-
result: t1,
|
|
621
|
-
info: `${stepCount} steps, max size ${max_size}`,
|
|
622
|
-
halted,
|
|
623
|
-
errored: halted && mode != 0,
|
|
624
|
-
};
|
|
545
|
+
return { entity: current, path };
|
|
625
546
|
}
|
|
626
|
-
|
|
627
|
-
|
|
628
|
-
|
|
629
|
-
|
|
630
|
-
|
|
631
|
-
|
|
632
|
-
|
|
633
|
-
if (!validTerms.includes(term)) {
|
|
634
|
-
throw new Error(`Invalid BCL term: ${term}\nExpected one of: ${validTerms.join(", ")}`);
|
|
635
|
-
}
|
|
636
|
-
let processedTerm = 0;
|
|
637
|
-
if (term === "00" || term === "K")
|
|
638
|
-
processedTerm = 1;
|
|
639
|
-
else if (term === "01" || term === "S")
|
|
640
|
-
processedTerm = 2;
|
|
641
|
-
this.bclTerm += termset[processedTerm];
|
|
642
|
-
if (processedTerm === 0) {
|
|
643
|
-
if (this.bclCtr === 0)
|
|
644
|
-
this.bclCtr += 1;
|
|
645
|
-
this.bclCtr += 1;
|
|
646
|
-
}
|
|
647
|
-
else {
|
|
648
|
-
this.bclCtr -= 1;
|
|
649
|
-
}
|
|
650
|
-
if (this.bclCtr <= 0) {
|
|
651
|
-
const constructedProgram = this.bclTerm;
|
|
652
|
-
this.bclCtr = 0;
|
|
653
|
-
this.bclTerm = "";
|
|
654
|
-
return `Constructed Program: ${constructedProgram}`;
|
|
655
|
-
}
|
|
656
|
-
else {
|
|
657
|
-
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
|
|
658
554
|
}
|
|
555
|
+
return hash || 1; // Ensure non-zero for xorshift
|
|
659
556
|
}
|
|
660
|
-
|
|
661
|
-
|
|
662
|
-
|
|
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;
|
|
663
594
|
}
|
|
664
595
|
async addThought(observations, previousCtxId) {
|
|
665
596
|
return this.withLock(async () => {
|
|
@@ -714,7 +645,7 @@ export function createServer(memoryFilePath) {
|
|
|
714
645
|
sizes: ["any"]
|
|
715
646
|
}
|
|
716
647
|
],
|
|
717
|
-
version: "0.0.
|
|
648
|
+
version: "0.0.11",
|
|
718
649
|
}, {
|
|
719
650
|
capabilities: {
|
|
720
651
|
tools: {},
|
|
@@ -998,37 +929,27 @@ export function createServer(memoryFilePath) {
|
|
|
998
929
|
},
|
|
999
930
|
},
|
|
1000
931
|
{
|
|
1001
|
-
name: "
|
|
1002
|
-
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.",
|
|
1003
934
|
inputSchema: {
|
|
1004
935
|
type: "object",
|
|
1005
936
|
properties: {
|
|
1006
|
-
|
|
1007
|
-
|
|
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" },
|
|
1008
939
|
},
|
|
1009
|
-
required: ["program"],
|
|
1010
940
|
},
|
|
1011
941
|
},
|
|
1012
942
|
{
|
|
1013
|
-
name: "
|
|
1014
|
-
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.",
|
|
1015
945
|
inputSchema: {
|
|
1016
946
|
type: "object",
|
|
1017
947
|
properties: {
|
|
1018
|
-
|
|
1019
|
-
|
|
1020
|
-
|
|
1021
|
-
},
|
|
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." },
|
|
1022
951
|
},
|
|
1023
|
-
required: ["
|
|
1024
|
-
},
|
|
1025
|
-
},
|
|
1026
|
-
{
|
|
1027
|
-
name: "clear_bcl_term",
|
|
1028
|
-
description: "Clear the current BCL term being constructed and reset the constructor state",
|
|
1029
|
-
inputSchema: {
|
|
1030
|
-
type: "object",
|
|
1031
|
-
properties: {},
|
|
952
|
+
required: ["start"],
|
|
1032
953
|
},
|
|
1033
954
|
},
|
|
1034
955
|
{
|
|
@@ -1113,13 +1034,12 @@ Use this to build chains of reasoning that persist in the graph. Each thought ca
|
|
|
1113
1034
|
}
|
|
1114
1035
|
case "validate_graph":
|
|
1115
1036
|
return { content: [{ type: "text", text: JSON.stringify(await knowledgeGraphManager.validateGraph(), null, 2) }] };
|
|
1116
|
-
case "
|
|
1117
|
-
return { content: [{ type: "text", text: JSON.stringify(
|
|
1118
|
-
case "
|
|
1119
|
-
|
|
1120
|
-
|
|
1121
|
-
|
|
1122
|
-
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
|
+
}
|
|
1123
1043
|
case "sequentialthinking": {
|
|
1124
1044
|
const result = await knowledgeGraphManager.addThought(args.observations, args.previousCtxId);
|
|
1125
1045
|
return { content: [{ type: "text", text: JSON.stringify(result) }] };
|
|
@@ -457,35 +457,6 @@ describe('MCP Memory Server E2E Tests', () => {
|
|
|
457
457
|
expect(result.missingEntities).toContain('Missing');
|
|
458
458
|
});
|
|
459
459
|
});
|
|
460
|
-
describe('BCL Evaluator', () => {
|
|
461
|
-
it('should evaluate K combinator (identity for first arg)', async () => {
|
|
462
|
-
// K = 00, evaluating K applied to two args should return first
|
|
463
|
-
// This is a simplified test - BCL semantics are complex
|
|
464
|
-
const result = await callTool(client, 'evaluate_bcl', {
|
|
465
|
-
program: '00',
|
|
466
|
-
maxSteps: 100
|
|
467
|
-
});
|
|
468
|
-
expect(result.halted).toBe(true);
|
|
469
|
-
});
|
|
470
|
-
it('should construct BCL terms incrementally', async () => {
|
|
471
|
-
let result = await callTool(client, 'add_bcl_term', { term: 'App' });
|
|
472
|
-
expect(result).toContain('more term');
|
|
473
|
-
result = await callTool(client, 'add_bcl_term', { term: 'K' });
|
|
474
|
-
expect(result).toContain('more term');
|
|
475
|
-
result = await callTool(client, 'add_bcl_term', { term: 'S' });
|
|
476
|
-
expect(result).toContain('Constructed Program');
|
|
477
|
-
});
|
|
478
|
-
it('should clear BCL constructor state', async () => {
|
|
479
|
-
await callTool(client, 'add_bcl_term', { term: 'App' });
|
|
480
|
-
await callTool(client, 'clear_bcl_term', {});
|
|
481
|
-
// After clearing, we should need to start fresh
|
|
482
|
-
const result = await callTool(client, 'add_bcl_term', { term: 'K' });
|
|
483
|
-
expect(result).toContain('Constructed Program');
|
|
484
|
-
});
|
|
485
|
-
it('should reject invalid BCL terms', async () => {
|
|
486
|
-
await expect(callTool(client, 'add_bcl_term', { term: 'invalid' })).rejects.toThrow(/Invalid BCL term/);
|
|
487
|
-
});
|
|
488
|
-
});
|
|
489
460
|
describe('Sequential Thinking', () => {
|
|
490
461
|
it('should create a thought and return ctxId', async () => {
|
|
491
462
|
const result = await callTool(client, 'sequentialthinking', {
|
|
@@ -541,6 +512,96 @@ describe('MCP Memory Server E2E Tests', () => {
|
|
|
541
512
|
expect(thought.obsMtime).toBeDefined();
|
|
542
513
|
});
|
|
543
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
|
+
});
|
|
544
605
|
describe('Sorting', () => {
|
|
545
606
|
// Helper to create entities with controlled timestamps
|
|
546
607
|
async function createEntitiesWithDelay(entities) {
|
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,7 +15,7 @@
|
|
|
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
|
},
|
|
@@ -27,6 +27,7 @@
|
|
|
27
27
|
"@types/jest": "^30.0.0",
|
|
28
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",
|