@hasna/mementos 0.4.41 → 0.6.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 (38) hide show
  1. package/dist/cli/index.js +2247 -1548
  2. package/dist/db/database.d.ts.map +1 -1
  3. package/dist/db/memories.d.ts +1 -0
  4. package/dist/db/memories.d.ts.map +1 -1
  5. package/dist/index.d.ts +6 -1
  6. package/dist/index.d.ts.map +1 -1
  7. package/dist/index.js +2041 -1336
  8. package/dist/lib/auto-memory-queue.d.ts +46 -0
  9. package/dist/lib/auto-memory-queue.d.ts.map +1 -0
  10. package/dist/lib/auto-memory.d.ts +18 -0
  11. package/dist/lib/auto-memory.d.ts.map +1 -0
  12. package/dist/lib/dedup.d.ts +33 -0
  13. package/dist/lib/dedup.d.ts.map +1 -0
  14. package/dist/lib/focus.d.ts +58 -0
  15. package/dist/lib/focus.d.ts.map +1 -0
  16. package/dist/lib/providers/anthropic.d.ts +21 -0
  17. package/dist/lib/providers/anthropic.d.ts.map +1 -0
  18. package/dist/lib/providers/base.d.ts +96 -0
  19. package/dist/lib/providers/base.d.ts.map +1 -0
  20. package/dist/lib/providers/cerebras.d.ts +20 -0
  21. package/dist/lib/providers/cerebras.d.ts.map +1 -0
  22. package/dist/lib/providers/grok.d.ts +19 -0
  23. package/dist/lib/providers/grok.d.ts.map +1 -0
  24. package/dist/lib/providers/index.d.ts +7 -0
  25. package/dist/lib/providers/index.d.ts.map +1 -0
  26. package/dist/lib/providers/openai-compat.d.ts +18 -0
  27. package/dist/lib/providers/openai-compat.d.ts.map +1 -0
  28. package/dist/lib/providers/openai.d.ts +20 -0
  29. package/dist/lib/providers/openai.d.ts.map +1 -0
  30. package/dist/lib/providers/registry.d.ts +38 -0
  31. package/dist/lib/providers/registry.d.ts.map +1 -0
  32. package/dist/lib/search.d.ts.map +1 -1
  33. package/dist/mcp/index.js +1781 -1101
  34. package/dist/server/index.d.ts.map +1 -1
  35. package/dist/server/index.js +1480 -941
  36. package/dist/types/index.d.ts +7 -0
  37. package/dist/types/index.d.ts.map +1 -1
  38. package/package.json +2 -2
@@ -351,6 +351,11 @@ var MIGRATIONS = [
351
351
  CREATE INDEX IF NOT EXISTS idx_resource_locks_agent ON resource_locks(agent_id);
352
352
  CREATE INDEX IF NOT EXISTS idx_resource_locks_expires ON resource_locks(expires_at);
353
353
  INSERT OR IGNORE INTO _migrations (id) VALUES (8);
354
+ `,
355
+ `
356
+ ALTER TABLE memories ADD COLUMN recall_count INTEGER NOT NULL DEFAULT 0;
357
+ CREATE INDEX IF NOT EXISTS idx_memories_recall_count ON memories(recall_count DESC);
358
+ INSERT OR IGNORE INTO _migrations (id) VALUES (9);
354
359
  `
355
360
  ];
356
361
  var _db = null;
@@ -421,877 +426,133 @@ function redactSecrets(text) {
421
426
  return result;
422
427
  }
423
428
 
424
- // src/db/agents.ts
425
- var CONFLICT_WINDOW_MS = 30 * 60 * 1000;
426
- function parseAgentRow(row) {
429
+ // src/db/entity-memories.ts
430
+ function parseEntityMemoryRow(row) {
427
431
  return {
428
- id: row["id"],
429
- name: row["name"],
430
- session_id: row["session_id"] || null,
431
- description: row["description"] || null,
432
- role: row["role"] || null,
433
- metadata: JSON.parse(row["metadata"] || "{}"),
434
- active_project_id: row["active_project_id"] || null,
435
- created_at: row["created_at"],
436
- last_seen_at: row["last_seen_at"]
432
+ entity_id: row["entity_id"],
433
+ memory_id: row["memory_id"],
434
+ role: row["role"],
435
+ created_at: row["created_at"]
437
436
  };
438
437
  }
439
- function registerAgent(name, sessionId, description, role, projectId, db) {
438
+ function linkEntityToMemory(entityId, memoryId, role = "context", db) {
440
439
  const d = db || getDatabase();
441
440
  const timestamp = now();
442
- const normalizedName = name.trim().toLowerCase();
443
- const existing = d.query("SELECT * FROM agents WHERE LOWER(name) = ?").get(normalizedName);
444
- if (existing) {
445
- const existingId = existing["id"];
446
- const existingSessionId = existing["session_id"] || null;
447
- const existingLastSeen = existing["last_seen_at"];
448
- if (sessionId && existingSessionId && existingSessionId !== sessionId) {
449
- const lastSeenMs = new Date(existingLastSeen).getTime();
450
- const nowMs = Date.now();
451
- if (nowMs - lastSeenMs < CONFLICT_WINDOW_MS) {
452
- throw new AgentConflictError({
453
- existing_id: existingId,
454
- existing_name: normalizedName,
455
- last_seen_at: existingLastSeen,
456
- session_hint: existingSessionId.slice(0, 8),
457
- working_dir: null
458
- });
459
- }
460
- }
461
- d.run("UPDATE agents SET last_seen_at = ?, session_id = ? WHERE id = ?", [
462
- timestamp,
463
- sessionId ?? existingSessionId,
464
- existingId
465
- ]);
466
- if (description) {
467
- d.run("UPDATE agents SET description = ? WHERE id = ?", [description, existingId]);
468
- }
469
- if (role) {
470
- d.run("UPDATE agents SET role = ? WHERE id = ?", [role, existingId]);
471
- }
472
- if (projectId !== undefined) {
473
- d.run("UPDATE agents SET active_project_id = ? WHERE id = ?", [projectId, existingId]);
474
- }
475
- return getAgent(existingId, d);
476
- }
477
- const id = shortUuid();
478
- d.run("INSERT INTO agents (id, name, session_id, description, role, active_project_id, created_at, last_seen_at) VALUES (?, ?, ?, ?, ?, ?, ?, ?)", [id, normalizedName, sessionId ?? null, description || null, role || "agent", projectId ?? null, timestamp, timestamp]);
479
- return getAgent(id, d);
480
- }
481
- function getAgent(idOrName, db) {
482
- const d = db || getDatabase();
483
- let row = d.query("SELECT * FROM agents WHERE id = ?").get(idOrName);
484
- if (row)
485
- return parseAgentRow(row);
486
- row = d.query("SELECT * FROM agents WHERE LOWER(name) = ?").get(idOrName.trim().toLowerCase());
487
- if (row)
488
- return parseAgentRow(row);
489
- const rows = d.query("SELECT * FROM agents WHERE id LIKE ?").all(`${idOrName}%`);
490
- if (rows.length === 1)
491
- return parseAgentRow(rows[0]);
492
- return null;
441
+ d.run(`INSERT OR IGNORE INTO entity_memories (entity_id, memory_id, role, created_at)
442
+ VALUES (?, ?, ?, ?)`, [entityId, memoryId, role, timestamp]);
443
+ const row = d.query("SELECT * FROM entity_memories WHERE entity_id = ? AND memory_id = ?").get(entityId, memoryId);
444
+ return parseEntityMemoryRow(row);
493
445
  }
494
- function listAgents(db) {
446
+ function unlinkEntityFromMemory(entityId, memoryId, db) {
495
447
  const d = db || getDatabase();
496
- const rows = d.query("SELECT * FROM agents ORDER BY last_seen_at DESC").all();
497
- return rows.map(parseAgentRow);
448
+ d.run("DELETE FROM entity_memories WHERE entity_id = ? AND memory_id = ?", [entityId, memoryId]);
498
449
  }
499
- function listAgentsByProject(projectId, db) {
450
+ function getMemoriesForEntity(entityId, db) {
500
451
  const d = db || getDatabase();
501
- const rows = d.query("SELECT * FROM agents WHERE active_project_id = ? ORDER BY last_seen_at DESC").all(projectId);
502
- return rows.map(parseAgentRow);
452
+ const rows = d.query(`SELECT m.* FROM memories m
453
+ INNER JOIN entity_memories em ON em.memory_id = m.id
454
+ WHERE em.entity_id = ?
455
+ ORDER BY m.importance DESC, m.created_at DESC`).all(entityId);
456
+ return rows.map(parseMemoryRow);
503
457
  }
504
- function updateAgent(id, updates, db) {
458
+ function getEntityMemoryLinks(entityId, memoryId, db) {
505
459
  const d = db || getDatabase();
506
- const agent = getAgent(id, d);
507
- if (!agent)
508
- return null;
509
- const timestamp = now();
510
- if (updates.name) {
511
- const normalizedNewName = updates.name.trim().toLowerCase();
512
- if (normalizedNewName !== agent.name) {
513
- const existing = d.query("SELECT id FROM agents WHERE LOWER(name) = ? AND id != ?").get(normalizedNewName, agent.id);
514
- if (existing) {
515
- throw new Error(`Agent name already taken: ${normalizedNewName}`);
516
- }
517
- d.run("UPDATE agents SET name = ? WHERE id = ?", [normalizedNewName, agent.id]);
518
- }
519
- }
520
- if (updates.description !== undefined) {
521
- d.run("UPDATE agents SET description = ? WHERE id = ?", [updates.description, agent.id]);
522
- }
523
- if (updates.role !== undefined) {
524
- d.run("UPDATE agents SET role = ? WHERE id = ?", [updates.role, agent.id]);
460
+ const conditions = [];
461
+ const params = [];
462
+ if (entityId) {
463
+ conditions.push("entity_id = ?");
464
+ params.push(entityId);
525
465
  }
526
- if (updates.metadata !== undefined) {
527
- d.run("UPDATE agents SET metadata = ? WHERE id = ?", [JSON.stringify(updates.metadata), agent.id]);
466
+ if (memoryId) {
467
+ conditions.push("memory_id = ?");
468
+ params.push(memoryId);
528
469
  }
529
- if ("active_project_id" in updates) {
530
- d.run("UPDATE agents SET active_project_id = ? WHERE id = ?", [updates.active_project_id ?? null, agent.id]);
470
+ let sql = "SELECT * FROM entity_memories";
471
+ if (conditions.length > 0) {
472
+ sql += ` WHERE ${conditions.join(" AND ")}`;
531
473
  }
532
- d.run("UPDATE agents SET last_seen_at = ? WHERE id = ?", [timestamp, agent.id]);
533
- return getAgent(agent.id, d);
474
+ sql += " ORDER BY created_at DESC";
475
+ const rows = d.query(sql).all(...params);
476
+ return rows.map(parseEntityMemoryRow);
534
477
  }
535
478
 
536
- // src/db/projects.ts
537
- function parseProjectRow(row) {
479
+ // src/db/memories.ts
480
+ function runEntityExtraction(_memory, _projectId, _d) {}
481
+ function parseMemoryRow(row) {
538
482
  return {
539
483
  id: row["id"],
540
- name: row["name"],
541
- path: row["path"],
542
- description: row["description"] || null,
543
- memory_prefix: row["memory_prefix"] || null,
484
+ key: row["key"],
485
+ value: row["value"],
486
+ category: row["category"],
487
+ scope: row["scope"],
488
+ summary: row["summary"] || null,
489
+ tags: JSON.parse(row["tags"] || "[]"),
490
+ importance: row["importance"],
491
+ source: row["source"],
492
+ status: row["status"],
493
+ pinned: !!row["pinned"],
494
+ agent_id: row["agent_id"] || null,
495
+ project_id: row["project_id"] || null,
496
+ session_id: row["session_id"] || null,
497
+ metadata: JSON.parse(row["metadata"] || "{}"),
498
+ access_count: row["access_count"],
499
+ version: row["version"],
500
+ expires_at: row["expires_at"] || null,
544
501
  created_at: row["created_at"],
545
- updated_at: row["updated_at"]
502
+ updated_at: row["updated_at"],
503
+ accessed_at: row["accessed_at"] || null
546
504
  };
547
505
  }
548
- function registerProject(name, path, description, memoryPrefix, db) {
506
+ function createMemory(input, dedupeMode = "merge", db) {
549
507
  const d = db || getDatabase();
550
508
  const timestamp = now();
551
- const existing = d.query("SELECT * FROM projects WHERE path = ?").get(path);
552
- if (existing) {
553
- const existingId = existing["id"];
554
- d.run("UPDATE projects SET updated_at = ? WHERE id = ?", [
555
- timestamp,
556
- existingId
557
- ]);
558
- return parseProjectRow(existing);
509
+ let expiresAt = input.expires_at || null;
510
+ if (input.ttl_ms && !expiresAt) {
511
+ expiresAt = new Date(Date.now() + input.ttl_ms).toISOString();
559
512
  }
560
513
  const id = uuid();
561
- 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]);
562
- return getProject(id, d);
563
- }
564
- function getProject(idOrPath, db) {
565
- const d = db || getDatabase();
566
- let row = d.query("SELECT * FROM projects WHERE id = ?").get(idOrPath);
567
- if (row)
568
- return parseProjectRow(row);
569
- row = d.query("SELECT * FROM projects WHERE path = ?").get(idOrPath);
570
- if (row)
571
- return parseProjectRow(row);
572
- row = d.query("SELECT * FROM projects WHERE LOWER(name) = ?").get(idOrPath.toLowerCase());
573
- if (row)
574
- return parseProjectRow(row);
575
- return null;
576
- }
577
- function listProjects(db) {
578
- const d = db || getDatabase();
579
- const rows = d.query("SELECT * FROM projects ORDER BY updated_at DESC").all();
580
- return rows.map(parseProjectRow);
581
- }
582
-
583
- // src/lib/extractor.ts
584
- var TECH_KEYWORDS = new Set([
585
- "typescript",
586
- "javascript",
587
- "python",
588
- "rust",
589
- "go",
590
- "java",
591
- "ruby",
592
- "swift",
593
- "kotlin",
594
- "react",
595
- "vue",
596
- "angular",
597
- "svelte",
598
- "nextjs",
599
- "bun",
600
- "node",
601
- "deno",
602
- "sqlite",
603
- "postgres",
604
- "mysql",
605
- "redis",
606
- "docker",
607
- "kubernetes",
608
- "git",
609
- "npm",
610
- "yarn",
611
- "pnpm",
612
- "webpack",
613
- "vite",
614
- "tailwind",
615
- "prisma",
616
- "drizzle",
617
- "zod",
618
- "commander",
619
- "express",
620
- "fastify",
621
- "hono"
622
- ]);
623
- var FILE_PATH_RE = /(?:^|\s)((?:\/|\.\/|~\/)?(?:[\w.-]+\/)+[\w.-]+\.\w+)/g;
624
- var URL_RE = /https?:\/\/[^\s)]+/g;
625
- var NPM_PACKAGE_RE = /@[\w-]+\/[\w.-]+/g;
626
- var PASCAL_CASE_RE = /\b([A-Z][a-z]+(?:[A-Z][a-z]+)+)\b/g;
627
- function getSearchText(memory) {
628
- const parts = [memory.key, memory.value];
629
- if (memory.summary)
630
- parts.push(memory.summary);
631
- return parts.join(" ");
632
- }
633
- function extractEntities(memory, db) {
634
- const text = getSearchText(memory);
635
- const entityMap = new Map;
636
- function add(name, type, confidence) {
637
- const normalized = name.toLowerCase();
638
- if (normalized.length < 3)
639
- return;
640
- const existing = entityMap.get(normalized);
641
- if (!existing || existing.confidence < confidence) {
642
- entityMap.set(normalized, { name: normalized, type, confidence });
643
- }
644
- }
645
- for (const match of text.matchAll(FILE_PATH_RE)) {
646
- add(match[1].trim(), "file", 0.9);
647
- }
648
- for (const match of text.matchAll(URL_RE)) {
649
- add(match[0], "api", 0.8);
650
- }
651
- for (const match of text.matchAll(NPM_PACKAGE_RE)) {
652
- add(match[0], "tool", 0.85);
653
- }
654
- try {
655
- const d = db || getDatabase();
656
- const agents = listAgents(d);
657
- const textLower2 = text.toLowerCase();
658
- for (const agent of agents) {
659
- const nameLower = agent.name.toLowerCase();
660
- if (nameLower.length >= 3 && textLower2.includes(nameLower)) {
661
- add(agent.name, "person", 0.95);
662
- }
663
- }
664
- } catch {}
665
- try {
666
- const d = db || getDatabase();
667
- const projects = listProjects(d);
668
- const textLower2 = text.toLowerCase();
669
- for (const project of projects) {
670
- const nameLower = project.name.toLowerCase();
671
- if (nameLower.length >= 3 && textLower2.includes(nameLower)) {
672
- add(project.name, "project", 0.95);
514
+ const tags = input.tags || [];
515
+ const tagsJson = JSON.stringify(tags);
516
+ const metadataJson = JSON.stringify(input.metadata || {});
517
+ const safeValue = redactSecrets(input.value);
518
+ const safeSummary = input.summary ? redactSecrets(input.summary) : null;
519
+ if (dedupeMode === "merge") {
520
+ const existing = d.query(`SELECT id, version FROM memories
521
+ WHERE key = ? AND scope = ?
522
+ AND COALESCE(agent_id, '') = ?
523
+ AND COALESCE(project_id, '') = ?
524
+ AND COALESCE(session_id, '') = ?`).get(input.key, input.scope || "private", input.agent_id || "", input.project_id || "", input.session_id || "");
525
+ if (existing) {
526
+ d.run(`UPDATE memories SET
527
+ value = ?, category = ?, summary = ?, tags = ?,
528
+ importance = ?, metadata = ?, expires_at = ?,
529
+ pinned = COALESCE(pinned, 0),
530
+ version = version + 1, updated_at = ?
531
+ WHERE id = ?`, [
532
+ safeValue,
533
+ input.category || "knowledge",
534
+ safeSummary,
535
+ tagsJson,
536
+ input.importance ?? 5,
537
+ metadataJson,
538
+ expiresAt,
539
+ timestamp,
540
+ existing.id
541
+ ]);
542
+ d.run("DELETE FROM memory_tags WHERE memory_id = ?", [existing.id]);
543
+ const insertTag2 = d.prepare("INSERT OR IGNORE INTO memory_tags (memory_id, tag) VALUES (?, ?)");
544
+ for (const tag of tags) {
545
+ insertTag2.run(existing.id, tag);
673
546
  }
674
- }
675
- } catch {}
676
- const textLower = text.toLowerCase();
677
- for (const keyword of TECH_KEYWORDS) {
678
- const re = new RegExp(`\\b${keyword}\\b`, "i");
679
- if (re.test(textLower)) {
680
- add(keyword, "tool", 0.7);
681
- }
682
- }
683
- for (const match of text.matchAll(PASCAL_CASE_RE)) {
684
- add(match[1], "concept", 0.5);
685
- }
686
- return Array.from(entityMap.values()).sort((a, b) => b.confidence - a.confidence);
687
- }
688
-
689
- // src/db/entities.ts
690
- function parseEntityRow(row) {
691
- return {
692
- id: row["id"],
693
- name: row["name"],
694
- type: row["type"],
695
- description: row["description"] || null,
696
- metadata: JSON.parse(row["metadata"] || "{}"),
697
- project_id: row["project_id"] || null,
698
- created_at: row["created_at"],
699
- updated_at: row["updated_at"]
700
- };
701
- }
702
- function createEntity(input, db) {
703
- const d = db || getDatabase();
704
- const timestamp = now();
705
- const metadataJson = JSON.stringify(input.metadata || {});
706
- const existing = d.query(`SELECT * FROM entities
707
- WHERE name = ? AND type = ? AND COALESCE(project_id, '') = ?`).get(input.name, input.type, input.project_id || "");
708
- if (existing) {
709
- const sets = ["updated_at = ?"];
710
- const params = [timestamp];
711
- if (input.description !== undefined) {
712
- sets.push("description = ?");
713
- params.push(input.description);
714
- }
715
- if (input.metadata !== undefined) {
716
- sets.push("metadata = ?");
717
- params.push(metadataJson);
718
- }
719
- const existingId = existing["id"];
720
- params.push(existingId);
721
- d.run(`UPDATE entities SET ${sets.join(", ")} WHERE id = ?`, params);
722
- return getEntity(existingId, d);
723
- }
724
- const id = shortUuid();
725
- d.run(`INSERT INTO entities (id, name, type, description, metadata, project_id, created_at, updated_at)
726
- VALUES (?, ?, ?, ?, ?, ?, ?, ?)`, [
727
- id,
728
- input.name,
729
- input.type,
730
- input.description || null,
731
- metadataJson,
732
- input.project_id || null,
733
- timestamp,
734
- timestamp
735
- ]);
736
- return getEntity(id, d);
737
- }
738
- function getEntity(id, db) {
739
- const d = db || getDatabase();
740
- const row = d.query("SELECT * FROM entities WHERE id = ?").get(id);
741
- if (!row)
742
- throw new EntityNotFoundError(id);
743
- return parseEntityRow(row);
744
- }
745
- function getEntityByName(name, type, projectId, db) {
746
- const d = db || getDatabase();
747
- let sql = "SELECT * FROM entities WHERE name = ?";
748
- const params = [name];
749
- if (type) {
750
- sql += " AND type = ?";
751
- params.push(type);
752
- }
753
- if (projectId !== undefined) {
754
- sql += " AND project_id = ?";
755
- params.push(projectId);
756
- }
757
- sql += " LIMIT 1";
758
- const row = d.query(sql).get(...params);
759
- if (!row)
760
- return null;
761
- return parseEntityRow(row);
762
- }
763
- function listEntities(filter = {}, db) {
764
- const d = db || getDatabase();
765
- const conditions = [];
766
- const params = [];
767
- if (filter.type) {
768
- conditions.push("type = ?");
769
- params.push(filter.type);
770
- }
771
- if (filter.project_id) {
772
- conditions.push("project_id = ?");
773
- params.push(filter.project_id);
774
- }
775
- if (filter.search) {
776
- conditions.push("(name LIKE ? OR description LIKE ?)");
777
- const term = `%${filter.search}%`;
778
- params.push(term, term);
779
- }
780
- let sql = "SELECT * FROM entities";
781
- if (conditions.length > 0) {
782
- sql += ` WHERE ${conditions.join(" AND ")}`;
783
- }
784
- sql += " ORDER BY updated_at DESC";
785
- if (filter.limit) {
786
- sql += " LIMIT ?";
787
- params.push(filter.limit);
788
- }
789
- if (filter.offset) {
790
- sql += " OFFSET ?";
791
- params.push(filter.offset);
792
- }
793
- const rows = d.query(sql).all(...params);
794
- return rows.map(parseEntityRow);
795
- }
796
- function updateEntity(id, input, db) {
797
- const d = db || getDatabase();
798
- const existing = d.query("SELECT id FROM entities WHERE id = ?").get(id);
799
- if (!existing)
800
- throw new EntityNotFoundError(id);
801
- const sets = ["updated_at = ?"];
802
- const params = [now()];
803
- if (input.name !== undefined) {
804
- sets.push("name = ?");
805
- params.push(input.name);
806
- }
807
- if (input.type !== undefined) {
808
- sets.push("type = ?");
809
- params.push(input.type);
810
- }
811
- if (input.description !== undefined) {
812
- sets.push("description = ?");
813
- params.push(input.description);
814
- }
815
- if (input.metadata !== undefined) {
816
- sets.push("metadata = ?");
817
- params.push(JSON.stringify(input.metadata));
818
- }
819
- params.push(id);
820
- d.run(`UPDATE entities SET ${sets.join(", ")} WHERE id = ?`, params);
821
- return getEntity(id, d);
822
- }
823
- function deleteEntity(id, db) {
824
- const d = db || getDatabase();
825
- const result = d.run("DELETE FROM entities WHERE id = ?", [id]);
826
- if (result.changes === 0)
827
- throw new EntityNotFoundError(id);
828
- }
829
- function mergeEntities(sourceId, targetId, db) {
830
- const d = db || getDatabase();
831
- getEntity(sourceId, d);
832
- getEntity(targetId, d);
833
- d.run(`UPDATE OR IGNORE relations SET source_entity_id = ? WHERE source_entity_id = ?`, [targetId, sourceId]);
834
- d.run(`UPDATE OR IGNORE relations SET target_entity_id = ? WHERE target_entity_id = ?`, [targetId, sourceId]);
835
- d.run("DELETE FROM relations WHERE source_entity_id = ? OR target_entity_id = ?", [
836
- sourceId,
837
- sourceId
838
- ]);
839
- d.run(`UPDATE OR IGNORE entity_memories SET entity_id = ? WHERE entity_id = ?`, [targetId, sourceId]);
840
- d.run("DELETE FROM entity_memories WHERE entity_id = ?", [sourceId]);
841
- d.run("DELETE FROM entities WHERE id = ?", [sourceId]);
842
- d.run("UPDATE entities SET updated_at = ? WHERE id = ?", [now(), targetId]);
843
- return getEntity(targetId, d);
844
- }
845
-
846
- // src/db/entity-memories.ts
847
- function parseEntityMemoryRow(row) {
848
- return {
849
- entity_id: row["entity_id"],
850
- memory_id: row["memory_id"],
851
- role: row["role"],
852
- created_at: row["created_at"]
853
- };
854
- }
855
- function linkEntityToMemory(entityId, memoryId, role = "context", db) {
856
- const d = db || getDatabase();
857
- const timestamp = now();
858
- d.run(`INSERT OR IGNORE INTO entity_memories (entity_id, memory_id, role, created_at)
859
- VALUES (?, ?, ?, ?)`, [entityId, memoryId, role, timestamp]);
860
- const row = d.query("SELECT * FROM entity_memories WHERE entity_id = ? AND memory_id = ?").get(entityId, memoryId);
861
- return parseEntityMemoryRow(row);
862
- }
863
- function unlinkEntityFromMemory(entityId, memoryId, db) {
864
- const d = db || getDatabase();
865
- d.run("DELETE FROM entity_memories WHERE entity_id = ? AND memory_id = ?", [entityId, memoryId]);
866
- }
867
- function getMemoriesForEntity(entityId, db) {
868
- const d = db || getDatabase();
869
- const rows = d.query(`SELECT m.* FROM memories m
870
- INNER JOIN entity_memories em ON em.memory_id = m.id
871
- WHERE em.entity_id = ?
872
- ORDER BY m.importance DESC, m.created_at DESC`).all(entityId);
873
- return rows.map(parseMemoryRow);
874
- }
875
- function getEntityMemoryLinks(entityId, memoryId, db) {
876
- const d = db || getDatabase();
877
- const conditions = [];
878
- const params = [];
879
- if (entityId) {
880
- conditions.push("entity_id = ?");
881
- params.push(entityId);
882
- }
883
- if (memoryId) {
884
- conditions.push("memory_id = ?");
885
- params.push(memoryId);
886
- }
887
- let sql = "SELECT * FROM entity_memories";
888
- if (conditions.length > 0) {
889
- sql += ` WHERE ${conditions.join(" AND ")}`;
890
- }
891
- sql += " ORDER BY created_at DESC";
892
- const rows = d.query(sql).all(...params);
893
- return rows.map(parseEntityMemoryRow);
894
- }
895
-
896
- // src/db/relations.ts
897
- function parseRelationRow(row) {
898
- return {
899
- id: row["id"],
900
- source_entity_id: row["source_entity_id"],
901
- target_entity_id: row["target_entity_id"],
902
- relation_type: row["relation_type"],
903
- weight: row["weight"],
904
- metadata: JSON.parse(row["metadata"] || "{}"),
905
- created_at: row["created_at"]
906
- };
907
- }
908
- function parseEntityRow2(row) {
909
- return {
910
- id: row["id"],
911
- name: row["name"],
912
- type: row["type"],
913
- description: row["description"] || null,
914
- metadata: JSON.parse(row["metadata"] || "{}"),
915
- project_id: row["project_id"] || null,
916
- created_at: row["created_at"],
917
- updated_at: row["updated_at"]
918
- };
919
- }
920
- function createRelation(input, db) {
921
- const d = db || getDatabase();
922
- const id = shortUuid();
923
- const timestamp = now();
924
- const weight = input.weight ?? 1;
925
- const metadata = JSON.stringify(input.metadata ?? {});
926
- d.run(`INSERT INTO relations (id, source_entity_id, target_entity_id, relation_type, weight, metadata, created_at)
927
- VALUES (?, ?, ?, ?, ?, ?, ?)
928
- ON CONFLICT(source_entity_id, target_entity_id, relation_type)
929
- DO UPDATE SET weight = excluded.weight, metadata = excluded.metadata`, [id, input.source_entity_id, input.target_entity_id, input.relation_type, weight, metadata, timestamp]);
930
- const row = d.query(`SELECT * FROM relations
931
- WHERE source_entity_id = ? AND target_entity_id = ? AND relation_type = ?`).get(input.source_entity_id, input.target_entity_id, input.relation_type);
932
- return parseRelationRow(row);
933
- }
934
- function getRelation(id, db) {
935
- const d = db || getDatabase();
936
- const row = d.query("SELECT * FROM relations WHERE id = ?").get(id);
937
- if (!row)
938
- throw new Error(`Relation not found: ${id}`);
939
- return parseRelationRow(row);
940
- }
941
- function listRelations(filter, db) {
942
- const d = db || getDatabase();
943
- const conditions = [];
944
- const params = [];
945
- if (filter.entity_id) {
946
- const dir = filter.direction || "both";
947
- if (dir === "outgoing") {
948
- conditions.push("source_entity_id = ?");
949
- params.push(filter.entity_id);
950
- } else if (dir === "incoming") {
951
- conditions.push("target_entity_id = ?");
952
- params.push(filter.entity_id);
953
- } else {
954
- conditions.push("(source_entity_id = ? OR target_entity_id = ?)");
955
- params.push(filter.entity_id, filter.entity_id);
956
- }
957
- }
958
- if (filter.relation_type) {
959
- conditions.push("relation_type = ?");
960
- params.push(filter.relation_type);
961
- }
962
- const where = conditions.length > 0 ? `WHERE ${conditions.join(" AND ")}` : "";
963
- const rows = d.query(`SELECT * FROM relations ${where} ORDER BY created_at DESC`).all(...params);
964
- return rows.map(parseRelationRow);
965
- }
966
- function deleteRelation(id, db) {
967
- const d = db || getDatabase();
968
- const result = d.run("DELETE FROM relations WHERE id = ?", [id]);
969
- if (result.changes === 0)
970
- throw new Error(`Relation not found: ${id}`);
971
- }
972
- function getEntityGraph(entityId, depth = 2, db) {
973
- const d = db || getDatabase();
974
- const entityRows = d.query(`WITH RECURSIVE graph(id, depth) AS (
975
- VALUES(?, 0)
976
- UNION
977
- SELECT CASE WHEN r.source_entity_id = g.id THEN r.target_entity_id ELSE r.source_entity_id END, g.depth + 1
978
- FROM relations r JOIN graph g ON (r.source_entity_id = g.id OR r.target_entity_id = g.id)
979
- WHERE g.depth < ?
980
- )
981
- SELECT DISTINCT e.* FROM entities e JOIN graph g ON e.id = g.id`).all(entityId, depth);
982
- const entities = entityRows.map(parseEntityRow2);
983
- const entityIds = new Set(entities.map((e) => e.id));
984
- if (entityIds.size === 0) {
985
- return { entities: [], relations: [] };
986
- }
987
- const placeholders = Array.from(entityIds).map(() => "?").join(",");
988
- const relationRows = d.query(`SELECT * FROM relations
989
- WHERE source_entity_id IN (${placeholders})
990
- AND target_entity_id IN (${placeholders})`).all(...Array.from(entityIds), ...Array.from(entityIds));
991
- const relations = relationRows.map(parseRelationRow);
992
- return { entities, relations };
993
- }
994
- function findPath(fromEntityId, toEntityId, maxDepth = 5, db) {
995
- const d = db || getDatabase();
996
- const rows = d.query(`WITH RECURSIVE path(id, trail, depth) AS (
997
- SELECT ?, ?, 0
998
- UNION
999
- SELECT
1000
- CASE WHEN r.source_entity_id = p.id THEN r.target_entity_id ELSE r.source_entity_id END,
1001
- p.trail || ',' || CASE WHEN r.source_entity_id = p.id THEN r.target_entity_id ELSE r.source_entity_id END,
1002
- p.depth + 1
1003
- FROM relations r JOIN path p ON (r.source_entity_id = p.id OR r.target_entity_id = p.id)
1004
- WHERE p.depth < ?
1005
- AND INSTR(p.trail, CASE WHEN r.source_entity_id = p.id THEN r.target_entity_id ELSE r.source_entity_id END) = 0
1006
- )
1007
- SELECT trail FROM path WHERE id = ? ORDER BY depth ASC LIMIT 1`).get(fromEntityId, fromEntityId, maxDepth, toEntityId);
1008
- if (!rows)
1009
- return null;
1010
- const ids = rows.trail.split(",");
1011
- const entities = [];
1012
- for (const id of ids) {
1013
- const row = d.query("SELECT * FROM entities WHERE id = ?").get(id);
1014
- if (row)
1015
- entities.push(parseEntityRow2(row));
1016
- }
1017
- return entities.length > 0 ? entities : null;
1018
- }
1019
-
1020
- // src/lib/config.ts
1021
- import { existsSync as existsSync2, mkdirSync as mkdirSync2, readFileSync, readdirSync, writeFileSync, unlinkSync } from "fs";
1022
- import { homedir } from "os";
1023
- import { basename, dirname as dirname2, join as join2, resolve as resolve2 } from "path";
1024
- var DEFAULT_CONFIG = {
1025
- default_scope: "private",
1026
- default_category: "knowledge",
1027
- default_importance: 5,
1028
- max_entries: 1000,
1029
- max_entries_per_scope: {
1030
- global: 500,
1031
- shared: 300,
1032
- private: 200
1033
- },
1034
- injection: {
1035
- max_tokens: 500,
1036
- min_importance: 5,
1037
- categories: ["preference", "fact"],
1038
- refresh_interval: 5
1039
- },
1040
- extraction: {
1041
- enabled: true,
1042
- min_confidence: 0.5
1043
- },
1044
- sync_agents: ["claude", "codex", "gemini"],
1045
- auto_cleanup: {
1046
- enabled: true,
1047
- expired_check_interval: 3600,
1048
- unused_archive_days: 7,
1049
- stale_deprioritize_days: 14
1050
- }
1051
- };
1052
- function deepMerge(target, source) {
1053
- const result = { ...target };
1054
- for (const key of Object.keys(source)) {
1055
- const sourceVal = source[key];
1056
- const targetVal = result[key];
1057
- if (sourceVal !== null && typeof sourceVal === "object" && !Array.isArray(sourceVal) && targetVal !== null && typeof targetVal === "object" && !Array.isArray(targetVal)) {
1058
- result[key] = deepMerge(targetVal, sourceVal);
1059
- } else {
1060
- result[key] = sourceVal;
1061
- }
1062
- }
1063
- return result;
1064
- }
1065
- var VALID_SCOPES = ["global", "shared", "private"];
1066
- var VALID_CATEGORIES = [
1067
- "preference",
1068
- "fact",
1069
- "knowledge",
1070
- "history"
1071
- ];
1072
- function isValidScope(value) {
1073
- return VALID_SCOPES.includes(value);
1074
- }
1075
- function isValidCategory(value) {
1076
- return VALID_CATEGORIES.includes(value);
1077
- }
1078
- function loadConfig() {
1079
- const configPath = join2(homedir(), ".mementos", "config.json");
1080
- let fileConfig = {};
1081
- if (existsSync2(configPath)) {
1082
- try {
1083
- const raw = readFileSync(configPath, "utf-8");
1084
- fileConfig = JSON.parse(raw);
1085
- } catch {}
1086
- }
1087
- const merged = deepMerge(DEFAULT_CONFIG, fileConfig);
1088
- const envScope = process.env["MEMENTOS_DEFAULT_SCOPE"];
1089
- if (envScope && isValidScope(envScope)) {
1090
- merged.default_scope = envScope;
1091
- }
1092
- const envCategory = process.env["MEMENTOS_DEFAULT_CATEGORY"];
1093
- if (envCategory && isValidCategory(envCategory)) {
1094
- merged.default_category = envCategory;
1095
- }
1096
- const envImportance = process.env["MEMENTOS_DEFAULT_IMPORTANCE"];
1097
- if (envImportance) {
1098
- const parsed = parseInt(envImportance, 10);
1099
- if (!Number.isNaN(parsed) && parsed >= 1 && parsed <= 10) {
1100
- merged.default_importance = parsed;
1101
- }
1102
- }
1103
- return merged;
1104
- }
1105
- function findFileWalkingUp(filename) {
1106
- let dir = process.cwd();
1107
- while (true) {
1108
- const candidate = join2(dir, filename);
1109
- if (existsSync2(candidate)) {
1110
- return candidate;
1111
- }
1112
- const parent = dirname2(dir);
1113
- if (parent === dir) {
1114
- return null;
1115
- }
1116
- dir = parent;
1117
- }
1118
- }
1119
- function findGitRoot2() {
1120
- let dir = process.cwd();
1121
- while (true) {
1122
- if (existsSync2(join2(dir, ".git"))) {
1123
- return dir;
1124
- }
1125
- const parent = dirname2(dir);
1126
- if (parent === dir) {
1127
- return null;
1128
- }
1129
- dir = parent;
1130
- }
1131
- }
1132
- function profilesDir() {
1133
- return join2(homedir(), ".mementos", "profiles");
1134
- }
1135
- function globalConfigPath() {
1136
- return join2(homedir(), ".mementos", "config.json");
1137
- }
1138
- function readGlobalConfig() {
1139
- const p = globalConfigPath();
1140
- if (!existsSync2(p))
1141
- return {};
1142
- try {
1143
- return JSON.parse(readFileSync(p, "utf-8"));
1144
- } catch {
1145
- return {};
1146
- }
1147
- }
1148
- function getActiveProfile() {
1149
- const envProfile = process.env["MEMENTOS_PROFILE"];
1150
- if (envProfile)
1151
- return envProfile.trim();
1152
- const cfg = readGlobalConfig();
1153
- return cfg["active_profile"] || null;
1154
- }
1155
- function listProfiles() {
1156
- const dir = profilesDir();
1157
- if (!existsSync2(dir))
1158
- return [];
1159
- return readdirSync(dir).filter((f) => f.endsWith(".db")).map((f) => basename(f, ".db")).sort();
1160
- }
1161
- function getDbPath2() {
1162
- const envDbPath = process.env["MEMENTOS_DB_PATH"];
1163
- if (envDbPath) {
1164
- const resolved = resolve2(envDbPath);
1165
- ensureDir2(dirname2(resolved));
1166
- return resolved;
1167
- }
1168
- const profile = getActiveProfile();
1169
- if (profile) {
1170
- const profilePath = join2(profilesDir(), `${profile}.db`);
1171
- ensureDir2(dirname2(profilePath));
1172
- return profilePath;
1173
- }
1174
- const dbScope = process.env["MEMENTOS_DB_SCOPE"];
1175
- if (dbScope === "project") {
1176
- const gitRoot = findGitRoot2();
1177
- if (gitRoot) {
1178
- const dbPath = join2(gitRoot, ".mementos", "mementos.db");
1179
- ensureDir2(dirname2(dbPath));
1180
- return dbPath;
1181
- }
1182
- }
1183
- const found = findFileWalkingUp(join2(".mementos", "mementos.db"));
1184
- if (found) {
1185
- return found;
1186
- }
1187
- const fallback = join2(homedir(), ".mementos", "mementos.db");
1188
- ensureDir2(dirname2(fallback));
1189
- return fallback;
1190
- }
1191
- function ensureDir2(dir) {
1192
- if (!existsSync2(dir)) {
1193
- mkdirSync2(dir, { recursive: true });
1194
- }
1195
- }
1196
-
1197
- // src/db/memories.ts
1198
- function runEntityExtraction(memory, projectId, d) {
1199
- const config = loadConfig();
1200
- if (config.extraction?.enabled === false)
1201
- return;
1202
- const extracted = extractEntities(memory, d);
1203
- const minConfidence = config.extraction?.min_confidence ?? 0.5;
1204
- const entityIds = [];
1205
- for (const ext of extracted) {
1206
- if (ext.confidence >= minConfidence) {
1207
- const entity = createEntity({ name: ext.name, type: ext.type, project_id: projectId }, d);
1208
- linkEntityToMemory(entity.id, memory.id, "context", d);
1209
- entityIds.push(entity.id);
1210
- }
1211
- }
1212
- for (let i = 0;i < entityIds.length; i++) {
1213
- for (let j = i + 1;j < entityIds.length; j++) {
1214
- try {
1215
- createRelation({ source_entity_id: entityIds[i], target_entity_id: entityIds[j], relation_type: "related_to" }, d);
1216
- } catch {}
1217
- }
1218
- }
1219
- }
1220
- function parseMemoryRow(row) {
1221
- return {
1222
- id: row["id"],
1223
- key: row["key"],
1224
- value: row["value"],
1225
- category: row["category"],
1226
- scope: row["scope"],
1227
- summary: row["summary"] || null,
1228
- tags: JSON.parse(row["tags"] || "[]"),
1229
- importance: row["importance"],
1230
- source: row["source"],
1231
- status: row["status"],
1232
- pinned: !!row["pinned"],
1233
- agent_id: row["agent_id"] || null,
1234
- project_id: row["project_id"] || null,
1235
- session_id: row["session_id"] || null,
1236
- metadata: JSON.parse(row["metadata"] || "{}"),
1237
- access_count: row["access_count"],
1238
- version: row["version"],
1239
- expires_at: row["expires_at"] || null,
1240
- created_at: row["created_at"],
1241
- updated_at: row["updated_at"],
1242
- accessed_at: row["accessed_at"] || null
1243
- };
1244
- }
1245
- function createMemory(input, dedupeMode = "merge", db) {
1246
- const d = db || getDatabase();
1247
- const timestamp = now();
1248
- let expiresAt = input.expires_at || null;
1249
- if (input.ttl_ms && !expiresAt) {
1250
- expiresAt = new Date(Date.now() + input.ttl_ms).toISOString();
1251
- }
1252
- const id = uuid();
1253
- const tags = input.tags || [];
1254
- const tagsJson = JSON.stringify(tags);
1255
- const metadataJson = JSON.stringify(input.metadata || {});
1256
- const safeValue = redactSecrets(input.value);
1257
- const safeSummary = input.summary ? redactSecrets(input.summary) : null;
1258
- if (dedupeMode === "merge") {
1259
- const existing = d.query(`SELECT id, version FROM memories
1260
- WHERE key = ? AND scope = ?
1261
- AND COALESCE(agent_id, '') = ?
1262
- AND COALESCE(project_id, '') = ?
1263
- AND COALESCE(session_id, '') = ?`).get(input.key, input.scope || "private", input.agent_id || "", input.project_id || "", input.session_id || "");
1264
- if (existing) {
1265
- d.run(`UPDATE memories SET
1266
- value = ?, category = ?, summary = ?, tags = ?,
1267
- importance = ?, metadata = ?, expires_at = ?,
1268
- pinned = COALESCE(pinned, 0),
1269
- version = version + 1, updated_at = ?
1270
- WHERE id = ?`, [
1271
- safeValue,
1272
- input.category || "knowledge",
1273
- safeSummary,
1274
- tagsJson,
1275
- input.importance ?? 5,
1276
- metadataJson,
1277
- expiresAt,
1278
- timestamp,
1279
- existing.id
1280
- ]);
1281
- d.run("DELETE FROM memory_tags WHERE memory_id = ?", [existing.id]);
1282
- const insertTag2 = d.prepare("INSERT OR IGNORE INTO memory_tags (memory_id, tag) VALUES (?, ?)");
1283
- for (const tag of tags) {
1284
- insertTag2.run(existing.id, tag);
1285
- }
1286
- const merged = getMemory(existing.id, d);
1287
- try {
1288
- const oldLinks = getEntityMemoryLinks(undefined, merged.id, d);
1289
- for (const link of oldLinks) {
1290
- unlinkEntityFromMemory(link.entity_id, merged.id, d);
1291
- }
1292
- runEntityExtraction(merged, input.project_id, d);
1293
- } catch {}
1294
- return merged;
547
+ const merged = getMemory(existing.id, d);
548
+ try {
549
+ const oldLinks = getEntityMemoryLinks(undefined, merged.id, d);
550
+ for (const link of oldLinks) {
551
+ unlinkEntityFromMemory(link.entity_id, merged.id, d);
552
+ }
553
+ runEntityExtraction(merged, input.project_id, d);
554
+ } catch {}
555
+ return merged;
1295
556
  }
1296
557
  }
1297
558
  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)
@@ -1514,41 +775,153 @@ function deleteMemory(id, db) {
1514
775
  const result = d.run("DELETE FROM memories WHERE id = ?", [id]);
1515
776
  return result.changes > 0;
1516
777
  }
1517
- function touchMemory(id, db) {
778
+ function touchMemory(id, db) {
779
+ const d = db || getDatabase();
780
+ d.run("UPDATE memories SET access_count = access_count + 1, accessed_at = ? WHERE id = ?", [now(), id]);
781
+ }
782
+ function cleanExpiredMemories(db) {
783
+ const d = db || getDatabase();
784
+ const timestamp = now();
785
+ const countRow = d.query("SELECT COUNT(*) as c FROM memories WHERE expires_at IS NOT NULL AND expires_at < ?").get(timestamp);
786
+ const count = countRow.c;
787
+ if (count > 0) {
788
+ d.run("DELETE FROM memories WHERE expires_at IS NOT NULL AND expires_at < ?", [timestamp]);
789
+ }
790
+ return count;
791
+ }
792
+ function getMemoryVersions(memoryId, db) {
793
+ const d = db || getDatabase();
794
+ try {
795
+ const rows = d.query("SELECT * FROM memory_versions WHERE memory_id = ? ORDER BY version ASC").all(memoryId);
796
+ return rows.map((row) => ({
797
+ id: row["id"],
798
+ memory_id: row["memory_id"],
799
+ version: row["version"],
800
+ value: row["value"],
801
+ importance: row["importance"],
802
+ scope: row["scope"],
803
+ category: row["category"],
804
+ tags: JSON.parse(row["tags"] || "[]"),
805
+ summary: row["summary"] || null,
806
+ pinned: !!row["pinned"],
807
+ status: row["status"],
808
+ created_at: row["created_at"]
809
+ }));
810
+ } catch {
811
+ return [];
812
+ }
813
+ }
814
+
815
+ // src/db/agents.ts
816
+ var CONFLICT_WINDOW_MS = 30 * 60 * 1000;
817
+ function parseAgentRow(row) {
818
+ return {
819
+ id: row["id"],
820
+ name: row["name"],
821
+ session_id: row["session_id"] || null,
822
+ description: row["description"] || null,
823
+ role: row["role"] || null,
824
+ metadata: JSON.parse(row["metadata"] || "{}"),
825
+ active_project_id: row["active_project_id"] || null,
826
+ created_at: row["created_at"],
827
+ last_seen_at: row["last_seen_at"]
828
+ };
829
+ }
830
+ function registerAgent(name, sessionId, description, role, projectId, db) {
831
+ const d = db || getDatabase();
832
+ const timestamp = now();
833
+ const normalizedName = name.trim().toLowerCase();
834
+ const existing = d.query("SELECT * FROM agents WHERE LOWER(name) = ?").get(normalizedName);
835
+ if (existing) {
836
+ const existingId = existing["id"];
837
+ const existingSessionId = existing["session_id"] || null;
838
+ const existingLastSeen = existing["last_seen_at"];
839
+ if (sessionId && existingSessionId && existingSessionId !== sessionId) {
840
+ const lastSeenMs = new Date(existingLastSeen).getTime();
841
+ const nowMs = Date.now();
842
+ if (nowMs - lastSeenMs < CONFLICT_WINDOW_MS) {
843
+ throw new AgentConflictError({
844
+ existing_id: existingId,
845
+ existing_name: normalizedName,
846
+ last_seen_at: existingLastSeen,
847
+ session_hint: existingSessionId.slice(0, 8),
848
+ working_dir: null
849
+ });
850
+ }
851
+ }
852
+ d.run("UPDATE agents SET last_seen_at = ?, session_id = ? WHERE id = ?", [
853
+ timestamp,
854
+ sessionId ?? existingSessionId,
855
+ existingId
856
+ ]);
857
+ if (description) {
858
+ d.run("UPDATE agents SET description = ? WHERE id = ?", [description, existingId]);
859
+ }
860
+ if (role) {
861
+ d.run("UPDATE agents SET role = ? WHERE id = ?", [role, existingId]);
862
+ }
863
+ if (projectId !== undefined) {
864
+ d.run("UPDATE agents SET active_project_id = ? WHERE id = ?", [projectId, existingId]);
865
+ }
866
+ return getAgent(existingId, d);
867
+ }
868
+ const id = shortUuid();
869
+ d.run("INSERT INTO agents (id, name, session_id, description, role, active_project_id, created_at, last_seen_at) VALUES (?, ?, ?, ?, ?, ?, ?, ?)", [id, normalizedName, sessionId ?? null, description || null, role || "agent", projectId ?? null, timestamp, timestamp]);
870
+ return getAgent(id, d);
871
+ }
872
+ function getAgent(idOrName, db) {
873
+ const d = db || getDatabase();
874
+ let row = d.query("SELECT * FROM agents WHERE id = ?").get(idOrName);
875
+ if (row)
876
+ return parseAgentRow(row);
877
+ row = d.query("SELECT * FROM agents WHERE LOWER(name) = ?").get(idOrName.trim().toLowerCase());
878
+ if (row)
879
+ return parseAgentRow(row);
880
+ const rows = d.query("SELECT * FROM agents WHERE id LIKE ?").all(`${idOrName}%`);
881
+ if (rows.length === 1)
882
+ return parseAgentRow(rows[0]);
883
+ return null;
884
+ }
885
+ function listAgents(db) {
886
+ const d = db || getDatabase();
887
+ const rows = d.query("SELECT * FROM agents ORDER BY last_seen_at DESC").all();
888
+ return rows.map(parseAgentRow);
889
+ }
890
+ function listAgentsByProject(projectId, db) {
1518
891
  const d = db || getDatabase();
1519
- d.run("UPDATE memories SET access_count = access_count + 1, accessed_at = ? WHERE id = ?", [now(), id]);
892
+ const rows = d.query("SELECT * FROM agents WHERE active_project_id = ? ORDER BY last_seen_at DESC").all(projectId);
893
+ return rows.map(parseAgentRow);
1520
894
  }
1521
- function cleanExpiredMemories(db) {
895
+ function updateAgent(id, updates, db) {
1522
896
  const d = db || getDatabase();
897
+ const agent = getAgent(id, d);
898
+ if (!agent)
899
+ return null;
1523
900
  const timestamp = now();
1524
- const countRow = d.query("SELECT COUNT(*) as c FROM memories WHERE expires_at IS NOT NULL AND expires_at < ?").get(timestamp);
1525
- const count = countRow.c;
1526
- if (count > 0) {
1527
- d.run("DELETE FROM memories WHERE expires_at IS NOT NULL AND expires_at < ?", [timestamp]);
901
+ if (updates.name) {
902
+ const normalizedNewName = updates.name.trim().toLowerCase();
903
+ if (normalizedNewName !== agent.name) {
904
+ const existing = d.query("SELECT id FROM agents WHERE LOWER(name) = ? AND id != ?").get(normalizedNewName, agent.id);
905
+ if (existing) {
906
+ throw new Error(`Agent name already taken: ${normalizedNewName}`);
907
+ }
908
+ d.run("UPDATE agents SET name = ? WHERE id = ?", [normalizedNewName, agent.id]);
909
+ }
1528
910
  }
1529
- return count;
1530
- }
1531
- function getMemoryVersions(memoryId, db) {
1532
- const d = db || getDatabase();
1533
- try {
1534
- const rows = d.query("SELECT * FROM memory_versions WHERE memory_id = ? ORDER BY version ASC").all(memoryId);
1535
- return rows.map((row) => ({
1536
- id: row["id"],
1537
- memory_id: row["memory_id"],
1538
- version: row["version"],
1539
- value: row["value"],
1540
- importance: row["importance"],
1541
- scope: row["scope"],
1542
- category: row["category"],
1543
- tags: JSON.parse(row["tags"] || "[]"),
1544
- summary: row["summary"] || null,
1545
- pinned: !!row["pinned"],
1546
- status: row["status"],
1547
- created_at: row["created_at"]
1548
- }));
1549
- } catch {
1550
- return [];
911
+ if (updates.description !== undefined) {
912
+ d.run("UPDATE agents SET description = ? WHERE id = ?", [updates.description, agent.id]);
913
+ }
914
+ if (updates.role !== undefined) {
915
+ d.run("UPDATE agents SET role = ? WHERE id = ?", [updates.role, agent.id]);
916
+ }
917
+ if (updates.metadata !== undefined) {
918
+ d.run("UPDATE agents SET metadata = ? WHERE id = ?", [JSON.stringify(updates.metadata), agent.id]);
1551
919
  }
920
+ if ("active_project_id" in updates) {
921
+ d.run("UPDATE agents SET active_project_id = ? WHERE id = ?", [updates.active_project_id ?? null, agent.id]);
922
+ }
923
+ d.run("UPDATE agents SET last_seen_at = ? WHERE id = ?", [timestamp, agent.id]);
924
+ return getAgent(agent.id, d);
1552
925
  }
1553
926
 
1554
927
  // src/db/locks.ts
@@ -1582,46 +955,346 @@ function acquireLock(agentId, resourceType, resourceId, lockType = "exclusive",
1582
955
  }
1583
956
  }
1584
957
  const id = shortUuid();
1585
- const lockedAt = now();
1586
- const expiresAt = new Date(Date.now() + ttlSeconds * 1000).toISOString();
1587
- d.run("INSERT INTO resource_locks (id, resource_type, resource_id, agent_id, lock_type, locked_at, expires_at) VALUES (?, ?, ?, ?, ?, ?, ?)", [id, resourceType, resourceId, agentId, lockType, lockedAt, expiresAt]);
1588
- return {
958
+ const lockedAt = now();
959
+ const expiresAt = new Date(Date.now() + ttlSeconds * 1000).toISOString();
960
+ d.run("INSERT INTO resource_locks (id, resource_type, resource_id, agent_id, lock_type, locked_at, expires_at) VALUES (?, ?, ?, ?, ?, ?, ?)", [id, resourceType, resourceId, agentId, lockType, lockedAt, expiresAt]);
961
+ return {
962
+ id,
963
+ resource_type: resourceType,
964
+ resource_id: resourceId,
965
+ agent_id: agentId,
966
+ lock_type: lockType,
967
+ locked_at: lockedAt,
968
+ expires_at: expiresAt
969
+ };
970
+ }
971
+ function releaseLock(lockId, agentId, db) {
972
+ const d = db || getDatabase();
973
+ const result = d.run("DELETE FROM resource_locks WHERE id = ? AND agent_id = ?", [lockId, agentId]);
974
+ return result.changes > 0;
975
+ }
976
+ function releaseAllAgentLocks(agentId, db) {
977
+ const d = db || getDatabase();
978
+ const result = d.run("DELETE FROM resource_locks WHERE agent_id = ?", [agentId]);
979
+ return result.changes;
980
+ }
981
+ function checkLock(resourceType, resourceId, lockType, db) {
982
+ const d = db || getDatabase();
983
+ cleanExpiredLocks(d);
984
+ const query = lockType ? "SELECT * FROM resource_locks WHERE resource_type = ? AND resource_id = ? AND lock_type = ? AND expires_at > datetime('now')" : "SELECT * FROM resource_locks WHERE resource_type = ? AND resource_id = ? AND expires_at > datetime('now')";
985
+ const rows = lockType ? d.query(query).all(resourceType, resourceId, lockType) : d.query(query).all(resourceType, resourceId);
986
+ return rows.map(parseLockRow);
987
+ }
988
+ function listAgentLocks(agentId, db) {
989
+ const d = db || getDatabase();
990
+ cleanExpiredLocks(d);
991
+ const rows = d.query("SELECT * FROM resource_locks WHERE agent_id = ? AND expires_at > datetime('now') ORDER BY locked_at DESC").all(agentId);
992
+ return rows.map(parseLockRow);
993
+ }
994
+ function cleanExpiredLocks(db) {
995
+ const d = db || getDatabase();
996
+ const result = d.run("DELETE FROM resource_locks WHERE expires_at <= datetime('now')");
997
+ return result.changes;
998
+ }
999
+
1000
+ // src/db/projects.ts
1001
+ function parseProjectRow(row) {
1002
+ return {
1003
+ id: row["id"],
1004
+ name: row["name"],
1005
+ path: row["path"],
1006
+ description: row["description"] || null,
1007
+ memory_prefix: row["memory_prefix"] || null,
1008
+ created_at: row["created_at"],
1009
+ updated_at: row["updated_at"]
1010
+ };
1011
+ }
1012
+ function registerProject(name, path, description, memoryPrefix, db) {
1013
+ const d = db || getDatabase();
1014
+ const timestamp = now();
1015
+ const existing = d.query("SELECT * FROM projects WHERE path = ?").get(path);
1016
+ if (existing) {
1017
+ const existingId = existing["id"];
1018
+ d.run("UPDATE projects SET updated_at = ? WHERE id = ?", [
1019
+ timestamp,
1020
+ existingId
1021
+ ]);
1022
+ return parseProjectRow(existing);
1023
+ }
1024
+ const id = uuid();
1025
+ 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]);
1026
+ return getProject(id, d);
1027
+ }
1028
+ function getProject(idOrPath, db) {
1029
+ const d = db || getDatabase();
1030
+ let row = d.query("SELECT * FROM projects WHERE id = ?").get(idOrPath);
1031
+ if (row)
1032
+ return parseProjectRow(row);
1033
+ row = d.query("SELECT * FROM projects WHERE path = ?").get(idOrPath);
1034
+ if (row)
1035
+ return parseProjectRow(row);
1036
+ row = d.query("SELECT * FROM projects WHERE LOWER(name) = ?").get(idOrPath.toLowerCase());
1037
+ if (row)
1038
+ return parseProjectRow(row);
1039
+ return null;
1040
+ }
1041
+ function listProjects(db) {
1042
+ const d = db || getDatabase();
1043
+ const rows = d.query("SELECT * FROM projects ORDER BY updated_at DESC").all();
1044
+ return rows.map(parseProjectRow);
1045
+ }
1046
+
1047
+ // src/lib/config.ts
1048
+ import { existsSync as existsSync2, mkdirSync as mkdirSync2, readFileSync, readdirSync, writeFileSync, unlinkSync } from "fs";
1049
+ import { homedir } from "os";
1050
+ import { basename, dirname as dirname2, join as join2, resolve as resolve2 } from "path";
1051
+ function findFileWalkingUp(filename) {
1052
+ let dir = process.cwd();
1053
+ while (true) {
1054
+ const candidate = join2(dir, filename);
1055
+ if (existsSync2(candidate)) {
1056
+ return candidate;
1057
+ }
1058
+ const parent = dirname2(dir);
1059
+ if (parent === dir) {
1060
+ return null;
1061
+ }
1062
+ dir = parent;
1063
+ }
1064
+ }
1065
+ function findGitRoot2() {
1066
+ let dir = process.cwd();
1067
+ while (true) {
1068
+ if (existsSync2(join2(dir, ".git"))) {
1069
+ return dir;
1070
+ }
1071
+ const parent = dirname2(dir);
1072
+ if (parent === dir) {
1073
+ return null;
1074
+ }
1075
+ dir = parent;
1076
+ }
1077
+ }
1078
+ function profilesDir() {
1079
+ return join2(homedir(), ".mementos", "profiles");
1080
+ }
1081
+ function globalConfigPath() {
1082
+ return join2(homedir(), ".mementos", "config.json");
1083
+ }
1084
+ function readGlobalConfig() {
1085
+ const p = globalConfigPath();
1086
+ if (!existsSync2(p))
1087
+ return {};
1088
+ try {
1089
+ return JSON.parse(readFileSync(p, "utf-8"));
1090
+ } catch {
1091
+ return {};
1092
+ }
1093
+ }
1094
+ function getActiveProfile() {
1095
+ const envProfile = process.env["MEMENTOS_PROFILE"];
1096
+ if (envProfile)
1097
+ return envProfile.trim();
1098
+ const cfg = readGlobalConfig();
1099
+ return cfg["active_profile"] || null;
1100
+ }
1101
+ function listProfiles() {
1102
+ const dir = profilesDir();
1103
+ if (!existsSync2(dir))
1104
+ return [];
1105
+ return readdirSync(dir).filter((f) => f.endsWith(".db")).map((f) => basename(f, ".db")).sort();
1106
+ }
1107
+ function getDbPath2() {
1108
+ const envDbPath = process.env["MEMENTOS_DB_PATH"];
1109
+ if (envDbPath) {
1110
+ const resolved = resolve2(envDbPath);
1111
+ ensureDir2(dirname2(resolved));
1112
+ return resolved;
1113
+ }
1114
+ const profile = getActiveProfile();
1115
+ if (profile) {
1116
+ const profilePath = join2(profilesDir(), `${profile}.db`);
1117
+ ensureDir2(dirname2(profilePath));
1118
+ return profilePath;
1119
+ }
1120
+ const dbScope = process.env["MEMENTOS_DB_SCOPE"];
1121
+ if (dbScope === "project") {
1122
+ const gitRoot = findGitRoot2();
1123
+ if (gitRoot) {
1124
+ const dbPath = join2(gitRoot, ".mementos", "mementos.db");
1125
+ ensureDir2(dirname2(dbPath));
1126
+ return dbPath;
1127
+ }
1128
+ }
1129
+ const found = findFileWalkingUp(join2(".mementos", "mementos.db"));
1130
+ if (found) {
1131
+ return found;
1132
+ }
1133
+ const fallback = join2(homedir(), ".mementos", "mementos.db");
1134
+ ensureDir2(dirname2(fallback));
1135
+ return fallback;
1136
+ }
1137
+ function ensureDir2(dir) {
1138
+ if (!existsSync2(dir)) {
1139
+ mkdirSync2(dir, { recursive: true });
1140
+ }
1141
+ }
1142
+
1143
+ // src/db/entities.ts
1144
+ function parseEntityRow(row) {
1145
+ return {
1146
+ id: row["id"],
1147
+ name: row["name"],
1148
+ type: row["type"],
1149
+ description: row["description"] || null,
1150
+ metadata: JSON.parse(row["metadata"] || "{}"),
1151
+ project_id: row["project_id"] || null,
1152
+ created_at: row["created_at"],
1153
+ updated_at: row["updated_at"]
1154
+ };
1155
+ }
1156
+ function createEntity(input, db) {
1157
+ const d = db || getDatabase();
1158
+ const timestamp = now();
1159
+ const metadataJson = JSON.stringify(input.metadata || {});
1160
+ const existing = d.query(`SELECT * FROM entities
1161
+ WHERE name = ? AND type = ? AND COALESCE(project_id, '') = ?`).get(input.name, input.type, input.project_id || "");
1162
+ if (existing) {
1163
+ const sets = ["updated_at = ?"];
1164
+ const params = [timestamp];
1165
+ if (input.description !== undefined) {
1166
+ sets.push("description = ?");
1167
+ params.push(input.description);
1168
+ }
1169
+ if (input.metadata !== undefined) {
1170
+ sets.push("metadata = ?");
1171
+ params.push(metadataJson);
1172
+ }
1173
+ const existingId = existing["id"];
1174
+ params.push(existingId);
1175
+ d.run(`UPDATE entities SET ${sets.join(", ")} WHERE id = ?`, params);
1176
+ return getEntity(existingId, d);
1177
+ }
1178
+ const id = shortUuid();
1179
+ d.run(`INSERT INTO entities (id, name, type, description, metadata, project_id, created_at, updated_at)
1180
+ VALUES (?, ?, ?, ?, ?, ?, ?, ?)`, [
1589
1181
  id,
1590
- resource_type: resourceType,
1591
- resource_id: resourceId,
1592
- agent_id: agentId,
1593
- lock_type: lockType,
1594
- locked_at: lockedAt,
1595
- expires_at: expiresAt
1596
- };
1182
+ input.name,
1183
+ input.type,
1184
+ input.description || null,
1185
+ metadataJson,
1186
+ input.project_id || null,
1187
+ timestamp,
1188
+ timestamp
1189
+ ]);
1190
+ return getEntity(id, d);
1597
1191
  }
1598
- function releaseLock(lockId, agentId, db) {
1192
+ function getEntity(id, db) {
1599
1193
  const d = db || getDatabase();
1600
- const result = d.run("DELETE FROM resource_locks WHERE id = ? AND agent_id = ?", [lockId, agentId]);
1601
- return result.changes > 0;
1194
+ const row = d.query("SELECT * FROM entities WHERE id = ?").get(id);
1195
+ if (!row)
1196
+ throw new EntityNotFoundError(id);
1197
+ return parseEntityRow(row);
1602
1198
  }
1603
- function releaseAllAgentLocks(agentId, db) {
1199
+ function getEntityByName(name, type, projectId, db) {
1604
1200
  const d = db || getDatabase();
1605
- const result = d.run("DELETE FROM resource_locks WHERE agent_id = ?", [agentId]);
1606
- return result.changes;
1201
+ let sql = "SELECT * FROM entities WHERE name = ?";
1202
+ const params = [name];
1203
+ if (type) {
1204
+ sql += " AND type = ?";
1205
+ params.push(type);
1206
+ }
1207
+ if (projectId !== undefined) {
1208
+ sql += " AND project_id = ?";
1209
+ params.push(projectId);
1210
+ }
1211
+ sql += " LIMIT 1";
1212
+ const row = d.query(sql).get(...params);
1213
+ if (!row)
1214
+ return null;
1215
+ return parseEntityRow(row);
1607
1216
  }
1608
- function checkLock(resourceType, resourceId, lockType, db) {
1217
+ function listEntities(filter = {}, db) {
1609
1218
  const d = db || getDatabase();
1610
- cleanExpiredLocks(d);
1611
- const query = lockType ? "SELECT * FROM resource_locks WHERE resource_type = ? AND resource_id = ? AND lock_type = ? AND expires_at > datetime('now')" : "SELECT * FROM resource_locks WHERE resource_type = ? AND resource_id = ? AND expires_at > datetime('now')";
1612
- const rows = lockType ? d.query(query).all(resourceType, resourceId, lockType) : d.query(query).all(resourceType, resourceId);
1613
- return rows.map(parseLockRow);
1219
+ const conditions = [];
1220
+ const params = [];
1221
+ if (filter.type) {
1222
+ conditions.push("type = ?");
1223
+ params.push(filter.type);
1224
+ }
1225
+ if (filter.project_id) {
1226
+ conditions.push("project_id = ?");
1227
+ params.push(filter.project_id);
1228
+ }
1229
+ if (filter.search) {
1230
+ conditions.push("(name LIKE ? OR description LIKE ?)");
1231
+ const term = `%${filter.search}%`;
1232
+ params.push(term, term);
1233
+ }
1234
+ let sql = "SELECT * FROM entities";
1235
+ if (conditions.length > 0) {
1236
+ sql += ` WHERE ${conditions.join(" AND ")}`;
1237
+ }
1238
+ sql += " ORDER BY updated_at DESC";
1239
+ if (filter.limit) {
1240
+ sql += " LIMIT ?";
1241
+ params.push(filter.limit);
1242
+ }
1243
+ if (filter.offset) {
1244
+ sql += " OFFSET ?";
1245
+ params.push(filter.offset);
1246
+ }
1247
+ const rows = d.query(sql).all(...params);
1248
+ return rows.map(parseEntityRow);
1614
1249
  }
1615
- function listAgentLocks(agentId, db) {
1250
+ function updateEntity(id, input, db) {
1616
1251
  const d = db || getDatabase();
1617
- cleanExpiredLocks(d);
1618
- const rows = d.query("SELECT * FROM resource_locks WHERE agent_id = ? AND expires_at > datetime('now') ORDER BY locked_at DESC").all(agentId);
1619
- return rows.map(parseLockRow);
1252
+ const existing = d.query("SELECT id FROM entities WHERE id = ?").get(id);
1253
+ if (!existing)
1254
+ throw new EntityNotFoundError(id);
1255
+ const sets = ["updated_at = ?"];
1256
+ const params = [now()];
1257
+ if (input.name !== undefined) {
1258
+ sets.push("name = ?");
1259
+ params.push(input.name);
1260
+ }
1261
+ if (input.type !== undefined) {
1262
+ sets.push("type = ?");
1263
+ params.push(input.type);
1264
+ }
1265
+ if (input.description !== undefined) {
1266
+ sets.push("description = ?");
1267
+ params.push(input.description);
1268
+ }
1269
+ if (input.metadata !== undefined) {
1270
+ sets.push("metadata = ?");
1271
+ params.push(JSON.stringify(input.metadata));
1272
+ }
1273
+ params.push(id);
1274
+ d.run(`UPDATE entities SET ${sets.join(", ")} WHERE id = ?`, params);
1275
+ return getEntity(id, d);
1620
1276
  }
1621
- function cleanExpiredLocks(db) {
1277
+ function deleteEntity(id, db) {
1622
1278
  const d = db || getDatabase();
1623
- const result = d.run("DELETE FROM resource_locks WHERE expires_at <= datetime('now')");
1624
- return result.changes;
1279
+ const result = d.run("DELETE FROM entities WHERE id = ?", [id]);
1280
+ if (result.changes === 0)
1281
+ throw new EntityNotFoundError(id);
1282
+ }
1283
+ function mergeEntities(sourceId, targetId, db) {
1284
+ const d = db || getDatabase();
1285
+ getEntity(sourceId, d);
1286
+ getEntity(targetId, d);
1287
+ d.run(`UPDATE OR IGNORE relations SET source_entity_id = ? WHERE source_entity_id = ?`, [targetId, sourceId]);
1288
+ d.run(`UPDATE OR IGNORE relations SET target_entity_id = ? WHERE target_entity_id = ?`, [targetId, sourceId]);
1289
+ d.run("DELETE FROM relations WHERE source_entity_id = ? OR target_entity_id = ?", [
1290
+ sourceId,
1291
+ sourceId
1292
+ ]);
1293
+ d.run(`UPDATE OR IGNORE entity_memories SET entity_id = ? WHERE entity_id = ?`, [targetId, sourceId]);
1294
+ d.run("DELETE FROM entity_memories WHERE entity_id = ?", [sourceId]);
1295
+ d.run("DELETE FROM entities WHERE id = ?", [sourceId]);
1296
+ d.run("UPDATE entities SET updated_at = ? WHERE id = ?", [now(), targetId]);
1297
+ return getEntity(targetId, d);
1625
1298
  }
1626
1299
 
1627
1300
  // src/lib/search.ts
@@ -2114,49 +1787,179 @@ function scoreResults(rows, queryLower, graphBoostedIds) {
2114
1787
  }
2115
1788
  function searchMemories(query, filter, db) {
2116
1789
  const d = db || getDatabase();
2117
- query = preprocessQuery(query);
2118
- if (!query)
2119
- return [];
2120
- const queryLower = query.toLowerCase();
2121
- const graphBoostedIds = getGraphBoostedMemoryIds(query, d);
2122
- let scored;
2123
- if (hasFts5Table(d)) {
2124
- const ftsResult = searchWithFts5(d, query, queryLower, filter, graphBoostedIds);
2125
- if (ftsResult !== null) {
2126
- scored = ftsResult;
1790
+ query = preprocessQuery(query);
1791
+ if (!query)
1792
+ return [];
1793
+ const queryLower = query.toLowerCase();
1794
+ const graphBoostedIds = getGraphBoostedMemoryIds(query, d);
1795
+ let scored;
1796
+ if (hasFts5Table(d)) {
1797
+ const ftsResult = searchWithFts5(d, query, queryLower, filter, graphBoostedIds);
1798
+ if (ftsResult !== null) {
1799
+ scored = ftsResult;
1800
+ } else {
1801
+ scored = searchWithLike(d, query, queryLower, filter, graphBoostedIds);
1802
+ }
1803
+ } else {
1804
+ scored = searchWithLike(d, query, queryLower, filter, graphBoostedIds);
1805
+ }
1806
+ if (scored.length < 3) {
1807
+ const fuzzyResults = searchWithFuzzy(d, query, filter, graphBoostedIds);
1808
+ const seenIds = new Set(scored.map((r) => r.memory.id));
1809
+ for (const fr of fuzzyResults) {
1810
+ if (!seenIds.has(fr.memory.id)) {
1811
+ scored.push(fr);
1812
+ seenIds.add(fr.memory.id);
1813
+ }
1814
+ }
1815
+ scored.sort((a, b) => {
1816
+ if (b.score !== a.score)
1817
+ return b.score - a.score;
1818
+ return b.memory.importance - a.memory.importance;
1819
+ });
1820
+ }
1821
+ const offset = filter?.offset ?? 0;
1822
+ const limit = filter?.limit ?? scored.length;
1823
+ const finalResults = scored.slice(offset, offset + limit);
1824
+ if (finalResults.length > 0 && scored.length > 0) {
1825
+ const topScore = scored[0]?.score ?? 0;
1826
+ const secondScore = scored[1]?.score ?? 0;
1827
+ const confidence = topScore > 0 ? Math.max(0, Math.min(1, (topScore - secondScore) / topScore)) : 0;
1828
+ finalResults[0] = { ...finalResults[0], confidence };
1829
+ }
1830
+ logSearchQuery(query, scored.length, filter?.agent_id, filter?.project_id, d);
1831
+ return finalResults;
1832
+ }
1833
+ function logSearchQuery(query, resultCount, agentId, projectId, db) {
1834
+ try {
1835
+ const d = db || getDatabase();
1836
+ const id = crypto.randomUUID().slice(0, 8);
1837
+ d.run("INSERT INTO search_history (id, query, result_count, agent_id, project_id) VALUES (?, ?, ?, ?, ?)", [id, query, resultCount, agentId || null, projectId || null]);
1838
+ } catch {}
1839
+ }
1840
+
1841
+ // src/db/relations.ts
1842
+ function parseRelationRow(row) {
1843
+ return {
1844
+ id: row["id"],
1845
+ source_entity_id: row["source_entity_id"],
1846
+ target_entity_id: row["target_entity_id"],
1847
+ relation_type: row["relation_type"],
1848
+ weight: row["weight"],
1849
+ metadata: JSON.parse(row["metadata"] || "{}"),
1850
+ created_at: row["created_at"]
1851
+ };
1852
+ }
1853
+ function parseEntityRow2(row) {
1854
+ return {
1855
+ id: row["id"],
1856
+ name: row["name"],
1857
+ type: row["type"],
1858
+ description: row["description"] || null,
1859
+ metadata: JSON.parse(row["metadata"] || "{}"),
1860
+ project_id: row["project_id"] || null,
1861
+ created_at: row["created_at"],
1862
+ updated_at: row["updated_at"]
1863
+ };
1864
+ }
1865
+ function createRelation(input, db) {
1866
+ const d = db || getDatabase();
1867
+ const id = shortUuid();
1868
+ const timestamp = now();
1869
+ const weight = input.weight ?? 1;
1870
+ const metadata = JSON.stringify(input.metadata ?? {});
1871
+ d.run(`INSERT INTO relations (id, source_entity_id, target_entity_id, relation_type, weight, metadata, created_at)
1872
+ VALUES (?, ?, ?, ?, ?, ?, ?)
1873
+ ON CONFLICT(source_entity_id, target_entity_id, relation_type)
1874
+ DO UPDATE SET weight = excluded.weight, metadata = excluded.metadata`, [id, input.source_entity_id, input.target_entity_id, input.relation_type, weight, metadata, timestamp]);
1875
+ const row = d.query(`SELECT * FROM relations
1876
+ WHERE source_entity_id = ? AND target_entity_id = ? AND relation_type = ?`).get(input.source_entity_id, input.target_entity_id, input.relation_type);
1877
+ return parseRelationRow(row);
1878
+ }
1879
+ function getRelation(id, db) {
1880
+ const d = db || getDatabase();
1881
+ const row = d.query("SELECT * FROM relations WHERE id = ?").get(id);
1882
+ if (!row)
1883
+ throw new Error(`Relation not found: ${id}`);
1884
+ return parseRelationRow(row);
1885
+ }
1886
+ function listRelations(filter, db) {
1887
+ const d = db || getDatabase();
1888
+ const conditions = [];
1889
+ const params = [];
1890
+ if (filter.entity_id) {
1891
+ const dir = filter.direction || "both";
1892
+ if (dir === "outgoing") {
1893
+ conditions.push("source_entity_id = ?");
1894
+ params.push(filter.entity_id);
1895
+ } else if (dir === "incoming") {
1896
+ conditions.push("target_entity_id = ?");
1897
+ params.push(filter.entity_id);
2127
1898
  } else {
2128
- scored = searchWithLike(d, query, queryLower, filter, graphBoostedIds);
1899
+ conditions.push("(source_entity_id = ? OR target_entity_id = ?)");
1900
+ params.push(filter.entity_id, filter.entity_id);
2129
1901
  }
2130
- } else {
2131
- scored = searchWithLike(d, query, queryLower, filter, graphBoostedIds);
2132
1902
  }
2133
- if (scored.length < 3) {
2134
- const fuzzyResults = searchWithFuzzy(d, query, filter, graphBoostedIds);
2135
- const seenIds = new Set(scored.map((r) => r.memory.id));
2136
- for (const fr of fuzzyResults) {
2137
- if (!seenIds.has(fr.memory.id)) {
2138
- scored.push(fr);
2139
- seenIds.add(fr.memory.id);
2140
- }
2141
- }
2142
- scored.sort((a, b) => {
2143
- if (b.score !== a.score)
2144
- return b.score - a.score;
2145
- return b.memory.importance - a.memory.importance;
2146
- });
1903
+ if (filter.relation_type) {
1904
+ conditions.push("relation_type = ?");
1905
+ params.push(filter.relation_type);
2147
1906
  }
2148
- const offset = filter?.offset ?? 0;
2149
- const limit = filter?.limit ?? scored.length;
2150
- const finalResults = scored.slice(offset, offset + limit);
2151
- logSearchQuery(query, scored.length, filter?.agent_id, filter?.project_id, d);
2152
- return finalResults;
1907
+ const where = conditions.length > 0 ? `WHERE ${conditions.join(" AND ")}` : "";
1908
+ const rows = d.query(`SELECT * FROM relations ${where} ORDER BY created_at DESC`).all(...params);
1909
+ return rows.map(parseRelationRow);
2153
1910
  }
2154
- function logSearchQuery(query, resultCount, agentId, projectId, db) {
2155
- try {
2156
- const d = db || getDatabase();
2157
- const id = crypto.randomUUID().slice(0, 8);
2158
- d.run("INSERT INTO search_history (id, query, result_count, agent_id, project_id) VALUES (?, ?, ?, ?, ?)", [id, query, resultCount, agentId || null, projectId || null]);
2159
- } catch {}
1911
+ function deleteRelation(id, db) {
1912
+ const d = db || getDatabase();
1913
+ const result = d.run("DELETE FROM relations WHERE id = ?", [id]);
1914
+ if (result.changes === 0)
1915
+ throw new Error(`Relation not found: ${id}`);
1916
+ }
1917
+ function getEntityGraph(entityId, depth = 2, db) {
1918
+ const d = db || getDatabase();
1919
+ const entityRows = d.query(`WITH RECURSIVE graph(id, depth) AS (
1920
+ VALUES(?, 0)
1921
+ UNION
1922
+ SELECT CASE WHEN r.source_entity_id = g.id THEN r.target_entity_id ELSE r.source_entity_id END, g.depth + 1
1923
+ FROM relations r JOIN graph g ON (r.source_entity_id = g.id OR r.target_entity_id = g.id)
1924
+ WHERE g.depth < ?
1925
+ )
1926
+ SELECT DISTINCT e.* FROM entities e JOIN graph g ON e.id = g.id`).all(entityId, depth);
1927
+ const entities = entityRows.map(parseEntityRow2);
1928
+ const entityIds = new Set(entities.map((e) => e.id));
1929
+ if (entityIds.size === 0) {
1930
+ return { entities: [], relations: [] };
1931
+ }
1932
+ const placeholders = Array.from(entityIds).map(() => "?").join(",");
1933
+ const relationRows = d.query(`SELECT * FROM relations
1934
+ WHERE source_entity_id IN (${placeholders})
1935
+ AND target_entity_id IN (${placeholders})`).all(...Array.from(entityIds), ...Array.from(entityIds));
1936
+ const relations = relationRows.map(parseRelationRow);
1937
+ return { entities, relations };
1938
+ }
1939
+ function findPath(fromEntityId, toEntityId, maxDepth = 5, db) {
1940
+ const d = db || getDatabase();
1941
+ const rows = d.query(`WITH RECURSIVE path(id, trail, depth) AS (
1942
+ SELECT ?, ?, 0
1943
+ UNION
1944
+ SELECT
1945
+ CASE WHEN r.source_entity_id = p.id THEN r.target_entity_id ELSE r.source_entity_id END,
1946
+ p.trail || ',' || CASE WHEN r.source_entity_id = p.id THEN r.target_entity_id ELSE r.source_entity_id END,
1947
+ p.depth + 1
1948
+ FROM relations r JOIN path p ON (r.source_entity_id = p.id OR r.target_entity_id = p.id)
1949
+ WHERE p.depth < ?
1950
+ AND INSTR(p.trail, CASE WHEN r.source_entity_id = p.id THEN r.target_entity_id ELSE r.source_entity_id END) = 0
1951
+ )
1952
+ SELECT trail FROM path WHERE id = ? ORDER BY depth ASC LIMIT 1`).get(fromEntityId, fromEntityId, maxDepth, toEntityId);
1953
+ if (!rows)
1954
+ return null;
1955
+ const ids = rows.trail.split(",");
1956
+ const entities = [];
1957
+ for (const id of ids) {
1958
+ const row = d.query("SELECT * FROM entities WHERE id = ?").get(id);
1959
+ if (row)
1960
+ entities.push(parseEntityRow2(row));
1961
+ }
1962
+ return entities.length > 0 ? entities : null;
2160
1963
  }
2161
1964
 
2162
1965
  // src/lib/duration.ts
@@ -2202,6 +2005,690 @@ var FORMAT_UNITS = [
2202
2005
  ["s", UNIT_MS["s"]]
2203
2006
  ];
2204
2007
 
2008
+ // src/lib/providers/base.ts
2009
+ var DEFAULT_AUTO_MEMORY_CONFIG = {
2010
+ provider: "anthropic",
2011
+ model: "claude-haiku-4-5",
2012
+ enabled: true,
2013
+ minImportance: 4,
2014
+ autoEntityLink: true,
2015
+ fallback: ["cerebras", "openai"]
2016
+ };
2017
+
2018
+ class BaseProvider {
2019
+ config;
2020
+ constructor(config) {
2021
+ this.config = config;
2022
+ }
2023
+ parseJSON(raw) {
2024
+ try {
2025
+ const cleaned = raw.replace(/^```(?:json)?\s*/m, "").replace(/\s*```$/m, "").trim();
2026
+ return JSON.parse(cleaned);
2027
+ } catch {
2028
+ return null;
2029
+ }
2030
+ }
2031
+ clampImportance(value) {
2032
+ const n = Number(value);
2033
+ if (isNaN(n))
2034
+ return 5;
2035
+ return Math.max(0, Math.min(10, Math.round(n)));
2036
+ }
2037
+ normaliseMemory(raw) {
2038
+ if (!raw || typeof raw !== "object")
2039
+ return null;
2040
+ const m = raw;
2041
+ if (typeof m.content !== "string" || !m.content.trim())
2042
+ return null;
2043
+ const validScopes = ["private", "shared", "global"];
2044
+ const validCategories = [
2045
+ "preference",
2046
+ "fact",
2047
+ "knowledge",
2048
+ "history"
2049
+ ];
2050
+ return {
2051
+ content: m.content.trim(),
2052
+ category: validCategories.includes(m.category) ? m.category : "knowledge",
2053
+ importance: this.clampImportance(m.importance),
2054
+ tags: Array.isArray(m.tags) ? m.tags.filter((t) => typeof t === "string").map((t) => t.toLowerCase()) : [],
2055
+ suggestedScope: validScopes.includes(m.suggestedScope) ? m.suggestedScope : "shared",
2056
+ reasoning: typeof m.reasoning === "string" ? m.reasoning : undefined
2057
+ };
2058
+ }
2059
+ }
2060
+ var MEMORY_EXTRACTION_SYSTEM_PROMPT = `You are a precise memory extraction engine for an AI agent.
2061
+ Given text, extract facts worth remembering as structured JSON.
2062
+ Focus on: decisions made, preferences revealed, corrections, architectural choices, established facts, user preferences.
2063
+ Ignore: greetings, filler, questions without answers, temporary states.
2064
+ Output ONLY a JSON array \u2014 no markdown, no explanation.`;
2065
+ var MEMORY_EXTRACTION_USER_TEMPLATE = (text, context) => `Extract memories from this text.
2066
+ ${context.projectName ? `Project: ${context.projectName}` : ""}
2067
+ ${context.existingMemoriesSummary ? `Existing memories (avoid duplicates):
2068
+ ${context.existingMemoriesSummary}` : ""}
2069
+
2070
+ Text:
2071
+ ${text}
2072
+
2073
+ Return a JSON array of objects with these exact fields:
2074
+ - content: string (the memory, concise and specific)
2075
+ - category: "preference" | "fact" | "knowledge" | "history"
2076
+ - importance: number 0-10 (10 = critical, 0 = trivial)
2077
+ - tags: string[] (lowercase keywords)
2078
+ - suggestedScope: "private" | "shared" | "global"
2079
+ - reasoning: string (one sentence why this is worth remembering)
2080
+
2081
+ Return [] if nothing is worth remembering.`;
2082
+ var ENTITY_EXTRACTION_SYSTEM_PROMPT = `You are a knowledge graph entity extractor.
2083
+ Given text, identify named entities and their relationships.
2084
+ Output ONLY valid JSON \u2014 no markdown, no explanation.`;
2085
+ var ENTITY_EXTRACTION_USER_TEMPLATE = (text) => `Extract entities and relations from this text.
2086
+
2087
+ Text: ${text}
2088
+
2089
+ Return JSON with this exact shape:
2090
+ {
2091
+ "entities": [
2092
+ { "name": string, "type": "person"|"project"|"tool"|"concept"|"file"|"api"|"pattern"|"organization", "confidence": 0-1 }
2093
+ ],
2094
+ "relations": [
2095
+ { "from": string, "to": string, "type": "uses"|"knows"|"depends_on"|"created_by"|"related_to"|"contradicts"|"part_of"|"implements" }
2096
+ ]
2097
+ }`;
2098
+
2099
+ // src/lib/providers/anthropic.ts
2100
+ var ANTHROPIC_MODELS = {
2101
+ default: "claude-haiku-4-5",
2102
+ premium: "claude-sonnet-4-5"
2103
+ };
2104
+
2105
+ class AnthropicProvider extends BaseProvider {
2106
+ name = "anthropic";
2107
+ baseUrl = "https://api.anthropic.com/v1";
2108
+ constructor(config) {
2109
+ const apiKey = config?.apiKey ?? process.env.ANTHROPIC_API_KEY ?? "";
2110
+ super({
2111
+ apiKey,
2112
+ model: config?.model ?? ANTHROPIC_MODELS.default,
2113
+ maxTokens: config?.maxTokens ?? 1024,
2114
+ temperature: config?.temperature ?? 0,
2115
+ timeoutMs: config?.timeoutMs ?? 15000
2116
+ });
2117
+ }
2118
+ async extractMemories(text, context) {
2119
+ if (!this.config.apiKey)
2120
+ return [];
2121
+ try {
2122
+ const response = await this.callAPI(MEMORY_EXTRACTION_SYSTEM_PROMPT, MEMORY_EXTRACTION_USER_TEMPLATE(text, context));
2123
+ const parsed = this.parseJSON(response);
2124
+ if (!Array.isArray(parsed))
2125
+ return [];
2126
+ return parsed.map((item) => this.normaliseMemory(item)).filter((m) => m !== null);
2127
+ } catch (err) {
2128
+ console.error("[anthropic] extractMemories failed:", err);
2129
+ return [];
2130
+ }
2131
+ }
2132
+ async extractEntities(text) {
2133
+ const empty = { entities: [], relations: [] };
2134
+ if (!this.config.apiKey)
2135
+ return empty;
2136
+ try {
2137
+ const response = await this.callAPI(ENTITY_EXTRACTION_SYSTEM_PROMPT, ENTITY_EXTRACTION_USER_TEMPLATE(text));
2138
+ const parsed = this.parseJSON(response);
2139
+ if (!parsed || typeof parsed !== "object")
2140
+ return empty;
2141
+ return {
2142
+ entities: Array.isArray(parsed.entities) ? parsed.entities : [],
2143
+ relations: Array.isArray(parsed.relations) ? parsed.relations : []
2144
+ };
2145
+ } catch (err) {
2146
+ console.error("[anthropic] extractEntities failed:", err);
2147
+ return empty;
2148
+ }
2149
+ }
2150
+ async scoreImportance(content, _context) {
2151
+ if (!this.config.apiKey)
2152
+ return 5;
2153
+ try {
2154
+ const response = await this.callAPI("You are an importance scorer. Return only a single integer 0-10. No explanation.", `How important is this memory for an AI agent to retain long-term?
2155
+
2156
+ "${content}"
2157
+
2158
+ Return only a number 0-10.`);
2159
+ return this.clampImportance(response.trim());
2160
+ } catch {
2161
+ return 5;
2162
+ }
2163
+ }
2164
+ async callAPI(systemPrompt, userMessage) {
2165
+ const controller = new AbortController;
2166
+ const timeout = setTimeout(() => controller.abort(), this.config.timeoutMs ?? 15000);
2167
+ try {
2168
+ const res = await fetch(`${this.baseUrl}/messages`, {
2169
+ method: "POST",
2170
+ headers: {
2171
+ "Content-Type": "application/json",
2172
+ "x-api-key": this.config.apiKey,
2173
+ "anthropic-version": "2023-06-01"
2174
+ },
2175
+ body: JSON.stringify({
2176
+ model: this.config.model,
2177
+ max_tokens: this.config.maxTokens ?? 1024,
2178
+ temperature: this.config.temperature ?? 0,
2179
+ system: systemPrompt,
2180
+ messages: [{ role: "user", content: userMessage }]
2181
+ }),
2182
+ signal: controller.signal
2183
+ });
2184
+ if (!res.ok) {
2185
+ const body = await res.text().catch(() => "");
2186
+ throw new Error(`Anthropic API ${res.status}: ${body.slice(0, 200)}`);
2187
+ }
2188
+ const data = await res.json();
2189
+ return data.content?.[0]?.text ?? "";
2190
+ } finally {
2191
+ clearTimeout(timeout);
2192
+ }
2193
+ }
2194
+ }
2195
+
2196
+ // src/lib/providers/openai-compat.ts
2197
+ class OpenAICompatProvider extends BaseProvider {
2198
+ constructor(config) {
2199
+ super(config);
2200
+ }
2201
+ async extractMemories(text, context) {
2202
+ if (!this.config.apiKey)
2203
+ return [];
2204
+ try {
2205
+ const response = await this.callWithRetry(MEMORY_EXTRACTION_SYSTEM_PROMPT, MEMORY_EXTRACTION_USER_TEMPLATE(text, context));
2206
+ const parsed = this.parseJSON(response);
2207
+ if (!Array.isArray(parsed))
2208
+ return [];
2209
+ return parsed.map((item) => this.normaliseMemory(item)).filter((m) => m !== null);
2210
+ } catch (err) {
2211
+ console.error(`[${this.name}] extractMemories failed:`, err);
2212
+ return [];
2213
+ }
2214
+ }
2215
+ async extractEntities(text) {
2216
+ const empty = { entities: [], relations: [] };
2217
+ if (!this.config.apiKey)
2218
+ return empty;
2219
+ try {
2220
+ const response = await this.callWithRetry(ENTITY_EXTRACTION_SYSTEM_PROMPT, ENTITY_EXTRACTION_USER_TEMPLATE(text));
2221
+ const parsed = this.parseJSON(response);
2222
+ if (!parsed || typeof parsed !== "object")
2223
+ return empty;
2224
+ return {
2225
+ entities: Array.isArray(parsed.entities) ? parsed.entities : [],
2226
+ relations: Array.isArray(parsed.relations) ? parsed.relations : []
2227
+ };
2228
+ } catch (err) {
2229
+ console.error(`[${this.name}] extractEntities failed:`, err);
2230
+ return empty;
2231
+ }
2232
+ }
2233
+ async scoreImportance(content, _context) {
2234
+ if (!this.config.apiKey)
2235
+ return 5;
2236
+ try {
2237
+ const response = await this.callWithRetry("You are an importance scorer. Return only a single integer 0-10. No explanation.", `How important is this memory for an AI agent to retain long-term?
2238
+
2239
+ "${content}"
2240
+
2241
+ Return only a number 0-10.`);
2242
+ return this.clampImportance(response.trim());
2243
+ } catch {
2244
+ return 5;
2245
+ }
2246
+ }
2247
+ async callWithRetry(systemPrompt, userMessage, retries = 3) {
2248
+ let lastError = null;
2249
+ for (let attempt = 0;attempt < retries; attempt++) {
2250
+ try {
2251
+ return await this.callAPI(systemPrompt, userMessage);
2252
+ } catch (err) {
2253
+ lastError = err instanceof Error ? err : new Error(String(err));
2254
+ const isRateLimit = lastError.message.includes("429") || lastError.message.toLowerCase().includes("rate limit");
2255
+ if (!isRateLimit || attempt === retries - 1)
2256
+ throw lastError;
2257
+ await new Promise((r) => setTimeout(r, 1000 * Math.pow(2, attempt)));
2258
+ }
2259
+ }
2260
+ throw lastError ?? new Error("Unknown error");
2261
+ }
2262
+ async callAPI(systemPrompt, userMessage) {
2263
+ const controller = new AbortController;
2264
+ const timeout = setTimeout(() => controller.abort(), this.config.timeoutMs ?? 15000);
2265
+ try {
2266
+ const res = await fetch(`${this.baseUrl}/chat/completions`, {
2267
+ method: "POST",
2268
+ headers: {
2269
+ "Content-Type": "application/json",
2270
+ [this.authHeader]: `Bearer ${this.config.apiKey}`
2271
+ },
2272
+ body: JSON.stringify({
2273
+ model: this.config.model,
2274
+ max_tokens: this.config.maxTokens ?? 1024,
2275
+ temperature: this.config.temperature ?? 0,
2276
+ messages: [
2277
+ { role: "system", content: systemPrompt },
2278
+ { role: "user", content: userMessage }
2279
+ ]
2280
+ }),
2281
+ signal: controller.signal
2282
+ });
2283
+ if (!res.ok) {
2284
+ const body = await res.text().catch(() => "");
2285
+ throw new Error(`${this.name} API ${res.status}: ${body.slice(0, 200)}`);
2286
+ }
2287
+ const data = await res.json();
2288
+ return data.choices?.[0]?.message?.content ?? "";
2289
+ } finally {
2290
+ clearTimeout(timeout);
2291
+ }
2292
+ }
2293
+ }
2294
+
2295
+ // src/lib/providers/openai.ts
2296
+ var OPENAI_MODELS = {
2297
+ default: "gpt-4.1-nano",
2298
+ mini: "gpt-4.1-mini",
2299
+ full: "gpt-4.1"
2300
+ };
2301
+
2302
+ class OpenAIProvider extends OpenAICompatProvider {
2303
+ name = "openai";
2304
+ baseUrl = "https://api.openai.com/v1";
2305
+ authHeader = "Authorization";
2306
+ constructor(config) {
2307
+ super({
2308
+ apiKey: config?.apiKey ?? process.env.OPENAI_API_KEY ?? "",
2309
+ model: config?.model ?? OPENAI_MODELS.default,
2310
+ maxTokens: config?.maxTokens ?? 1024,
2311
+ temperature: config?.temperature ?? 0,
2312
+ timeoutMs: config?.timeoutMs ?? 15000
2313
+ });
2314
+ }
2315
+ }
2316
+
2317
+ // src/lib/providers/cerebras.ts
2318
+ var CEREBRAS_MODELS = {
2319
+ default: "llama-3.3-70b",
2320
+ fast: "llama3.1-8b"
2321
+ };
2322
+
2323
+ class CerebrasProvider extends OpenAICompatProvider {
2324
+ name = "cerebras";
2325
+ baseUrl = "https://api.cerebras.ai/v1";
2326
+ authHeader = "Authorization";
2327
+ constructor(config) {
2328
+ super({
2329
+ apiKey: config?.apiKey ?? process.env.CEREBRAS_API_KEY ?? "",
2330
+ model: config?.model ?? CEREBRAS_MODELS.default,
2331
+ maxTokens: config?.maxTokens ?? 1024,
2332
+ temperature: config?.temperature ?? 0,
2333
+ timeoutMs: config?.timeoutMs ?? 1e4
2334
+ });
2335
+ }
2336
+ }
2337
+
2338
+ // src/lib/providers/grok.ts
2339
+ var GROK_MODELS = {
2340
+ default: "grok-3-mini",
2341
+ premium: "grok-3"
2342
+ };
2343
+
2344
+ class GrokProvider extends OpenAICompatProvider {
2345
+ name = "grok";
2346
+ baseUrl = "https://api.x.ai/v1";
2347
+ authHeader = "Authorization";
2348
+ constructor(config) {
2349
+ super({
2350
+ apiKey: config?.apiKey ?? process.env.XAI_API_KEY ?? "",
2351
+ model: config?.model ?? GROK_MODELS.default,
2352
+ maxTokens: config?.maxTokens ?? 1024,
2353
+ temperature: config?.temperature ?? 0,
2354
+ timeoutMs: config?.timeoutMs ?? 15000
2355
+ });
2356
+ }
2357
+ }
2358
+
2359
+ // src/lib/providers/registry.ts
2360
+ class ProviderRegistry {
2361
+ config = { ...DEFAULT_AUTO_MEMORY_CONFIG };
2362
+ _instances = new Map;
2363
+ configure(partial) {
2364
+ this.config = { ...this.config, ...partial };
2365
+ this._instances.clear();
2366
+ }
2367
+ getConfig() {
2368
+ return this.config;
2369
+ }
2370
+ getPrimary() {
2371
+ return this.getProvider(this.config.provider);
2372
+ }
2373
+ getFallbacks() {
2374
+ const fallbackNames = this.config.fallback ?? [];
2375
+ return fallbackNames.filter((n) => n !== this.config.provider).map((n) => this.getProvider(n)).filter((p) => p !== null);
2376
+ }
2377
+ getAvailable() {
2378
+ const primary = this.getPrimary();
2379
+ if (primary)
2380
+ return primary;
2381
+ const fallbacks = this.getFallbacks();
2382
+ return fallbacks[0] ?? null;
2383
+ }
2384
+ getProvider(name) {
2385
+ const cached = this._instances.get(name);
2386
+ if (cached)
2387
+ return cached;
2388
+ const provider = this.createProvider(name);
2389
+ if (!provider)
2390
+ return null;
2391
+ if (!provider.config.apiKey)
2392
+ return null;
2393
+ this._instances.set(name, provider);
2394
+ return provider;
2395
+ }
2396
+ health() {
2397
+ const providers = ["anthropic", "openai", "cerebras", "grok"];
2398
+ const result = {};
2399
+ for (const name of providers) {
2400
+ const p = this.createProvider(name);
2401
+ result[name] = {
2402
+ available: Boolean(p?.config.apiKey),
2403
+ model: p?.config.model ?? "unknown"
2404
+ };
2405
+ }
2406
+ return result;
2407
+ }
2408
+ createProvider(name) {
2409
+ const modelOverride = name === this.config.provider ? this.config.model : undefined;
2410
+ switch (name) {
2411
+ case "anthropic":
2412
+ return new AnthropicProvider(modelOverride ? { model: modelOverride } : undefined);
2413
+ case "openai":
2414
+ return new OpenAIProvider(modelOverride ? { model: modelOverride } : undefined);
2415
+ case "cerebras":
2416
+ return new CerebrasProvider(modelOverride ? { model: modelOverride } : undefined);
2417
+ case "grok":
2418
+ return new GrokProvider(modelOverride ? { model: modelOverride } : undefined);
2419
+ default:
2420
+ return null;
2421
+ }
2422
+ }
2423
+ }
2424
+ var providerRegistry = new ProviderRegistry;
2425
+ function autoConfigureFromEnv() {
2426
+ const hasAnthropicKey = Boolean(process.env.ANTHROPIC_API_KEY);
2427
+ const hasCerebrasKey = Boolean(process.env.CEREBRAS_API_KEY);
2428
+ const hasOpenAIKey = Boolean(process.env.OPENAI_API_KEY);
2429
+ const hasGrokKey = Boolean(process.env.XAI_API_KEY);
2430
+ if (!hasAnthropicKey) {
2431
+ if (hasCerebrasKey) {
2432
+ providerRegistry.configure({ provider: "cerebras" });
2433
+ } else if (hasOpenAIKey) {
2434
+ providerRegistry.configure({ provider: "openai" });
2435
+ } else if (hasGrokKey) {
2436
+ providerRegistry.configure({ provider: "grok" });
2437
+ }
2438
+ }
2439
+ const allProviders = ["anthropic", "cerebras", "openai", "grok"];
2440
+ const available = allProviders.filter((p) => {
2441
+ switch (p) {
2442
+ case "anthropic":
2443
+ return hasAnthropicKey;
2444
+ case "cerebras":
2445
+ return hasCerebrasKey;
2446
+ case "openai":
2447
+ return hasOpenAIKey;
2448
+ case "grok":
2449
+ return hasGrokKey;
2450
+ }
2451
+ });
2452
+ const primary = providerRegistry.getConfig().provider;
2453
+ const fallback = available.filter((p) => p !== primary);
2454
+ providerRegistry.configure({ fallback });
2455
+ }
2456
+ autoConfigureFromEnv();
2457
+
2458
+ // src/lib/auto-memory-queue.ts
2459
+ var MAX_QUEUE_SIZE = 100;
2460
+ var CONCURRENCY = 3;
2461
+
2462
+ class AutoMemoryQueue {
2463
+ queue = [];
2464
+ handler = null;
2465
+ running = false;
2466
+ activeCount = 0;
2467
+ stats = {
2468
+ pending: 0,
2469
+ processing: 0,
2470
+ processed: 0,
2471
+ failed: 0,
2472
+ dropped: 0
2473
+ };
2474
+ setHandler(handler) {
2475
+ this.handler = handler;
2476
+ if (!this.running)
2477
+ this.startLoop();
2478
+ }
2479
+ enqueue(job) {
2480
+ if (this.queue.length >= MAX_QUEUE_SIZE) {
2481
+ this.queue.shift();
2482
+ this.stats.dropped++;
2483
+ this.stats.pending = Math.max(0, this.stats.pending - 1);
2484
+ }
2485
+ this.queue.push(job);
2486
+ this.stats.pending++;
2487
+ if (!this.running && this.handler)
2488
+ this.startLoop();
2489
+ }
2490
+ getStats() {
2491
+ return { ...this.stats, pending: this.queue.length };
2492
+ }
2493
+ startLoop() {
2494
+ this.running = true;
2495
+ this.loop();
2496
+ }
2497
+ async loop() {
2498
+ while (this.queue.length > 0 || this.activeCount > 0) {
2499
+ while (this.queue.length > 0 && this.activeCount < CONCURRENCY) {
2500
+ const job = this.queue.shift();
2501
+ if (!job)
2502
+ break;
2503
+ this.stats.pending = Math.max(0, this.stats.pending - 1);
2504
+ this.activeCount++;
2505
+ this.stats.processing = this.activeCount;
2506
+ this.processJob(job);
2507
+ }
2508
+ await new Promise((r) => setImmediate(r));
2509
+ }
2510
+ this.running = false;
2511
+ }
2512
+ async processJob(job) {
2513
+ if (!this.handler) {
2514
+ this.activeCount--;
2515
+ this.stats.processing = this.activeCount;
2516
+ return;
2517
+ }
2518
+ try {
2519
+ await this.handler(job);
2520
+ this.stats.processed++;
2521
+ } catch (err) {
2522
+ this.stats.failed++;
2523
+ console.error("[auto-memory-queue] job failed:", err);
2524
+ } finally {
2525
+ this.activeCount--;
2526
+ this.stats.processing = this.activeCount;
2527
+ }
2528
+ }
2529
+ }
2530
+ var autoMemoryQueue = new AutoMemoryQueue;
2531
+
2532
+ // src/lib/auto-memory.ts
2533
+ var DEDUP_SIMILARITY_THRESHOLD = 0.85;
2534
+ function isDuplicate(content, agentId, projectId) {
2535
+ try {
2536
+ const query = content.split(/\s+/).filter((w) => w.length > 3).slice(0, 10).join(" ");
2537
+ if (!query)
2538
+ return false;
2539
+ const results = searchMemories(query, {
2540
+ agent_id: agentId,
2541
+ project_id: projectId,
2542
+ limit: 3
2543
+ });
2544
+ if (results.length === 0)
2545
+ return false;
2546
+ const contentWords = new Set(content.toLowerCase().split(/\W+/).filter((w) => w.length > 3));
2547
+ for (const result of results) {
2548
+ const existingWords = new Set(result.memory.value.toLowerCase().split(/\W+/).filter((w) => w.length > 3));
2549
+ if (contentWords.size === 0 || existingWords.size === 0)
2550
+ continue;
2551
+ const intersection = [...contentWords].filter((w) => existingWords.has(w)).length;
2552
+ const union = new Set([...contentWords, ...existingWords]).size;
2553
+ const similarity = intersection / union;
2554
+ if (similarity >= DEDUP_SIMILARITY_THRESHOLD)
2555
+ return true;
2556
+ }
2557
+ return false;
2558
+ } catch {
2559
+ return false;
2560
+ }
2561
+ }
2562
+ async function linkEntitiesToMemory(memoryId, content, _agentId, projectId) {
2563
+ const provider = providerRegistry.getAvailable();
2564
+ if (!provider)
2565
+ return;
2566
+ try {
2567
+ const { entities, relations } = await provider.extractEntities(content);
2568
+ const entityIdMap = new Map;
2569
+ for (const extracted of entities) {
2570
+ if (extracted.confidence < 0.6)
2571
+ continue;
2572
+ try {
2573
+ const existing = getEntityByName(extracted.name);
2574
+ const entityId = existing ? existing.id : createEntity({
2575
+ name: extracted.name,
2576
+ type: extracted.type,
2577
+ project_id: projectId
2578
+ }).id;
2579
+ entityIdMap.set(extracted.name, entityId);
2580
+ linkEntityToMemory(entityId, memoryId, "subject");
2581
+ } catch {}
2582
+ }
2583
+ for (const rel of relations) {
2584
+ const fromId = entityIdMap.get(rel.from);
2585
+ const toId = entityIdMap.get(rel.to);
2586
+ if (!fromId || !toId)
2587
+ continue;
2588
+ try {
2589
+ createRelation({
2590
+ source_entity_id: fromId,
2591
+ target_entity_id: toId,
2592
+ relation_type: rel.type
2593
+ });
2594
+ } catch {}
2595
+ }
2596
+ } catch (err) {
2597
+ console.error("[auto-memory] entity linking failed:", err);
2598
+ }
2599
+ }
2600
+ async function saveExtractedMemory(extracted, context) {
2601
+ const minImportance = providerRegistry.getConfig().minImportance;
2602
+ if (extracted.importance < minImportance)
2603
+ return null;
2604
+ if (!extracted.content.trim())
2605
+ return null;
2606
+ if (isDuplicate(extracted.content, context.agentId, context.projectId)) {
2607
+ return null;
2608
+ }
2609
+ try {
2610
+ const input = {
2611
+ key: extracted.content.slice(0, 120).replace(/\s+/g, "-").toLowerCase(),
2612
+ value: extracted.content,
2613
+ category: extracted.category,
2614
+ scope: extracted.suggestedScope,
2615
+ importance: extracted.importance,
2616
+ tags: [
2617
+ ...extracted.tags,
2618
+ "auto-extracted",
2619
+ ...context.sessionId ? [`session:${context.sessionId}`] : []
2620
+ ],
2621
+ agent_id: context.agentId,
2622
+ project_id: context.projectId,
2623
+ session_id: context.sessionId,
2624
+ metadata: {
2625
+ reasoning: extracted.reasoning,
2626
+ auto_extracted: true,
2627
+ extracted_at: new Date().toISOString()
2628
+ }
2629
+ };
2630
+ const memory = createMemory(input, "merge");
2631
+ return memory.id;
2632
+ } catch (err) {
2633
+ console.error("[auto-memory] saveExtractedMemory failed:", err);
2634
+ return null;
2635
+ }
2636
+ }
2637
+ async function processJob(job) {
2638
+ if (!providerRegistry.getConfig().enabled)
2639
+ return;
2640
+ const provider = providerRegistry.getAvailable();
2641
+ if (!provider)
2642
+ return;
2643
+ const context = {
2644
+ agentId: job.agentId,
2645
+ projectId: job.projectId,
2646
+ sessionId: job.sessionId
2647
+ };
2648
+ let extracted = [];
2649
+ try {
2650
+ extracted = await provider.extractMemories(job.turn, context);
2651
+ } catch {
2652
+ const fallbacks = providerRegistry.getFallbacks();
2653
+ for (const fallback of fallbacks) {
2654
+ try {
2655
+ extracted = await fallback.extractMemories(job.turn, context);
2656
+ if (extracted.length > 0)
2657
+ break;
2658
+ } catch {
2659
+ continue;
2660
+ }
2661
+ }
2662
+ }
2663
+ if (extracted.length === 0)
2664
+ return;
2665
+ for (const memory of extracted) {
2666
+ const memoryId = await saveExtractedMemory(memory, context);
2667
+ if (!memoryId)
2668
+ continue;
2669
+ if (providerRegistry.getConfig().autoEntityLink) {
2670
+ linkEntitiesToMemory(memoryId, memory.content, job.agentId, job.projectId);
2671
+ }
2672
+ }
2673
+ }
2674
+ autoMemoryQueue.setHandler(processJob);
2675
+ function processConversationTurn(turn, context, source = "turn") {
2676
+ if (!turn?.trim())
2677
+ return;
2678
+ autoMemoryQueue.enqueue({
2679
+ ...context,
2680
+ turn,
2681
+ timestamp: Date.now(),
2682
+ source
2683
+ });
2684
+ }
2685
+ function getAutoMemoryStats() {
2686
+ return autoMemoryQueue.getStats();
2687
+ }
2688
+ function configureAutoMemory(config) {
2689
+ providerRegistry.configure(config);
2690
+ }
2691
+
2205
2692
  // src/server/index.ts
2206
2693
  var DEFAULT_PORT = 19428;
2207
2694
  function parsePort() {
@@ -3161,6 +3648,58 @@ async function findFreePort(start) {
3161
3648
  }
3162
3649
  return start;
3163
3650
  }
3651
+ addRoute("POST", "/api/auto-memory/process", async (req) => {
3652
+ const body = await readJson(req);
3653
+ const turn = body?.turn;
3654
+ if (!turn)
3655
+ return errorResponse("turn is required", 400);
3656
+ processConversationTurn(turn, { agentId: body?.agent_id, projectId: body?.project_id, sessionId: body?.session_id });
3657
+ const stats = getAutoMemoryStats();
3658
+ return json({ queued: true, queue: stats }, 202);
3659
+ });
3660
+ addRoute("GET", "/api/auto-memory/status", () => {
3661
+ return json({
3662
+ queue: getAutoMemoryStats(),
3663
+ config: providerRegistry.getConfig(),
3664
+ providers: providerRegistry.health()
3665
+ });
3666
+ });
3667
+ addRoute("GET", "/api/auto-memory/config", () => {
3668
+ return json(providerRegistry.getConfig());
3669
+ });
3670
+ addRoute("PATCH", "/api/auto-memory/config", async (req) => {
3671
+ const body = await readJson(req) ?? {};
3672
+ const patch = {};
3673
+ if (body.provider)
3674
+ patch.provider = body.provider;
3675
+ if (body.model)
3676
+ patch.model = body.model;
3677
+ if (body.enabled !== undefined)
3678
+ patch.enabled = Boolean(body.enabled);
3679
+ if (body.min_importance !== undefined)
3680
+ patch.minImportance = Number(body.min_importance);
3681
+ if (body.auto_entity_link !== undefined)
3682
+ patch.autoEntityLink = Boolean(body.auto_entity_link);
3683
+ configureAutoMemory(patch);
3684
+ return json({ updated: true, config: providerRegistry.getConfig() });
3685
+ });
3686
+ addRoute("POST", "/api/auto-memory/test", async (req) => {
3687
+ const body = await readJson(req) ?? {};
3688
+ const { turn, provider: providerName, agent_id, project_id } = body;
3689
+ if (!turn)
3690
+ return errorResponse("turn is required", 400);
3691
+ const provider = providerName ? providerRegistry.getProvider(providerName) : providerRegistry.getAvailable();
3692
+ if (!provider)
3693
+ return errorResponse("No LLM provider configured. Set an API key (ANTHROPIC_API_KEY, OPENAI_API_KEY, CEREBRAS_API_KEY, or XAI_API_KEY).", 503);
3694
+ const memories = await provider.extractMemories(turn, { agentId: agent_id, projectId: project_id });
3695
+ return json({
3696
+ provider: provider.name,
3697
+ model: provider.config.model,
3698
+ extracted: memories,
3699
+ count: memories.length,
3700
+ note: "DRY RUN \u2014 nothing was saved"
3701
+ });
3702
+ });
3164
3703
  function startServer(port) {
3165
3704
  const hostname = process.env["MEMENTOS_HOST"] ?? "127.0.0.1";
3166
3705
  Bun.serve({