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 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
@@ -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 embedding = await memoryServer.embeddingService.embed(name + " " + type);
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;