@hasna/mementos 0.3.9 → 0.4.1

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.
@@ -2,11 +2,18 @@
2
2
  // @bun
3
3
 
4
4
  // src/server/index.ts
5
- import { existsSync as existsSync2 } from "fs";
6
- import { dirname as dirname2, extname, join as join2 } from "path";
5
+ import { existsSync as existsSync3 } from "fs";
6
+ import { dirname as dirname3, extname, join as join3 } from "path";
7
7
  import { fileURLToPath } from "url";
8
8
 
9
9
  // src/types/index.ts
10
+ class EntityNotFoundError extends Error {
11
+ constructor(id) {
12
+ super(`Entity not found: ${id}`);
13
+ this.name = "EntityNotFoundError";
14
+ }
15
+ }
16
+
10
17
  class MemoryNotFoundError extends Error {
11
18
  constructor(id) {
12
19
  super(`Memory not found: ${id}`);
@@ -175,6 +182,113 @@ var MIGRATIONS = [
175
182
  );
176
183
 
177
184
  INSERT OR IGNORE INTO _migrations (id) VALUES (1);
185
+ `,
186
+ `
187
+ CREATE TABLE IF NOT EXISTS memory_versions (
188
+ id TEXT PRIMARY KEY,
189
+ memory_id TEXT NOT NULL,
190
+ version INTEGER NOT NULL,
191
+ value TEXT NOT NULL,
192
+ importance INTEGER NOT NULL,
193
+ scope TEXT NOT NULL,
194
+ category TEXT NOT NULL,
195
+ tags TEXT NOT NULL DEFAULT '[]',
196
+ summary TEXT,
197
+ pinned INTEGER NOT NULL DEFAULT 0,
198
+ status TEXT NOT NULL DEFAULT 'active',
199
+ created_at TEXT NOT NULL DEFAULT (datetime('now')),
200
+ UNIQUE(memory_id, version)
201
+ );
202
+
203
+ CREATE INDEX IF NOT EXISTS idx_memory_versions_memory ON memory_versions(memory_id);
204
+ CREATE INDEX IF NOT EXISTS idx_memory_versions_version ON memory_versions(memory_id, version);
205
+
206
+ INSERT OR IGNORE INTO _migrations (id) VALUES (2);
207
+ `,
208
+ `
209
+ CREATE VIRTUAL TABLE IF NOT EXISTS memories_fts USING fts5(
210
+ key, value, summary,
211
+ content='memories',
212
+ content_rowid='rowid'
213
+ );
214
+
215
+ CREATE TRIGGER IF NOT EXISTS memories_ai AFTER INSERT ON memories BEGIN
216
+ INSERT INTO memories_fts(rowid, key, value, summary) VALUES (new.rowid, new.key, new.value, new.summary);
217
+ END;
218
+
219
+ CREATE TRIGGER IF NOT EXISTS memories_ad AFTER DELETE ON memories BEGIN
220
+ INSERT INTO memories_fts(memories_fts, rowid, key, value, summary) VALUES('delete', old.rowid, old.key, old.value, old.summary);
221
+ END;
222
+
223
+ CREATE TRIGGER IF NOT EXISTS memories_au AFTER UPDATE ON memories BEGIN
224
+ INSERT INTO memories_fts(memories_fts, rowid, key, value, summary) VALUES('delete', old.rowid, old.key, old.value, old.summary);
225
+ INSERT INTO memories_fts(rowid, key, value, summary) VALUES (new.rowid, new.key, new.value, new.summary);
226
+ END;
227
+
228
+ INSERT INTO memories_fts(memories_fts) VALUES('rebuild');
229
+
230
+ INSERT OR IGNORE INTO _migrations (id) VALUES (3);
231
+ `,
232
+ `
233
+ CREATE TABLE IF NOT EXISTS search_history (
234
+ id TEXT PRIMARY KEY,
235
+ query TEXT NOT NULL,
236
+ result_count INTEGER NOT NULL DEFAULT 0,
237
+ agent_id TEXT,
238
+ project_id TEXT,
239
+ created_at TEXT NOT NULL DEFAULT (datetime('now'))
240
+ );
241
+ CREATE INDEX IF NOT EXISTS idx_search_history_query ON search_history(query);
242
+ CREATE INDEX IF NOT EXISTS idx_search_history_created ON search_history(created_at);
243
+
244
+ INSERT OR IGNORE INTO _migrations (id) VALUES (4);
245
+ `,
246
+ `
247
+ CREATE TABLE IF NOT EXISTS entities (
248
+ id TEXT PRIMARY KEY,
249
+ name TEXT NOT NULL,
250
+ type TEXT NOT NULL CHECK (type IN ('person','project','tool','concept','file','api','pattern','organization')),
251
+ description TEXT,
252
+ metadata TEXT NOT NULL DEFAULT '{}',
253
+ project_id TEXT,
254
+ created_at TEXT NOT NULL DEFAULT (datetime('now')),
255
+ updated_at TEXT NOT NULL DEFAULT (datetime('now')),
256
+ FOREIGN KEY (project_id) REFERENCES projects(id) ON DELETE SET NULL
257
+ );
258
+ CREATE UNIQUE INDEX IF NOT EXISTS idx_entities_unique_name_type_project
259
+ ON entities(name, type, COALESCE(project_id, ''));
260
+ CREATE INDEX IF NOT EXISTS idx_entities_name ON entities(name);
261
+ CREATE INDEX IF NOT EXISTS idx_entities_type ON entities(type);
262
+ CREATE INDEX IF NOT EXISTS idx_entities_project ON entities(project_id);
263
+
264
+ CREATE TABLE IF NOT EXISTS relations (
265
+ id TEXT PRIMARY KEY,
266
+ source_entity_id TEXT NOT NULL,
267
+ target_entity_id TEXT NOT NULL,
268
+ relation_type TEXT NOT NULL CHECK (relation_type IN ('uses','knows','depends_on','created_by','related_to','contradicts','part_of','implements')),
269
+ weight REAL NOT NULL DEFAULT 1.0,
270
+ metadata TEXT NOT NULL DEFAULT '{}',
271
+ created_at TEXT NOT NULL DEFAULT (datetime('now')),
272
+ UNIQUE(source_entity_id, target_entity_id, relation_type),
273
+ FOREIGN KEY (source_entity_id) REFERENCES entities(id) ON DELETE CASCADE,
274
+ FOREIGN KEY (target_entity_id) REFERENCES entities(id) ON DELETE CASCADE
275
+ );
276
+ CREATE INDEX IF NOT EXISTS idx_relations_source ON relations(source_entity_id);
277
+ CREATE INDEX IF NOT EXISTS idx_relations_target ON relations(target_entity_id);
278
+ CREATE INDEX IF NOT EXISTS idx_relations_type ON relations(relation_type);
279
+
280
+ CREATE TABLE IF NOT EXISTS entity_memories (
281
+ entity_id TEXT NOT NULL,
282
+ memory_id TEXT NOT NULL,
283
+ role TEXT NOT NULL DEFAULT 'context' CHECK (role IN ('subject','object','context')),
284
+ created_at TEXT NOT NULL DEFAULT (datetime('now')),
285
+ PRIMARY KEY (entity_id, memory_id),
286
+ FOREIGN KEY (entity_id) REFERENCES entities(id) ON DELETE CASCADE,
287
+ FOREIGN KEY (memory_id) REFERENCES memories(id) ON DELETE CASCADE
288
+ );
289
+ CREATE INDEX IF NOT EXISTS idx_entity_memories_memory ON entity_memories(memory_id);
290
+
291
+ INSERT OR IGNORE INTO _migrations (id) VALUES (5);
178
292
  `
179
293
  ];
180
294
  var _db = null;
@@ -245,7 +359,656 @@ function redactSecrets(text) {
245
359
  return result;
246
360
  }
247
361
 
362
+ // src/db/agents.ts
363
+ function parseAgentRow(row) {
364
+ return {
365
+ id: row["id"],
366
+ name: row["name"],
367
+ description: row["description"] || null,
368
+ role: row["role"] || null,
369
+ metadata: JSON.parse(row["metadata"] || "{}"),
370
+ created_at: row["created_at"],
371
+ last_seen_at: row["last_seen_at"]
372
+ };
373
+ }
374
+ function registerAgent(name, description, role, db) {
375
+ const d = db || getDatabase();
376
+ const timestamp = now();
377
+ const normalizedName = name.trim().toLowerCase();
378
+ const existing = d.query("SELECT * FROM agents WHERE LOWER(name) = ?").get(normalizedName);
379
+ if (existing) {
380
+ const existingId = existing["id"];
381
+ d.run("UPDATE agents SET last_seen_at = ? WHERE id = ?", [
382
+ timestamp,
383
+ existingId
384
+ ]);
385
+ if (description) {
386
+ d.run("UPDATE agents SET description = ? WHERE id = ?", [
387
+ description,
388
+ existingId
389
+ ]);
390
+ }
391
+ if (role) {
392
+ d.run("UPDATE agents SET role = ? WHERE id = ?", [
393
+ role,
394
+ existingId
395
+ ]);
396
+ }
397
+ return getAgent(existingId, d);
398
+ }
399
+ const id = shortUuid();
400
+ d.run("INSERT INTO agents (id, name, description, role, created_at, last_seen_at) VALUES (?, ?, ?, ?, ?, ?)", [id, normalizedName, description || null, role || "agent", timestamp, timestamp]);
401
+ return getAgent(id, d);
402
+ }
403
+ function getAgent(idOrName, db) {
404
+ const d = db || getDatabase();
405
+ let row = d.query("SELECT * FROM agents WHERE id = ?").get(idOrName);
406
+ if (row)
407
+ return parseAgentRow(row);
408
+ row = d.query("SELECT * FROM agents WHERE LOWER(name) = ?").get(idOrName.trim().toLowerCase());
409
+ if (row)
410
+ return parseAgentRow(row);
411
+ const rows = d.query("SELECT * FROM agents WHERE id LIKE ?").all(`${idOrName}%`);
412
+ if (rows.length === 1)
413
+ return parseAgentRow(rows[0]);
414
+ return null;
415
+ }
416
+ function listAgents(db) {
417
+ const d = db || getDatabase();
418
+ const rows = d.query("SELECT * FROM agents ORDER BY last_seen_at DESC").all();
419
+ return rows.map(parseAgentRow);
420
+ }
421
+
422
+ // src/db/projects.ts
423
+ function parseProjectRow(row) {
424
+ return {
425
+ id: row["id"],
426
+ name: row["name"],
427
+ path: row["path"],
428
+ description: row["description"] || null,
429
+ memory_prefix: row["memory_prefix"] || null,
430
+ created_at: row["created_at"],
431
+ updated_at: row["updated_at"]
432
+ };
433
+ }
434
+ function registerProject(name, path, description, memoryPrefix, db) {
435
+ const d = db || getDatabase();
436
+ const timestamp = now();
437
+ const existing = d.query("SELECT * FROM projects WHERE path = ?").get(path);
438
+ if (existing) {
439
+ const existingId = existing["id"];
440
+ d.run("UPDATE projects SET updated_at = ? WHERE id = ?", [
441
+ timestamp,
442
+ existingId
443
+ ]);
444
+ return parseProjectRow(existing);
445
+ }
446
+ const id = uuid();
447
+ d.run("INSERT INTO projects (id, name, path, description, memory_prefix, created_at, updated_at) VALUES (?, ?, ?, ?, ?, ?, ?)", [id, name, path, description || null, memoryPrefix || null, timestamp, timestamp]);
448
+ return getProject(id, d);
449
+ }
450
+ function getProject(idOrPath, db) {
451
+ const d = db || getDatabase();
452
+ let row = d.query("SELECT * FROM projects WHERE id = ?").get(idOrPath);
453
+ if (row)
454
+ return parseProjectRow(row);
455
+ row = d.query("SELECT * FROM projects WHERE path = ?").get(idOrPath);
456
+ if (row)
457
+ return parseProjectRow(row);
458
+ return null;
459
+ }
460
+ function listProjects(db) {
461
+ const d = db || getDatabase();
462
+ const rows = d.query("SELECT * FROM projects ORDER BY updated_at DESC").all();
463
+ return rows.map(parseProjectRow);
464
+ }
465
+
466
+ // src/lib/extractor.ts
467
+ var TECH_KEYWORDS = new Set([
468
+ "typescript",
469
+ "javascript",
470
+ "python",
471
+ "rust",
472
+ "go",
473
+ "java",
474
+ "ruby",
475
+ "swift",
476
+ "kotlin",
477
+ "react",
478
+ "vue",
479
+ "angular",
480
+ "svelte",
481
+ "nextjs",
482
+ "bun",
483
+ "node",
484
+ "deno",
485
+ "sqlite",
486
+ "postgres",
487
+ "mysql",
488
+ "redis",
489
+ "docker",
490
+ "kubernetes",
491
+ "git",
492
+ "npm",
493
+ "yarn",
494
+ "pnpm",
495
+ "webpack",
496
+ "vite",
497
+ "tailwind",
498
+ "prisma",
499
+ "drizzle",
500
+ "zod",
501
+ "commander",
502
+ "express",
503
+ "fastify",
504
+ "hono"
505
+ ]);
506
+ var FILE_PATH_RE = /(?:^|\s)((?:\/|\.\/|~\/)?(?:[\w.-]+\/)+[\w.-]+\.\w+)/g;
507
+ var URL_RE = /https?:\/\/[^\s)]+/g;
508
+ var NPM_PACKAGE_RE = /@[\w-]+\/[\w.-]+/g;
509
+ var PASCAL_CASE_RE = /\b([A-Z][a-z]+(?:[A-Z][a-z]+)+)\b/g;
510
+ function getSearchText(memory) {
511
+ const parts = [memory.key, memory.value];
512
+ if (memory.summary)
513
+ parts.push(memory.summary);
514
+ return parts.join(" ");
515
+ }
516
+ function extractEntities(memory, db) {
517
+ const text = getSearchText(memory);
518
+ const entityMap = new Map;
519
+ function add(name, type, confidence) {
520
+ const normalized = name.toLowerCase();
521
+ if (normalized.length < 3)
522
+ return;
523
+ const existing = entityMap.get(normalized);
524
+ if (!existing || existing.confidence < confidence) {
525
+ entityMap.set(normalized, { name: normalized, type, confidence });
526
+ }
527
+ }
528
+ for (const match of text.matchAll(FILE_PATH_RE)) {
529
+ add(match[1].trim(), "file", 0.9);
530
+ }
531
+ for (const match of text.matchAll(URL_RE)) {
532
+ add(match[0], "api", 0.8);
533
+ }
534
+ for (const match of text.matchAll(NPM_PACKAGE_RE)) {
535
+ add(match[0], "tool", 0.85);
536
+ }
537
+ try {
538
+ const d = db || getDatabase();
539
+ const agents = listAgents(d);
540
+ const textLower2 = text.toLowerCase();
541
+ for (const agent of agents) {
542
+ const nameLower = agent.name.toLowerCase();
543
+ if (nameLower.length >= 3 && textLower2.includes(nameLower)) {
544
+ add(agent.name, "person", 0.95);
545
+ }
546
+ }
547
+ } catch {}
548
+ try {
549
+ const d = db || getDatabase();
550
+ const projects = listProjects(d);
551
+ const textLower2 = text.toLowerCase();
552
+ for (const project of projects) {
553
+ const nameLower = project.name.toLowerCase();
554
+ if (nameLower.length >= 3 && textLower2.includes(nameLower)) {
555
+ add(project.name, "project", 0.95);
556
+ }
557
+ }
558
+ } catch {}
559
+ const textLower = text.toLowerCase();
560
+ for (const keyword of TECH_KEYWORDS) {
561
+ const re = new RegExp(`\\b${keyword}\\b`, "i");
562
+ if (re.test(textLower)) {
563
+ add(keyword, "tool", 0.7);
564
+ }
565
+ }
566
+ for (const match of text.matchAll(PASCAL_CASE_RE)) {
567
+ add(match[1], "concept", 0.5);
568
+ }
569
+ return Array.from(entityMap.values()).sort((a, b) => b.confidence - a.confidence);
570
+ }
571
+
572
+ // src/db/entities.ts
573
+ function parseEntityRow(row) {
574
+ return {
575
+ id: row["id"],
576
+ name: row["name"],
577
+ type: row["type"],
578
+ description: row["description"] || null,
579
+ metadata: JSON.parse(row["metadata"] || "{}"),
580
+ project_id: row["project_id"] || null,
581
+ created_at: row["created_at"],
582
+ updated_at: row["updated_at"]
583
+ };
584
+ }
585
+ function createEntity(input, db) {
586
+ const d = db || getDatabase();
587
+ const timestamp = now();
588
+ const metadataJson = JSON.stringify(input.metadata || {});
589
+ const existing = d.query(`SELECT * FROM entities
590
+ WHERE name = ? AND type = ? AND COALESCE(project_id, '') = ?`).get(input.name, input.type, input.project_id || "");
591
+ if (existing) {
592
+ const sets = ["updated_at = ?"];
593
+ const params = [timestamp];
594
+ if (input.description !== undefined) {
595
+ sets.push("description = ?");
596
+ params.push(input.description);
597
+ }
598
+ if (input.metadata !== undefined) {
599
+ sets.push("metadata = ?");
600
+ params.push(metadataJson);
601
+ }
602
+ const existingId = existing["id"];
603
+ params.push(existingId);
604
+ d.run(`UPDATE entities SET ${sets.join(", ")} WHERE id = ?`, params);
605
+ return getEntity(existingId, d);
606
+ }
607
+ const id = shortUuid();
608
+ d.run(`INSERT INTO entities (id, name, type, description, metadata, project_id, created_at, updated_at)
609
+ VALUES (?, ?, ?, ?, ?, ?, ?, ?)`, [
610
+ id,
611
+ input.name,
612
+ input.type,
613
+ input.description || null,
614
+ metadataJson,
615
+ input.project_id || null,
616
+ timestamp,
617
+ timestamp
618
+ ]);
619
+ return getEntity(id, d);
620
+ }
621
+ function getEntity(id, db) {
622
+ const d = db || getDatabase();
623
+ const row = d.query("SELECT * FROM entities WHERE id = ?").get(id);
624
+ if (!row)
625
+ throw new EntityNotFoundError(id);
626
+ return parseEntityRow(row);
627
+ }
628
+ function getEntityByName(name, type, projectId, db) {
629
+ const d = db || getDatabase();
630
+ let sql = "SELECT * FROM entities WHERE name = ?";
631
+ const params = [name];
632
+ if (type) {
633
+ sql += " AND type = ?";
634
+ params.push(type);
635
+ }
636
+ if (projectId !== undefined) {
637
+ sql += " AND project_id = ?";
638
+ params.push(projectId);
639
+ }
640
+ sql += " LIMIT 1";
641
+ const row = d.query(sql).get(...params);
642
+ if (!row)
643
+ return null;
644
+ return parseEntityRow(row);
645
+ }
646
+ function listEntities(filter = {}, db) {
647
+ const d = db || getDatabase();
648
+ const conditions = [];
649
+ const params = [];
650
+ if (filter.type) {
651
+ conditions.push("type = ?");
652
+ params.push(filter.type);
653
+ }
654
+ if (filter.project_id) {
655
+ conditions.push("project_id = ?");
656
+ params.push(filter.project_id);
657
+ }
658
+ if (filter.search) {
659
+ conditions.push("(name LIKE ? OR description LIKE ?)");
660
+ const term = `%${filter.search}%`;
661
+ params.push(term, term);
662
+ }
663
+ let sql = "SELECT * FROM entities";
664
+ if (conditions.length > 0) {
665
+ sql += ` WHERE ${conditions.join(" AND ")}`;
666
+ }
667
+ sql += " ORDER BY updated_at DESC";
668
+ if (filter.limit) {
669
+ sql += " LIMIT ?";
670
+ params.push(filter.limit);
671
+ }
672
+ if (filter.offset) {
673
+ sql += " OFFSET ?";
674
+ params.push(filter.offset);
675
+ }
676
+ const rows = d.query(sql).all(...params);
677
+ return rows.map(parseEntityRow);
678
+ }
679
+ function updateEntity(id, input, db) {
680
+ const d = db || getDatabase();
681
+ const existing = d.query("SELECT id FROM entities WHERE id = ?").get(id);
682
+ if (!existing)
683
+ throw new EntityNotFoundError(id);
684
+ const sets = ["updated_at = ?"];
685
+ const params = [now()];
686
+ if (input.name !== undefined) {
687
+ sets.push("name = ?");
688
+ params.push(input.name);
689
+ }
690
+ if (input.type !== undefined) {
691
+ sets.push("type = ?");
692
+ params.push(input.type);
693
+ }
694
+ if (input.description !== undefined) {
695
+ sets.push("description = ?");
696
+ params.push(input.description);
697
+ }
698
+ if (input.metadata !== undefined) {
699
+ sets.push("metadata = ?");
700
+ params.push(JSON.stringify(input.metadata));
701
+ }
702
+ params.push(id);
703
+ d.run(`UPDATE entities SET ${sets.join(", ")} WHERE id = ?`, params);
704
+ return getEntity(id, d);
705
+ }
706
+ function deleteEntity(id, db) {
707
+ const d = db || getDatabase();
708
+ const result = d.run("DELETE FROM entities WHERE id = ?", [id]);
709
+ if (result.changes === 0)
710
+ throw new EntityNotFoundError(id);
711
+ }
712
+ function mergeEntities(sourceId, targetId, db) {
713
+ const d = db || getDatabase();
714
+ getEntity(sourceId, d);
715
+ getEntity(targetId, d);
716
+ d.run(`UPDATE OR IGNORE relations SET source_entity_id = ? WHERE source_entity_id = ?`, [targetId, sourceId]);
717
+ d.run(`UPDATE OR IGNORE relations SET target_entity_id = ? WHERE target_entity_id = ?`, [targetId, sourceId]);
718
+ d.run("DELETE FROM relations WHERE source_entity_id = ? OR target_entity_id = ?", [
719
+ sourceId,
720
+ sourceId
721
+ ]);
722
+ d.run(`UPDATE OR IGNORE entity_memories SET entity_id = ? WHERE entity_id = ?`, [targetId, sourceId]);
723
+ d.run("DELETE FROM entity_memories WHERE entity_id = ?", [sourceId]);
724
+ d.run("DELETE FROM entities WHERE id = ?", [sourceId]);
725
+ d.run("UPDATE entities SET updated_at = ? WHERE id = ?", [now(), targetId]);
726
+ return getEntity(targetId, d);
727
+ }
728
+
729
+ // src/db/entity-memories.ts
730
+ function parseEntityMemoryRow(row) {
731
+ return {
732
+ entity_id: row["entity_id"],
733
+ memory_id: row["memory_id"],
734
+ role: row["role"],
735
+ created_at: row["created_at"]
736
+ };
737
+ }
738
+ function linkEntityToMemory(entityId, memoryId, role = "context", db) {
739
+ const d = db || getDatabase();
740
+ const timestamp = now();
741
+ d.run(`INSERT OR IGNORE INTO entity_memories (entity_id, memory_id, role, created_at)
742
+ VALUES (?, ?, ?, ?)`, [entityId, memoryId, role, timestamp]);
743
+ const row = d.query("SELECT * FROM entity_memories WHERE entity_id = ? AND memory_id = ?").get(entityId, memoryId);
744
+ return parseEntityMemoryRow(row);
745
+ }
746
+ function unlinkEntityFromMemory(entityId, memoryId, db) {
747
+ const d = db || getDatabase();
748
+ d.run("DELETE FROM entity_memories WHERE entity_id = ? AND memory_id = ?", [entityId, memoryId]);
749
+ }
750
+ function getMemoriesForEntity(entityId, db) {
751
+ const d = db || getDatabase();
752
+ const rows = d.query(`SELECT m.* FROM memories m
753
+ INNER JOIN entity_memories em ON em.memory_id = m.id
754
+ WHERE em.entity_id = ?
755
+ ORDER BY m.importance DESC, m.created_at DESC`).all(entityId);
756
+ return rows.map(parseMemoryRow);
757
+ }
758
+ function getEntityMemoryLinks(entityId, memoryId, db) {
759
+ const d = db || getDatabase();
760
+ const conditions = [];
761
+ const params = [];
762
+ if (entityId) {
763
+ conditions.push("entity_id = ?");
764
+ params.push(entityId);
765
+ }
766
+ if (memoryId) {
767
+ conditions.push("memory_id = ?");
768
+ params.push(memoryId);
769
+ }
770
+ let sql = "SELECT * FROM entity_memories";
771
+ if (conditions.length > 0) {
772
+ sql += ` WHERE ${conditions.join(" AND ")}`;
773
+ }
774
+ sql += " ORDER BY created_at DESC";
775
+ const rows = d.query(sql).all(...params);
776
+ return rows.map(parseEntityMemoryRow);
777
+ }
778
+
779
+ // src/db/relations.ts
780
+ function parseRelationRow(row) {
781
+ return {
782
+ id: row["id"],
783
+ source_entity_id: row["source_entity_id"],
784
+ target_entity_id: row["target_entity_id"],
785
+ relation_type: row["relation_type"],
786
+ weight: row["weight"],
787
+ metadata: JSON.parse(row["metadata"] || "{}"),
788
+ created_at: row["created_at"]
789
+ };
790
+ }
791
+ function parseEntityRow2(row) {
792
+ return {
793
+ id: row["id"],
794
+ name: row["name"],
795
+ type: row["type"],
796
+ description: row["description"] || null,
797
+ metadata: JSON.parse(row["metadata"] || "{}"),
798
+ project_id: row["project_id"] || null,
799
+ created_at: row["created_at"],
800
+ updated_at: row["updated_at"]
801
+ };
802
+ }
803
+ function createRelation(input, db) {
804
+ const d = db || getDatabase();
805
+ const id = shortUuid();
806
+ const timestamp = now();
807
+ const weight = input.weight ?? 1;
808
+ const metadata = JSON.stringify(input.metadata ?? {});
809
+ d.run(`INSERT INTO relations (id, source_entity_id, target_entity_id, relation_type, weight, metadata, created_at)
810
+ VALUES (?, ?, ?, ?, ?, ?, ?)
811
+ ON CONFLICT(source_entity_id, target_entity_id, relation_type)
812
+ DO UPDATE SET weight = excluded.weight, metadata = excluded.metadata`, [id, input.source_entity_id, input.target_entity_id, input.relation_type, weight, metadata, timestamp]);
813
+ const row = d.query(`SELECT * FROM relations
814
+ WHERE source_entity_id = ? AND target_entity_id = ? AND relation_type = ?`).get(input.source_entity_id, input.target_entity_id, input.relation_type);
815
+ return parseRelationRow(row);
816
+ }
817
+ function getRelation(id, db) {
818
+ const d = db || getDatabase();
819
+ const row = d.query("SELECT * FROM relations WHERE id = ?").get(id);
820
+ if (!row)
821
+ throw new Error(`Relation not found: ${id}`);
822
+ return parseRelationRow(row);
823
+ }
824
+ function listRelations(filter, db) {
825
+ const d = db || getDatabase();
826
+ const conditions = [];
827
+ const params = [];
828
+ if (filter.entity_id) {
829
+ const dir = filter.direction || "both";
830
+ if (dir === "outgoing") {
831
+ conditions.push("source_entity_id = ?");
832
+ params.push(filter.entity_id);
833
+ } else if (dir === "incoming") {
834
+ conditions.push("target_entity_id = ?");
835
+ params.push(filter.entity_id);
836
+ } else {
837
+ conditions.push("(source_entity_id = ? OR target_entity_id = ?)");
838
+ params.push(filter.entity_id, filter.entity_id);
839
+ }
840
+ }
841
+ if (filter.relation_type) {
842
+ conditions.push("relation_type = ?");
843
+ params.push(filter.relation_type);
844
+ }
845
+ const where = conditions.length > 0 ? `WHERE ${conditions.join(" AND ")}` : "";
846
+ const rows = d.query(`SELECT * FROM relations ${where} ORDER BY created_at DESC`).all(...params);
847
+ return rows.map(parseRelationRow);
848
+ }
849
+ function deleteRelation(id, db) {
850
+ const d = db || getDatabase();
851
+ const result = d.run("DELETE FROM relations WHERE id = ?", [id]);
852
+ if (result.changes === 0)
853
+ throw new Error(`Relation not found: ${id}`);
854
+ }
855
+ function getEntityGraph(entityId, depth = 2, db) {
856
+ const d = db || getDatabase();
857
+ const entityRows = d.query(`WITH RECURSIVE graph(id, depth) AS (
858
+ VALUES(?, 0)
859
+ UNION
860
+ SELECT CASE WHEN r.source_entity_id = g.id THEN r.target_entity_id ELSE r.source_entity_id END, g.depth + 1
861
+ FROM relations r JOIN graph g ON (r.source_entity_id = g.id OR r.target_entity_id = g.id)
862
+ WHERE g.depth < ?
863
+ )
864
+ SELECT DISTINCT e.* FROM entities e JOIN graph g ON e.id = g.id`).all(entityId, depth);
865
+ const entities = entityRows.map(parseEntityRow2);
866
+ const entityIds = new Set(entities.map((e) => e.id));
867
+ if (entityIds.size === 0) {
868
+ return { entities: [], relations: [] };
869
+ }
870
+ const placeholders = Array.from(entityIds).map(() => "?").join(",");
871
+ const relationRows = d.query(`SELECT * FROM relations
872
+ WHERE source_entity_id IN (${placeholders})
873
+ AND target_entity_id IN (${placeholders})`).all(...Array.from(entityIds), ...Array.from(entityIds));
874
+ const relations = relationRows.map(parseRelationRow);
875
+ return { entities, relations };
876
+ }
877
+ function findPath(fromEntityId, toEntityId, maxDepth = 5, db) {
878
+ const d = db || getDatabase();
879
+ const rows = d.query(`WITH RECURSIVE path(id, trail, depth) AS (
880
+ SELECT ?, ?, 0
881
+ UNION
882
+ SELECT
883
+ CASE WHEN r.source_entity_id = p.id THEN r.target_entity_id ELSE r.source_entity_id END,
884
+ p.trail || ',' || CASE WHEN r.source_entity_id = p.id THEN r.target_entity_id ELSE r.source_entity_id END,
885
+ p.depth + 1
886
+ FROM relations r JOIN path p ON (r.source_entity_id = p.id OR r.target_entity_id = p.id)
887
+ WHERE p.depth < ?
888
+ AND INSTR(p.trail, CASE WHEN r.source_entity_id = p.id THEN r.target_entity_id ELSE r.source_entity_id END) = 0
889
+ )
890
+ SELECT trail FROM path WHERE id = ? ORDER BY depth ASC LIMIT 1`).get(fromEntityId, fromEntityId, maxDepth, toEntityId);
891
+ if (!rows)
892
+ return null;
893
+ const ids = rows.trail.split(",");
894
+ const entities = [];
895
+ for (const id of ids) {
896
+ const row = d.query("SELECT * FROM entities WHERE id = ?").get(id);
897
+ if (row)
898
+ entities.push(parseEntityRow2(row));
899
+ }
900
+ return entities.length > 0 ? entities : null;
901
+ }
902
+
903
+ // src/lib/config.ts
904
+ import { existsSync as existsSync2, mkdirSync as mkdirSync2, readFileSync } from "fs";
905
+ import { homedir } from "os";
906
+ import { dirname as dirname2, join as join2, resolve as resolve2 } from "path";
907
+ var DEFAULT_CONFIG = {
908
+ default_scope: "private",
909
+ default_category: "knowledge",
910
+ default_importance: 5,
911
+ max_entries: 1000,
912
+ max_entries_per_scope: {
913
+ global: 500,
914
+ shared: 300,
915
+ private: 200
916
+ },
917
+ injection: {
918
+ max_tokens: 500,
919
+ min_importance: 5,
920
+ categories: ["preference", "fact"],
921
+ refresh_interval: 5
922
+ },
923
+ extraction: {
924
+ enabled: true,
925
+ min_confidence: 0.5
926
+ },
927
+ sync_agents: ["claude", "codex", "gemini"],
928
+ auto_cleanup: {
929
+ enabled: true,
930
+ expired_check_interval: 3600,
931
+ unused_archive_days: 7,
932
+ stale_deprioritize_days: 14
933
+ }
934
+ };
935
+ function deepMerge(target, source) {
936
+ const result = { ...target };
937
+ for (const key of Object.keys(source)) {
938
+ const sourceVal = source[key];
939
+ const targetVal = result[key];
940
+ if (sourceVal !== null && typeof sourceVal === "object" && !Array.isArray(sourceVal) && targetVal !== null && typeof targetVal === "object" && !Array.isArray(targetVal)) {
941
+ result[key] = deepMerge(targetVal, sourceVal);
942
+ } else {
943
+ result[key] = sourceVal;
944
+ }
945
+ }
946
+ return result;
947
+ }
948
+ var VALID_SCOPES = ["global", "shared", "private"];
949
+ var VALID_CATEGORIES = [
950
+ "preference",
951
+ "fact",
952
+ "knowledge",
953
+ "history"
954
+ ];
955
+ function isValidScope(value) {
956
+ return VALID_SCOPES.includes(value);
957
+ }
958
+ function isValidCategory(value) {
959
+ return VALID_CATEGORIES.includes(value);
960
+ }
961
+ function loadConfig() {
962
+ const configPath = join2(homedir(), ".mementos", "config.json");
963
+ let fileConfig = {};
964
+ if (existsSync2(configPath)) {
965
+ try {
966
+ const raw = readFileSync(configPath, "utf-8");
967
+ fileConfig = JSON.parse(raw);
968
+ } catch {}
969
+ }
970
+ const merged = deepMerge(DEFAULT_CONFIG, fileConfig);
971
+ const envScope = process.env["MEMENTOS_DEFAULT_SCOPE"];
972
+ if (envScope && isValidScope(envScope)) {
973
+ merged.default_scope = envScope;
974
+ }
975
+ const envCategory = process.env["MEMENTOS_DEFAULT_CATEGORY"];
976
+ if (envCategory && isValidCategory(envCategory)) {
977
+ merged.default_category = envCategory;
978
+ }
979
+ const envImportance = process.env["MEMENTOS_DEFAULT_IMPORTANCE"];
980
+ if (envImportance) {
981
+ const parsed = parseInt(envImportance, 10);
982
+ if (!Number.isNaN(parsed) && parsed >= 1 && parsed <= 10) {
983
+ merged.default_importance = parsed;
984
+ }
985
+ }
986
+ return merged;
987
+ }
988
+
248
989
  // src/db/memories.ts
990
+ function runEntityExtraction(memory, projectId, d) {
991
+ const config = loadConfig();
992
+ if (config.extraction?.enabled === false)
993
+ return;
994
+ const extracted = extractEntities(memory, d);
995
+ const minConfidence = config.extraction?.min_confidence ?? 0.5;
996
+ const entityIds = [];
997
+ for (const ext of extracted) {
998
+ if (ext.confidence >= minConfidence) {
999
+ const entity = createEntity({ name: ext.name, type: ext.type, project_id: projectId }, d);
1000
+ linkEntityToMemory(entity.id, memory.id, "context", d);
1001
+ entityIds.push(entity.id);
1002
+ }
1003
+ }
1004
+ for (let i = 0;i < entityIds.length; i++) {
1005
+ for (let j = i + 1;j < entityIds.length; j++) {
1006
+ try {
1007
+ createRelation({ source_entity_id: entityIds[i], target_entity_id: entityIds[j], relation_type: "related_to" }, d);
1008
+ } catch {}
1009
+ }
1010
+ }
1011
+ }
249
1012
  function parseMemoryRow(row) {
250
1013
  return {
251
1014
  id: row["id"],
@@ -312,7 +1075,15 @@ function createMemory(input, dedupeMode = "merge", db) {
312
1075
  for (const tag of tags) {
313
1076
  insertTag2.run(existing.id, tag);
314
1077
  }
315
- return getMemory(existing.id, d);
1078
+ const merged = getMemory(existing.id, d);
1079
+ try {
1080
+ const oldLinks = getEntityMemoryLinks(undefined, merged.id, d);
1081
+ for (const link of oldLinks) {
1082
+ unlinkEntityFromMemory(link.entity_id, merged.id, d);
1083
+ }
1084
+ runEntityExtraction(merged, input.project_id, d);
1085
+ } catch {}
1086
+ return merged;
316
1087
  }
317
1088
  }
318
1089
  d.run(`INSERT INTO memories (id, key, value, category, scope, summary, tags, importance, source, status, pinned, agent_id, project_id, session_id, metadata, access_count, version, expires_at, created_at, updated_at)
@@ -338,7 +1109,11 @@ function createMemory(input, dedupeMode = "merge", db) {
338
1109
  for (const tag of tags) {
339
1110
  insertTag.run(id, tag);
340
1111
  }
341
- return getMemory(id, d);
1112
+ const memory = getMemory(id, d);
1113
+ try {
1114
+ runEntityExtraction(memory, input.project_id, d);
1115
+ } catch {}
1116
+ return memory;
342
1117
  }
343
1118
  function getMemory(id, db) {
344
1119
  const d = db || getDatabase();
@@ -448,6 +1223,23 @@ function updateMemory(id, input, db) {
448
1223
  if (existing.version !== input.version) {
449
1224
  throw new VersionConflictError(id, input.version, existing.version);
450
1225
  }
1226
+ try {
1227
+ d.run(`INSERT OR IGNORE INTO memory_versions (id, memory_id, version, value, importance, scope, category, tags, summary, pinned, status, created_at)
1228
+ VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)`, [
1229
+ uuid(),
1230
+ existing.id,
1231
+ existing.version,
1232
+ existing.value,
1233
+ existing.importance,
1234
+ existing.scope,
1235
+ existing.category,
1236
+ JSON.stringify(existing.tags),
1237
+ existing.summary,
1238
+ existing.pinned ? 1 : 0,
1239
+ existing.status,
1240
+ existing.updated_at
1241
+ ]);
1242
+ } catch {}
451
1243
  const sets = ["version = version + 1", "updated_at = ?"];
452
1244
  const params = [now()];
453
1245
  if (input.value !== undefined) {
@@ -497,7 +1289,17 @@ function updateMemory(id, input, db) {
497
1289
  }
498
1290
  params.push(id);
499
1291
  d.run(`UPDATE memories SET ${sets.join(", ")} WHERE id = ?`, params);
500
- return getMemory(id, d);
1292
+ const updated = getMemory(id, d);
1293
+ try {
1294
+ if (input.value !== undefined) {
1295
+ const oldLinks = getEntityMemoryLinks(undefined, updated.id, d);
1296
+ for (const link of oldLinks) {
1297
+ unlinkEntityFromMemory(link.entity_id, updated.id, d);
1298
+ }
1299
+ runEntityExtraction(updated, existing.project_id || undefined, d);
1300
+ }
1301
+ } catch {}
1302
+ return updated;
501
1303
  }
502
1304
  function deleteMemory(id, db) {
503
1305
  const d = db || getDatabase();
@@ -510,112 +1312,13 @@ function touchMemory(id, db) {
510
1312
  }
511
1313
  function cleanExpiredMemories(db) {
512
1314
  const d = db || getDatabase();
513
- const timestamp = now();
514
- const result = d.run("DELETE FROM memories WHERE expires_at IS NOT NULL AND expires_at < ?", [timestamp]);
515
- return result.changes;
516
- }
517
-
518
- // src/db/agents.ts
519
- function parseAgentRow(row) {
520
- return {
521
- id: row["id"],
522
- name: row["name"],
523
- description: row["description"] || null,
524
- role: row["role"] || null,
525
- metadata: JSON.parse(row["metadata"] || "{}"),
526
- created_at: row["created_at"],
527
- last_seen_at: row["last_seen_at"]
528
- };
529
- }
530
- function registerAgent(name, description, role, db) {
531
- const d = db || getDatabase();
532
- const timestamp = now();
533
- const existing = d.query("SELECT * FROM agents WHERE name = ?").get(name);
534
- if (existing) {
535
- const existingId = existing["id"];
536
- d.run("UPDATE agents SET last_seen_at = ? WHERE id = ?", [
537
- timestamp,
538
- existingId
539
- ]);
540
- if (description) {
541
- d.run("UPDATE agents SET description = ? WHERE id = ?", [
542
- description,
543
- existingId
544
- ]);
545
- }
546
- if (role) {
547
- d.run("UPDATE agents SET role = ? WHERE id = ?", [
548
- role,
549
- existingId
550
- ]);
551
- }
552
- return getAgent(existingId, d);
553
- }
554
- const id = shortUuid();
555
- d.run("INSERT INTO agents (id, name, description, role, created_at, last_seen_at) VALUES (?, ?, ?, ?, ?, ?)", [id, name, description || null, role || "agent", timestamp, timestamp]);
556
- return getAgent(id, d);
557
- }
558
- function getAgent(idOrName, db) {
559
- const d = db || getDatabase();
560
- let row = d.query("SELECT * FROM agents WHERE id = ?").get(idOrName);
561
- if (row)
562
- return parseAgentRow(row);
563
- row = d.query("SELECT * FROM agents WHERE name = ?").get(idOrName);
564
- if (row)
565
- return parseAgentRow(row);
566
- const rows = d.query("SELECT * FROM agents WHERE id LIKE ?").all(`${idOrName}%`);
567
- if (rows.length === 1)
568
- return parseAgentRow(rows[0]);
569
- return null;
570
- }
571
- function listAgents(db) {
572
- const d = db || getDatabase();
573
- const rows = d.query("SELECT * FROM agents ORDER BY last_seen_at DESC").all();
574
- return rows.map(parseAgentRow);
575
- }
576
-
577
- // src/db/projects.ts
578
- function parseProjectRow(row) {
579
- return {
580
- id: row["id"],
581
- name: row["name"],
582
- path: row["path"],
583
- description: row["description"] || null,
584
- memory_prefix: row["memory_prefix"] || null,
585
- created_at: row["created_at"],
586
- updated_at: row["updated_at"]
587
- };
588
- }
589
- function registerProject(name, path, description, memoryPrefix, db) {
590
- const d = db || getDatabase();
591
- const timestamp = now();
592
- const existing = d.query("SELECT * FROM projects WHERE path = ?").get(path);
593
- if (existing) {
594
- const existingId = existing["id"];
595
- d.run("UPDATE projects SET updated_at = ? WHERE id = ?", [
596
- timestamp,
597
- existingId
598
- ]);
599
- return parseProjectRow(existing);
600
- }
601
- const id = uuid();
602
- d.run("INSERT INTO projects (id, name, path, description, memory_prefix, created_at, updated_at) VALUES (?, ?, ?, ?, ?, ?, ?)", [id, name, path, description || null, memoryPrefix || null, timestamp, timestamp]);
603
- return getProject(id, d);
604
- }
605
- function getProject(idOrPath, db) {
606
- const d = db || getDatabase();
607
- let row = d.query("SELECT * FROM projects WHERE id = ?").get(idOrPath);
608
- if (row)
609
- return parseProjectRow(row);
610
- row = d.query("SELECT * FROM projects WHERE path = ?").get(idOrPath);
611
- if (row)
612
- return parseProjectRow(row);
613
- return null;
614
- }
615
- function listProjects(db) {
616
- const d = db || getDatabase();
617
- const rows = d.query("SELECT * FROM projects ORDER BY updated_at DESC").all();
618
- return rows.map(parseProjectRow);
1315
+ const timestamp = now();
1316
+ const countRow = d.query("SELECT COUNT(*) as c FROM memories WHERE expires_at IS NOT NULL AND expires_at < ?").get(timestamp);
1317
+ const count = countRow.c;
1318
+ if (count > 0) {
1319
+ d.run("DELETE FROM memories WHERE expires_at IS NOT NULL AND expires_at < ?", [timestamp]);
1320
+ }
1321
+ return count;
619
1322
  }
620
1323
 
621
1324
  // src/lib/search.ts
@@ -644,109 +1347,442 @@ function parseMemoryRow2(row) {
644
1347
  accessed_at: row["accessed_at"] || null
645
1348
  };
646
1349
  }
1350
+ function preprocessQuery(query) {
1351
+ let q = query.trim();
1352
+ q = q.replace(/\s+/g, " ");
1353
+ q = q.normalize("NFC");
1354
+ return q;
1355
+ }
1356
+ function escapeLikePattern(s) {
1357
+ return s.replace(/%/g, "\\%").replace(/_/g, "\\_");
1358
+ }
1359
+ var STOP_WORDS = new Set([
1360
+ "a",
1361
+ "an",
1362
+ "the",
1363
+ "is",
1364
+ "are",
1365
+ "was",
1366
+ "were",
1367
+ "be",
1368
+ "been",
1369
+ "being",
1370
+ "have",
1371
+ "has",
1372
+ "had",
1373
+ "do",
1374
+ "does",
1375
+ "did",
1376
+ "will",
1377
+ "would",
1378
+ "could",
1379
+ "should",
1380
+ "may",
1381
+ "might",
1382
+ "shall",
1383
+ "can",
1384
+ "need",
1385
+ "dare",
1386
+ "ought",
1387
+ "used",
1388
+ "to",
1389
+ "of",
1390
+ "in",
1391
+ "for",
1392
+ "on",
1393
+ "with",
1394
+ "at",
1395
+ "by",
1396
+ "from",
1397
+ "as",
1398
+ "into",
1399
+ "through",
1400
+ "during",
1401
+ "before",
1402
+ "after",
1403
+ "above",
1404
+ "below",
1405
+ "between",
1406
+ "out",
1407
+ "off",
1408
+ "over",
1409
+ "under",
1410
+ "again",
1411
+ "further",
1412
+ "then",
1413
+ "once",
1414
+ "here",
1415
+ "there",
1416
+ "when",
1417
+ "where",
1418
+ "why",
1419
+ "how",
1420
+ "all",
1421
+ "each",
1422
+ "every",
1423
+ "both",
1424
+ "few",
1425
+ "more",
1426
+ "most",
1427
+ "other",
1428
+ "some",
1429
+ "such",
1430
+ "no",
1431
+ "not",
1432
+ "only",
1433
+ "own",
1434
+ "same",
1435
+ "so",
1436
+ "than",
1437
+ "too",
1438
+ "very",
1439
+ "just",
1440
+ "because",
1441
+ "but",
1442
+ "and",
1443
+ "or",
1444
+ "if",
1445
+ "while",
1446
+ "that",
1447
+ "this",
1448
+ "it"
1449
+ ]);
1450
+ function removeStopWords(tokens) {
1451
+ if (tokens.length <= 1)
1452
+ return tokens;
1453
+ const filtered = tokens.filter((t) => !STOP_WORDS.has(t.toLowerCase()));
1454
+ return filtered.length > 0 ? filtered : tokens;
1455
+ }
1456
+ function extractHighlights(memory, queryLower) {
1457
+ const highlights = [];
1458
+ const tokens = queryLower.split(/\s+/).filter(Boolean);
1459
+ for (const field of ["key", "value", "summary"]) {
1460
+ const text = field === "summary" ? memory.summary : memory[field];
1461
+ if (!text)
1462
+ continue;
1463
+ const textLower = text.toLowerCase();
1464
+ const searchTerms = [queryLower, ...tokens].filter(Boolean);
1465
+ for (const term of searchTerms) {
1466
+ const idx = textLower.indexOf(term);
1467
+ if (idx !== -1) {
1468
+ const start = Math.max(0, idx - 30);
1469
+ const end = Math.min(text.length, idx + term.length + 30);
1470
+ const prefix = start > 0 ? "..." : "";
1471
+ const suffix = end < text.length ? "..." : "";
1472
+ highlights.push({
1473
+ field,
1474
+ snippet: prefix + text.slice(start, end) + suffix
1475
+ });
1476
+ break;
1477
+ }
1478
+ }
1479
+ }
1480
+ for (const tag of memory.tags) {
1481
+ if (tag.toLowerCase().includes(queryLower) || tokens.some((t) => tag.toLowerCase().includes(t))) {
1482
+ highlights.push({ field: "tag", snippet: tag });
1483
+ }
1484
+ }
1485
+ return highlights;
1486
+ }
647
1487
  function determineMatchType(memory, queryLower) {
648
1488
  if (memory.key.toLowerCase() === queryLower)
649
1489
  return "exact";
650
1490
  if (memory.tags.some((t) => t.toLowerCase() === queryLower))
651
1491
  return "tag";
1492
+ if (memory.tags.some((t) => t.toLowerCase().includes(queryLower)))
1493
+ return "tag";
652
1494
  return "fuzzy";
653
1495
  }
654
1496
  function computeScore(memory, queryLower) {
655
- let score = 0;
1497
+ const fieldScores = [];
656
1498
  const keyLower = memory.key.toLowerCase();
657
1499
  if (keyLower === queryLower) {
658
- score += 10;
1500
+ fieldScores.push(10);
659
1501
  } else if (keyLower.includes(queryLower)) {
660
- score += 7;
1502
+ fieldScores.push(7);
661
1503
  }
662
1504
  if (memory.tags.some((t) => t.toLowerCase() === queryLower)) {
663
- score += 6;
1505
+ fieldScores.push(6);
1506
+ } else if (memory.tags.some((t) => t.toLowerCase().includes(queryLower))) {
1507
+ fieldScores.push(3);
664
1508
  }
665
1509
  if (memory.summary && memory.summary.toLowerCase().includes(queryLower)) {
666
- score += 4;
1510
+ fieldScores.push(4);
667
1511
  }
668
1512
  if (memory.value.toLowerCase().includes(queryLower)) {
669
- score += 3;
1513
+ fieldScores.push(3);
1514
+ }
1515
+ const metadataStr = JSON.stringify(memory.metadata).toLowerCase();
1516
+ if (metadataStr !== "{}" && metadataStr.includes(queryLower)) {
1517
+ fieldScores.push(2);
1518
+ }
1519
+ fieldScores.sort((a, b) => b - a);
1520
+ const diminishingMultipliers = [1, 0.5, 0.25, 0.15, 0.15];
1521
+ let score = 0;
1522
+ for (let i = 0;i < fieldScores.length; i++) {
1523
+ score += fieldScores[i] * (diminishingMultipliers[i] ?? 0.15);
1524
+ }
1525
+ const { phrases } = extractQuotedPhrases(queryLower);
1526
+ for (const phrase of phrases) {
1527
+ if (keyLower.includes(phrase))
1528
+ score += 8;
1529
+ if (memory.value.toLowerCase().includes(phrase))
1530
+ score += 5;
1531
+ if (memory.summary && memory.summary.toLowerCase().includes(phrase))
1532
+ score += 4;
1533
+ }
1534
+ const { remainder } = extractQuotedPhrases(queryLower);
1535
+ const tokens = removeStopWords(remainder.split(/\s+/).filter(Boolean));
1536
+ if (tokens.length > 1) {
1537
+ let tokenScore = 0;
1538
+ for (const token of tokens) {
1539
+ if (keyLower === token) {
1540
+ tokenScore += 10 / tokens.length;
1541
+ } else if (keyLower.includes(token)) {
1542
+ tokenScore += 7 / tokens.length;
1543
+ }
1544
+ if (memory.tags.some((t) => t.toLowerCase() === token)) {
1545
+ tokenScore += 6 / tokens.length;
1546
+ } else if (memory.tags.some((t) => t.toLowerCase().includes(token))) {
1547
+ tokenScore += 3 / tokens.length;
1548
+ }
1549
+ if (memory.summary && memory.summary.toLowerCase().includes(token)) {
1550
+ tokenScore += 4 / tokens.length;
1551
+ }
1552
+ if (memory.value.toLowerCase().includes(token)) {
1553
+ tokenScore += 3 / tokens.length;
1554
+ }
1555
+ if (metadataStr !== "{}" && metadataStr.includes(token)) {
1556
+ tokenScore += 2 / tokens.length;
1557
+ }
1558
+ }
1559
+ if (score > 0) {
1560
+ score += tokenScore * 0.3;
1561
+ } else {
1562
+ score += tokenScore;
1563
+ }
670
1564
  }
671
1565
  return score;
672
1566
  }
673
- function searchMemories(query, filter, db) {
674
- const d = db || getDatabase();
675
- const queryLower = query.toLowerCase();
676
- const queryParam = `%${query}%`;
1567
+ function extractQuotedPhrases(query) {
1568
+ const phrases = [];
1569
+ const remainder = query.replace(/"([^"]+)"/g, (_match, phrase) => {
1570
+ phrases.push(phrase);
1571
+ return "";
1572
+ });
1573
+ return { phrases, remainder: remainder.trim() };
1574
+ }
1575
+ function escapeFts5Query(query) {
1576
+ const { phrases, remainder } = extractQuotedPhrases(query);
1577
+ const parts = [];
1578
+ for (const phrase of phrases) {
1579
+ parts.push(`"${phrase.replace(/"/g, '""')}"`);
1580
+ }
1581
+ const tokens = removeStopWords(remainder.split(/\s+/).filter(Boolean));
1582
+ for (const t of tokens) {
1583
+ parts.push(`"${t.replace(/"/g, '""')}"`);
1584
+ }
1585
+ return parts.join(" ");
1586
+ }
1587
+ function hasFts5Table(d) {
1588
+ try {
1589
+ const row = d.query("SELECT name FROM sqlite_master WHERE type='table' AND name='memories_fts'").get();
1590
+ return !!row;
1591
+ } catch {
1592
+ return false;
1593
+ }
1594
+ }
1595
+ function buildFilterConditions(filter) {
677
1596
  const conditions = [];
678
1597
  const params = [];
679
1598
  conditions.push("m.status = 'active'");
680
1599
  conditions.push("(m.expires_at IS NULL OR m.expires_at >= datetime('now'))");
681
- conditions.push(`(m.key LIKE ? OR m.value LIKE ? OR m.summary LIKE ? OR m.id IN (SELECT memory_id FROM memory_tags WHERE tag LIKE ?))`);
682
- params.push(queryParam, queryParam, queryParam, queryParam);
683
- if (filter) {
684
- if (filter.scope) {
685
- if (Array.isArray(filter.scope)) {
686
- conditions.push(`m.scope IN (${filter.scope.map(() => "?").join(",")})`);
687
- params.push(...filter.scope);
688
- } else {
689
- conditions.push("m.scope = ?");
690
- params.push(filter.scope);
691
- }
1600
+ if (!filter)
1601
+ return { conditions, params };
1602
+ if (filter.scope) {
1603
+ if (Array.isArray(filter.scope)) {
1604
+ conditions.push(`m.scope IN (${filter.scope.map(() => "?").join(",")})`);
1605
+ params.push(...filter.scope);
1606
+ } else {
1607
+ conditions.push("m.scope = ?");
1608
+ params.push(filter.scope);
692
1609
  }
693
- if (filter.category) {
694
- if (Array.isArray(filter.category)) {
695
- conditions.push(`m.category IN (${filter.category.map(() => "?").join(",")})`);
696
- params.push(...filter.category);
697
- } else {
698
- conditions.push("m.category = ?");
699
- params.push(filter.category);
700
- }
1610
+ }
1611
+ if (filter.category) {
1612
+ if (Array.isArray(filter.category)) {
1613
+ conditions.push(`m.category IN (${filter.category.map(() => "?").join(",")})`);
1614
+ params.push(...filter.category);
1615
+ } else {
1616
+ conditions.push("m.category = ?");
1617
+ params.push(filter.category);
701
1618
  }
702
- if (filter.source) {
703
- if (Array.isArray(filter.source)) {
704
- conditions.push(`m.source IN (${filter.source.map(() => "?").join(",")})`);
705
- params.push(...filter.source);
706
- } else {
707
- conditions.push("m.source = ?");
708
- params.push(filter.source);
709
- }
1619
+ }
1620
+ if (filter.source) {
1621
+ if (Array.isArray(filter.source)) {
1622
+ conditions.push(`m.source IN (${filter.source.map(() => "?").join(",")})`);
1623
+ params.push(...filter.source);
1624
+ } else {
1625
+ conditions.push("m.source = ?");
1626
+ params.push(filter.source);
710
1627
  }
711
- if (filter.status) {
712
- conditions.shift();
713
- if (Array.isArray(filter.status)) {
714
- conditions.push(`m.status IN (${filter.status.map(() => "?").join(",")})`);
715
- params.push(...filter.status);
716
- } else {
717
- conditions.push("m.status = ?");
718
- params.push(filter.status);
719
- }
1628
+ }
1629
+ if (filter.status) {
1630
+ conditions.shift();
1631
+ if (Array.isArray(filter.status)) {
1632
+ conditions.push(`m.status IN (${filter.status.map(() => "?").join(",")})`);
1633
+ params.push(...filter.status);
1634
+ } else {
1635
+ conditions.push("m.status = ?");
1636
+ params.push(filter.status);
720
1637
  }
721
- if (filter.project_id) {
722
- conditions.push("m.project_id = ?");
723
- params.push(filter.project_id);
1638
+ }
1639
+ if (filter.project_id) {
1640
+ conditions.push("m.project_id = ?");
1641
+ params.push(filter.project_id);
1642
+ }
1643
+ if (filter.agent_id) {
1644
+ conditions.push("m.agent_id = ?");
1645
+ params.push(filter.agent_id);
1646
+ }
1647
+ if (filter.session_id) {
1648
+ conditions.push("m.session_id = ?");
1649
+ params.push(filter.session_id);
1650
+ }
1651
+ if (filter.min_importance) {
1652
+ conditions.push("m.importance >= ?");
1653
+ params.push(filter.min_importance);
1654
+ }
1655
+ if (filter.pinned !== undefined) {
1656
+ conditions.push("m.pinned = ?");
1657
+ params.push(filter.pinned ? 1 : 0);
1658
+ }
1659
+ if (filter.tags && filter.tags.length > 0) {
1660
+ for (const tag of filter.tags) {
1661
+ conditions.push("m.id IN (SELECT memory_id FROM memory_tags WHERE tag = ?)");
1662
+ params.push(tag);
724
1663
  }
725
- if (filter.agent_id) {
726
- conditions.push("m.agent_id = ?");
727
- params.push(filter.agent_id);
1664
+ }
1665
+ return { conditions, params };
1666
+ }
1667
+ function searchWithFts5(d, query, queryLower, filter, graphBoostedIds) {
1668
+ const ftsQuery = escapeFts5Query(query);
1669
+ if (!ftsQuery)
1670
+ return null;
1671
+ try {
1672
+ const { conditions, params } = buildFilterConditions(filter);
1673
+ const queryParam = `%${query}%`;
1674
+ const ftsCondition = `(m.rowid IN (SELECT f.rowid FROM memories_fts f WHERE memories_fts MATCH ?) ` + `OR m.id IN (SELECT memory_id FROM memory_tags WHERE tag LIKE ?) ` + `OR m.metadata LIKE ?)`;
1675
+ const allConditions = [ftsCondition, ...conditions];
1676
+ const allParams = [ftsQuery, queryParam, queryParam, ...params];
1677
+ const candidateSql = `SELECT m.* FROM memories m WHERE ${allConditions.join(" AND ")}`;
1678
+ const rows = d.query(candidateSql).all(...allParams);
1679
+ return scoreResults(rows, queryLower, graphBoostedIds);
1680
+ } catch {
1681
+ return null;
1682
+ }
1683
+ }
1684
+ function searchWithLike(d, query, queryLower, filter, graphBoostedIds) {
1685
+ const { conditions, params } = buildFilterConditions(filter);
1686
+ const rawTokens = query.trim().split(/\s+/).filter(Boolean);
1687
+ const tokens = removeStopWords(rawTokens);
1688
+ const escapedQuery = escapeLikePattern(query);
1689
+ const likePatterns = [`%${escapedQuery}%`];
1690
+ if (tokens.length > 1) {
1691
+ for (const t of tokens)
1692
+ likePatterns.push(`%${escapeLikePattern(t)}%`);
1693
+ }
1694
+ const fieldClauses = [];
1695
+ for (const pattern of likePatterns) {
1696
+ fieldClauses.push("m.key LIKE ? ESCAPE '\\'");
1697
+ params.push(pattern);
1698
+ fieldClauses.push("m.value LIKE ? ESCAPE '\\'");
1699
+ params.push(pattern);
1700
+ fieldClauses.push("m.summary LIKE ? ESCAPE '\\'");
1701
+ params.push(pattern);
1702
+ fieldClauses.push("m.metadata LIKE ? ESCAPE '\\'");
1703
+ params.push(pattern);
1704
+ fieldClauses.push("m.id IN (SELECT memory_id FROM memory_tags WHERE tag LIKE ? ESCAPE '\\')");
1705
+ params.push(pattern);
1706
+ }
1707
+ conditions.push(`(${fieldClauses.join(" OR ")})`);
1708
+ const sql = `SELECT DISTINCT m.* FROM memories m WHERE ${conditions.join(" AND ")}`;
1709
+ const rows = d.query(sql).all(...params);
1710
+ return scoreResults(rows, queryLower, graphBoostedIds);
1711
+ }
1712
+ function generateTrigrams(s) {
1713
+ const lower = s.toLowerCase();
1714
+ const trigrams = new Set;
1715
+ for (let i = 0;i <= lower.length - 3; i++) {
1716
+ trigrams.add(lower.slice(i, i + 3));
1717
+ }
1718
+ return trigrams;
1719
+ }
1720
+ function trigramSimilarity(a, b) {
1721
+ const triA = generateTrigrams(a);
1722
+ const triB = generateTrigrams(b);
1723
+ if (triA.size === 0 || triB.size === 0)
1724
+ return 0;
1725
+ let intersection = 0;
1726
+ for (const t of triA) {
1727
+ if (triB.has(t))
1728
+ intersection++;
1729
+ }
1730
+ const union = triA.size + triB.size - intersection;
1731
+ return union === 0 ? 0 : intersection / union;
1732
+ }
1733
+ function searchWithFuzzy(d, query, filter, graphBoostedIds) {
1734
+ const { conditions, params } = buildFilterConditions(filter);
1735
+ const sql = `SELECT m.* FROM memories m WHERE ${conditions.join(" AND ")}`;
1736
+ const rows = d.query(sql).all(...params);
1737
+ const MIN_SIMILARITY = 0.3;
1738
+ const results = [];
1739
+ for (const row of rows) {
1740
+ const memory = parseMemoryRow2(row);
1741
+ let bestSimilarity = 0;
1742
+ bestSimilarity = Math.max(bestSimilarity, trigramSimilarity(query, memory.key));
1743
+ bestSimilarity = Math.max(bestSimilarity, trigramSimilarity(query, memory.value.slice(0, 200)));
1744
+ if (memory.summary) {
1745
+ bestSimilarity = Math.max(bestSimilarity, trigramSimilarity(query, memory.summary));
728
1746
  }
729
- if (filter.session_id) {
730
- conditions.push("m.session_id = ?");
731
- params.push(filter.session_id);
1747
+ for (const tag of memory.tags) {
1748
+ bestSimilarity = Math.max(bestSimilarity, trigramSimilarity(query, tag));
732
1749
  }
733
- if (filter.min_importance) {
734
- conditions.push("m.importance >= ?");
735
- params.push(filter.min_importance);
1750
+ if (bestSimilarity >= MIN_SIMILARITY) {
1751
+ const graphBoost = graphBoostedIds?.has(memory.id) ? 2 : 0;
1752
+ const score = bestSimilarity * 5 * memory.importance / 10 + graphBoost;
1753
+ results.push({ memory, score, match_type: "fuzzy" });
736
1754
  }
737
- if (filter.pinned !== undefined) {
738
- conditions.push("m.pinned = ?");
739
- params.push(filter.pinned ? 1 : 0);
1755
+ }
1756
+ results.sort((a, b) => b.score - a.score);
1757
+ return results;
1758
+ }
1759
+ function getGraphBoostedMemoryIds(query, d) {
1760
+ const boostedIds = new Set;
1761
+ try {
1762
+ const matchingEntities = listEntities({ search: query, limit: 10 }, d);
1763
+ const exactMatch = getEntityByName(query, undefined, undefined, d);
1764
+ if (exactMatch && !matchingEntities.find((e) => e.id === exactMatch.id)) {
1765
+ matchingEntities.push(exactMatch);
740
1766
  }
741
- if (filter.tags && filter.tags.length > 0) {
742
- for (const tag of filter.tags) {
743
- conditions.push("m.id IN (SELECT memory_id FROM memory_tags WHERE tag = ?)");
744
- params.push(tag);
1767
+ for (const entity of matchingEntities) {
1768
+ const memories = getMemoriesForEntity(entity.id, d);
1769
+ for (const mem of memories) {
1770
+ boostedIds.add(mem.id);
745
1771
  }
746
1772
  }
747
- }
748
- const sql = `SELECT m.* FROM memories m WHERE ${conditions.join(" AND ")}`;
749
- const rows = d.query(sql).all(...params);
1773
+ } catch {}
1774
+ return boostedIds;
1775
+ }
1776
+ function computeRecencyBoost(memory) {
1777
+ if (memory.pinned)
1778
+ return 1;
1779
+ const mostRecent = memory.accessed_at || memory.updated_at;
1780
+ if (!mostRecent)
1781
+ return 0;
1782
+ const daysSinceAccess = (Date.now() - Date.parse(mostRecent)) / (1000 * 60 * 60 * 24);
1783
+ return Math.max(0, 1 - daysSinceAccess / 30);
1784
+ }
1785
+ function scoreResults(rows, queryLower, graphBoostedIds) {
750
1786
  const scored = [];
751
1787
  for (const row of rows) {
752
1788
  const memory = parseMemoryRow2(row);
@@ -754,11 +1790,16 @@ function searchMemories(query, filter, db) {
754
1790
  if (rawScore === 0)
755
1791
  continue;
756
1792
  const weightedScore = rawScore * memory.importance / 10;
1793
+ const recencyBoost = computeRecencyBoost(memory);
1794
+ const accessBoost = Math.min(memory.access_count / 20, 0.2);
1795
+ const graphBoost = graphBoostedIds?.has(memory.id) ? 2 : 0;
1796
+ const finalScore = (weightedScore + graphBoost) * (1 + recencyBoost * 0.3) * (1 + accessBoost);
757
1797
  const matchType = determineMatchType(memory, queryLower);
758
1798
  scored.push({
759
1799
  memory,
760
- score: weightedScore,
761
- match_type: matchType
1800
+ score: finalScore,
1801
+ match_type: matchType,
1802
+ highlights: extractHighlights(memory, queryLower)
762
1803
  });
763
1804
  }
764
1805
  scored.sort((a, b) => {
@@ -766,10 +1807,97 @@ function searchMemories(query, filter, db) {
766
1807
  return b.score - a.score;
767
1808
  return b.memory.importance - a.memory.importance;
768
1809
  });
1810
+ return scored;
1811
+ }
1812
+ function searchMemories(query, filter, db) {
1813
+ const d = db || getDatabase();
1814
+ query = preprocessQuery(query);
1815
+ if (!query)
1816
+ return [];
1817
+ const queryLower = query.toLowerCase();
1818
+ const graphBoostedIds = getGraphBoostedMemoryIds(query, d);
1819
+ let scored;
1820
+ if (hasFts5Table(d)) {
1821
+ const ftsResult = searchWithFts5(d, query, queryLower, filter, graphBoostedIds);
1822
+ if (ftsResult !== null) {
1823
+ scored = ftsResult;
1824
+ } else {
1825
+ scored = searchWithLike(d, query, queryLower, filter, graphBoostedIds);
1826
+ }
1827
+ } else {
1828
+ scored = searchWithLike(d, query, queryLower, filter, graphBoostedIds);
1829
+ }
1830
+ if (scored.length < 3) {
1831
+ const fuzzyResults = searchWithFuzzy(d, query, filter, graphBoostedIds);
1832
+ const seenIds = new Set(scored.map((r) => r.memory.id));
1833
+ for (const fr of fuzzyResults) {
1834
+ if (!seenIds.has(fr.memory.id)) {
1835
+ scored.push(fr);
1836
+ seenIds.add(fr.memory.id);
1837
+ }
1838
+ }
1839
+ scored.sort((a, b) => {
1840
+ if (b.score !== a.score)
1841
+ return b.score - a.score;
1842
+ return b.memory.importance - a.memory.importance;
1843
+ });
1844
+ }
769
1845
  const offset = filter?.offset ?? 0;
770
1846
  const limit = filter?.limit ?? scored.length;
771
- return scored.slice(offset, offset + limit);
1847
+ const finalResults = scored.slice(offset, offset + limit);
1848
+ logSearchQuery(query, scored.length, filter?.agent_id, filter?.project_id, d);
1849
+ return finalResults;
772
1850
  }
1851
+ function logSearchQuery(query, resultCount, agentId, projectId, db) {
1852
+ try {
1853
+ const d = db || getDatabase();
1854
+ const id = crypto.randomUUID().slice(0, 8);
1855
+ d.run("INSERT INTO search_history (id, query, result_count, agent_id, project_id) VALUES (?, ?, ?, ?, ?)", [id, query, resultCount, agentId || null, projectId || null]);
1856
+ } catch {}
1857
+ }
1858
+
1859
+ // src/lib/duration.ts
1860
+ var UNIT_MS = {
1861
+ s: 1000,
1862
+ m: 60000,
1863
+ h: 3600000,
1864
+ d: 86400000,
1865
+ w: 604800000
1866
+ };
1867
+ var DURATION_RE = /^(\d+[smhdw])+$/;
1868
+ var SEGMENT_RE = /(\d+)([smhdw])/g;
1869
+ function parseDuration(input) {
1870
+ if (typeof input === "number")
1871
+ return input;
1872
+ const trimmed = input.trim();
1873
+ if (trimmed === "")
1874
+ throw new Error("Invalid duration: empty string");
1875
+ if (/^\d+$/.test(trimmed)) {
1876
+ return parseInt(trimmed, 10);
1877
+ }
1878
+ if (!DURATION_RE.test(trimmed)) {
1879
+ throw new Error(`Invalid duration format: "${trimmed}". Use combinations of Ns, Nm, Nh, Nd, Nw (e.g. "1d12h", "30m") or plain milliseconds.`);
1880
+ }
1881
+ let total = 0;
1882
+ let match;
1883
+ SEGMENT_RE.lastIndex = 0;
1884
+ while ((match = SEGMENT_RE.exec(trimmed)) !== null) {
1885
+ const value = parseInt(match[1], 10);
1886
+ const unit = match[2];
1887
+ total += value * UNIT_MS[unit];
1888
+ }
1889
+ if (total === 0) {
1890
+ throw new Error(`Invalid duration: "${trimmed}" resolves to 0ms`);
1891
+ }
1892
+ return total;
1893
+ }
1894
+ var FORMAT_UNITS = [
1895
+ ["w", UNIT_MS["w"]],
1896
+ ["d", UNIT_MS["d"]],
1897
+ ["h", UNIT_MS["h"]],
1898
+ ["m", UNIT_MS["m"]],
1899
+ ["s", UNIT_MS["s"]]
1900
+ ];
773
1901
 
774
1902
  // src/server/index.ts
775
1903
  var DEFAULT_PORT = 19428;
@@ -793,21 +1921,21 @@ function parsePort() {
793
1921
  function resolveDashboardDir() {
794
1922
  const candidates = [];
795
1923
  try {
796
- const scriptDir = dirname2(fileURLToPath(import.meta.url));
797
- candidates.push(join2(scriptDir, "..", "dashboard", "dist"));
798
- candidates.push(join2(scriptDir, "..", "..", "dashboard", "dist"));
1924
+ const scriptDir = dirname3(fileURLToPath(import.meta.url));
1925
+ candidates.push(join3(scriptDir, "..", "dashboard", "dist"));
1926
+ candidates.push(join3(scriptDir, "..", "..", "dashboard", "dist"));
799
1927
  } catch {}
800
1928
  if (process.argv[1]) {
801
- const mainDir = dirname2(process.argv[1]);
802
- candidates.push(join2(mainDir, "..", "dashboard", "dist"));
803
- candidates.push(join2(mainDir, "..", "..", "dashboard", "dist"));
1929
+ const mainDir = dirname3(process.argv[1]);
1930
+ candidates.push(join3(mainDir, "..", "dashboard", "dist"));
1931
+ candidates.push(join3(mainDir, "..", "..", "dashboard", "dist"));
804
1932
  }
805
- candidates.push(join2(process.cwd(), "dashboard", "dist"));
1933
+ candidates.push(join3(process.cwd(), "dashboard", "dist"));
806
1934
  for (const c of candidates) {
807
- if (existsSync2(c))
1935
+ if (existsSync3(c))
808
1936
  return c;
809
1937
  }
810
- return join2(process.cwd(), "dashboard", "dist");
1938
+ return join3(process.cwd(), "dashboard", "dist");
811
1939
  }
812
1940
  var MIME_TYPES = {
813
1941
  ".html": "text/html; charset=utf-8",
@@ -821,7 +1949,7 @@ var MIME_TYPES = {
821
1949
  ".woff2": "font/woff2"
822
1950
  };
823
1951
  function serveStaticFile(filePath) {
824
- if (!existsSync2(filePath))
1952
+ if (!existsSync3(filePath))
825
1953
  return null;
826
1954
  const ct = MIME_TYPES[extname(filePath)] || "application/octet-stream";
827
1955
  return new Response(Bun.file(filePath), {
@@ -1019,6 +2147,9 @@ addRoute("POST", "/api/memories", async (req) => {
1019
2147
  return errorResponse("Missing required fields: key, value", 400);
1020
2148
  }
1021
2149
  try {
2150
+ if (body["ttl_ms"] !== undefined && typeof body["ttl_ms"] === "string") {
2151
+ body["ttl_ms"] = parseDuration(body["ttl_ms"]);
2152
+ }
1022
2153
  const memory = createMemory(body);
1023
2154
  return json(memory, 201);
1024
2155
  } catch (e) {
@@ -1171,6 +2302,219 @@ ${lines.join(`
1171
2302
  </agent-memories>`;
1172
2303
  return json({ context, memories_count: lines.length });
1173
2304
  });
2305
+ addRoute("GET", "/api/entities", (_req, url) => {
2306
+ const q = getSearchParams(url);
2307
+ const filter = {};
2308
+ if (q["type"])
2309
+ filter.type = q["type"];
2310
+ if (q["project_id"])
2311
+ filter.project_id = q["project_id"];
2312
+ if (q["search"])
2313
+ filter.search = q["search"];
2314
+ if (q["limit"])
2315
+ filter.limit = parseInt(q["limit"], 10);
2316
+ if (q["offset"])
2317
+ filter.offset = parseInt(q["offset"], 10);
2318
+ const entities = listEntities(filter);
2319
+ return json({ entities, count: entities.length });
2320
+ });
2321
+ addRoute("POST", "/api/entities/merge", async (req) => {
2322
+ const body = await readJson(req);
2323
+ if (!body || !body["source_id"] || !body["target_id"]) {
2324
+ return errorResponse("Missing required fields: source_id, target_id", 400);
2325
+ }
2326
+ try {
2327
+ const merged = mergeEntities(body["source_id"], body["target_id"]);
2328
+ return json(merged);
2329
+ } catch (e) {
2330
+ if (e instanceof EntityNotFoundError) {
2331
+ return errorResponse(e.message, 404);
2332
+ }
2333
+ throw e;
2334
+ }
2335
+ });
2336
+ addRoute("POST", "/api/entities", async (req) => {
2337
+ const body = await readJson(req);
2338
+ if (!body || !body["name"] || !body["type"]) {
2339
+ return errorResponse("Missing required fields: name, type", 400);
2340
+ }
2341
+ try {
2342
+ const entity = createEntity(body);
2343
+ return json(entity, 201);
2344
+ } catch (e) {
2345
+ throw e;
2346
+ }
2347
+ });
2348
+ addRoute("GET", "/api/entities/:id/memories", (_req, _url, params) => {
2349
+ try {
2350
+ getEntity(params["id"]);
2351
+ const memories = getMemoriesForEntity(params["id"]);
2352
+ return json({ memories, count: memories.length });
2353
+ } catch (e) {
2354
+ if (e instanceof EntityNotFoundError) {
2355
+ return errorResponse(e.message, 404);
2356
+ }
2357
+ throw e;
2358
+ }
2359
+ });
2360
+ addRoute("POST", "/api/entities/:id/memories", async (req, _url, params) => {
2361
+ const body = await readJson(req);
2362
+ if (!body || !body["memory_id"]) {
2363
+ return errorResponse("Missing required field: memory_id", 400);
2364
+ }
2365
+ try {
2366
+ const link = linkEntityToMemory(params["id"], body["memory_id"], body["role"] || undefined);
2367
+ return json(link, 201);
2368
+ } catch (e) {
2369
+ if (e instanceof EntityNotFoundError) {
2370
+ return errorResponse(e.message, 404);
2371
+ }
2372
+ throw e;
2373
+ }
2374
+ });
2375
+ addRoute("GET", "/api/entities/:id/relations", (_req, url, params) => {
2376
+ const q = getSearchParams(url);
2377
+ try {
2378
+ getEntity(params["id"]);
2379
+ const relations = listRelations({
2380
+ entity_id: params["id"],
2381
+ relation_type: q["type"],
2382
+ direction: q["direction"]
2383
+ });
2384
+ return json({ relations, count: relations.length });
2385
+ } catch (e) {
2386
+ if (e instanceof EntityNotFoundError) {
2387
+ return errorResponse(e.message, 404);
2388
+ }
2389
+ throw e;
2390
+ }
2391
+ });
2392
+ addRoute("DELETE", "/api/entities/:entityId/memories/:memoryId", (_req, _url, params) => {
2393
+ unlinkEntityFromMemory(params["entityId"], params["memoryId"]);
2394
+ return json({ deleted: true });
2395
+ });
2396
+ addRoute("GET", "/api/entities/:id", (_req, _url, params) => {
2397
+ try {
2398
+ const entity = getEntity(params["id"]);
2399
+ const relations = listRelations({ entity_id: params["id"] });
2400
+ const memories = getMemoriesForEntity(params["id"]);
2401
+ return json({ ...entity, relations, memories });
2402
+ } catch (e) {
2403
+ if (e instanceof EntityNotFoundError) {
2404
+ return errorResponse(e.message, 404);
2405
+ }
2406
+ throw e;
2407
+ }
2408
+ });
2409
+ addRoute("PATCH", "/api/entities/:id", async (req, _url, params) => {
2410
+ const body = await readJson(req);
2411
+ if (!body) {
2412
+ return errorResponse("Invalid JSON body", 400);
2413
+ }
2414
+ try {
2415
+ const entity = updateEntity(params["id"], body);
2416
+ return json(entity);
2417
+ } catch (e) {
2418
+ if (e instanceof EntityNotFoundError) {
2419
+ return errorResponse(e.message, 404);
2420
+ }
2421
+ throw e;
2422
+ }
2423
+ });
2424
+ addRoute("DELETE", "/api/entities/:id", (_req, _url, params) => {
2425
+ try {
2426
+ deleteEntity(params["id"]);
2427
+ return json({ deleted: true });
2428
+ } catch (e) {
2429
+ if (e instanceof EntityNotFoundError) {
2430
+ return errorResponse(e.message, 404);
2431
+ }
2432
+ throw e;
2433
+ }
2434
+ });
2435
+ addRoute("POST", "/api/relations", async (req) => {
2436
+ const body = await readJson(req);
2437
+ if (!body || !body["source_entity_id"] || !body["target_entity_id"] || !body["relation_type"]) {
2438
+ return errorResponse("Missing required fields: source_entity_id, target_entity_id, relation_type", 400);
2439
+ }
2440
+ try {
2441
+ const relation = createRelation(body);
2442
+ return json(relation, 201);
2443
+ } catch (e) {
2444
+ throw e;
2445
+ }
2446
+ });
2447
+ addRoute("GET", "/api/relations/:id", (_req, _url, params) => {
2448
+ try {
2449
+ const relation = getRelation(params["id"]);
2450
+ return json(relation);
2451
+ } catch (e) {
2452
+ if (e instanceof Error && e.message.startsWith("Relation not found")) {
2453
+ return errorResponse(e.message, 404);
2454
+ }
2455
+ throw e;
2456
+ }
2457
+ });
2458
+ addRoute("DELETE", "/api/relations/:id", (_req, _url, params) => {
2459
+ try {
2460
+ deleteRelation(params["id"]);
2461
+ return json({ deleted: true });
2462
+ } catch (e) {
2463
+ if (e instanceof Error && e.message.startsWith("Relation not found")) {
2464
+ return errorResponse(e.message, 404);
2465
+ }
2466
+ throw e;
2467
+ }
2468
+ });
2469
+ addRoute("GET", "/api/graph/path", (_req, url) => {
2470
+ const q = getSearchParams(url);
2471
+ if (!q["from"] || !q["to"]) {
2472
+ return errorResponse("Missing required query params: from, to", 400);
2473
+ }
2474
+ const maxDepth = q["max_depth"] ? parseInt(q["max_depth"], 10) : 5;
2475
+ try {
2476
+ const path = findPath(q["from"], q["to"], maxDepth);
2477
+ if (!path) {
2478
+ return json({ path: null, found: false });
2479
+ }
2480
+ return json({ path, found: true, length: path.length });
2481
+ } catch (e) {
2482
+ if (e instanceof EntityNotFoundError) {
2483
+ return errorResponse(e.message, 404);
2484
+ }
2485
+ throw e;
2486
+ }
2487
+ });
2488
+ addRoute("GET", "/api/graph/stats", () => {
2489
+ const db = getDatabase();
2490
+ const entityCount = db.query("SELECT COUNT(*) as c FROM entities").get().c;
2491
+ const relationCount = db.query("SELECT COUNT(*) as c FROM relations").get().c;
2492
+ const entitiesByType = db.query("SELECT type, COUNT(*) as c FROM entities GROUP BY type").all();
2493
+ const relationsByType = db.query("SELECT relation_type, COUNT(*) as c FROM relations GROUP BY relation_type").all();
2494
+ return json({
2495
+ entities: {
2496
+ total: entityCount,
2497
+ by_type: Object.fromEntries(entitiesByType.map((r) => [r.type, r.c]))
2498
+ },
2499
+ relations: {
2500
+ total: relationCount,
2501
+ by_type: Object.fromEntries(relationsByType.map((r) => [r.relation_type, r.c]))
2502
+ }
2503
+ });
2504
+ });
2505
+ addRoute("GET", "/api/graph/:entityId", (_req, url, params) => {
2506
+ const q = getSearchParams(url);
2507
+ const depth = q["depth"] ? parseInt(q["depth"], 10) : 2;
2508
+ try {
2509
+ const graph = getEntityGraph(params["entityId"], depth);
2510
+ return json(graph);
2511
+ } catch (e) {
2512
+ if (e instanceof EntityNotFoundError) {
2513
+ return errorResponse(e.message, 404);
2514
+ }
2515
+ throw e;
2516
+ }
2517
+ });
1174
2518
  async function findFreePort(start) {
1175
2519
  for (let port = start;port < start + 100; port++) {
1176
2520
  try {
@@ -1235,13 +2579,13 @@ function startServer(port) {
1235
2579
  return errorResponse("Not found", 404);
1236
2580
  }
1237
2581
  const dashDir = resolveDashboardDir();
1238
- if (existsSync2(dashDir) && (req.method === "GET" || req.method === "HEAD")) {
2582
+ if (existsSync3(dashDir) && (req.method === "GET" || req.method === "HEAD")) {
1239
2583
  if (pathname !== "/") {
1240
- const staticRes = serveStaticFile(join2(dashDir, pathname));
2584
+ const staticRes = serveStaticFile(join3(dashDir, pathname));
1241
2585
  if (staticRes)
1242
2586
  return staticRes;
1243
2587
  }
1244
- const indexRes = serveStaticFile(join2(dashDir, "index.html"));
2588
+ const indexRes = serveStaticFile(join3(dashDir, "index.html"));
1245
2589
  if (indexRes)
1246
2590
  return indexRes;
1247
2591
  }