agent-memory-graph 0.1.0

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.
Files changed (58) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +341 -0
  3. package/config/default.json +28 -0
  4. package/config/graph.config.json +28 -0
  5. package/dist/cli/index.d.ts +3 -0
  6. package/dist/cli/index.d.ts.map +1 -0
  7. package/dist/cli/index.js +303 -0
  8. package/dist/cli/index.js.map +1 -0
  9. package/dist/plugin/entry.d.ts +3 -0
  10. package/dist/plugin/entry.d.ts.map +1 -0
  11. package/dist/plugin/entry.js +1652 -0
  12. package/dist/plugin/entry.js.map +1 -0
  13. package/dist/src/config/defaults.d.ts +8 -0
  14. package/dist/src/config/defaults.d.ts.map +1 -0
  15. package/dist/src/config/defaults.js +31 -0
  16. package/dist/src/config/defaults.js.map +1 -0
  17. package/dist/src/config/schema.d.ts +162 -0
  18. package/dist/src/config/schema.d.ts.map +1 -0
  19. package/dist/src/config/schema.js +39 -0
  20. package/dist/src/config/schema.js.map +1 -0
  21. package/dist/src/extract/dedup.d.ts +14 -0
  22. package/dist/src/extract/dedup.d.ts.map +1 -0
  23. package/dist/src/extract/dedup.js +79 -0
  24. package/dist/src/extract/dedup.js.map +1 -0
  25. package/dist/src/extract/extractor.d.ts +24 -0
  26. package/dist/src/extract/extractor.d.ts.map +1 -0
  27. package/dist/src/extract/extractor.js +162 -0
  28. package/dist/src/extract/extractor.js.map +1 -0
  29. package/dist/src/graph/engine.d.ts +90 -0
  30. package/dist/src/graph/engine.d.ts.map +1 -0
  31. package/dist/src/graph/engine.js +307 -0
  32. package/dist/src/graph/engine.js.map +1 -0
  33. package/dist/src/graph/schema.d.ts +12 -0
  34. package/dist/src/graph/schema.d.ts.map +1 -0
  35. package/dist/src/graph/schema.js +115 -0
  36. package/dist/src/graph/schema.js.map +1 -0
  37. package/dist/src/index.d.ts +129 -0
  38. package/dist/src/index.d.ts.map +1 -0
  39. package/dist/src/index.js +174 -0
  40. package/dist/src/index.js.map +1 -0
  41. package/dist/src/search/hybrid.d.ts +22 -0
  42. package/dist/src/search/hybrid.d.ts.map +1 -0
  43. package/dist/src/search/hybrid.js +38 -0
  44. package/dist/src/search/hybrid.js.map +1 -0
  45. package/dist/src/search/natural-language.d.ts +20 -0
  46. package/dist/src/search/natural-language.d.ts.map +1 -0
  47. package/dist/src/search/natural-language.js +429 -0
  48. package/dist/src/search/natural-language.js.map +1 -0
  49. package/dist/src/sync/export.d.ts +12 -0
  50. package/dist/src/sync/export.d.ts.map +1 -0
  51. package/dist/src/sync/export.js +117 -0
  52. package/dist/src/sync/export.js.map +1 -0
  53. package/dist/src/sync/memory-md.d.ts +19 -0
  54. package/dist/src/sync/memory-md.d.ts.map +1 -0
  55. package/dist/src/sync/memory-md.js +78 -0
  56. package/dist/src/sync/memory-md.js.map +1 -0
  57. package/openclaw.plugin.json +55 -0
  58. package/package.json +90 -0
@@ -0,0 +1,1652 @@
1
+ var __defProp = Object.defineProperty;
2
+ var __getOwnPropNames = Object.getOwnPropertyNames;
3
+ var __esm = (fn, res) => function __init() {
4
+ return fn && (res = (0, fn[__getOwnPropNames(fn)[0]])(fn = 0)), res;
5
+ };
6
+ var __export = (target, all) => {
7
+ for (var name in all)
8
+ __defProp(target, name, { get: all[name], enumerable: true });
9
+ };
10
+
11
+ // src/graph/schema.js
12
+ import Database from "better-sqlite3";
13
+ import { resolve } from "node:path";
14
+ import { mkdirSync } from "node:fs";
15
+ var SCHEMA_VERSION, SCHEMA_SQL, SchemaManager;
16
+ var init_schema = __esm({
17
+ "src/graph/schema.js"() {
18
+ "use strict";
19
+ SCHEMA_VERSION = 1;
20
+ SCHEMA_SQL = `
21
+ -- Schema version tracking
22
+ CREATE TABLE IF NOT EXISTS _meta (
23
+ key TEXT PRIMARY KEY,
24
+ value TEXT
25
+ );
26
+
27
+ -- Entities (graph nodes)
28
+ CREATE TABLE IF NOT EXISTS entities (
29
+ id TEXT PRIMARY KEY,
30
+ name TEXT NOT NULL,
31
+ type TEXT NOT NULL,
32
+ properties TEXT DEFAULT '{}',
33
+ created_at TEXT NOT NULL DEFAULT (datetime('now')),
34
+ updated_at TEXT NOT NULL DEFAULT (datetime('now')),
35
+ source TEXT,
36
+ confidence REAL DEFAULT 1.0
37
+ );
38
+
39
+ -- Relationships (graph edges)
40
+ CREATE TABLE IF NOT EXISTS relationships (
41
+ id TEXT PRIMARY KEY,
42
+ from_id TEXT NOT NULL REFERENCES entities(id) ON DELETE CASCADE,
43
+ to_id TEXT NOT NULL REFERENCES entities(id) ON DELETE CASCADE,
44
+ relation TEXT NOT NULL,
45
+ properties TEXT DEFAULT '{}',
46
+ created_at TEXT NOT NULL DEFAULT (datetime('now')),
47
+ updated_at TEXT NOT NULL DEFAULT (datetime('now')),
48
+ source TEXT,
49
+ confidence REAL DEFAULT 1.0
50
+ );
51
+
52
+ -- Memory log (audit trail of extractions)
53
+ CREATE TABLE IF NOT EXISTS memory_log (
54
+ id TEXT PRIMARY KEY,
55
+ raw_text TEXT NOT NULL,
56
+ extracted_entities TEXT DEFAULT '[]',
57
+ extracted_relations TEXT DEFAULT '[]',
58
+ timestamp TEXT NOT NULL DEFAULT (datetime('now')),
59
+ session_id TEXT
60
+ );
61
+
62
+ -- Indexes for fast lookups
63
+ CREATE INDEX IF NOT EXISTS idx_entities_type ON entities(type);
64
+ CREATE INDEX IF NOT EXISTS idx_entities_name ON entities(name COLLATE NOCASE);
65
+ CREATE INDEX IF NOT EXISTS idx_rel_from ON relationships(from_id);
66
+ CREATE INDEX IF NOT EXISTS idx_rel_to ON relationships(to_id);
67
+ CREATE INDEX IF NOT EXISTS idx_rel_relation ON relationships(relation);
68
+ CREATE INDEX IF NOT EXISTS idx_rel_pair ON relationships(from_id, to_id, relation);
69
+
70
+ -- Full-text search on entities
71
+ CREATE VIRTUAL TABLE IF NOT EXISTS entities_fts USING fts5(
72
+ name,
73
+ type,
74
+ properties,
75
+ content=entities,
76
+ content_rowid=rowid
77
+ );
78
+
79
+ -- FTS triggers to keep in sync
80
+ CREATE TRIGGER IF NOT EXISTS entities_ai AFTER INSERT ON entities BEGIN
81
+ INSERT INTO entities_fts(rowid, name, type, properties)
82
+ VALUES (new.rowid, new.name, new.type, new.properties);
83
+ END;
84
+
85
+ CREATE TRIGGER IF NOT EXISTS entities_ad AFTER DELETE ON entities BEGIN
86
+ INSERT INTO entities_fts(entities_fts, rowid, name, type, properties)
87
+ VALUES ('delete', old.rowid, old.name, old.type, old.properties);
88
+ END;
89
+
90
+ CREATE TRIGGER IF NOT EXISTS entities_au AFTER UPDATE ON entities BEGIN
91
+ INSERT INTO entities_fts(entities_fts, rowid, name, type, properties)
92
+ VALUES ('delete', old.rowid, old.name, old.type, old.properties);
93
+ INSERT INTO entities_fts(rowid, name, type, properties)
94
+ VALUES (new.rowid, new.name, new.type, new.properties);
95
+ END;
96
+ `;
97
+ SchemaManager = class {
98
+ db;
99
+ constructor(dbPath) {
100
+ const dir = resolve(dbPath, "..");
101
+ mkdirSync(dir, { recursive: true });
102
+ this.db = new Database(dbPath);
103
+ this.db.pragma("journal_mode = WAL");
104
+ this.db.pragma("foreign_keys = ON");
105
+ }
106
+ /** Initialize schema (idempotent) */
107
+ initialize() {
108
+ this.db.exec(SCHEMA_SQL);
109
+ const stmt = this.db.prepare(`INSERT OR REPLACE INTO _meta (key, value) VALUES ('schema_version', ?)`);
110
+ stmt.run(String(SCHEMA_VERSION));
111
+ return this.db;
112
+ }
113
+ /** Get current schema version */
114
+ getVersion() {
115
+ try {
116
+ const row = this.db.prepare(`SELECT value FROM _meta WHERE key = 'schema_version'`).get();
117
+ return row ? parseInt(row.value, 10) : 0;
118
+ } catch {
119
+ return 0;
120
+ }
121
+ }
122
+ /** Close database connection */
123
+ close() {
124
+ this.db.close();
125
+ }
126
+ };
127
+ }
128
+ });
129
+
130
+ // src/graph/engine.js
131
+ import { nanoid } from "nanoid";
132
+ var GraphEngine;
133
+ var init_engine = __esm({
134
+ "src/graph/engine.js"() {
135
+ "use strict";
136
+ init_schema();
137
+ GraphEngine = class {
138
+ db;
139
+ constructor(dbPath) {
140
+ const schema = new SchemaManager(dbPath);
141
+ this.db = schema.initialize();
142
+ }
143
+ // ─── Entity CRUD ───────────────────────────────────────────────
144
+ addEntity(name, type, properties = {}, options = {}) {
145
+ const existing = this.findEntityByName(name, type);
146
+ if (existing) {
147
+ return this.updateEntity(existing.id, { properties, ...options });
148
+ }
149
+ const id = `e-${nanoid(12)}`;
150
+ const now = (/* @__PURE__ */ new Date()).toISOString();
151
+ this.db.prepare(`
152
+ INSERT INTO entities (id, name, type, properties, created_at, updated_at, source, confidence)
153
+ VALUES (?, ?, ?, ?, ?, ?, ?, ?)
154
+ `).run(id, name, type, JSON.stringify(properties), now, now, options.source ?? null, options.confidence ?? 1);
155
+ return { id, name, type, properties, created_at: now, updated_at: now, source: options.source, confidence: options.confidence ?? 1 };
156
+ }
157
+ getEntity(id) {
158
+ const row = this.db.prepare(`SELECT * FROM entities WHERE id = ?`).get(id);
159
+ return row ? this.rowToEntity(row) : null;
160
+ }
161
+ findEntityByName(name, type) {
162
+ const query = type ? `SELECT * FROM entities WHERE name = ? COLLATE NOCASE AND type = ? COLLATE NOCASE LIMIT 1` : `SELECT * FROM entities WHERE name = ? COLLATE NOCASE LIMIT 1`;
163
+ const row = type ? this.db.prepare(query).get(name, type) : this.db.prepare(query).get(name);
164
+ return row ? this.rowToEntity(row) : null;
165
+ }
166
+ updateEntity(id, updates) {
167
+ const existing = this.getEntity(id);
168
+ if (!existing)
169
+ throw new Error(`Entity ${id} not found`);
170
+ const merged = {
171
+ name: updates.name ?? existing.name,
172
+ type: updates.type ?? existing.type,
173
+ properties: updates.properties ? { ...existing.properties, ...updates.properties } : existing.properties,
174
+ source: updates.source ?? existing.source,
175
+ confidence: updates.confidence ?? existing.confidence
176
+ };
177
+ const now = (/* @__PURE__ */ new Date()).toISOString();
178
+ this.db.prepare(`
179
+ UPDATE entities SET name = ?, type = ?, properties = ?, source = ?, confidence = ?, updated_at = ?
180
+ WHERE id = ?
181
+ `).run(merged.name, merged.type, JSON.stringify(merged.properties), merged.source ?? null, merged.confidence, now, id);
182
+ return { ...existing, ...merged, updated_at: now };
183
+ }
184
+ deleteEntity(id) {
185
+ const result = this.db.prepare(`DELETE FROM entities WHERE id = ?`).run(id);
186
+ return result.changes > 0;
187
+ }
188
+ listEntities(options = {}) {
189
+ const { type, limit = 100, offset = 0 } = options;
190
+ const query = type ? `SELECT * FROM entities WHERE type = ? COLLATE NOCASE ORDER BY updated_at DESC LIMIT ? OFFSET ?` : `SELECT * FROM entities ORDER BY updated_at DESC LIMIT ? OFFSET ?`;
191
+ const rows = type ? this.db.prepare(query).all(type, limit, offset) : this.db.prepare(query).all(limit, offset);
192
+ return rows.map((r) => this.rowToEntity(r));
193
+ }
194
+ // ─── Relationship CRUD ─────────────────────────────────────────
195
+ addRelation(fromName, relation, toName, options = {}) {
196
+ let fromEntity = this.findEntityByName(fromName);
197
+ if (!fromEntity) {
198
+ fromEntity = this.addEntity(fromName, options.fromType ?? "Unknown", {}, { source: options.source });
199
+ }
200
+ let toEntity = this.findEntityByName(toName);
201
+ if (!toEntity) {
202
+ toEntity = this.addEntity(toName, options.toType ?? "Unknown", {}, { source: options.source });
203
+ }
204
+ const existing = this.db.prepare(`
205
+ SELECT * FROM relationships WHERE from_id = ? AND to_id = ? AND relation = ? COLLATE NOCASE LIMIT 1
206
+ `).get(fromEntity.id, toEntity.id, relation);
207
+ if (existing) {
208
+ const now2 = (/* @__PURE__ */ new Date()).toISOString();
209
+ const mergedProps = { ...JSON.parse(existing.properties || "{}"), ...options.properties ?? {} };
210
+ this.db.prepare(`
211
+ UPDATE relationships SET properties = ?, confidence = ?, updated_at = ? WHERE id = ?
212
+ `).run(JSON.stringify(mergedProps), options.confidence ?? existing.confidence, now2, existing.id);
213
+ return this.rowToRelationship({ ...existing, properties: JSON.stringify(mergedProps), updated_at: now2 });
214
+ }
215
+ const id = `r-${nanoid(12)}`;
216
+ const now = (/* @__PURE__ */ new Date()).toISOString();
217
+ this.db.prepare(`
218
+ INSERT INTO relationships (id, from_id, to_id, relation, properties, created_at, updated_at, source, confidence)
219
+ VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?)
220
+ `).run(id, fromEntity.id, toEntity.id, relation, JSON.stringify(options.properties ?? {}), now, now, options.source ?? null, options.confidence ?? 1);
221
+ return {
222
+ id,
223
+ from_id: fromEntity.id,
224
+ to_id: toEntity.id,
225
+ relation,
226
+ properties: options.properties ?? {},
227
+ created_at: now,
228
+ updated_at: now,
229
+ source: options.source,
230
+ confidence: options.confidence ?? 1
231
+ };
232
+ }
233
+ getRelationsFrom(entityId) {
234
+ const rows = this.db.prepare(`
235
+ SELECT r.*, e.name as to_name, e.type as to_type
236
+ FROM relationships r
237
+ JOIN entities e ON r.to_id = e.id
238
+ WHERE r.from_id = ?
239
+ ORDER BY r.updated_at DESC
240
+ `).all(entityId);
241
+ return rows.map((r) => ({ ...this.rowToRelationship(r), to_name: r.to_name, to_type: r.to_type }));
242
+ }
243
+ getRelationsTo(entityId) {
244
+ const rows = this.db.prepare(`
245
+ SELECT r.*, e.name as from_name, e.type as from_type
246
+ FROM relationships r
247
+ JOIN entities e ON r.from_id = e.id
248
+ WHERE r.to_id = ?
249
+ ORDER BY r.updated_at DESC
250
+ `).all(entityId);
251
+ return rows.map((r) => ({ ...this.rowToRelationship(r), from_name: r.from_name, from_type: r.from_type }));
252
+ }
253
+ deleteRelation(id) {
254
+ const result = this.db.prepare(`DELETE FROM relationships WHERE id = ?`).run(id);
255
+ return result.changes > 0;
256
+ }
257
+ // ─── Search ────────────────────────────────────────────────────
258
+ searchEntities(query, limit = 10) {
259
+ const sanitized = query.replace(/[?!@#$%^&*(){}\[\]<>:;"'`~|/\\+=]/g, " ").trim();
260
+ if (sanitized.length > 0) {
261
+ try {
262
+ const ftsRows = this.db.prepare(`
263
+ SELECT e.* FROM entities_fts fts
264
+ JOIN entities e ON e.rowid = fts.rowid
265
+ WHERE entities_fts MATCH ?
266
+ LIMIT ?
267
+ `).all(sanitized, limit);
268
+ if (ftsRows.length > 0) {
269
+ return ftsRows.map((r) => this.rowToEntity(r));
270
+ }
271
+ } catch {
272
+ }
273
+ }
274
+ const likeQuery = sanitized.length > 0 ? sanitized : query;
275
+ const likeRows = this.db.prepare(`
276
+ SELECT * FROM entities
277
+ WHERE name LIKE ? COLLATE NOCASE OR type LIKE ? COLLATE NOCASE
278
+ LIMIT ?
279
+ `).all(`%${likeQuery}%`, `%${likeQuery}%`, limit);
280
+ return likeRows.map((r) => this.rowToEntity(r));
281
+ }
282
+ // ─── Graph Traversal ───────────────────────────────────────────
283
+ /**
284
+ * Find path between two entities (BFS, max depth)
285
+ */
286
+ findPath(fromName, toName, maxHops = 3) {
287
+ const fromEntity = this.findEntityByName(fromName);
288
+ const toEntity = this.findEntityByName(toName);
289
+ if (!fromEntity || !toEntity)
290
+ return null;
291
+ const queue = [
292
+ { entityId: fromEntity.id, path: [fromEntity.name], relations: [] }
293
+ ];
294
+ const visited = /* @__PURE__ */ new Set([fromEntity.id]);
295
+ while (queue.length > 0) {
296
+ const current = queue.shift();
297
+ if (current.path.length > maxHops + 1)
298
+ break;
299
+ const outgoing = this.db.prepare(`
300
+ SELECT r.relation, r.to_id as neighbor_id, e.name as neighbor_name
301
+ FROM relationships r JOIN entities e ON r.to_id = e.id
302
+ WHERE r.from_id = ?
303
+ `).all(current.entityId);
304
+ const incoming = this.db.prepare(`
305
+ SELECT r.relation, r.from_id as neighbor_id, e.name as neighbor_name
306
+ FROM relationships r JOIN entities e ON r.from_id = e.id
307
+ WHERE r.to_id = ?
308
+ `).all(current.entityId);
309
+ const neighbors = [
310
+ ...outgoing.map((n) => ({ ...n, direction: "->" })),
311
+ ...incoming.map((n) => ({ ...n, direction: "<-" }))
312
+ ];
313
+ for (const neighbor of neighbors) {
314
+ if (neighbor.neighbor_id === toEntity.id) {
315
+ return {
316
+ path: [...current.path, neighbor.neighbor_name],
317
+ relations: [...current.relations, `${neighbor.direction}[${neighbor.relation}]`]
318
+ };
319
+ }
320
+ if (!visited.has(neighbor.neighbor_id)) {
321
+ visited.add(neighbor.neighbor_id);
322
+ queue.push({
323
+ entityId: neighbor.neighbor_id,
324
+ path: [...current.path, neighbor.neighbor_name],
325
+ relations: [...current.relations, `${neighbor.direction}[${neighbor.relation}]`]
326
+ });
327
+ }
328
+ }
329
+ }
330
+ return null;
331
+ }
332
+ /**
333
+ * Get neighborhood of an entity (all connected within N hops)
334
+ */
335
+ getNeighborhood(entityName, hops = 1) {
336
+ const entity = this.findEntityByName(entityName);
337
+ if (!entity)
338
+ return { entities: [], relationships: [] };
339
+ const entityIds = /* @__PURE__ */ new Set([entity.id]);
340
+ const relIds = /* @__PURE__ */ new Set();
341
+ let frontier = [entity.id];
342
+ for (let i = 0; i < hops; i++) {
343
+ const nextFrontier = [];
344
+ for (const nodeId of frontier) {
345
+ const rels = this.db.prepare(`
346
+ SELECT * FROM relationships WHERE from_id = ? OR to_id = ?
347
+ `).all(nodeId, nodeId);
348
+ for (const rel of rels) {
349
+ relIds.add(rel.id);
350
+ const neighborId = rel.from_id === nodeId ? rel.to_id : rel.from_id;
351
+ if (!entityIds.has(neighborId)) {
352
+ entityIds.add(neighborId);
353
+ nextFrontier.push(neighborId);
354
+ }
355
+ }
356
+ }
357
+ frontier = nextFrontier;
358
+ }
359
+ const entities = [...entityIds].map((id) => this.getEntity(id)).filter((e) => e !== null);
360
+ const relationships = [...relIds].map((id) => {
361
+ const row = this.db.prepare(`SELECT * FROM relationships WHERE id = ?`).get(id);
362
+ return row ? this.rowToRelationship(row) : null;
363
+ }).filter((r) => r !== null);
364
+ return { entities, relationships };
365
+ }
366
+ // ─── Stats ─────────────────────────────────────────────────────
367
+ stats() {
368
+ const entityCount = this.db.prepare(`SELECT COUNT(*) as c FROM entities`).get().c;
369
+ const relCount = this.db.prepare(`SELECT COUNT(*) as c FROM relationships`).get().c;
370
+ const entityTypes = this.db.prepare(`SELECT DISTINCT type FROM entities ORDER BY type`).all().map((r) => r.type);
371
+ const relationTypes = this.db.prepare(`SELECT DISTINCT relation FROM relationships ORDER BY relation`).all().map((r) => r.relation);
372
+ const oldest = this.db.prepare(`SELECT MIN(created_at) as t FROM entities`).get();
373
+ const newest = this.db.prepare(`SELECT MAX(updated_at) as t FROM entities`).get();
374
+ return {
375
+ entities: entityCount,
376
+ relationships: relCount,
377
+ entityTypes,
378
+ relationTypes,
379
+ oldestEntry: oldest?.t ?? null,
380
+ newestEntry: newest?.t ?? null
381
+ };
382
+ }
383
+ // ─── Memory Log ────────────────────────────────────────────────
384
+ logExtraction(rawText, entities, relations, sessionId) {
385
+ const id = `log-${nanoid(12)}`;
386
+ this.db.prepare(`
387
+ INSERT INTO memory_log (id, raw_text, extracted_entities, extracted_relations, session_id)
388
+ VALUES (?, ?, ?, ?, ?)
389
+ `).run(id, rawText, JSON.stringify(entities), JSON.stringify(relations), sessionId ?? null);
390
+ }
391
+ // ─── Helpers ───────────────────────────────────────────────────
392
+ rowToEntity(row) {
393
+ return {
394
+ id: row.id,
395
+ name: row.name,
396
+ type: row.type,
397
+ properties: JSON.parse(row.properties || "{}"),
398
+ created_at: row.created_at,
399
+ updated_at: row.updated_at,
400
+ source: row.source ?? void 0,
401
+ confidence: row.confidence
402
+ };
403
+ }
404
+ rowToRelationship(row) {
405
+ return {
406
+ id: row.id,
407
+ from_id: row.from_id,
408
+ to_id: row.to_id,
409
+ relation: row.relation,
410
+ properties: JSON.parse(row.properties || "{}"),
411
+ created_at: row.created_at,
412
+ updated_at: row.updated_at,
413
+ source: row.source ?? void 0,
414
+ confidence: row.confidence
415
+ };
416
+ }
417
+ /** Close database */
418
+ close() {
419
+ this.db.close();
420
+ }
421
+ };
422
+ }
423
+ });
424
+
425
+ // src/config/schema.js
426
+ import { z } from "zod";
427
+ var DomainSchema, SyncSchema, ConfigSchema;
428
+ var init_schema2 = __esm({
429
+ "src/config/schema.js"() {
430
+ "use strict";
431
+ DomainSchema = z.object({
432
+ name: z.string(),
433
+ entityHints: z.array(z.string()).default([]),
434
+ relationHints: z.array(z.string()).default([])
435
+ });
436
+ SyncSchema = z.object({
437
+ memoryMd: z.string().nullable().default(null),
438
+ neuralMemory: z.string().nullable().default(null),
439
+ importOnStart: z.boolean().default(false)
440
+ });
441
+ ConfigSchema = z.object({
442
+ storage: z.object({
443
+ path: z.string().default("./memory-graph.db"),
444
+ maxSizeMb: z.number().default(500)
445
+ }).default({}),
446
+ extraction: z.object({
447
+ provider: z.enum(["auto", "openai", "anthropic", "ollama"]).default("auto"),
448
+ model: z.string().default("auto"),
449
+ autoExtract: z.boolean().default(true),
450
+ minConfidence: z.number().min(0).max(1).default(0.7),
451
+ batchSize: z.number().default(5)
452
+ }).default({}),
453
+ domains: z.array(DomainSchema).default([]),
454
+ deduplication: z.object({
455
+ enabled: z.boolean().default(true),
456
+ similarityThreshold: z.number().min(0).max(1).default(0.85)
457
+ }).default({}),
458
+ sync: SyncSchema.default({}),
459
+ query: z.object({
460
+ maxHops: z.number().default(5),
461
+ maxResults: z.number().default(50),
462
+ includeConfidence: z.boolean().default(true)
463
+ }).default({})
464
+ });
465
+ }
466
+ });
467
+
468
+ // src/config/defaults.js
469
+ import { readFileSync, existsSync } from "node:fs";
470
+ import { resolve as resolve2 } from "node:path";
471
+ function loadConfig(configPath) {
472
+ const paths = configPath ? [configPath] : [
473
+ resolve2(process.cwd(), "config", CONFIG_FILENAME),
474
+ resolve2(process.cwd(), CONFIG_FILENAME)
475
+ ];
476
+ for (const p of paths) {
477
+ if (existsSync(p)) {
478
+ try {
479
+ const raw = JSON.parse(readFileSync(p, "utf-8"));
480
+ return ConfigSchema.parse(raw);
481
+ } catch (err) {
482
+ console.warn(`[agent-memory-graph] Invalid config at ${p}, using defaults.`);
483
+ }
484
+ }
485
+ }
486
+ return ConfigSchema.parse({});
487
+ }
488
+ var CONFIG_FILENAME;
489
+ var init_defaults = __esm({
490
+ "src/config/defaults.js"() {
491
+ "use strict";
492
+ init_schema2();
493
+ CONFIG_FILENAME = "graph.config.json";
494
+ }
495
+ });
496
+
497
+ // src/extract/extractor.js
498
+ function buildPrompt(text, domains) {
499
+ const domainContext = domains.length > 0 ? `
500
+ Domain hints (use these to improve accuracy):
501
+ ${domains.map((d) => `- ${d.name}: entities=[${d.entityHints.join(", ")}], relations=[${d.relationHints.join(", ")}]`).join("\n")}
502
+ ` : "";
503
+ return `You are an entity and relationship extractor. Given text, extract all meaningful entities and their relationships.
504
+
505
+ Rules:
506
+ 1. Extract entities with their most specific type (Person, Project, Tool, Company, Location, Concept, etc.)
507
+ 2. Extract directional relationships between entities (FROM -[RELATION]-> TO)
508
+ 3. Be domain-agnostic \u2014 work with any topic
509
+ 4. Assign confidence scores (0.0 to 1.0) based on how explicit the mention is
510
+ 5. Resolve pronouns to their referents when clearly determinable
511
+ 6. Do NOT hallucinate entities not mentioned or strongly implied in the text
512
+ 7. Normalize entity names (capitalize properly, use full names when available)
513
+ 8. Use UPPER_SNAKE_CASE for relationship types (WORKS_ON, USES, OWNS, etc.)
514
+ ${domainContext}
515
+ Return ONLY valid JSON (no markdown, no explanation):
516
+ {
517
+ "entities": [
518
+ {"name": "Entity Name", "type": "Type", "properties": {}, "confidence": 0.9}
519
+ ],
520
+ "relationships": [
521
+ {"from": "Entity A", "relation": "RELATION_TYPE", "to": "Entity B", "fromType": "TypeA", "toType": "TypeB", "confidence": 0.85}
522
+ ]
523
+ }
524
+
525
+ If the text contains no meaningful entities or relationships, return:
526
+ {"entities": [], "relationships": []}
527
+
528
+ Text to extract from:
529
+ """
530
+ ${text}
531
+ """`;
532
+ }
533
+ function detectProvider(config) {
534
+ if (config.extraction.provider !== "auto") {
535
+ return config.extraction.provider;
536
+ }
537
+ if (process.env.OPENAI_API_KEY)
538
+ return "openai";
539
+ if (process.env.ANTHROPIC_API_KEY)
540
+ return "anthropic";
541
+ if (process.env.OLLAMA_HOST || process.env.OLLAMA_URL)
542
+ return "ollama";
543
+ return null;
544
+ }
545
+ async function callOpenAI(prompt, model) {
546
+ const { default: OpenAI } = await import("openai");
547
+ const client = new OpenAI();
548
+ const response = await client.chat.completions.create({
549
+ model: model === "auto" ? "gpt-4o-mini" : model,
550
+ messages: [{ role: "user", content: prompt }],
551
+ temperature: 0.1,
552
+ response_format: { type: "json_object" }
553
+ });
554
+ return response.choices[0]?.message?.content ?? '{"entities":[],"relationships":[]}';
555
+ }
556
+ async function callAnthropic(prompt, model) {
557
+ const { default: Anthropic } = await import("@anthropic-ai/sdk");
558
+ const client = new Anthropic();
559
+ const response = await client.messages.create({
560
+ model: model === "auto" ? "claude-3-5-haiku-20241022" : model,
561
+ max_tokens: 4096,
562
+ messages: [{ role: "user", content: prompt }]
563
+ });
564
+ const block = response.content[0];
565
+ return block.type === "text" ? block.text : '{"entities":[],"relationships":[]}';
566
+ }
567
+ async function callOllama(prompt, model) {
568
+ const host = process.env.OLLAMA_HOST || process.env.OLLAMA_URL || "http://localhost:11434";
569
+ const response = await fetch(`${host}/api/generate`, {
570
+ method: "POST",
571
+ headers: { "Content-Type": "application/json" },
572
+ body: JSON.stringify({
573
+ model: model === "auto" ? "llama3.1" : model,
574
+ prompt,
575
+ stream: false,
576
+ format: "json"
577
+ })
578
+ });
579
+ if (!response.ok) {
580
+ throw new Error(`Ollama error: ${response.status} ${response.statusText}`);
581
+ }
582
+ const data = await response.json();
583
+ return data.response;
584
+ }
585
+ async function extractFromText(text, config) {
586
+ const provider = detectProvider(config);
587
+ if (!provider) {
588
+ throw new Error("No LLM provider available. Set OPENAI_API_KEY, ANTHROPIC_API_KEY, or OLLAMA_HOST.");
589
+ }
590
+ const prompt = buildPrompt(text, config.domains);
591
+ const model = config.extraction.model;
592
+ let rawResponse;
593
+ switch (provider) {
594
+ case "openai":
595
+ rawResponse = await callOpenAI(prompt, model);
596
+ break;
597
+ case "anthropic":
598
+ rawResponse = await callAnthropic(prompt, model);
599
+ break;
600
+ case "ollama":
601
+ rawResponse = await callOllama(prompt, model);
602
+ break;
603
+ }
604
+ try {
605
+ const jsonMatch = rawResponse.match(/\{[\s\S]*\}/);
606
+ if (!jsonMatch) {
607
+ return { entities: [], relationships: [] };
608
+ }
609
+ const parsed = JSON.parse(jsonMatch[0]);
610
+ const entities = (parsed.entities || []).filter((e) => e.name && e.type && (e.confidence ?? 1) >= config.extraction.minConfidence).map((e) => ({
611
+ name: String(e.name).trim(),
612
+ type: String(e.type).trim(),
613
+ properties: e.properties ?? {},
614
+ confidence: Math.min(1, Math.max(0, Number(e.confidence) || 0.8))
615
+ }));
616
+ const relationships = (parsed.relationships || []).filter((r) => r.from && r.relation && r.to && (r.confidence ?? 1) >= config.extraction.minConfidence).map((r) => ({
617
+ from: String(r.from).trim(),
618
+ relation: String(r.relation).trim().toUpperCase().replace(/\s+/g, "_"),
619
+ to: String(r.to).trim(),
620
+ fromType: r.fromType?.trim(),
621
+ toType: r.toType?.trim(),
622
+ confidence: Math.min(1, Math.max(0, Number(r.confidence) || 0.8))
623
+ }));
624
+ return { entities, relationships };
625
+ } catch (err) {
626
+ console.warn("[agent-memory-graph] Failed to parse extraction response:", err);
627
+ return { entities: [], relationships: [] };
628
+ }
629
+ }
630
+ var init_extractor = __esm({
631
+ "src/extract/extractor.js"() {
632
+ "use strict";
633
+ }
634
+ });
635
+
636
+ // src/search/hybrid.js
637
+ function hybridSearch(engine, query, config) {
638
+ const limit = config.query.maxResults;
639
+ const entities = engine.searchEntities(query, limit);
640
+ const results = entities.map((entity) => {
641
+ const outgoing = engine.getRelationsFrom(entity.id);
642
+ const incoming = engine.getRelationsTo(entity.id);
643
+ const relations = [
644
+ ...outgoing.map((r) => ({
645
+ direction: "outgoing",
646
+ relation: r.relation,
647
+ target: r.to_name,
648
+ targetType: r.to_type
649
+ })),
650
+ ...incoming.map((r) => ({
651
+ direction: "incoming",
652
+ relation: r.relation,
653
+ target: r.from_name,
654
+ targetType: r.from_type
655
+ }))
656
+ ];
657
+ return {
658
+ entity: {
659
+ id: entity.id,
660
+ name: entity.name,
661
+ type: entity.type,
662
+ properties: entity.properties
663
+ },
664
+ relations
665
+ };
666
+ });
667
+ return results;
668
+ }
669
+ var init_hybrid = __esm({
670
+ "src/search/hybrid.js"() {
671
+ "use strict";
672
+ }
673
+ });
674
+
675
+ // src/search/natural-language.js
676
+ async function naturalLanguageQuery(question, engine, config) {
677
+ const q = question.toLowerCase().trim();
678
+ const whereDidMatch = q.match(/where (?:did|does|has) (.+?) (?:work|come from|work before|previously work|used to work)/);
679
+ if (whereDidMatch) {
680
+ return queryEntityRelations(engine, whereDidMatch[1].trim(), ["WORKED_AT", "PREVIOUSLY_WORKED_AT", "CAME_FROM"], config);
681
+ }
682
+ const roleMatch = q.match(/what (?:is|are) (.+?)(?:'s|s') (?:role|position|title|job)/);
683
+ if (roleMatch) {
684
+ return queryEntityProperties(engine, roleMatch[1].trim(), ["role", "position", "title"], config);
685
+ }
686
+ const roleMatch2 = q.match(/what (?:role|position|title) (?:does|did|is) (.+?) (?:have|hold|play)/);
687
+ if (roleMatch2) {
688
+ return queryEntityProperties(engine, roleMatch2[1].trim(), ["role", "position", "title"], config);
689
+ }
690
+ const whoWorksAtMatch = q.match(/who (?:works|is|are|worked) (?:at|for|in) (.+?)(?:\?|$)/);
691
+ if (whoWorksAtMatch) {
692
+ return queryWhoAtEntity(engine, whoWorksAtMatch[1].trim(), config);
693
+ }
694
+ const whoPossessiveMatch = q.match(/who (?:are|is|were) (.+?)(?:'s|s') (\w+)(?:\?|$)/);
695
+ if (whoPossessiveMatch) {
696
+ return queryAboutEntity(engine, whoPossessiveMatch[1].trim(), config);
697
+ }
698
+ const whoVerbMatch = q.match(/who (\w+(?:ed|s|es)?) (.+?)(?:\?|$)/);
699
+ if (whoVerbMatch) {
700
+ return queryWhoDidEntity(engine, whoVerbMatch[1], whoVerbMatch[2].trim(), config);
701
+ }
702
+ const workingOnMatch = q.match(/what (?:am i|do i|are we) (\w+(?:\s+\w+)*?)(?:\?|$)/);
703
+ if (workingOnMatch) {
704
+ return queryByRelationPattern(engine, workingOnMatch[1], config);
705
+ }
706
+ const connectionMatch = q.match(/(?:how is|connection between|relationship between|path from) (.+?) (?:connected to|and|to) (.+?)(?:\?|$)/);
707
+ if (connectionMatch) {
708
+ return queryConnection(engine, connectionMatch[1].trim(), connectionMatch[2].trim(), config);
709
+ }
710
+ const whatDoesMatch = q.match(/what (?:does|did|is|are|has) (.+?) (?:work on|use|own|manage|maintain|know|do|build|create|handle|work|run|have|lead|suggest)/);
711
+ if (whatDoesMatch) {
712
+ return queryAboutEntity(engine, whatDoesMatch[1].trim(), config);
713
+ }
714
+ const whatNounMatch = q.match(/what (\w+)s? (?:does|did|do|is|are|has) (.+?) (?:use|own|work on|manage|know|have|build|run|lead|need)/);
715
+ if (whatNounMatch) {
716
+ return queryAboutEntity(engine, whatNounMatch[2].trim(), config);
717
+ }
718
+ const whatIsVerbingMatch = q.match(/what ([\w\s]+?) (?:is|are) (.+?) (\w+ing)(?: on| with| at| for)?(?:\?|$)/);
719
+ if (whatIsVerbingMatch) {
720
+ const entityCandidate = whatIsVerbingMatch[2].trim();
721
+ if (entityCandidate && !entityCandidate.match(/^\w+ing$/)) {
722
+ return queryAboutEntity(engine, entityCandidate, config);
723
+ }
724
+ }
725
+ const listMatch = q.match(/(?:list|show|get|find) (?:all |my |every )?(\w+)s?(?:\?|$)/);
726
+ if (listMatch) {
727
+ return queryListType(engine, listMatch[1], config);
728
+ }
729
+ const whatTypeMentioned = q.match(/what (\w+)s? (?:are|were|is) (?:mentioned|used|listed|included|involved)/);
730
+ if (whatTypeMentioned) {
731
+ return queryListType(engine, whatTypeMentioned[1], config);
732
+ }
733
+ const aboutMatch = q.match(/(?:tell me about|what is|who is|describe|info on|about) (.+?)(?:\?|$)/);
734
+ if (aboutMatch) {
735
+ return queryAboutEntity(engine, aboutMatch[1].trim(), config);
736
+ }
737
+ const possessiveMatch = q.match(/([\w\s]+?)(?:'s|s') ([\w\s]+?)(?:\?|$)/);
738
+ if (possessiveMatch) {
739
+ return queryAboutEntity(engine, possessiveMatch[1].trim(), config);
740
+ }
741
+ return querySmartFallback(engine, question, config);
742
+ }
743
+ async function queryEntityRelations(engine, entityName, relationTypes, config) {
744
+ let entity = engine.findEntityByName(entityName);
745
+ if (!entity) {
746
+ const results = engine.searchEntities(entityName, 1);
747
+ if (results.length === 0) {
748
+ return { answer: `"${entityName}" not found in graph.`, entities: [], confidence: 0.2 };
749
+ }
750
+ entity = results[0];
751
+ }
752
+ const outgoing = engine.getRelationsFrom(entity.id);
753
+ const incoming = engine.getRelationsTo(entity.id);
754
+ const matchedOut = outgoing.filter(
755
+ (r) => relationTypes.some((rt) => r.relation.toUpperCase().includes(rt.toUpperCase()))
756
+ );
757
+ const matchedIn = incoming.filter(
758
+ (r) => relationTypes.some((rt) => r.relation.toUpperCase().includes(rt.toUpperCase()))
759
+ );
760
+ if (matchedOut.length > 0 || matchedIn.length > 0) {
761
+ const parts = [];
762
+ if (matchedOut.length > 0) parts.push(matchedOut.map((r) => `${r.relation} \u2192 ${r.to_name}`).join(", "));
763
+ if (matchedIn.length > 0) parts.push(matchedIn.map((r) => `${r.from_name} \u2192 ${r.relation}`).join(", "));
764
+ return {
765
+ answer: `${entity.name}: ${parts.join("; ")}`,
766
+ entities: [...matchedOut.map((r) => ({ name: r.to_name, type: r.to_type })), ...matchedIn.map((r) => ({ name: r.from_name, type: r.from_type }))],
767
+ confidence: 0.85
768
+ };
769
+ }
770
+ return queryAboutEntity(engine, entity.name, config);
771
+ }
772
+ async function queryEntityProperties(engine, entityName, propertyKeys, config) {
773
+ let entity = engine.findEntityByName(entityName);
774
+ if (!entity) {
775
+ const results = engine.searchEntities(entityName, 1);
776
+ if (results.length === 0) {
777
+ return { answer: `"${entityName}" not found in graph.`, entities: [], confidence: 0.2 };
778
+ }
779
+ entity = results[0];
780
+ }
781
+ const props = entity.properties || {};
782
+ const matchedProps = propertyKeys.filter((k) => props[k]).map((k) => `${k}: ${props[k]}`);
783
+ if (matchedProps.length > 0) {
784
+ return {
785
+ answer: `${entity.name}: ${matchedProps.join(", ")}`,
786
+ entities: [{ name: entity.name, type: entity.type }],
787
+ confidence: 0.9
788
+ };
789
+ }
790
+ const outgoing = engine.getRelationsFrom(entity.id);
791
+ const roleRelations = outgoing.filter(
792
+ (r) => ["LEADS", "MANAGES", "HEADS", "WORKS_AS", "ROLE_IS"].includes(r.relation.toUpperCase())
793
+ );
794
+ if (roleRelations.length > 0) {
795
+ return {
796
+ answer: `${entity.name}: ${roleRelations.map((r) => `${r.relation} \u2192 ${r.to_name}`).join(", ")}`,
797
+ entities: [{ name: entity.name, type: entity.type }],
798
+ confidence: 0.85
799
+ };
800
+ }
801
+ return queryAboutEntity(engine, entity.name, config);
802
+ }
803
+ async function queryWhoAtEntity(engine, entityName, config) {
804
+ let entity = engine.findEntityByName(entityName);
805
+ if (!entity) {
806
+ const results = engine.searchEntities(entityName, 1);
807
+ if (results.length === 0) {
808
+ return { answer: `"${entityName}" not found in graph.`, entities: [], confidence: 0.2 };
809
+ }
810
+ entity = results[0];
811
+ }
812
+ const incoming = engine.getRelationsTo(entity.id);
813
+ const workers = incoming.filter((r) => ["WORKS_AT", "EMPLOYED_BY", "MEMBER_OF", "BELONGS_TO", "WORKS_FOR", "HIRED_BY"].includes(r.relation.toUpperCase())).map((r) => ({ name: r.from_name, type: r.from_type, relation: r.relation }));
814
+ const outgoing = engine.getRelationsFrom(entity.id);
815
+ const employed = outgoing.filter((r) => ["EMPLOYS", "HIRED", "HAS_MEMBER", "HAS_EMPLOYEE"].includes(r.relation.toUpperCase())).map((r) => ({ name: r.to_name, type: r.to_type, relation: r.relation }));
816
+ const all = [...workers, ...employed];
817
+ if (all.length > 0) {
818
+ return {
819
+ answer: `People at ${entity.name}: ${all.map((p) => p.name).join(", ")}`,
820
+ entities: all.map((p) => ({ name: p.name, type: p.type })),
821
+ confidence: 0.85
822
+ };
823
+ }
824
+ const incomingPeople = incoming.filter((r) => r.from_type?.toLowerCase() === "person").map((r) => ({ name: r.from_name, type: r.from_type }));
825
+ const outgoingPeople = outgoing.filter((r) => r.to_type?.toLowerCase() === "person").map((r) => ({ name: r.to_name, type: r.to_type }));
826
+ const allConnected = [...incomingPeople, ...outgoingPeople];
827
+ const unique = [...new Map(allConnected.map((r) => [r.name, r])).values()];
828
+ if (unique.length > 0) {
829
+ return {
830
+ answer: `People connected to ${entity.name}: ${unique.map((p) => p.name).join(", ")}`,
831
+ entities: unique,
832
+ confidence: 0.75
833
+ };
834
+ }
835
+ return { answer: `No people found at "${entityName}".`, entities: [], confidence: 0.3 };
836
+ }
837
+ async function queryWhoDidEntity(engine, verb, entityName, config) {
838
+ let entity = engine.findEntityByName(entityName);
839
+ if (!entity) {
840
+ const results = engine.searchEntities(entityName, 1);
841
+ if (results.length === 0) {
842
+ return querySmartFallback(engine, `who ${verb} ${entityName}`, config);
843
+ }
844
+ entity = results[0];
845
+ }
846
+ const verbRoot = verb.replace(/ed$|s$|es$|ing$/, "").toUpperCase();
847
+ const verbVariants = [verbRoot, `${verbRoot}S`, `${verbRoot}ED`, `${verbRoot}ES`];
848
+ const incoming = engine.getRelationsTo(entity.id);
849
+ const matched = incoming.filter((r) => {
850
+ const rel = r.relation.toUpperCase();
851
+ return verbVariants.some((v) => rel.includes(v)) || rel.includes(verbRoot);
852
+ });
853
+ if (matched.length > 0) {
854
+ return {
855
+ answer: `${matched.map((r) => `${r.from_name} ${r.relation} ${entity.name}`).join(", ")}`,
856
+ entities: matched.map((r) => ({ name: r.from_name, type: r.from_type })),
857
+ confidence: 0.85
858
+ };
859
+ }
860
+ if (incoming.length > 0) {
861
+ const people = incoming.filter((r) => r.from_type?.toLowerCase() === "person");
862
+ if (people.length > 0) {
863
+ return {
864
+ answer: `People connected to ${entity.name}: ${people.map((r) => `${r.from_name} (${r.relation})`).join(", ")}`,
865
+ entities: people.map((r) => ({ name: r.from_name, type: r.from_type })),
866
+ confidence: 0.7
867
+ };
868
+ }
869
+ }
870
+ return queryAboutEntity(engine, entity.name, config);
871
+ }
872
+ async function querySmartFallback(engine, question, config) {
873
+ const allEntities = engine.listEntities({ limit: 500 });
874
+ if (allEntities.length === 0) {
875
+ return { answer: "Graph is empty. Ingest some data first.", entities: [], confidence: 0.2 };
876
+ }
877
+ const qLower = question.toLowerCase();
878
+ const mentioned = allEntities.filter((e) => qLower.includes(e.name.toLowerCase())).sort((a, b) => b.name.length - a.name.length);
879
+ if (mentioned.length > 0) {
880
+ return queryAboutEntity(engine, mentioned[0].name, config);
881
+ }
882
+ const stopWords = /* @__PURE__ */ new Set(["what", "who", "where", "when", "how", "why", "does", "did", "the", "are", "is", "was", "were", "has", "have", "had", "all", "any", "some", "this", "that", "which", "there", "their", "about", "from", "with", "for", "and", "but", "not", "can", "will", "would", "should", "could", "been", "being", "mentioned", "used", "listed"]);
883
+ const words = question.replace(/[?!.,;:'"]/g, "").split(/\s+/).filter((w) => w.length > 2 && !stopWords.has(w.toLowerCase()));
884
+ for (const word of words) {
885
+ const results = engine.searchEntities(word, 3);
886
+ if (results.length > 0) {
887
+ return queryAboutEntity(engine, results[0].name, config);
888
+ }
889
+ }
890
+ return { answer: `No relevant entities found for: "${question}"`, entities: [], confidence: 0.2 };
891
+ }
892
+ async function queryByRelationPattern(engine, relationHint, config) {
893
+ const relationMap = {
894
+ "working on": ["WORKS_ON", "CONTRIBUTES_TO", "MAINTAINS"],
895
+ "using": ["USES", "DEPENDS_ON"],
896
+ "own": ["OWNS", "CREATED"],
897
+ "managing": ["MANAGES", "LEADS"],
898
+ "learning": ["LEARNING", "STUDIES"],
899
+ "holding": ["HOLDS", "OWNS"],
900
+ "mining": ["MINES"],
901
+ "building": ["BUILDS", "CREATES"]
902
+ };
903
+ const matchedRelations = Object.entries(relationMap).filter(([phrase]) => relationHint.includes(phrase)).flatMap(([, rels]) => rels);
904
+ const selfEntities = engine.searchEntities("self user me", 5).filter((e) => e.type.toLowerCase() === "person");
905
+ const results = [];
906
+ for (const self of selfEntities) {
907
+ const rels = engine.getRelationsFrom(self.id);
908
+ for (const rel of rels) {
909
+ if (matchedRelations.length === 0 || matchedRelations.includes(rel.relation)) {
910
+ results.push({ name: rel.to_name, type: rel.to_type });
911
+ }
912
+ }
913
+ }
914
+ const unique = [...new Map(results.map((r) => [r.name, r])).values()];
915
+ return {
916
+ answer: unique.length > 0 ? `Found ${unique.length} result(s): ${unique.map((r) => `${r.name} (${r.type})`).join(", ")}` : "No matching relationships found in the graph.",
917
+ entities: unique,
918
+ confidence: unique.length > 0 ? 0.8 : 0.3
919
+ };
920
+ }
921
+ async function queryConnection(engine, fromName, toName, config) {
922
+ const path = engine.findPath(fromName, toName, config.query.maxHops);
923
+ if (!path) {
924
+ return {
925
+ answer: `No connection found between "${fromName}" and "${toName}" within ${config.query.maxHops} hops.`,
926
+ entities: [],
927
+ confidence: 0.5
928
+ };
929
+ }
930
+ const pathStr = path.path.map((node, i) => i < path.relations.length ? `${node} ->[${path.relations[i]}]` : node).join(" ");
931
+ return {
932
+ answer: `Path: ${pathStr}`,
933
+ entities: path.path.map((name) => ({ name, type: "Unknown" })),
934
+ paths: [path],
935
+ confidence: 0.9
936
+ };
937
+ }
938
+ async function queryListType(engine, typeName, config) {
939
+ const typeNormMap = {
940
+ "people": "person",
941
+ "persons": "person",
942
+ "companies": "company",
943
+ "organizations": "organization",
944
+ "organisations": "organization",
945
+ "tools": "tool",
946
+ "projects": "project",
947
+ "languages": "language",
948
+ "programming languages": "language",
949
+ "databases": "database",
950
+ "services": "service",
951
+ "teams": "team",
952
+ "technologies": "technology"
953
+ };
954
+ const normalized = typeNormMap[typeName.toLowerCase()] || typeName;
955
+ const variations = [
956
+ normalized,
957
+ normalized.charAt(0).toUpperCase() + normalized.slice(1),
958
+ normalized.toUpperCase(),
959
+ normalized.toLowerCase(),
960
+ // Also try without trailing 's'
961
+ normalized.endsWith("s") ? normalized.slice(0, -1) : normalized,
962
+ normalized.endsWith("s") ? normalized.slice(0, -1).charAt(0).toUpperCase() + normalized.slice(0, -1).slice(1) : normalized.charAt(0).toUpperCase() + normalized.slice(1)
963
+ ];
964
+ for (const v of [...new Set(variations)]) {
965
+ const found = engine.listEntities({ type: v, limit: config.query.maxResults });
966
+ if (found.length > 0) {
967
+ return {
968
+ answer: `Found ${found.length} ${typeName}(s): ${found.map((e) => e.name).join(", ")}`,
969
+ entities: found.map((e) => ({ name: e.name, type: e.type })),
970
+ confidence: 0.85
971
+ };
972
+ }
973
+ }
974
+ const searchResults = engine.searchEntities(typeName, config.query.maxResults);
975
+ if (searchResults.length > 0) {
976
+ return {
977
+ answer: `Found ${searchResults.length} result(s) for "${typeName}": ${searchResults.map((e) => `${e.name} (${e.type})`).join(", ")}`,
978
+ entities: searchResults.map((e) => ({ name: e.name, type: e.type })),
979
+ confidence: 0.7
980
+ };
981
+ }
982
+ return {
983
+ answer: `No entities of type "${typeName}" found.`,
984
+ entities: [],
985
+ confidence: 0.3
986
+ };
987
+ }
988
+ async function queryAboutEntity(engine, entityName, config) {
989
+ let entity = engine.findEntityByName(entityName);
990
+ if (!entity) {
991
+ const results = engine.searchEntities(entityName, 1);
992
+ if (results.length === 0) {
993
+ return { answer: `"${entityName}" not found in graph.`, entities: [], confidence: 0.2 };
994
+ }
995
+ entity = results[0];
996
+ }
997
+ const outgoing = engine.getRelationsFrom(entity.id);
998
+ const incoming = engine.getRelationsTo(entity.id);
999
+ const lines = [
1000
+ `${entity.name} (${entity.type})`
1001
+ ];
1002
+ if (Object.keys(entity.properties).length > 0) {
1003
+ lines.push(`Properties: ${JSON.stringify(entity.properties)}`);
1004
+ }
1005
+ if (outgoing.length > 0) {
1006
+ lines.push(`Outgoing: ${outgoing.map((r) => `${r.relation} \u2192 ${r.to_name}`).join(", ")}`);
1007
+ }
1008
+ if (incoming.length > 0) {
1009
+ lines.push(`Incoming: ${incoming.map((r) => `${r.from_name} \u2192 ${r.relation}`).join(", ")}`);
1010
+ }
1011
+ return {
1012
+ answer: lines.join("\n"),
1013
+ entities: [
1014
+ { name: entity.name, type: entity.type },
1015
+ ...outgoing.map((r) => ({ name: r.to_name, type: r.to_type })),
1016
+ ...incoming.map((r) => ({ name: r.from_name, type: r.from_type }))
1017
+ ],
1018
+ confidence: 0.9
1019
+ };
1020
+ }
1021
+ var init_natural_language = __esm({
1022
+ "src/search/natural-language.js"() {
1023
+ "use strict";
1024
+ }
1025
+ });
1026
+
1027
+ // src/sync/export.js
1028
+ function exportGraph(engine, options) {
1029
+ const { format, includeProperties = false, maxEntities = 500 } = options;
1030
+ const entities = engine.listEntities({ limit: maxEntities });
1031
+ const stats = engine.stats();
1032
+ switch (format) {
1033
+ case "json":
1034
+ return exportJSON(engine, entities, includeProperties);
1035
+ case "mermaid":
1036
+ return exportMermaid(engine, entities);
1037
+ case "dot":
1038
+ return exportDOT(engine, entities);
1039
+ case "csv":
1040
+ return exportCSV(engine, entities);
1041
+ default:
1042
+ throw new Error(`Unsupported export format: ${format}`);
1043
+ }
1044
+ }
1045
+ function exportJSON(engine, entities, includeProperties) {
1046
+ const nodes = entities.map((e) => ({
1047
+ id: e.id,
1048
+ name: e.name,
1049
+ type: e.type,
1050
+ ...includeProperties ? { properties: e.properties } : {},
1051
+ confidence: e.confidence,
1052
+ created_at: e.created_at
1053
+ }));
1054
+ const edges = [];
1055
+ for (const entity of entities) {
1056
+ const rels = engine.getRelationsFrom(entity.id);
1057
+ for (const rel of rels) {
1058
+ edges.push({
1059
+ from: entity.name,
1060
+ to: rel.to_name,
1061
+ relation: rel.relation,
1062
+ confidence: rel.confidence
1063
+ });
1064
+ }
1065
+ }
1066
+ return JSON.stringify({ nodes, edges, stats: engine.stats() }, null, 2);
1067
+ }
1068
+ function exportMermaid(engine, entities) {
1069
+ const lines = ["graph LR"];
1070
+ const nodeIds = /* @__PURE__ */ new Map();
1071
+ entities.forEach((e, i) => {
1072
+ const shortId = `n${i}`;
1073
+ nodeIds.set(e.id, shortId);
1074
+ const shape = getNodeShape(e.type);
1075
+ lines.push(` ${shortId}${shape[0]}"${escapeMermaid(e.name)}"${shape[1]}`);
1076
+ });
1077
+ for (const entity of entities) {
1078
+ const rels = engine.getRelationsFrom(entity.id);
1079
+ for (const rel of rels) {
1080
+ const fromId = nodeIds.get(entity.id);
1081
+ const toId = nodeIds.get(rel.to_id);
1082
+ if (fromId && toId) {
1083
+ lines.push(` ${fromId} -->|${escapeMermaid(rel.relation)}| ${toId}`);
1084
+ }
1085
+ }
1086
+ }
1087
+ return lines.join("\n");
1088
+ }
1089
+ function exportDOT(engine, entities) {
1090
+ const lines = [
1091
+ "digraph MemoryGraph {",
1092
+ " rankdir=LR;",
1093
+ " node [shape=box, style=rounded];",
1094
+ ""
1095
+ ];
1096
+ for (const entity of entities) {
1097
+ const label = `${entity.name}\\n(${entity.type})`;
1098
+ lines.push(` "${entity.id}" [label="${escapeDOT(label)}"];`);
1099
+ }
1100
+ lines.push("");
1101
+ for (const entity of entities) {
1102
+ const rels = engine.getRelationsFrom(entity.id);
1103
+ for (const rel of rels) {
1104
+ lines.push(` "${entity.id}" -> "${rel.to_id}" [label="${escapeDOT(rel.relation)}"];`);
1105
+ }
1106
+ }
1107
+ lines.push("}");
1108
+ return lines.join("\n");
1109
+ }
1110
+ function exportCSV(engine, entities) {
1111
+ const lines = ["from_name,from_type,relation,to_name,to_type,confidence"];
1112
+ for (const entity of entities) {
1113
+ const rels = engine.getRelationsFrom(entity.id);
1114
+ for (const rel of rels) {
1115
+ lines.push(`"${entity.name}","${entity.type}","${rel.relation}","${rel.to_name}","${rel.to_type}",${rel.confidence}`);
1116
+ }
1117
+ }
1118
+ return lines.join("\n");
1119
+ }
1120
+ function getNodeShape(type) {
1121
+ switch (type.toLowerCase()) {
1122
+ case "person":
1123
+ return ["((", "))"];
1124
+ // Circle
1125
+ case "project":
1126
+ return ["[/", "/]"];
1127
+ // Parallelogram
1128
+ case "tool":
1129
+ case "technology":
1130
+ return ["{{", "}}"];
1131
+ // Hexagon
1132
+ default:
1133
+ return ["[", "]"];
1134
+ }
1135
+ }
1136
+ function escapeMermaid(text) {
1137
+ return text.replace(/"/g, "'").replace(/[[\]{}()]/g, "");
1138
+ }
1139
+ function escapeDOT(text) {
1140
+ return text.replace(/"/g, '\\"').replace(/\n/g, "\\n");
1141
+ }
1142
+ var init_export = __esm({
1143
+ "src/sync/export.js"() {
1144
+ "use strict";
1145
+ }
1146
+ });
1147
+
1148
+ // src/sync/memory-md.js
1149
+ import { readFileSync as readFileSync2, existsSync as existsSync2 } from "node:fs";
1150
+ async function importFromMemoryMd(filePath, engine, config) {
1151
+ if (!existsSync2(filePath)) {
1152
+ throw new Error(`File not found: ${filePath}`);
1153
+ }
1154
+ const content = readFileSync2(filePath, "utf-8");
1155
+ const sections = splitIntoSections(content);
1156
+ let totalEntities = 0;
1157
+ let totalRelationships = 0;
1158
+ for (const section of sections) {
1159
+ if (section.trim().length < 20)
1160
+ continue;
1161
+ try {
1162
+ const result = await extractFromText(section, config);
1163
+ for (const entity of result.entities) {
1164
+ engine.addEntity(entity.name, entity.type, entity.properties ?? {}, {
1165
+ source: filePath,
1166
+ confidence: entity.confidence
1167
+ });
1168
+ totalEntities++;
1169
+ }
1170
+ for (const rel of result.relationships) {
1171
+ engine.addRelation(rel.from, rel.relation, rel.to, {
1172
+ source: filePath,
1173
+ confidence: rel.confidence,
1174
+ fromType: rel.fromType,
1175
+ toType: rel.toType
1176
+ });
1177
+ totalRelationships++;
1178
+ }
1179
+ } catch (err) {
1180
+ console.warn(`[agent-memory-graph] Failed to extract from section: ${err}`);
1181
+ }
1182
+ }
1183
+ return { entities: totalEntities, relationships: totalRelationships };
1184
+ }
1185
+ async function importFromDirectory(dirPath, engine, config) {
1186
+ const { readdirSync, statSync } = await import("node:fs");
1187
+ const { join } = await import("node:path");
1188
+ let totalEntities = 0;
1189
+ let totalRelationships = 0;
1190
+ let fileCount = 0;
1191
+ const entries = readdirSync(dirPath);
1192
+ for (const entry of entries) {
1193
+ const fullPath = join(dirPath, entry);
1194
+ const stat = statSync(fullPath);
1195
+ if (stat.isFile() && (entry.endsWith(".md") || entry.endsWith(".txt"))) {
1196
+ const result = await importFromMemoryMd(fullPath, engine, config);
1197
+ totalEntities += result.entities;
1198
+ totalRelationships += result.relationships;
1199
+ fileCount++;
1200
+ }
1201
+ }
1202
+ return { entities: totalEntities, relationships: totalRelationships, files: fileCount };
1203
+ }
1204
+ function splitIntoSections(content) {
1205
+ const sections = content.split(/(?=^#{1,3}\s)/m);
1206
+ if (sections.length <= 1) {
1207
+ return content.split(/\n\n+/).filter((s) => s.trim().length > 0);
1208
+ }
1209
+ return sections.filter((s) => s.trim().length > 0);
1210
+ }
1211
+ var init_memory_md = __esm({
1212
+ "src/sync/memory-md.js"() {
1213
+ "use strict";
1214
+ init_extractor();
1215
+ }
1216
+ });
1217
+
1218
+ // src/extract/dedup.js
1219
+ function findDuplicates(engine, threshold = 0.85) {
1220
+ const entities = engine.listEntities({ limit: 1e4 });
1221
+ const duplicates = [];
1222
+ const processed = /* @__PURE__ */ new Set();
1223
+ for (let i = 0; i < entities.length; i++) {
1224
+ if (processed.has(entities[i].id))
1225
+ continue;
1226
+ for (let j = i + 1; j < entities.length; j++) {
1227
+ if (processed.has(entities[j].id))
1228
+ continue;
1229
+ if (entities[i].type.toLowerCase() !== entities[j].type.toLowerCase())
1230
+ continue;
1231
+ const sim = nameSimilarity(entities[i].name, entities[j].name);
1232
+ if (sim >= threshold) {
1233
+ duplicates.push({
1234
+ entity: entities[j],
1235
+ duplicateOf: entities[i],
1236
+ similarity: sim
1237
+ });
1238
+ processed.add(entities[j].id);
1239
+ }
1240
+ }
1241
+ }
1242
+ return duplicates;
1243
+ }
1244
+ function mergeEntities(engine, keepId, mergeId) {
1245
+ const keep = engine.getEntity(keepId);
1246
+ const merge = engine.getEntity(mergeId);
1247
+ if (!keep || !merge)
1248
+ return false;
1249
+ const mergedProps = { ...merge.properties, ...keep.properties };
1250
+ engine.updateEntity(keepId, { properties: mergedProps });
1251
+ engine.deleteEntity(mergeId);
1252
+ return true;
1253
+ }
1254
+ function nameSimilarity(a, b) {
1255
+ const na = a.toLowerCase().trim();
1256
+ const nb = b.toLowerCase().trim();
1257
+ if (na === nb)
1258
+ return 1;
1259
+ if (na.includes(nb) || nb.includes(na))
1260
+ return 0.9;
1261
+ const dist = levenshtein(na, nb);
1262
+ const maxLen = Math.max(na.length, nb.length);
1263
+ return maxLen === 0 ? 1 : 1 - dist / maxLen;
1264
+ }
1265
+ function levenshtein(a, b) {
1266
+ const m = a.length;
1267
+ const n = b.length;
1268
+ const dp = Array.from({ length: m + 1 }, () => Array(n + 1).fill(0));
1269
+ for (let i = 0; i <= m; i++)
1270
+ dp[i][0] = i;
1271
+ for (let j = 0; j <= n; j++)
1272
+ dp[0][j] = j;
1273
+ for (let i = 1; i <= m; i++) {
1274
+ for (let j = 1; j <= n; j++) {
1275
+ dp[i][j] = a[i - 1] === b[j - 1] ? dp[i - 1][j - 1] : 1 + Math.min(dp[i - 1][j], dp[i][j - 1], dp[i - 1][j - 1]);
1276
+ }
1277
+ }
1278
+ return dp[m][n];
1279
+ }
1280
+ var init_dedup = __esm({
1281
+ "src/extract/dedup.js"() {
1282
+ "use strict";
1283
+ }
1284
+ });
1285
+
1286
+ // src/index.js
1287
+ var src_exports = {};
1288
+ __export(src_exports, {
1289
+ MemoryGraph: () => MemoryGraph
1290
+ });
1291
+ var MemoryGraph;
1292
+ var init_src = __esm({
1293
+ "src/index.js"() {
1294
+ "use strict";
1295
+ init_engine();
1296
+ init_defaults();
1297
+ init_extractor();
1298
+ init_hybrid();
1299
+ init_natural_language();
1300
+ init_export();
1301
+ init_memory_md();
1302
+ init_dedup();
1303
+ MemoryGraph = class {
1304
+ engine;
1305
+ config;
1306
+ constructor(options = {}) {
1307
+ this.config = loadConfig(options.configPath);
1308
+ if (options.config) {
1309
+ this.config = { ...this.config, ...options.config };
1310
+ }
1311
+ const dbPath = options.path ?? this.config.storage.path;
1312
+ this.engine = new GraphEngine(dbPath);
1313
+ }
1314
+ // ─── Core API ──────────────────────────────────────────────────
1315
+ /**
1316
+ * Ingest text: extract entities and relationships, store in graph.
1317
+ */
1318
+ async ingest(text, options = {}) {
1319
+ const result = await extractFromText(text, this.config);
1320
+ for (const entity of result.entities) {
1321
+ this.engine.addEntity(entity.name, entity.type, entity.properties ?? {}, {
1322
+ source: options.source,
1323
+ confidence: entity.confidence
1324
+ });
1325
+ }
1326
+ for (const rel of result.relationships) {
1327
+ this.engine.addRelation(rel.from, rel.relation, rel.to, {
1328
+ source: options.source,
1329
+ confidence: rel.confidence,
1330
+ fromType: rel.fromType,
1331
+ toType: rel.toType
1332
+ });
1333
+ }
1334
+ this.engine.logExtraction(text, result.entities, result.relationships, options.sessionId);
1335
+ return result;
1336
+ }
1337
+ /**
1338
+ * Ask a natural language question against the graph.
1339
+ */
1340
+ async ask(question) {
1341
+ return naturalLanguageQuery(question, this.engine, this.config);
1342
+ }
1343
+ /**
1344
+ * Search entities by keyword.
1345
+ */
1346
+ search(query, limit) {
1347
+ return hybridSearch(this.engine, query, {
1348
+ ...this.config,
1349
+ query: { ...this.config.query, maxResults: limit ?? this.config.query.maxResults }
1350
+ });
1351
+ }
1352
+ // ─── Entity Management ─────────────────────────────────────────
1353
+ /**
1354
+ * Add an entity manually.
1355
+ */
1356
+ addEntity(name, type, properties = {}) {
1357
+ return this.engine.addEntity(name, type, properties);
1358
+ }
1359
+ /**
1360
+ * Add a relationship manually.
1361
+ */
1362
+ addRelation(from, relation, to, options) {
1363
+ return this.engine.addRelation(from, relation, to, options);
1364
+ }
1365
+ /**
1366
+ * Find an entity by name.
1367
+ */
1368
+ findEntity(name, type) {
1369
+ return this.engine.findEntityByName(name, type);
1370
+ }
1371
+ /**
1372
+ * List entities, optionally filtered by type.
1373
+ */
1374
+ listEntities(options) {
1375
+ return this.engine.listEntities(options);
1376
+ }
1377
+ /**
1378
+ * Delete an entity and its relationships.
1379
+ */
1380
+ deleteEntity(nameOrId) {
1381
+ const entity = this.engine.getEntity(nameOrId) ?? this.engine.findEntityByName(nameOrId);
1382
+ if (!entity)
1383
+ return false;
1384
+ return this.engine.deleteEntity(entity.id);
1385
+ }
1386
+ // ─── Graph Operations ──────────────────────────────────────────
1387
+ /**
1388
+ * Find shortest path between two entities.
1389
+ */
1390
+ findPath(from, to, maxHops) {
1391
+ return this.engine.findPath(from, to, maxHops ?? this.config.query.maxHops);
1392
+ }
1393
+ /**
1394
+ * Get all entities and relationships within N hops of an entity.
1395
+ */
1396
+ neighborhood(entityName, hops = 1) {
1397
+ return this.engine.getNeighborhood(entityName, hops);
1398
+ }
1399
+ // ─── Import / Export ───────────────────────────────────────────
1400
+ /**
1401
+ * Export graph to a format (json, mermaid, dot, csv).
1402
+ */
1403
+ export(format, options) {
1404
+ return exportGraph(this.engine, { format, ...options });
1405
+ }
1406
+ /**
1407
+ * Import from a MEMORY.md file or directory.
1408
+ */
1409
+ async importFrom(path) {
1410
+ const { statSync } = await import("node:fs");
1411
+ const stat = statSync(path);
1412
+ if (stat.isDirectory()) {
1413
+ const result = await importFromDirectory(path, this.engine, this.config);
1414
+ return { entities: result.entities, relationships: result.relationships };
1415
+ }
1416
+ return importFromMemoryMd(path, this.engine, this.config);
1417
+ }
1418
+ // ─── Maintenance ───────────────────────────────────────────────
1419
+ /**
1420
+ * Find and optionally merge duplicate entities.
1421
+ */
1422
+ deduplicate(options) {
1423
+ const threshold = options?.threshold ?? this.config.deduplication.similarityThreshold;
1424
+ const duplicates = findDuplicates(this.engine, threshold);
1425
+ if (options?.autoMerge) {
1426
+ for (const dup of duplicates) {
1427
+ mergeEntities(this.engine, dup.duplicateOf.id, dup.entity.id);
1428
+ }
1429
+ }
1430
+ return duplicates.map((d) => ({
1431
+ entity: d.entity.name,
1432
+ duplicateOf: d.duplicateOf.name,
1433
+ similarity: d.similarity
1434
+ }));
1435
+ }
1436
+ /**
1437
+ * Get graph statistics.
1438
+ */
1439
+ stats() {
1440
+ return this.engine.stats();
1441
+ }
1442
+ /**
1443
+ * Close database connection.
1444
+ */
1445
+ close() {
1446
+ this.engine.close();
1447
+ }
1448
+ };
1449
+ }
1450
+ });
1451
+
1452
+ // plugin/entry.ts
1453
+ import { definePluginEntry } from "openclaw/plugin-sdk/plugin-entry";
1454
+ import { resolve as resolve3 } from "node:path";
1455
+ import { homedir } from "node:os";
1456
+ if (!process.env.OPENAI_API_KEY) {
1457
+ process.env.OPENAI_API_KEY = process.env.MEMORY_GRAPH_API_KEY || "sk-local";
1458
+ }
1459
+ if (!process.env.OPENAI_BASE_URL) {
1460
+ process.env.OPENAI_BASE_URL = process.env.MEMORY_GRAPH_BASE_URL || "http://127.0.0.1:20128/v1";
1461
+ }
1462
+ var Type = {
1463
+ Object: (props) => ({ type: "object", properties: props, required: Object.keys(props).filter((k) => !props[k]._optional) }),
1464
+ String: (opts) => ({ type: "string", ...opts }),
1465
+ Number: (opts) => ({ type: "number", ...opts }),
1466
+ Optional: (schema) => ({ ...schema, _optional: true })
1467
+ };
1468
+ var graphInstance = null;
1469
+ function getDbPath(config) {
1470
+ const raw = config?.dbPath || "~/.openclaw/data/memory-graph.db";
1471
+ return raw.startsWith("~") ? resolve3(homedir(), raw.slice(2)) : resolve3(raw);
1472
+ }
1473
+ async function getGraph(config) {
1474
+ if (!graphInstance) {
1475
+ const { MemoryGraph: MemoryGraph2 } = await Promise.resolve().then(() => (init_src(), src_exports));
1476
+ graphInstance = new MemoryGraph2({
1477
+ path: getDbPath(config),
1478
+ config: {
1479
+ extraction: {
1480
+ provider: "openai",
1481
+ model: config?.extractionModel || process.env.MEMORY_GRAPH_MODEL || "kr/claude-haiku-4.5",
1482
+ autoExtract: true,
1483
+ minConfidence: config?.minConfidence ?? 0.7,
1484
+ batchSize: 5
1485
+ },
1486
+ domains: config?.domains ?? [],
1487
+ query: {
1488
+ maxHops: config?.maxHops ?? 3,
1489
+ maxResults: 10,
1490
+ includeConfidence: true
1491
+ }
1492
+ }
1493
+ });
1494
+ if (!process.env.OPENAI_API_KEY) {
1495
+ process.env.OPENAI_API_KEY = "sk-local";
1496
+ }
1497
+ if (!process.env.OPENAI_BASE_URL) {
1498
+ process.env.OPENAI_BASE_URL = "http://127.0.0.1:20128/v1";
1499
+ }
1500
+ }
1501
+ return graphInstance;
1502
+ }
1503
+ var entry_default = definePluginEntry({
1504
+ id: "memory-graph",
1505
+ name: "Memory Graph",
1506
+ description: "Auto-builds a knowledge graph from conversations. Extracts entities/relationships and exposes graph query tools.",
1507
+ register(api) {
1508
+ api.on(
1509
+ "message_received",
1510
+ async (event) => {
1511
+ const config = event.context?.pluginConfig;
1512
+ if (config?.autoIngest === false) return;
1513
+ const text = typeof event.content === "string" ? event.content : event.content?.text || event.content?.body || "";
1514
+ if (!text || text.length < 20) return;
1515
+ try {
1516
+ const graph = await getGraph(config);
1517
+ await graph.ingest(text, {
1518
+ source: `chat:${event.senderId || "unknown"}`,
1519
+ sessionId: event.sessionKey
1520
+ });
1521
+ } catch (err) {
1522
+ console.warn("[memory-graph] Auto-ingest failed:", err.message);
1523
+ }
1524
+ },
1525
+ { priority: 10 }
1526
+ );
1527
+ api.on("gateway_stop", async () => {
1528
+ if (graphInstance) {
1529
+ graphInstance.close();
1530
+ graphInstance = null;
1531
+ }
1532
+ });
1533
+ api.registerTool({
1534
+ name: "memory_graph_query",
1535
+ description: "Ask a natural language question against the knowledge graph. Use for relationship questions like 'What does Alice work on?', 'How is X connected to Y?', 'List all projects'.",
1536
+ parameters: Type.Object({
1537
+ question: Type.String({ description: "Natural language question" })
1538
+ }),
1539
+ async execute(_id, params, ctx) {
1540
+ const graph = await getGraph(ctx?.pluginConfig);
1541
+ const result = await graph.ask(params.question);
1542
+ return {
1543
+ content: [
1544
+ {
1545
+ type: "text",
1546
+ text: `${result.answer}${result.confidence < 0.5 ? `
1547
+ (Low confidence: ${(result.confidence * 100).toFixed(0)}%)` : ""}`
1548
+ }
1549
+ ]
1550
+ };
1551
+ }
1552
+ });
1553
+ api.registerTool({
1554
+ name: "memory_graph_ingest",
1555
+ description: "Manually extract entities and relationships from text and store in the knowledge graph. Use when you want to explicitly add information.",
1556
+ parameters: Type.Object({
1557
+ text: Type.String({ description: "Text to extract entities from" }),
1558
+ source: Type.Optional(Type.String({ description: "Source label" }))
1559
+ }),
1560
+ async execute(_id, params, ctx) {
1561
+ const graph = await getGraph(ctx?.pluginConfig);
1562
+ const result = await graph.ingest(params.text, { source: params.source || "manual" });
1563
+ const entities = result.entities.map((e) => `${e.name} (${e.type})`).join(", ");
1564
+ const rels = result.relationships.map((r) => `${r.from} -[${r.relation}]-> ${r.to}`).join(", ");
1565
+ return {
1566
+ content: [
1567
+ {
1568
+ type: "text",
1569
+ text: `Extracted ${result.entities.length} entities: ${entities}
1570
+ Relationships (${result.relationships.length}): ${rels}`
1571
+ }
1572
+ ]
1573
+ };
1574
+ }
1575
+ });
1576
+ api.registerTool({
1577
+ name: "memory_graph_search",
1578
+ description: "Search entities in the knowledge graph by keyword or type. Returns matching entities with their relationships.",
1579
+ parameters: Type.Object({
1580
+ query: Type.String({ description: "Search keyword" }),
1581
+ type: Type.Optional(Type.String({ description: "Filter by entity type" })),
1582
+ limit: Type.Optional(Type.Number({ description: "Max results", default: 10 }))
1583
+ }),
1584
+ async execute(_id, params, ctx) {
1585
+ const graph = await getGraph(ctx?.pluginConfig);
1586
+ if (params.type) {
1587
+ const entities = graph.listEntities({ type: params.type, limit: params.limit || 10 });
1588
+ const lines2 = entities.map((e) => `${e.name} (${e.type})`);
1589
+ return {
1590
+ content: [{ type: "text", text: lines2.length > 0 ? lines2.join("\n") : "No entities found." }]
1591
+ };
1592
+ }
1593
+ const results = graph.search(params.query, params.limit || 10);
1594
+ if (results.length === 0) {
1595
+ return { content: [{ type: "text", text: "No results found." }] };
1596
+ }
1597
+ const lines = results.map((r) => {
1598
+ const rels = r.relations.map((rel) => `${rel.direction === "outgoing" ? "\u2192" : "\u2190"} ${rel.relation} ${rel.target}`).join("; ");
1599
+ return `${r.entity.name} (${r.entity.type})${rels ? ": " + rels : ""}`;
1600
+ });
1601
+ return { content: [{ type: "text", text: lines.join("\n") }] };
1602
+ }
1603
+ });
1604
+ api.registerTool({
1605
+ name: "memory_graph_path",
1606
+ description: "Find the shortest path between two entities in the knowledge graph. Shows how they are connected through relationships.",
1607
+ parameters: Type.Object({
1608
+ from: Type.String({ description: "Starting entity name" }),
1609
+ to: Type.String({ description: "Target entity name" }),
1610
+ maxHops: Type.Optional(Type.Number({ description: "Maximum traversal hops", default: 3 }))
1611
+ }),
1612
+ async execute(_id, params, ctx) {
1613
+ const graph = await getGraph(ctx?.pluginConfig);
1614
+ const path = graph.findPath(params.from, params.to, params.maxHops || 3);
1615
+ if (!path) {
1616
+ return {
1617
+ content: [{ type: "text", text: `No path found between "${params.from}" and "${params.to}".` }]
1618
+ };
1619
+ }
1620
+ const display = path.path.map((node, i) => i < path.relations.length ? `${node} ${path.relations[i]}` : node).join(" ");
1621
+ return { content: [{ type: "text", text: `Path: ${display}` }] };
1622
+ }
1623
+ });
1624
+ api.registerTool({
1625
+ name: "memory_graph_stats",
1626
+ description: "Show knowledge graph statistics: entity count, relationship count, types.",
1627
+ parameters: Type.Object({}),
1628
+ async execute(_id, _params, ctx) {
1629
+ const graph = await getGraph(ctx?.pluginConfig);
1630
+ const stats = graph.stats();
1631
+ return {
1632
+ content: [
1633
+ {
1634
+ type: "text",
1635
+ text: [
1636
+ `Entities: ${stats.entities}`,
1637
+ `Relationships: ${stats.relationships}`,
1638
+ `Entity types: ${stats.entityTypes.join(", ") || "(none)"}`,
1639
+ `Relation types: ${stats.relationTypes.join(", ") || "(none)"}`,
1640
+ `Oldest: ${stats.oldestEntry || "(empty)"}`,
1641
+ `Newest: ${stats.newestEntry || "(empty)"}`
1642
+ ].join("\n")
1643
+ }
1644
+ ]
1645
+ };
1646
+ }
1647
+ });
1648
+ }
1649
+ });
1650
+ export {
1651
+ entry_default as default
1652
+ };