cozo-memory 1.0.5 → 1.0.7
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 +122 -0
- package/dist/api_bridge.js +6 -4
- package/dist/cli-commands.js +210 -0
- package/dist/cli.js +490 -0
- package/dist/embedding-service.js +1 -1
- package/dist/hybrid-search.js +8 -3
- package/dist/index.js +108 -2
- package/dist/temporal-normalizer.js +2 -0
- package/dist/test-hybrid-debug.js +52 -0
- package/dist/test-mcp-search.js +47 -0
- package/dist/test-search-simple.js +27 -0
- package/dist/test-user-profile.js +59 -0
- package/dist/timestamp-utils.js +44 -0
- package/dist/tui-blessed.js +789 -0
- package/dist/tui-launcher.js +61 -0
- package/dist/tui.js +133 -0
- package/dist/tui.py +481 -0
- package/package.json +19 -2
package/README.md
CHANGED
|
@@ -83,6 +83,10 @@ Now you can add the server to your MCP client (e.g. Claude Desktop).
|
|
|
83
83
|
|
|
84
84
|
📦 **Export/Import (since v1.8)** - Export to JSON, Markdown, or Obsidian-ready ZIP; import from Mem0, MemGPT, Markdown, or native format
|
|
85
85
|
|
|
86
|
+
📄 **PDF Support (since v1.9)** - Direct PDF ingestion with text extraction via pdfjs-dist; supports file path and content parameters
|
|
87
|
+
|
|
88
|
+
🕐 **Dual Timestamp Format (since v1.9)** - All timestamps returned in both Unix microseconds and ISO 8601 format for maximum flexibility
|
|
89
|
+
|
|
86
90
|
### Detailed Features
|
|
87
91
|
- **Hybrid Search (v0.7 Optimized)**: Combination of semantic search (HNSW), **Full-Text Search (FTS)**, and graph signals, merged via Reciprocal Rank Fusion (RRF).
|
|
88
92
|
- **Full-Text Search (FTS)**: Native CozoDB v0.7 FTS indices with stemming, stopword filtering, and robust query sanitizing (cleaning of `+ - * / \ ( ) ? .`) for maximum stability.
|
|
@@ -331,6 +335,67 @@ npm run start
|
|
|
331
335
|
|
|
332
336
|
Default database path: `memory_db.cozo.db` in project root (created automatically).
|
|
333
337
|
|
|
338
|
+
### CLI Tool
|
|
339
|
+
|
|
340
|
+
CozoDB Memory includes a full-featured CLI for all operations:
|
|
341
|
+
|
|
342
|
+
```bash
|
|
343
|
+
# System operations
|
|
344
|
+
cozo-memory system health
|
|
345
|
+
cozo-memory system metrics
|
|
346
|
+
|
|
347
|
+
# Entity operations
|
|
348
|
+
cozo-memory entity create -n "MyEntity" -t "person" -m '{"age": 30}'
|
|
349
|
+
cozo-memory entity get -i <entity-id>
|
|
350
|
+
cozo-memory entity delete -i <entity-id>
|
|
351
|
+
|
|
352
|
+
# Observations
|
|
353
|
+
cozo-memory observation add -i <entity-id> -t "Some note"
|
|
354
|
+
|
|
355
|
+
# Relations
|
|
356
|
+
cozo-memory relation create --from <id1> --to <id2> --type "knows" -s 0.8
|
|
357
|
+
|
|
358
|
+
# Search
|
|
359
|
+
cozo-memory search query -q "search term" -l 10
|
|
360
|
+
cozo-memory search context -q "context query"
|
|
361
|
+
|
|
362
|
+
# Graph operations
|
|
363
|
+
cozo-memory graph explore -s <entity-id> -h 3
|
|
364
|
+
cozo-memory graph pagerank
|
|
365
|
+
cozo-memory graph communities
|
|
366
|
+
|
|
367
|
+
# Export/Import
|
|
368
|
+
cozo-memory export json -o backup.json --include-metadata --include-relationships --include-observations
|
|
369
|
+
cozo-memory export markdown -o notes.md
|
|
370
|
+
cozo-memory export obsidian -o vault.zip
|
|
371
|
+
cozo-memory import file -i data.json -f cozo
|
|
372
|
+
|
|
373
|
+
# All commands support -f json or -f pretty for output formatting
|
|
374
|
+
```
|
|
375
|
+
|
|
376
|
+
### TUI (Terminal User Interface)
|
|
377
|
+
|
|
378
|
+
Interactive TUI with mouse support powered by Python Textual:
|
|
379
|
+
|
|
380
|
+
```bash
|
|
381
|
+
# Install Python dependencies (one-time)
|
|
382
|
+
pip install textual
|
|
383
|
+
|
|
384
|
+
# Launch TUI
|
|
385
|
+
npm run tui
|
|
386
|
+
# or directly:
|
|
387
|
+
cozo-memory-tui
|
|
388
|
+
```
|
|
389
|
+
|
|
390
|
+
**TUI Features:**
|
|
391
|
+
- 🖱️ Full mouse support (click buttons, scroll, select inputs)
|
|
392
|
+
- ⌨️ Keyboard shortcuts (q=quit, h=help, r=refresh)
|
|
393
|
+
- 📊 Interactive menus for all operations
|
|
394
|
+
- 🎨 Rich terminal UI with colors and animations
|
|
395
|
+
- 📋 Real-time results display
|
|
396
|
+
- 🔍 Forms for entity creation, search, graph operations
|
|
397
|
+
- 📤 Export/Import wizards
|
|
398
|
+
|
|
334
399
|
### Claude Desktop Integration
|
|
335
400
|
|
|
336
401
|
#### Using npx (Recommended)
|
|
@@ -746,6 +811,24 @@ Returns deletion statistics showing exactly what was removed.
|
|
|
746
811
|
|
|
747
812
|
## Technical Highlights
|
|
748
813
|
|
|
814
|
+
### Dual Timestamp Format (v1.9)
|
|
815
|
+
|
|
816
|
+
All write operations (`create_entity`, `add_observation`, `create_relation`) return timestamps in both formats:
|
|
817
|
+
- `created_at`: Unix microseconds (CozoDB native format, precise for calculations)
|
|
818
|
+
- `created_at_iso`: ISO 8601 string (human-readable, e.g., `"2026-02-28T17:21:19.343Z"`)
|
|
819
|
+
|
|
820
|
+
This dual format provides maximum flexibility - use Unix timestamps for time calculations and comparisons, or ISO strings for display and logging.
|
|
821
|
+
|
|
822
|
+
Example response:
|
|
823
|
+
```json
|
|
824
|
+
{
|
|
825
|
+
"id": "...",
|
|
826
|
+
"created_at": 1772299279343000,
|
|
827
|
+
"created_at_iso": "2026-02-28T17:21:19.343Z",
|
|
828
|
+
"status": "Entity created"
|
|
829
|
+
}
|
|
830
|
+
```
|
|
831
|
+
|
|
749
832
|
### Local ONNX Embeddings (Transformers)
|
|
750
833
|
|
|
751
834
|
Default Model: `Xenova/bge-m3` (1024 dimensions).
|
|
@@ -829,6 +912,42 @@ The system maintains a persistent profile of the user (preferences, dislikes, wo
|
|
|
829
912
|
- **Mechanism**: All observations assigned to this entity receive a significant boost in search and context queries.
|
|
830
913
|
- **Initialization**: The profile is automatically created on first start.
|
|
831
914
|
|
|
915
|
+
### Manual Profile Editing
|
|
916
|
+
|
|
917
|
+
You can now directly edit the user profile using the `edit_user_profile` MCP tool:
|
|
918
|
+
|
|
919
|
+
```typescript
|
|
920
|
+
// View current profile
|
|
921
|
+
{ }
|
|
922
|
+
|
|
923
|
+
// Update metadata
|
|
924
|
+
{
|
|
925
|
+
metadata: { timezone: "Europe/Berlin", language: "de" }
|
|
926
|
+
}
|
|
927
|
+
|
|
928
|
+
// Add preferences
|
|
929
|
+
{
|
|
930
|
+
observations: [
|
|
931
|
+
{ text: "Prefers TypeScript over JavaScript" },
|
|
932
|
+
{ text: "Likes concise documentation" }
|
|
933
|
+
]
|
|
934
|
+
}
|
|
935
|
+
|
|
936
|
+
// Clear and reset preferences
|
|
937
|
+
{
|
|
938
|
+
clear_observations: true,
|
|
939
|
+
observations: [{ text: "New preference" }]
|
|
940
|
+
}
|
|
941
|
+
|
|
942
|
+
// Update name and type
|
|
943
|
+
{
|
|
944
|
+
name: "Developer Profile",
|
|
945
|
+
type: "UserProfile"
|
|
946
|
+
}
|
|
947
|
+
```
|
|
948
|
+
|
|
949
|
+
**Note**: You can still use the implicit method via `mutate_memory` with `action='add_observation'` and `entity_id='global_user_profile'`.
|
|
950
|
+
|
|
832
951
|
### Manual Tests
|
|
833
952
|
|
|
834
953
|
There are various test scripts for different features:
|
|
@@ -845,6 +964,9 @@ npx ts-node test-reflection.ts
|
|
|
845
964
|
|
|
846
965
|
# Tests user preference profiling and search boost
|
|
847
966
|
npx ts-node test-user-pref.ts
|
|
967
|
+
|
|
968
|
+
# Tests manual user profile editing
|
|
969
|
+
npx ts-node src/test-user-profile.ts
|
|
848
970
|
```
|
|
849
971
|
|
|
850
972
|
## Troubleshooting
|
package/dist/api_bridge.js
CHANGED
|
@@ -35,11 +35,13 @@ app.post("/api/entities", async (req, res) => {
|
|
|
35
35
|
try {
|
|
36
36
|
// We use the same logic as in create_entity tool
|
|
37
37
|
const id = (0, uuid_1.v4)();
|
|
38
|
-
const
|
|
38
|
+
const content = name + " " + type;
|
|
39
|
+
const embedding = await memoryServer.embeddingService.embed(content);
|
|
40
|
+
const nameEmbedding = await memoryServer.embeddingService.embed(name);
|
|
39
41
|
await memoryServer.db.run(`
|
|
40
|
-
?[id, created_at, name, type, embedding, metadata] <- [
|
|
41
|
-
[$id, "ASSERT", $name, $type, [${embedding.join(",")}], $metadata]
|
|
42
|
-
] :put entity {id, created_at => name, type, embedding, metadata}
|
|
42
|
+
?[id, created_at, name, type, embedding, name_embedding, metadata] <- [
|
|
43
|
+
[$id, "ASSERT", $name, $type, [${embedding.join(",")}], [${nameEmbedding.join(",")}], $metadata]
|
|
44
|
+
] :put entity {id, created_at => name, type, embedding, name_embedding, metadata}
|
|
43
45
|
`, { id, name, type, metadata: metadata || {} });
|
|
44
46
|
res.status(201).json({ id, name, type, metadata, status: "Entity created" });
|
|
45
47
|
}
|
|
@@ -0,0 +1,210 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
/**
|
|
3
|
+
* Shared CLI command logic for both pure CLI and TUI
|
|
4
|
+
* Calls MemoryServer public methods directly
|
|
5
|
+
*/
|
|
6
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
7
|
+
exports.CLICommands = void 0;
|
|
8
|
+
const index_js_1 = require("./index.js");
|
|
9
|
+
class CLICommands {
|
|
10
|
+
server;
|
|
11
|
+
initialized = false;
|
|
12
|
+
constructor() {
|
|
13
|
+
this.server = new index_js_1.MemoryServer();
|
|
14
|
+
}
|
|
15
|
+
async init() {
|
|
16
|
+
if (!this.initialized) {
|
|
17
|
+
await this.server.initPromise;
|
|
18
|
+
this.initialized = true;
|
|
19
|
+
}
|
|
20
|
+
}
|
|
21
|
+
async close() {
|
|
22
|
+
// CozoDB handles cleanup automatically
|
|
23
|
+
}
|
|
24
|
+
// Entity operations - use db directly
|
|
25
|
+
async createEntity(name, type, metadata) {
|
|
26
|
+
const { v4: uuidv4 } = await import('uuid');
|
|
27
|
+
const id = uuidv4();
|
|
28
|
+
const content = name + " " + type;
|
|
29
|
+
const embedding = await this.server.embeddingService.embed(content);
|
|
30
|
+
const nameEmbedding = await this.server.embeddingService.embed(name);
|
|
31
|
+
const now = Date.now() * 1000; // microseconds
|
|
32
|
+
const nowIso = new Date().toISOString();
|
|
33
|
+
await this.server.db.run(`
|
|
34
|
+
?[id, created_at, name, type, embedding, name_embedding, metadata] <- [
|
|
35
|
+
[$id, "ASSERT", $name, $type, [${embedding.join(",")}], [${nameEmbedding.join(",")}], $metadata]
|
|
36
|
+
] :put entity {id, created_at => name, type, embedding, name_embedding, metadata}
|
|
37
|
+
`, { id, name, type, metadata: metadata || {} });
|
|
38
|
+
return { id, name, type, metadata, created_at: now, created_at_iso: nowIso, status: "Entity created" };
|
|
39
|
+
}
|
|
40
|
+
async getEntity(entityId) {
|
|
41
|
+
const entityRes = await this.server.db.run('?[id, name, type, metadata, ts] := *entity{id, name, type, metadata, created_at, @ "NOW"}, id = $id, ts = to_int(created_at)', { id: entityId });
|
|
42
|
+
if (entityRes.rows.length === 0) {
|
|
43
|
+
throw new Error("Entity not found");
|
|
44
|
+
}
|
|
45
|
+
const obsRes = await this.server.db.run('?[id, text, metadata, ts] := *observation{id, entity_id, text, metadata, created_at, @ "NOW"}, entity_id = $id, ts = to_int(created_at)', { id: entityId });
|
|
46
|
+
const relRes = await this.server.db.run(`
|
|
47
|
+
?[target_id, type, strength, metadata, direction] := *relationship{from_id, to_id, relation_type: type, strength, metadata, @ "NOW"}, from_id = $id, target_id = to_id, direction = 'outgoing'
|
|
48
|
+
?[target_id, type, strength, metadata, direction] := *relationship{from_id, to_id, relation_type: type, strength, metadata, @ "NOW"}, to_id = $id, target_id = from_id, direction = 'incoming'
|
|
49
|
+
`, { id: entityId });
|
|
50
|
+
return {
|
|
51
|
+
entity: {
|
|
52
|
+
id: entityRes.rows[0][0],
|
|
53
|
+
name: entityRes.rows[0][1],
|
|
54
|
+
type: entityRes.rows[0][2],
|
|
55
|
+
metadata: entityRes.rows[0][3],
|
|
56
|
+
created_at: entityRes.rows[0][4]
|
|
57
|
+
},
|
|
58
|
+
observations: obsRes.rows.map((r) => ({ id: r[0], text: r[1], metadata: r[2], created_at: r[3] })),
|
|
59
|
+
relations: relRes.rows.map((r) => ({ target_id: r[0], type: r[1], strength: r[2], metadata: r[3], direction: r[4] }))
|
|
60
|
+
};
|
|
61
|
+
}
|
|
62
|
+
async deleteEntity(entityId) {
|
|
63
|
+
await this.server.db.run(`
|
|
64
|
+
{ ?[id, created_at] := *observation{id, entity_id, created_at}, entity_id = $target_id :rm observation {id, created_at} }
|
|
65
|
+
{ ?[from_id, to_id, relation_type, created_at] := *relationship{from_id, to_id, relation_type, created_at}, from_id = $target_id :rm relationship {from_id, to_id, relation_type, created_at} }
|
|
66
|
+
{ ?[from_id, to_id, relation_type, created_at] := *relationship{from_id, to_id, relation_type, created_at}, to_id = $target_id :rm relationship {from_id, to_id, relation_type, created_at} }
|
|
67
|
+
{ ?[id, created_at] := *entity{id, created_at}, id = $target_id :rm entity {id, created_at} }
|
|
68
|
+
`, { target_id: entityId });
|
|
69
|
+
return { status: "Entity and related data deleted" };
|
|
70
|
+
}
|
|
71
|
+
// Observation operations
|
|
72
|
+
async addObservation(entityId, text, metadata) {
|
|
73
|
+
const { v4: uuidv4 } = await import('uuid');
|
|
74
|
+
const id = uuidv4();
|
|
75
|
+
const embedding = await this.server.embeddingService.embed(text);
|
|
76
|
+
const now = Date.now() * 1000;
|
|
77
|
+
const nowIso = new Date().toISOString();
|
|
78
|
+
await this.server.db.run(`
|
|
79
|
+
?[id, created_at, entity_id, text, embedding, metadata] <- [
|
|
80
|
+
[$id, "ASSERT", $entity_id, $text, [${embedding.join(",")}], $metadata]
|
|
81
|
+
] :put observation {id, created_at => entity_id, text, embedding, metadata}
|
|
82
|
+
`, { id, entity_id: entityId, text, metadata: metadata || {} });
|
|
83
|
+
return { id, entity_id: entityId, text, metadata, created_at: now, created_at_iso: nowIso, status: "Observation added" };
|
|
84
|
+
}
|
|
85
|
+
// Relation operations
|
|
86
|
+
async createRelation(fromId, toId, relationType, strength, metadata) {
|
|
87
|
+
const str = strength !== undefined ? strength : 1.0;
|
|
88
|
+
const now = Date.now() * 1000;
|
|
89
|
+
const nowIso = new Date().toISOString();
|
|
90
|
+
await this.server.db.run(`
|
|
91
|
+
?[from_id, to_id, relation_type, created_at, strength, metadata] <- [
|
|
92
|
+
[$from_id, $to_id, $relation_type, "ASSERT", $strength, $metadata]
|
|
93
|
+
] :put relationship {from_id, to_id, relation_type, created_at => strength, metadata}
|
|
94
|
+
`, { from_id: fromId, to_id: toId, relation_type: relationType, strength: str, metadata: metadata || {} });
|
|
95
|
+
return { from_id: fromId, to_id: toId, relation_type: relationType, strength: str, metadata, created_at: now, created_at_iso: nowIso, status: "Relation created" };
|
|
96
|
+
}
|
|
97
|
+
// Search operations - use the MCP tool directly
|
|
98
|
+
async search(query, limit = 10, entityTypes, includeEntities = true, includeObservations = true) {
|
|
99
|
+
// Call the search method from the server's query_memory tool
|
|
100
|
+
const result = await this.server.hybridSearch.search({
|
|
101
|
+
query,
|
|
102
|
+
limit,
|
|
103
|
+
entityTypes,
|
|
104
|
+
includeEntities,
|
|
105
|
+
includeObservations
|
|
106
|
+
});
|
|
107
|
+
// If result is empty or has issues, return it as-is
|
|
108
|
+
return result;
|
|
109
|
+
}
|
|
110
|
+
async advancedSearch(params) {
|
|
111
|
+
return await this.server.advancedSearch(params);
|
|
112
|
+
}
|
|
113
|
+
async context(query, contextWindow, timeRangeHours) {
|
|
114
|
+
// Use advancedSearch with appropriate parameters
|
|
115
|
+
return await this.server.advancedSearch({
|
|
116
|
+
query,
|
|
117
|
+
limit: contextWindow || 10,
|
|
118
|
+
timeRangeHours
|
|
119
|
+
});
|
|
120
|
+
}
|
|
121
|
+
// Graph operations
|
|
122
|
+
async explore(startEntity, endEntity, maxHops, relationTypes) {
|
|
123
|
+
// Use graph_walking or advancedSearch
|
|
124
|
+
if (endEntity) {
|
|
125
|
+
// Path finding
|
|
126
|
+
return await this.server.computeShortestPath({
|
|
127
|
+
start_entity: startEntity,
|
|
128
|
+
end_entity: endEntity
|
|
129
|
+
});
|
|
130
|
+
}
|
|
131
|
+
else {
|
|
132
|
+
// Graph exploration - use advancedSearch with graph constraints
|
|
133
|
+
return await this.server.advancedSearch({
|
|
134
|
+
query: '',
|
|
135
|
+
graphConstraints: {
|
|
136
|
+
maxDepth: maxHops || 3,
|
|
137
|
+
requiredRelations: relationTypes,
|
|
138
|
+
targetEntityIds: [startEntity]
|
|
139
|
+
}
|
|
140
|
+
});
|
|
141
|
+
}
|
|
142
|
+
}
|
|
143
|
+
async pagerank() {
|
|
144
|
+
return await this.server.recomputePageRank();
|
|
145
|
+
}
|
|
146
|
+
async communities() {
|
|
147
|
+
return await this.server.recomputeCommunities();
|
|
148
|
+
}
|
|
149
|
+
// System operations
|
|
150
|
+
async health() {
|
|
151
|
+
const entityCount = await this.server.db.run('?[count(id)] := *entity{id, @ "NOW"}');
|
|
152
|
+
const obsCount = await this.server.db.run('?[count(id)] := *observation{id, @ "NOW"}');
|
|
153
|
+
const relCount = await this.server.db.run('?[count(from_id)] := *relationship{from_id, @ "NOW"}');
|
|
154
|
+
return {
|
|
155
|
+
status: "healthy",
|
|
156
|
+
entities: entityCount.rows[0][0],
|
|
157
|
+
observations: obsCount.rows[0][0],
|
|
158
|
+
relationships: relCount.rows[0][0]
|
|
159
|
+
};
|
|
160
|
+
}
|
|
161
|
+
async metrics() {
|
|
162
|
+
// Access private metrics via type assertion
|
|
163
|
+
return this.server.metrics;
|
|
164
|
+
}
|
|
165
|
+
async exportMemory(format, options) {
|
|
166
|
+
const { ExportImportService } = await import('./export-import-service.js');
|
|
167
|
+
// Create a simple wrapper that implements DbService interface
|
|
168
|
+
const dbService = {
|
|
169
|
+
run: async (query, params) => {
|
|
170
|
+
return await this.server.db.run(query, params);
|
|
171
|
+
}
|
|
172
|
+
};
|
|
173
|
+
const exportService = new ExportImportService(dbService);
|
|
174
|
+
return await exportService.exportMemory({
|
|
175
|
+
format,
|
|
176
|
+
includeMetadata: options?.includeMetadata,
|
|
177
|
+
includeRelationships: options?.includeRelationships,
|
|
178
|
+
includeObservations: options?.includeObservations,
|
|
179
|
+
entityTypes: options?.entityTypes,
|
|
180
|
+
since: options?.since
|
|
181
|
+
});
|
|
182
|
+
}
|
|
183
|
+
async importMemory(data, sourceFormat, options) {
|
|
184
|
+
const { ExportImportService } = await import('./export-import-service.js');
|
|
185
|
+
// Create a simple wrapper that implements DbService interface
|
|
186
|
+
const dbService = {
|
|
187
|
+
run: async (query, params) => {
|
|
188
|
+
return await this.server.db.run(query, params);
|
|
189
|
+
}
|
|
190
|
+
};
|
|
191
|
+
const exportService = new ExportImportService(dbService);
|
|
192
|
+
return await exportService.importMemory(data, {
|
|
193
|
+
sourceFormat: sourceFormat,
|
|
194
|
+
mergeStrategy: options?.mergeStrategy || 'skip',
|
|
195
|
+
defaultEntityType: options?.defaultEntityType
|
|
196
|
+
});
|
|
197
|
+
}
|
|
198
|
+
async ingestFile(entityId, format, filePath, content, options) {
|
|
199
|
+
// This would need to be implemented similar to the MCP tool
|
|
200
|
+
// For now, return a placeholder
|
|
201
|
+
return { status: "not_implemented", message: "Use MCP server for file ingestion" };
|
|
202
|
+
}
|
|
203
|
+
async editUserProfile(args) {
|
|
204
|
+
return await this.server.editUserProfile(args);
|
|
205
|
+
}
|
|
206
|
+
async getUserProfile() {
|
|
207
|
+
return await this.server.editUserProfile({});
|
|
208
|
+
}
|
|
209
|
+
}
|
|
210
|
+
exports.CLICommands = CLICommands;
|