cozo-memory 1.0.3 → 1.0.5

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/index.js CHANGED
@@ -6,17 +6,18 @@ var __importDefault = (this && this.__importDefault) || function (mod) {
6
6
  Object.defineProperty(exports, "__esModule", { value: true });
7
7
  exports.MemoryServer = exports.USER_ENTITY_TYPE = exports.USER_ENTITY_NAME = exports.USER_ENTITY_ID = exports.DB_PATH = void 0;
8
8
  const embedding_service_1 = require("./embedding-service");
9
+ const export_import_service_1 = require("./export-import-service");
9
10
  const fastmcp_1 = require("fastmcp");
10
11
  const cozo_node_1 = require("cozo-node");
11
12
  const zod_1 = require("zod");
12
13
  const uuid_1 = require("uuid");
13
14
  const path_1 = __importDefault(require("path"));
15
+ const fs_1 = __importDefault(require("fs"));
16
+ const pdf_mjs_1 = require("pdfjs-dist/legacy/build/pdf.mjs");
14
17
  const hybrid_search_1 = require("./hybrid-search");
15
18
  const inference_engine_1 = require("./inference-engine");
16
19
  exports.DB_PATH = path_1.default.resolve(__dirname, "..", "memory_db.cozo");
17
20
  const DB_ENGINE = process.env.DB_ENGINE || "sqlite"; // "sqlite" or "rocksdb"
18
- const EMBEDDING_MODEL = "Xenova/bge-m3";
19
- const EMBEDDING_DIM = 1024;
20
21
  exports.USER_ENTITY_ID = "global_user_profile";
21
22
  exports.USER_ENTITY_NAME = "The User";
22
23
  exports.USER_ENTITY_TYPE = "User";
@@ -27,6 +28,47 @@ class MemoryServer {
27
28
  hybridSearch;
28
29
  inferenceEngine;
29
30
  initPromise;
31
+ // Metrics tracking
32
+ metrics = {
33
+ operations: {
34
+ create_entity: 0,
35
+ update_entity: 0,
36
+ delete_entity: 0,
37
+ add_observation: 0,
38
+ create_relation: 0,
39
+ search: 0,
40
+ graph_operations: 0
41
+ },
42
+ errors: {
43
+ total: 0,
44
+ by_operation: {}
45
+ },
46
+ performance: {
47
+ last_operation_ms: 0,
48
+ avg_operation_ms: 0,
49
+ total_operations: 0
50
+ }
51
+ };
52
+ trackOperation(operation, startTime) {
53
+ const duration = Date.now() - startTime;
54
+ this.metrics.performance.last_operation_ms = duration;
55
+ this.metrics.performance.total_operations++;
56
+ // Calculate running average
57
+ const prevAvg = this.metrics.performance.avg_operation_ms;
58
+ const n = this.metrics.performance.total_operations;
59
+ this.metrics.performance.avg_operation_ms = (prevAvg * (n - 1) + duration) / n;
60
+ // Track operation count
61
+ if (operation in this.metrics.operations) {
62
+ this.metrics.operations[operation]++;
63
+ }
64
+ }
65
+ trackError(operation) {
66
+ this.metrics.errors.total++;
67
+ if (!this.metrics.errors.by_operation[operation]) {
68
+ this.metrics.errors.by_operation[operation] = 0;
69
+ }
70
+ this.metrics.errors.by_operation[operation]++;
71
+ }
30
72
  constructor(dbPath = exports.DB_PATH) {
31
73
  const fullDbPath = DB_ENGINE === "sqlite" ? dbPath + ".db" : dbPath;
32
74
  this.db = new cozo_node_1.CozoDb(DB_ENGINE, fullDbPath);
@@ -442,6 +484,9 @@ class MemoryServer {
442
484
  async setupSchema() {
443
485
  try {
444
486
  console.error("[Schema] Initializing schema...");
487
+ // Get embedding dimensions from service
488
+ const EMBEDDING_DIM = this.embeddingService.getDimensions();
489
+ console.error(`[Schema] Using embedding dimensions: ${EMBEDDING_DIM}`);
445
490
  const existingRelations = await this.db.run("::relations");
446
491
  const relations = existingRelations.rows.map((r) => r[0]);
447
492
  // Entity Table
@@ -934,6 +979,7 @@ class MemoryServer {
934
979
  });
935
980
  }
936
981
  async createEntity(args) {
982
+ const startTime = Date.now();
937
983
  try {
938
984
  if (!args.name || args.name.trim() === "") {
939
985
  return { error: "Entity name must not be empty" };
@@ -957,9 +1003,12 @@ class MemoryServer {
957
1003
  }
958
1004
  }
959
1005
  const id = (0, uuid_1.v4)();
960
- return this.createEntityWithId(id, args.name, args.type, args.metadata);
1006
+ const result = await this.createEntityWithId(id, args.name, args.type, args.metadata);
1007
+ this.trackOperation('create_entity', startTime);
1008
+ return result;
961
1009
  }
962
1010
  catch (error) {
1011
+ this.trackError('create_entity');
963
1012
  console.error("Error in create_entity:", error);
964
1013
  if (error.display) {
965
1014
  console.error("CozoDB Error Details:", error.display);
@@ -1168,6 +1217,28 @@ class MemoryServer {
1168
1217
  if (toEntity.rows.length === 0) {
1169
1218
  return { error: `Target entity with ID '${args.to_id}' not found` };
1170
1219
  }
1220
+ // FIX: Check for duplicate relations
1221
+ try {
1222
+ const existingRel = await this.db.run('?[from_id, to_id, relation_type, strength, metadata] := *relationship{from_id, to_id, relation_type, strength, metadata, @ "NOW"}, from_id = $from, to_id = $to, relation_type = $type', { from: args.from_id, to: args.to_id, type: args.relation_type });
1223
+ if (existingRel.rows.length > 0) {
1224
+ // Update existing relation instead of creating duplicate
1225
+ const now = Date.now() * 1000;
1226
+ await this.db.run(`
1227
+ ?[from_id, to_id, relation_type, created_at, strength, metadata] <- [[$from_id, $to_id, $relation_type, [${now}, true], $strength, $metadata]]
1228
+ :put relationship {from_id, to_id, relation_type, created_at => strength, metadata}
1229
+ `, {
1230
+ from_id: args.from_id,
1231
+ to_id: args.to_id,
1232
+ relation_type: args.relation_type,
1233
+ strength: args.strength ?? 1.0,
1234
+ metadata: args.metadata || {}
1235
+ });
1236
+ return { status: "Relationship updated (was duplicate)" };
1237
+ }
1238
+ }
1239
+ catch (e) {
1240
+ console.error('[Relation] Duplicate check failed, proceeding with insert:', e);
1241
+ }
1171
1242
  const now = Date.now() * 1000;
1172
1243
  await this.db.run(`?[from_id, to_id, relation_type, created_at, strength, metadata] <- [[$from_id, $to_id, $relation_type, [${now}, true], $strength, $metadata]] :insert relationship {from_id, to_id, relation_type, created_at => strength, metadata}`, {
1173
1244
  from_id: args.from_id,
@@ -1596,7 +1667,39 @@ ids[id] <- $ids
1596
1667
  async ingestFile(args) {
1597
1668
  await this.initPromise;
1598
1669
  try {
1599
- const content = (args.content ?? "").trim();
1670
+ // Check that either file_path or content is provided
1671
+ if (!args.file_path && !args.content) {
1672
+ return { error: "Either file_path or content must be provided" };
1673
+ }
1674
+ // Read content from file if file_path is provided
1675
+ let content;
1676
+ if (args.file_path) {
1677
+ try {
1678
+ if (args.format === "pdf") {
1679
+ // Read PDF file and extract text using pdfjs-dist
1680
+ const data = new Uint8Array(fs_1.default.readFileSync(args.file_path));
1681
+ const loadingTask = (0, pdf_mjs_1.getDocument)({ data });
1682
+ const pdf = await loadingTask.promise;
1683
+ const numPages = pdf.numPages;
1684
+ const pageTextPromises = Array.from({ length: numPages }, async (_, i) => {
1685
+ const page = await pdf.getPage(i + 1);
1686
+ const textContent = await page.getTextContent();
1687
+ return textContent.items.map((item) => item.str).join(' ');
1688
+ });
1689
+ const pageTexts = await Promise.all(pageTextPromises);
1690
+ content = pageTexts.join('\n').trim();
1691
+ }
1692
+ else {
1693
+ content = fs_1.default.readFileSync(args.file_path, 'utf-8').trim();
1694
+ }
1695
+ }
1696
+ catch (error) {
1697
+ return { error: `Failed to read file: ${error.message}` };
1698
+ }
1699
+ }
1700
+ else {
1701
+ content = (args.content ?? "").trim();
1702
+ }
1600
1703
  if (!content)
1601
1704
  return { error: "Content must not be empty" };
1602
1705
  let entityId = undefined;
@@ -1633,7 +1736,7 @@ ids[id] <- $ids
1633
1736
  const deduplicate = args.deduplicate ?? true;
1634
1737
  const chunking = args.chunking ?? "none";
1635
1738
  const observations = [];
1636
- if (args.format === "markdown") {
1739
+ if (args.format === "markdown" || args.format === "pdf") {
1637
1740
  if (chunking === "paragraphs") {
1638
1741
  const parts = content
1639
1742
  .split(/\r?\n\s*\r?\n+/g)
@@ -1707,23 +1810,54 @@ ids[id] <- $ids
1707
1810
  }
1708
1811
  }
1709
1812
  async deleteEntity(args) {
1813
+ const startTime = Date.now();
1710
1814
  try {
1815
+ console.error(`[Delete] Starting deletion for entity: ${args.entity_id}`);
1711
1816
  // 1. Check if entity exists
1712
1817
  const entityRes = await this.db.run('?[name] := *entity{id: $id, name, @ "NOW"}', { id: args.entity_id });
1713
1818
  if (entityRes.rows.length === 0) {
1819
+ console.error(`[Delete] Entity not found: ${args.entity_id}`);
1714
1820
  return { error: `Entity with ID '${args.entity_id}' not found` };
1715
1821
  }
1716
- // 2. Delete all related data in a transaction (block)
1822
+ console.error(`[Delete] Entity found: ${entityRes.rows[0][0]}`);
1823
+ // 2. Count related data before deletion
1824
+ const obsCount = await this.db.run('?[count(id)] := *observation{id, entity_id, @ "NOW"}, entity_id = $id', { id: args.entity_id });
1825
+ const relOutCount = await this.db.run('?[count(from_id)] := *relationship{from_id, to_id, @ "NOW"}, from_id = $id', { id: args.entity_id });
1826
+ const relInCount = await this.db.run('?[count(to_id)] := *relationship{from_id, to_id, @ "NOW"}, to_id = $id', { id: args.entity_id });
1827
+ console.error(`[Delete] Related data: ${obsCount.rows[0][0]} observations, ${relOutCount.rows[0][0]} outgoing relations, ${relInCount.rows[0][0]} incoming relations`);
1828
+ // 3. Delete all related data in a transaction (block)
1829
+ console.error(`[Delete] Executing deletion transaction...`);
1717
1830
  await this.db.run(`
1718
1831
  { ?[id, created_at] := *observation{id, entity_id, created_at}, entity_id = $target_id :rm observation {id, created_at} }
1719
1832
  { ?[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} }
1720
1833
  { ?[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} }
1721
1834
  { ?[id, created_at] := *entity{id, created_at}, id = $target_id :rm entity {id, created_at} }
1722
1835
  `, { target_id: args.entity_id });
1723
- return { status: "Entity and all related data deleted" };
1836
+ // 4. Verify deletion
1837
+ const verifyEntity = await this.db.run('?[id] := *entity{id, @ "NOW"}, id = $id', { id: args.entity_id });
1838
+ const verifyObs = await this.db.run('?[count(id)] := *observation{id, entity_id, @ "NOW"}, entity_id = $id', { id: args.entity_id });
1839
+ const verifyRelOut = await this.db.run('?[count(from_id)] := *relationship{from_id, @ "NOW"}, from_id = $id', { id: args.entity_id });
1840
+ const verifyRelIn = await this.db.run('?[count(to_id)] := *relationship{to_id, @ "NOW"}, to_id = $id', { id: args.entity_id });
1841
+ console.error(`[Delete] Verification: entity exists=${verifyEntity.rows.length > 0}, observations=${verifyObs.rows[0][0]}, outgoing_relations=${verifyRelOut.rows[0][0]}, incoming_relations=${verifyRelIn.rows[0][0]}`);
1842
+ if (verifyEntity.rows.length > 0) {
1843
+ console.error(`[Delete] WARNING: Entity still visible after deletion!`);
1844
+ }
1845
+ else {
1846
+ console.error(`[Delete] ✓ Entity successfully deleted`);
1847
+ }
1848
+ this.trackOperation('delete_entity', startTime);
1849
+ return {
1850
+ status: "Entity and all related data deleted",
1851
+ deleted: {
1852
+ observations: obsCount.rows[0][0],
1853
+ outgoing_relations: relOutCount.rows[0][0],
1854
+ incoming_relations: relInCount.rows[0][0]
1855
+ }
1856
+ };
1724
1857
  }
1725
1858
  catch (error) {
1726
- console.error("Error during deletion:", error);
1859
+ console.error("[Delete] Error during deletion:", error);
1860
+ this.trackError('delete_entity');
1727
1861
  return { error: "Deletion failed", message: error.message };
1728
1862
  }
1729
1863
  }
@@ -1865,6 +1999,26 @@ ids[id] <- $ids
1865
1999
  to_id = existingId;
1866
2000
  }
1867
2001
  }
2002
+ // FIX: Validate that both entities exist before creating relation
2003
+ try {
2004
+ const [fromExists, toExists] = await Promise.all([
2005
+ this.db.run('?[id, name] := *entity{id, name, @ "NOW"}, id = $id', { id: from_id }),
2006
+ this.db.run('?[id, name] := *entity{id, name, @ "NOW"}, id = $id', { id: to_id })
2007
+ ]);
2008
+ if (fromExists.rows.length === 0) {
2009
+ return { error: `Transaction failed: Source entity '${from_id}' not found in operation ${i}` };
2010
+ }
2011
+ if (toExists.rows.length === 0) {
2012
+ return { error: `Transaction failed: Target entity '${to_id}' not found in operation ${i}` };
2013
+ }
2014
+ }
2015
+ catch (e) {
2016
+ return { error: `Transaction failed: Entity validation error in operation ${i}: ${e.message}` };
2017
+ }
2018
+ // FIX: Check for self-reference
2019
+ if (from_id === to_id) {
2020
+ return { error: `Transaction failed: Self-references not allowed in operation ${i}` };
2021
+ }
1868
2022
  const now = Date.now() * 1000;
1869
2023
  allParams[`rel_from${suffix}`] = from_id;
1870
2024
  allParams[`rel_to${suffix}`] = to_id;
@@ -1991,6 +2145,22 @@ ids[id] <- $ids
1991
2145
  await this.initPromise;
1992
2146
  return this.hybridSearch.graphRag(args);
1993
2147
  }
2148
+ async exportMemory(args) {
2149
+ await this.initPromise;
2150
+ const dbService = { run: (query, params) => this.db.run(query, params) };
2151
+ const exportService = new export_import_service_1.ExportImportService(dbService);
2152
+ return exportService.exportMemory(args);
2153
+ }
2154
+ async importMemory(args) {
2155
+ await this.initPromise;
2156
+ const dbService = { run: (query, params) => this.db.run(query, params) };
2157
+ const exportService = new export_import_service_1.ExportImportService(dbService);
2158
+ return exportService.importMemory(args.data, {
2159
+ sourceFormat: args.sourceFormat,
2160
+ mergeStrategy: args.mergeStrategy,
2161
+ defaultEntityType: args.defaultEntityType
2162
+ });
2163
+ }
1994
2164
  registerTools() {
1995
2165
  const MetadataSchema = zod_1.z.record(zod_1.z.string(), zod_1.z.any());
1996
2166
  const MutateMemorySchema = zod_1.z.discriminatedUnion("action", [
@@ -2095,9 +2265,10 @@ ids[id] <- $ids
2095
2265
  entity_id: zod_1.z.string().optional().describe("ID of the target entity"),
2096
2266
  entity_name: zod_1.z.string().optional().describe("Name of the target entity (will be created if not exists)"),
2097
2267
  entity_type: zod_1.z.string().optional().default("Document").describe("Type of the target entity (only when creating)"),
2098
- format: zod_1.z.enum(["markdown", "json"]).describe("Input format"),
2268
+ format: zod_1.z.enum(["markdown", "json", "pdf"]).describe("Input format"),
2099
2269
  chunking: zod_1.z.enum(["none", "paragraphs"]).optional().default("none").describe("Chunking for Markdown"),
2100
- content: zod_1.z.string().describe("File content (or LLM summary)"),
2270
+ file_path: zod_1.z.string().optional().describe("Path to file on disk (alternative to content parameter)"),
2271
+ content: zod_1.z.string().optional().describe("File content (or LLM summary) - required if file_path not provided"),
2101
2272
  metadata: MetadataSchema.optional().describe("Metadata for entity creation"),
2102
2273
  observation_metadata: MetadataSchema.optional().describe("Metadata applied to all observations"),
2103
2274
  deduplicate: zod_1.z.boolean().optional().default(true).describe("Skip exact duplicates"),
@@ -2105,6 +2276,9 @@ ids[id] <- $ids
2105
2276
  }).refine((v) => Boolean(v.entity_id) || Boolean(v.entity_name), {
2106
2277
  message: "entity_id or entity_name is required for ingest_file",
2107
2278
  path: ["entity_id"],
2279
+ }).refine((v) => Boolean(v.file_path) || Boolean(v.content), {
2280
+ message: "file_path or content is required for ingest_file",
2281
+ path: ["file_path"],
2108
2282
  }),
2109
2283
  ]);
2110
2284
  const MutateMemoryParameters = zod_1.z.object({
@@ -2119,9 +2293,10 @@ ids[id] <- $ids
2119
2293
  entity_type: zod_1.z.string().optional().describe("Only when entity_name is used and entity is created new"),
2120
2294
  text: zod_1.z.string().optional().describe("For add_observation (required)"),
2121
2295
  datalog: zod_1.z.string().optional().describe("For add_inference_rule (required)"),
2122
- format: zod_1.z.enum(["markdown", "json"]).optional().describe("For ingest_file (required)"),
2296
+ format: zod_1.z.enum(["markdown", "json", "pdf"]).optional().describe("For ingest_file (required)"),
2123
2297
  chunking: zod_1.z.enum(["none", "paragraphs"]).optional().describe("Optional for ingest_file (for markdown)"),
2124
- content: zod_1.z.string().optional().describe("For ingest_file (required)"),
2298
+ file_path: zod_1.z.string().optional().describe("For ingest_file - path to file on disk (alternative to content)"),
2299
+ content: zod_1.z.string().optional().describe("For ingest_file - file content (required if file_path not provided)"),
2125
2300
  observation_metadata: MetadataSchema.optional().describe("Optional for ingest_file"),
2126
2301
  deduplicate: zod_1.z.boolean().optional().describe("Optional for ingest_file and add_observation"),
2127
2302
  max_observations: zod_1.z.number().optional().describe("Optional for ingest_file"),
@@ -2881,6 +3056,23 @@ Supported actions:
2881
3056
  });
2882
3057
  const ManageSystemSchema = zod_1.z.discriminatedUnion("action", [
2883
3058
  zod_1.z.object({ action: zod_1.z.literal("health") }),
3059
+ zod_1.z.object({ action: zod_1.z.literal("metrics") }),
3060
+ zod_1.z.object({
3061
+ action: zod_1.z.literal("export_memory"),
3062
+ format: zod_1.z.enum(["json", "markdown", "obsidian"]).describe("Export format"),
3063
+ includeMetadata: zod_1.z.boolean().optional().describe("Include metadata in export"),
3064
+ includeRelationships: zod_1.z.boolean().optional().describe("Include relationships in export"),
3065
+ includeObservations: zod_1.z.boolean().optional().describe("Include observations in export"),
3066
+ entityTypes: zod_1.z.array(zod_1.z.string()).optional().describe("Filter by entity types"),
3067
+ since: zod_1.z.number().optional().describe("Unix timestamp (ms) - only export data created after this time"),
3068
+ }),
3069
+ zod_1.z.object({
3070
+ action: zod_1.z.literal("import_memory"),
3071
+ data: zod_1.z.union([zod_1.z.string(), zod_1.z.any()]).describe("Import data (JSON string or object)"),
3072
+ sourceFormat: zod_1.z.enum(["mem0", "memgpt", "markdown", "cozo"]).describe("Source format"),
3073
+ mergeStrategy: zod_1.z.enum(["skip", "overwrite", "merge"]).optional().default("skip").describe("How to handle existing entities"),
3074
+ defaultEntityType: zod_1.z.string().optional().default("Note").describe("Default type for imported entities"),
3075
+ }),
2884
3076
  zod_1.z.object({
2885
3077
  action: zod_1.z.literal("snapshot_create"),
2886
3078
  metadata: MetadataSchema.optional().describe("Additional metadata for the snapshot"),
@@ -2911,8 +3103,18 @@ Supported actions:
2911
3103
  ]);
2912
3104
  const ManageSystemParameters = zod_1.z.object({
2913
3105
  action: zod_1.z
2914
- .enum(["health", "snapshot_create", "snapshot_list", "snapshot_diff", "cleanup", "reflect", "clear_memory"])
3106
+ .enum(["health", "metrics", "export_memory", "import_memory", "snapshot_create", "snapshot_list", "snapshot_diff", "cleanup", "reflect", "clear_memory"])
2915
3107
  .describe("Action (determines which fields are required)"),
3108
+ format: zod_1.z.enum(["json", "markdown", "obsidian"]).optional().describe("Export format (for export_memory)"),
3109
+ includeMetadata: zod_1.z.boolean().optional().describe("Include metadata (for export_memory)"),
3110
+ includeRelationships: zod_1.z.boolean().optional().describe("Include relationships (for export_memory)"),
3111
+ includeObservations: zod_1.z.boolean().optional().describe("Include observations (for export_memory)"),
3112
+ entityTypes: zod_1.z.array(zod_1.z.string()).optional().describe("Filter by entity types (for export_memory)"),
3113
+ since: zod_1.z.number().optional().describe("Unix timestamp in ms (for export_memory)"),
3114
+ data: zod_1.z.union([zod_1.z.string(), zod_1.z.any()]).optional().describe("Import data (for import_memory)"),
3115
+ sourceFormat: zod_1.z.enum(["mem0", "memgpt", "markdown", "cozo"]).optional().describe("Source format (for import_memory)"),
3116
+ mergeStrategy: zod_1.z.enum(["skip", "overwrite", "merge"]).optional().describe("Merge strategy (for import_memory)"),
3117
+ defaultEntityType: zod_1.z.string().optional().describe("Default entity type (for import_memory)"),
2916
3118
  snapshot_id_a: zod_1.z.string().optional().describe("Required for snapshot_diff"),
2917
3119
  snapshot_id_b: zod_1.z.string().optional().describe("Required for snapshot_diff"),
2918
3120
  metadata: MetadataSchema.optional().describe("Optional for snapshot_create"),
@@ -2927,7 +3129,17 @@ Supported actions:
2927
3129
  name: "manage_system",
2928
3130
  description: `System maintenance and memory management. Select operation via 'action'.
2929
3131
  Supported actions:
2930
- - 'health': Status check. Returns DB counts and embedding cache statistics.
3132
+ - 'health': Status check. Returns DB counts, embedding cache statistics, and performance metrics.
3133
+ - 'metrics': Detailed metrics. Returns operation counts, error statistics, and performance data.
3134
+ - 'export_memory': Export memory to various formats. Params: { format: 'json'|'markdown'|'obsidian', includeMetadata?: boolean, includeRelationships?: boolean, includeObservations?: boolean, entityTypes?: string[], since?: number }.
3135
+ * format='json': Native Cozo format (re-importable)
3136
+ * format='markdown': Human-readable markdown document
3137
+ * format='obsidian': ZIP archive with wiki-links, ready for Obsidian vault
3138
+ - 'import_memory': Import memory from external sources. Params: { data: string|object, sourceFormat: 'mem0'|'memgpt'|'markdown'|'cozo', mergeStrategy?: 'skip'|'overwrite'|'merge', defaultEntityType?: string }.
3139
+ * sourceFormat='mem0': Import from Mem0 format (user_id becomes entity)
3140
+ * sourceFormat='memgpt': Import from MemGPT archival/recall memory
3141
+ * sourceFormat='markdown': Parse markdown sections as entities
3142
+ * sourceFormat='cozo': Import from native Cozo JSON export
2931
3143
  - 'snapshot_create': Creates a backup point. Params: { metadata?: object }.
2932
3144
  - 'snapshot_list': Lists all available snapshots.
2933
3145
  - 'snapshot_diff': Compares two snapshots. Params: { snapshot_id_a: string, snapshot_id_b: string }.
@@ -2957,7 +3169,16 @@ Supported actions:
2957
3169
  observations: obsResult.rows.length,
2958
3170
  relations: relResult.rows.length,
2959
3171
  },
2960
- performance: { embedding_cache: this.embeddingService.getCacheStats() },
3172
+ performance: {
3173
+ embedding_cache: this.embeddingService.getCacheStats(),
3174
+ last_operation_ms: this.metrics.performance.last_operation_ms,
3175
+ avg_operation_ms: this.metrics.performance.avg_operation_ms,
3176
+ total_operations: this.metrics.performance.total_operations
3177
+ },
3178
+ metrics: {
3179
+ operations: this.metrics.operations,
3180
+ errors: this.metrics.errors
3181
+ },
2961
3182
  timestamp: new Date().toISOString(),
2962
3183
  });
2963
3184
  }
@@ -2965,6 +3186,60 @@ Supported actions:
2965
3186
  return JSON.stringify({ status: "error", error: error.message, timestamp: new Date().toISOString() });
2966
3187
  }
2967
3188
  }
3189
+ if (input.action === "metrics") {
3190
+ try {
3191
+ return JSON.stringify({
3192
+ operations: this.metrics.operations,
3193
+ errors: this.metrics.errors,
3194
+ performance: this.metrics.performance,
3195
+ embedding_cache: this.embeddingService.getCacheStats(),
3196
+ timestamp: new Date().toISOString()
3197
+ });
3198
+ }
3199
+ catch (error) {
3200
+ return JSON.stringify({ error: "Failed to retrieve metrics", message: error.message });
3201
+ }
3202
+ }
3203
+ if (input.action === "export_memory") {
3204
+ try {
3205
+ const result = await this.exportMemory({
3206
+ format: input.format,
3207
+ includeMetadata: input.includeMetadata,
3208
+ includeRelationships: input.includeRelationships,
3209
+ includeObservations: input.includeObservations,
3210
+ entityTypes: input.entityTypes,
3211
+ since: input.since
3212
+ });
3213
+ if (result.format === 'obsidian' && result.zipBuffer) {
3214
+ // For Obsidian ZIP, return base64 encoded buffer
3215
+ return JSON.stringify({
3216
+ format: 'obsidian',
3217
+ data: result.zipBuffer.toString('base64'),
3218
+ stats: result.stats,
3219
+ encoding: 'base64',
3220
+ filename: `memory_export_${Date.now()}.zip`
3221
+ });
3222
+ }
3223
+ return JSON.stringify(result);
3224
+ }
3225
+ catch (error) {
3226
+ return JSON.stringify({ error: "Export failed", message: error.message });
3227
+ }
3228
+ }
3229
+ if (input.action === "import_memory") {
3230
+ try {
3231
+ const result = await this.importMemory({
3232
+ data: input.data,
3233
+ sourceFormat: input.sourceFormat,
3234
+ mergeStrategy: input.mergeStrategy,
3235
+ defaultEntityType: input.defaultEntityType
3236
+ });
3237
+ return JSON.stringify(result);
3238
+ }
3239
+ catch (error) {
3240
+ return JSON.stringify({ error: "Import failed", message: error.message });
3241
+ }
3242
+ }
2968
3243
  if (input.action === "snapshot_create") {
2969
3244
  try {
2970
3245
  // Optimization: Sequential execution and count aggregation instead of full fetch
@@ -324,9 +324,16 @@ class InferenceEngine {
324
324
  for (const r of res.rows) {
325
325
  if (!r || r.length < 5)
326
326
  continue;
327
+ const fromId = String(r[0]);
328
+ const toId = String(r[1]);
329
+ // FIX: Filter out self-references
330
+ if (fromId === toId) {
331
+ console.error(`[Inference] Skipping self-reference in custom rule ${ruleName}: ${fromId} -> ${toId}`);
332
+ continue;
333
+ }
327
334
  results.push({
328
- from_id: String(r[0]),
329
- to_id: String(r[1]),
335
+ from_id: fromId,
336
+ to_id: toId,
330
337
  relation_type: String(r[2]),
331
338
  confidence: Number(r[3]),
332
339
  reason: String(r[4]),
@@ -1,7 +1,42 @@
1
1
  "use strict";
2
+ var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
3
+ if (k2 === undefined) k2 = k;
4
+ var desc = Object.getOwnPropertyDescriptor(m, k);
5
+ if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
6
+ desc = { enumerable: true, get: function() { return m[k]; } };
7
+ }
8
+ Object.defineProperty(o, k2, desc);
9
+ }) : (function(o, m, k, k2) {
10
+ if (k2 === undefined) k2 = k;
11
+ o[k2] = m[k];
12
+ }));
13
+ var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
14
+ Object.defineProperty(o, "default", { enumerable: true, value: v });
15
+ }) : function(o, v) {
16
+ o["default"] = v;
17
+ });
18
+ var __importStar = (this && this.__importStar) || (function () {
19
+ var ownKeys = function(o) {
20
+ ownKeys = Object.getOwnPropertyNames || function (o) {
21
+ var ar = [];
22
+ for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k;
23
+ return ar;
24
+ };
25
+ return ownKeys(o);
26
+ };
27
+ return function (mod) {
28
+ if (mod && mod.__esModule) return mod;
29
+ var result = {};
30
+ if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]);
31
+ __setModuleDefault(result, mod);
32
+ return result;
33
+ };
34
+ })();
2
35
  Object.defineProperty(exports, "__esModule", { value: true });
3
36
  exports.MemoryService = void 0;
4
37
  const uuid_1 = require("uuid");
38
+ const pdf_mjs_1 = require("pdfjs-dist/legacy/build/pdf.mjs");
39
+ const fs = __importStar(require("fs"));
5
40
  class MemoryService {
6
41
  db;
7
42
  embeddings;
@@ -169,7 +204,7 @@ class MemoryService {
169
204
  console.error('[MemoryService] Snapshot created:', snapshotId, stats);
170
205
  return snapshotId;
171
206
  }
172
- async ingestFile(content, format, entityName, entityType = 'Document', chunking = 'paragraphs') {
207
+ async ingestFile(content, format, entityName, entityType = 'Document', chunking = 'paragraphs', filePath) {
173
208
  const searchResults = await this.search(entityName, 1);
174
209
  let entity;
175
210
  if (searchResults.length > 0 && searchResults[0].entity.name.toLowerCase() === entityName.toLowerCase()) {
@@ -179,16 +214,64 @@ class MemoryService {
179
214
  entity = await this.createEntity(entityName, entityType, { format: format });
180
215
  }
181
216
  let chunks = [];
182
- if (format === 'markdown' && chunking === 'paragraphs') {
183
- chunks = content.split(/\n\s*\n/).filter((c) => c.trim().length > 0);
217
+ if (format === 'pdf') {
218
+ try {
219
+ let data;
220
+ // If filePath is provided, read from file
221
+ if (filePath) {
222
+ data = new Uint8Array(fs.readFileSync(filePath));
223
+ }
224
+ else {
225
+ // Otherwise, assume content is base64
226
+ const buffer = Buffer.from(content, 'base64');
227
+ data = new Uint8Array(buffer);
228
+ }
229
+ const loadingTask = (0, pdf_mjs_1.getDocument)({ data });
230
+ const pdf = await loadingTask.promise;
231
+ const numPages = pdf.numPages;
232
+ const pageTextPromises = Array.from({ length: numPages }, async (_, i) => {
233
+ const page = await pdf.getPage(i + 1);
234
+ const textContent = await page.getTextContent();
235
+ return textContent.items.map((item) => item.str).join(' ');
236
+ });
237
+ const pageTexts = await Promise.all(pageTextPromises);
238
+ const text = pageTexts.join('\n');
239
+ if (chunking === 'paragraphs') {
240
+ chunks = text.split(/\n\s*\n/).filter((c) => c.trim().length > 0);
241
+ }
242
+ else {
243
+ chunks = [text];
244
+ }
245
+ }
246
+ catch (e) {
247
+ console.error('[MemoryService] PDF parsing error:', e);
248
+ throw new Error(`Failed to parse PDF: ${e instanceof Error ? e.message : String(e)}`);
249
+ }
250
+ }
251
+ else if (format === 'markdown') {
252
+ // For markdown, also support file path
253
+ let textContent = content;
254
+ if (filePath) {
255
+ textContent = fs.readFileSync(filePath, 'utf-8');
256
+ }
257
+ if (chunking === 'paragraphs') {
258
+ chunks = textContent.split(/\n\s*\n/).filter((c) => c.trim().length > 0);
259
+ }
260
+ else {
261
+ chunks = [textContent];
262
+ }
184
263
  }
185
264
  else if (format === 'json') {
265
+ let textContent = content;
266
+ if (filePath) {
267
+ textContent = fs.readFileSync(filePath, 'utf-8');
268
+ }
186
269
  try {
187
- const data = JSON.parse(content);
270
+ const data = JSON.parse(textContent);
188
271
  chunks = [JSON.stringify(data, null, 2)];
189
272
  }
190
273
  catch (e) {
191
- chunks = [content];
274
+ chunks = [textContent];
192
275
  }
193
276
  }
194
277
  else {