@hasna/mementos 0.4.39 → 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 (44) hide show
  1. package/dist/cli/index.js +2301 -1545
  2. package/dist/db/agents.d.ts +1 -1
  3. package/dist/db/agents.d.ts.map +1 -1
  4. package/dist/db/database.d.ts.map +1 -1
  5. package/dist/db/locks.d.ts +52 -0
  6. package/dist/db/locks.d.ts.map +1 -0
  7. package/dist/db/memories.d.ts +1 -0
  8. package/dist/db/memories.d.ts.map +1 -1
  9. package/dist/index.d.ts +9 -1
  10. package/dist/index.d.ts.map +1 -1
  11. package/dist/index.js +1905 -1003
  12. package/dist/lib/auto-memory-queue.d.ts +46 -0
  13. package/dist/lib/auto-memory-queue.d.ts.map +1 -0
  14. package/dist/lib/auto-memory.d.ts +18 -0
  15. package/dist/lib/auto-memory.d.ts.map +1 -0
  16. package/dist/lib/dedup.d.ts +33 -0
  17. package/dist/lib/dedup.d.ts.map +1 -0
  18. package/dist/lib/focus.d.ts +58 -0
  19. package/dist/lib/focus.d.ts.map +1 -0
  20. package/dist/lib/memory-lock.d.ts +58 -0
  21. package/dist/lib/memory-lock.d.ts.map +1 -0
  22. package/dist/lib/providers/anthropic.d.ts +21 -0
  23. package/dist/lib/providers/anthropic.d.ts.map +1 -0
  24. package/dist/lib/providers/base.d.ts +96 -0
  25. package/dist/lib/providers/base.d.ts.map +1 -0
  26. package/dist/lib/providers/cerebras.d.ts +20 -0
  27. package/dist/lib/providers/cerebras.d.ts.map +1 -0
  28. package/dist/lib/providers/grok.d.ts +19 -0
  29. package/dist/lib/providers/grok.d.ts.map +1 -0
  30. package/dist/lib/providers/index.d.ts +7 -0
  31. package/dist/lib/providers/index.d.ts.map +1 -0
  32. package/dist/lib/providers/openai-compat.d.ts +18 -0
  33. package/dist/lib/providers/openai-compat.d.ts.map +1 -0
  34. package/dist/lib/providers/openai.d.ts +20 -0
  35. package/dist/lib/providers/openai.d.ts.map +1 -0
  36. package/dist/lib/providers/registry.d.ts +38 -0
  37. package/dist/lib/providers/registry.d.ts.map +1 -0
  38. package/dist/lib/search.d.ts.map +1 -1
  39. package/dist/mcp/index.js +1917 -996
  40. package/dist/server/index.d.ts.map +1 -1
  41. package/dist/server/index.js +1602 -894
  42. package/dist/types/index.d.ts +24 -0
  43. package/dist/types/index.d.ts.map +1 -1
  44. package/package.json +2 -2
@@ -24,6 +24,24 @@ import { dirname as dirname3, extname, join as join3, resolve as resolve3, sep }
24
24
  import { fileURLToPath } from "url";
25
25
 
26
26
  // src/types/index.ts
27
+ class AgentConflictError extends Error {
28
+ conflict = true;
29
+ existing_id;
30
+ existing_name;
31
+ last_seen_at;
32
+ session_hint;
33
+ working_dir;
34
+ constructor(opts) {
35
+ const msg = `Agent "${opts.existing_name}" is already active (session hint: ${opts.session_hint ?? "unknown"}, last seen ${opts.last_seen_at}). Wait 30 minutes or use a different name.`;
36
+ super(msg);
37
+ this.name = "AgentConflictError";
38
+ this.existing_id = opts.existing_id;
39
+ this.existing_name = opts.existing_name;
40
+ this.last_seen_at = opts.last_seen_at;
41
+ this.session_hint = opts.session_hint;
42
+ this.working_dir = opts.working_dir ?? null;
43
+ }
44
+ }
27
45
  class EntityNotFoundError extends Error {
28
46
  constructor(id) {
29
47
  super(`Entity not found: ${id}`);
@@ -311,6 +329,33 @@ var MIGRATIONS = [
311
329
  ALTER TABLE agents ADD COLUMN active_project_id TEXT REFERENCES projects(id) ON DELETE SET NULL;
312
330
  CREATE INDEX IF NOT EXISTS idx_agents_active_project ON agents(active_project_id);
313
331
  INSERT OR IGNORE INTO _migrations (id) VALUES (6);
332
+ `,
333
+ `
334
+ ALTER TABLE agents ADD COLUMN session_id TEXT;
335
+ CREATE INDEX IF NOT EXISTS idx_agents_session ON agents(session_id);
336
+ INSERT OR IGNORE INTO _migrations (id) VALUES (7);
337
+ `,
338
+ `
339
+ CREATE TABLE IF NOT EXISTS resource_locks (
340
+ id TEXT PRIMARY KEY,
341
+ resource_type TEXT NOT NULL CHECK(resource_type IN ('project', 'memory', 'entity', 'agent', 'connector')),
342
+ resource_id TEXT NOT NULL,
343
+ agent_id TEXT NOT NULL REFERENCES agents(id) ON DELETE CASCADE,
344
+ lock_type TEXT NOT NULL DEFAULT 'exclusive' CHECK(lock_type IN ('advisory', 'exclusive')),
345
+ locked_at TEXT NOT NULL DEFAULT (datetime('now')),
346
+ expires_at TEXT NOT NULL
347
+ );
348
+ CREATE UNIQUE INDEX IF NOT EXISTS idx_resource_locks_exclusive
349
+ ON resource_locks(resource_type, resource_id)
350
+ WHERE lock_type = 'exclusive';
351
+ CREATE INDEX IF NOT EXISTS idx_resource_locks_agent ON resource_locks(agent_id);
352
+ CREATE INDEX IF NOT EXISTS idx_resource_locks_expires ON resource_locks(expires_at);
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);
314
359
  `
315
360
  ];
316
361
  var _db = null;
@@ -381,672 +426,628 @@ function redactSecrets(text) {
381
426
  return result;
382
427
  }
383
428
 
384
- // src/db/agents.ts
385
- function parseAgentRow(row) {
429
+ // src/db/entity-memories.ts
430
+ function parseEntityMemoryRow(row) {
386
431
  return {
387
- id: row["id"],
388
- name: row["name"],
389
- description: row["description"] || null,
390
- role: row["role"] || null,
391
- metadata: JSON.parse(row["metadata"] || "{}"),
392
- active_project_id: row["active_project_id"] || null,
393
- created_at: row["created_at"],
394
- 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"]
395
436
  };
396
437
  }
397
- function registerAgent(name, description, role, db) {
438
+ function linkEntityToMemory(entityId, memoryId, role = "context", db) {
398
439
  const d = db || getDatabase();
399
440
  const timestamp = now();
400
- const normalizedName = name.trim().toLowerCase();
401
- const existing = d.query("SELECT * FROM agents WHERE LOWER(name) = ?").get(normalizedName);
402
- if (existing) {
403
- const existingId = existing["id"];
404
- d.run("UPDATE agents SET last_seen_at = ? WHERE id = ?", [
405
- timestamp,
406
- existingId
407
- ]);
408
- if (description) {
409
- d.run("UPDATE agents SET description = ? WHERE id = ?", [
410
- description,
411
- existingId
412
- ]);
413
- }
414
- if (role) {
415
- d.run("UPDATE agents SET role = ? WHERE id = ?", [
416
- role,
417
- existingId
418
- ]);
419
- }
420
- return getAgent(existingId, d);
421
- }
422
- const id = shortUuid();
423
- d.run("INSERT INTO agents (id, name, description, role, created_at, last_seen_at) VALUES (?, ?, ?, ?, ?, ?)", [id, normalizedName, description || null, role || "agent", timestamp, timestamp]);
424
- return getAgent(id, d);
425
- }
426
- function getAgent(idOrName, db) {
427
- const d = db || getDatabase();
428
- let row = d.query("SELECT * FROM agents WHERE id = ?").get(idOrName);
429
- if (row)
430
- return parseAgentRow(row);
431
- row = d.query("SELECT * FROM agents WHERE LOWER(name) = ?").get(idOrName.trim().toLowerCase());
432
- if (row)
433
- return parseAgentRow(row);
434
- const rows = d.query("SELECT * FROM agents WHERE id LIKE ?").all(`${idOrName}%`);
435
- if (rows.length === 1)
436
- return parseAgentRow(rows[0]);
437
- 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);
438
445
  }
439
- function listAgents(db) {
446
+ function unlinkEntityFromMemory(entityId, memoryId, db) {
440
447
  const d = db || getDatabase();
441
- const rows = d.query("SELECT * FROM agents ORDER BY last_seen_at DESC").all();
442
- return rows.map(parseAgentRow);
448
+ d.run("DELETE FROM entity_memories WHERE entity_id = ? AND memory_id = ?", [entityId, memoryId]);
443
449
  }
444
- function listAgentsByProject(projectId, db) {
450
+ function getMemoriesForEntity(entityId, db) {
445
451
  const d = db || getDatabase();
446
- const rows = d.query("SELECT * FROM agents WHERE active_project_id = ? ORDER BY last_seen_at DESC").all(projectId);
447
- 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);
448
457
  }
449
- function updateAgent(id, updates, db) {
458
+ function getEntityMemoryLinks(entityId, memoryId, db) {
450
459
  const d = db || getDatabase();
451
- const agent = getAgent(id, d);
452
- if (!agent)
453
- return null;
454
- const timestamp = now();
455
- if (updates.name) {
456
- const normalizedNewName = updates.name.trim().toLowerCase();
457
- if (normalizedNewName !== agent.name) {
458
- const existing = d.query("SELECT id FROM agents WHERE LOWER(name) = ? AND id != ?").get(normalizedNewName, agent.id);
459
- if (existing) {
460
- throw new Error(`Agent name already taken: ${normalizedNewName}`);
461
- }
462
- d.run("UPDATE agents SET name = ? WHERE id = ?", [normalizedNewName, agent.id]);
463
- }
464
- }
465
- if (updates.description !== undefined) {
466
- d.run("UPDATE agents SET description = ? WHERE id = ?", [updates.description, agent.id]);
467
- }
468
- if (updates.role !== undefined) {
469
- 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);
470
465
  }
471
- if (updates.metadata !== undefined) {
472
- 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);
473
469
  }
474
- if ("active_project_id" in updates) {
475
- 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 ")}`;
476
473
  }
477
- d.run("UPDATE agents SET last_seen_at = ? WHERE id = ?", [timestamp, agent.id]);
478
- 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);
479
477
  }
480
478
 
481
- // src/db/projects.ts
482
- function parseProjectRow(row) {
479
+ // src/db/memories.ts
480
+ function runEntityExtraction(_memory, _projectId, _d) {}
481
+ function parseMemoryRow(row) {
483
482
  return {
484
483
  id: row["id"],
485
- name: row["name"],
486
- path: row["path"],
487
- description: row["description"] || null,
488
- 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,
489
501
  created_at: row["created_at"],
490
- updated_at: row["updated_at"]
502
+ updated_at: row["updated_at"],
503
+ accessed_at: row["accessed_at"] || null
491
504
  };
492
505
  }
493
- function registerProject(name, path, description, memoryPrefix, db) {
506
+ function createMemory(input, dedupeMode = "merge", db) {
494
507
  const d = db || getDatabase();
495
508
  const timestamp = now();
496
- const existing = d.query("SELECT * FROM projects WHERE path = ?").get(path);
497
- if (existing) {
498
- const existingId = existing["id"];
499
- d.run("UPDATE projects SET updated_at = ? WHERE id = ?", [
500
- timestamp,
501
- existingId
502
- ]);
503
- 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();
504
512
  }
505
513
  const id = uuid();
506
- 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]);
507
- return getProject(id, d);
508
- }
509
- function getProject(idOrPath, db) {
510
- const d = db || getDatabase();
511
- let row = d.query("SELECT * FROM projects WHERE id = ?").get(idOrPath);
512
- if (row)
513
- return parseProjectRow(row);
514
- row = d.query("SELECT * FROM projects WHERE path = ?").get(idOrPath);
515
- if (row)
516
- return parseProjectRow(row);
517
- row = d.query("SELECT * FROM projects WHERE LOWER(name) = ?").get(idOrPath.toLowerCase());
518
- if (row)
519
- return parseProjectRow(row);
520
- return null;
521
- }
522
- function listProjects(db) {
523
- const d = db || getDatabase();
524
- const rows = d.query("SELECT * FROM projects ORDER BY updated_at DESC").all();
525
- return rows.map(parseProjectRow);
526
- }
527
-
528
- // src/lib/extractor.ts
529
- var TECH_KEYWORDS = new Set([
530
- "typescript",
531
- "javascript",
532
- "python",
533
- "rust",
534
- "go",
535
- "java",
536
- "ruby",
537
- "swift",
538
- "kotlin",
539
- "react",
540
- "vue",
541
- "angular",
542
- "svelte",
543
- "nextjs",
544
- "bun",
545
- "node",
546
- "deno",
547
- "sqlite",
548
- "postgres",
549
- "mysql",
550
- "redis",
551
- "docker",
552
- "kubernetes",
553
- "git",
554
- "npm",
555
- "yarn",
556
- "pnpm",
557
- "webpack",
558
- "vite",
559
- "tailwind",
560
- "prisma",
561
- "drizzle",
562
- "zod",
563
- "commander",
564
- "express",
565
- "fastify",
566
- "hono"
567
- ]);
568
- var FILE_PATH_RE = /(?:^|\s)((?:\/|\.\/|~\/)?(?:[\w.-]+\/)+[\w.-]+\.\w+)/g;
569
- var URL_RE = /https?:\/\/[^\s)]+/g;
570
- var NPM_PACKAGE_RE = /@[\w-]+\/[\w.-]+/g;
571
- var PASCAL_CASE_RE = /\b([A-Z][a-z]+(?:[A-Z][a-z]+)+)\b/g;
572
- function getSearchText(memory) {
573
- const parts = [memory.key, memory.value];
574
- if (memory.summary)
575
- parts.push(memory.summary);
576
- return parts.join(" ");
577
- }
578
- function extractEntities(memory, db) {
579
- const text = getSearchText(memory);
580
- const entityMap = new Map;
581
- function add(name, type, confidence) {
582
- const normalized = name.toLowerCase();
583
- if (normalized.length < 3)
584
- return;
585
- const existing = entityMap.get(normalized);
586
- if (!existing || existing.confidence < confidence) {
587
- entityMap.set(normalized, { name: normalized, type, confidence });
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);
546
+ }
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;
588
556
  }
589
557
  }
590
- for (const match of text.matchAll(FILE_PATH_RE)) {
591
- add(match[1].trim(), "file", 0.9);
592
- }
593
- for (const match of text.matchAll(URL_RE)) {
594
- add(match[0], "api", 0.8);
595
- }
596
- for (const match of text.matchAll(NPM_PACKAGE_RE)) {
597
- add(match[0], "tool", 0.85);
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)
559
+ VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, 'active', 0, ?, ?, ?, ?, 0, 1, ?, ?, ?)`, [
560
+ id,
561
+ input.key,
562
+ input.value,
563
+ input.category || "knowledge",
564
+ input.scope || "private",
565
+ input.summary || null,
566
+ tagsJson,
567
+ input.importance ?? 5,
568
+ input.source || "agent",
569
+ input.agent_id || null,
570
+ input.project_id || null,
571
+ input.session_id || null,
572
+ metadataJson,
573
+ expiresAt,
574
+ timestamp,
575
+ timestamp
576
+ ]);
577
+ const insertTag = d.prepare("INSERT OR IGNORE INTO memory_tags (memory_id, tag) VALUES (?, ?)");
578
+ for (const tag of tags) {
579
+ insertTag.run(id, tag);
598
580
  }
581
+ const memory = getMemory(id, d);
599
582
  try {
600
- const d = db || getDatabase();
601
- const agents = listAgents(d);
602
- const textLower2 = text.toLowerCase();
603
- for (const agent of agents) {
604
- const nameLower = agent.name.toLowerCase();
605
- if (nameLower.length >= 3 && textLower2.includes(nameLower)) {
606
- add(agent.name, "person", 0.95);
607
- }
608
- }
583
+ runEntityExtraction(memory, input.project_id, d);
609
584
  } catch {}
610
- try {
611
- const d = db || getDatabase();
612
- const projects = listProjects(d);
613
- const textLower2 = text.toLowerCase();
614
- for (const project of projects) {
615
- const nameLower = project.name.toLowerCase();
616
- if (nameLower.length >= 3 && textLower2.includes(nameLower)) {
617
- add(project.name, "project", 0.95);
618
- }
619
- }
620
- } catch {}
621
- const textLower = text.toLowerCase();
622
- for (const keyword of TECH_KEYWORDS) {
623
- const re = new RegExp(`\\b${keyword}\\b`, "i");
624
- if (re.test(textLower)) {
625
- add(keyword, "tool", 0.7);
626
- }
627
- }
628
- for (const match of text.matchAll(PASCAL_CASE_RE)) {
629
- add(match[1], "concept", 0.5);
630
- }
631
- return Array.from(entityMap.values()).sort((a, b) => b.confidence - a.confidence);
632
- }
633
-
634
- // src/db/entities.ts
635
- function parseEntityRow(row) {
636
- return {
637
- id: row["id"],
638
- name: row["name"],
639
- type: row["type"],
640
- description: row["description"] || null,
641
- metadata: JSON.parse(row["metadata"] || "{}"),
642
- project_id: row["project_id"] || null,
643
- created_at: row["created_at"],
644
- updated_at: row["updated_at"]
645
- };
646
- }
647
- function createEntity(input, db) {
648
- const d = db || getDatabase();
649
- const timestamp = now();
650
- const metadataJson = JSON.stringify(input.metadata || {});
651
- const existing = d.query(`SELECT * FROM entities
652
- WHERE name = ? AND type = ? AND COALESCE(project_id, '') = ?`).get(input.name, input.type, input.project_id || "");
653
- if (existing) {
654
- const sets = ["updated_at = ?"];
655
- const params = [timestamp];
656
- if (input.description !== undefined) {
657
- sets.push("description = ?");
658
- params.push(input.description);
659
- }
660
- if (input.metadata !== undefined) {
661
- sets.push("metadata = ?");
662
- params.push(metadataJson);
663
- }
664
- const existingId = existing["id"];
665
- params.push(existingId);
666
- d.run(`UPDATE entities SET ${sets.join(", ")} WHERE id = ?`, params);
667
- return getEntity(existingId, d);
668
- }
669
- const id = shortUuid();
670
- d.run(`INSERT INTO entities (id, name, type, description, metadata, project_id, created_at, updated_at)
671
- VALUES (?, ?, ?, ?, ?, ?, ?, ?)`, [
672
- id,
673
- input.name,
674
- input.type,
675
- input.description || null,
676
- metadataJson,
677
- input.project_id || null,
678
- timestamp,
679
- timestamp
680
- ]);
681
- return getEntity(id, d);
682
- }
683
- function getEntity(id, db) {
684
- const d = db || getDatabase();
685
- const row = d.query("SELECT * FROM entities WHERE id = ?").get(id);
686
- if (!row)
687
- throw new EntityNotFoundError(id);
688
- return parseEntityRow(row);
585
+ return memory;
689
586
  }
690
- function getEntityByName(name, type, projectId, db) {
587
+ function getMemory(id, db) {
691
588
  const d = db || getDatabase();
692
- let sql = "SELECT * FROM entities WHERE name = ?";
693
- const params = [name];
694
- if (type) {
695
- sql += " AND type = ?";
696
- params.push(type);
697
- }
698
- if (projectId !== undefined) {
699
- sql += " AND project_id = ?";
700
- params.push(projectId);
701
- }
702
- sql += " LIMIT 1";
703
- const row = d.query(sql).get(...params);
589
+ const row = d.query("SELECT * FROM memories WHERE id = ?").get(id);
704
590
  if (!row)
705
591
  return null;
706
- return parseEntityRow(row);
592
+ return parseMemoryRow(row);
707
593
  }
708
- function listEntities(filter = {}, db) {
594
+ function listMemories(filter, db) {
709
595
  const d = db || getDatabase();
710
596
  const conditions = [];
711
597
  const params = [];
712
- if (filter.type) {
713
- conditions.push("type = ?");
714
- params.push(filter.type);
715
- }
716
- if (filter.project_id) {
717
- conditions.push("project_id = ?");
718
- params.push(filter.project_id);
719
- }
720
- if (filter.search) {
721
- conditions.push("(name LIKE ? OR description LIKE ?)");
722
- const term = `%${filter.search}%`;
723
- params.push(term, term);
598
+ if (filter) {
599
+ if (filter.scope) {
600
+ if (Array.isArray(filter.scope)) {
601
+ conditions.push(`scope IN (${filter.scope.map(() => "?").join(",")})`);
602
+ params.push(...filter.scope);
603
+ } else {
604
+ conditions.push("scope = ?");
605
+ params.push(filter.scope);
606
+ }
607
+ }
608
+ if (filter.category) {
609
+ if (Array.isArray(filter.category)) {
610
+ conditions.push(`category IN (${filter.category.map(() => "?").join(",")})`);
611
+ params.push(...filter.category);
612
+ } else {
613
+ conditions.push("category = ?");
614
+ params.push(filter.category);
615
+ }
616
+ }
617
+ if (filter.source) {
618
+ if (Array.isArray(filter.source)) {
619
+ conditions.push(`source IN (${filter.source.map(() => "?").join(",")})`);
620
+ params.push(...filter.source);
621
+ } else {
622
+ conditions.push("source = ?");
623
+ params.push(filter.source);
624
+ }
625
+ }
626
+ if (filter.status) {
627
+ if (Array.isArray(filter.status)) {
628
+ conditions.push(`status IN (${filter.status.map(() => "?").join(",")})`);
629
+ params.push(...filter.status);
630
+ } else {
631
+ conditions.push("status = ?");
632
+ params.push(filter.status);
633
+ }
634
+ } else {
635
+ conditions.push("status = 'active'");
636
+ }
637
+ if (filter.project_id) {
638
+ conditions.push("project_id = ?");
639
+ params.push(filter.project_id);
640
+ }
641
+ if (filter.agent_id) {
642
+ conditions.push("agent_id = ?");
643
+ params.push(filter.agent_id);
644
+ }
645
+ if (filter.session_id) {
646
+ conditions.push("session_id = ?");
647
+ params.push(filter.session_id);
648
+ }
649
+ if (filter.min_importance) {
650
+ conditions.push("importance >= ?");
651
+ params.push(filter.min_importance);
652
+ }
653
+ if (filter.pinned !== undefined) {
654
+ conditions.push("pinned = ?");
655
+ params.push(filter.pinned ? 1 : 0);
656
+ }
657
+ if (filter.tags && filter.tags.length > 0) {
658
+ for (const tag of filter.tags) {
659
+ conditions.push("id IN (SELECT memory_id FROM memory_tags WHERE tag = ?)");
660
+ params.push(tag);
661
+ }
662
+ }
663
+ if (filter.search) {
664
+ conditions.push("(key LIKE ? OR value LIKE ? OR summary LIKE ?)");
665
+ const term = `%${filter.search}%`;
666
+ params.push(term, term, term);
667
+ }
668
+ } else {
669
+ conditions.push("status = 'active'");
724
670
  }
725
- let sql = "SELECT * FROM entities";
671
+ let sql = "SELECT * FROM memories";
726
672
  if (conditions.length > 0) {
727
673
  sql += ` WHERE ${conditions.join(" AND ")}`;
728
674
  }
729
- sql += " ORDER BY updated_at DESC";
730
- if (filter.limit) {
675
+ sql += " ORDER BY importance DESC, created_at DESC";
676
+ if (filter?.limit) {
731
677
  sql += " LIMIT ?";
732
678
  params.push(filter.limit);
733
679
  }
734
- if (filter.offset) {
680
+ if (filter?.offset) {
735
681
  sql += " OFFSET ?";
736
682
  params.push(filter.offset);
737
683
  }
738
684
  const rows = d.query(sql).all(...params);
739
- return rows.map(parseEntityRow);
685
+ return rows.map(parseMemoryRow);
740
686
  }
741
- function updateEntity(id, input, db) {
687
+ function updateMemory(id, input, db) {
742
688
  const d = db || getDatabase();
743
- const existing = d.query("SELECT id FROM entities WHERE id = ?").get(id);
689
+ const existing = getMemory(id, d);
744
690
  if (!existing)
745
- throw new EntityNotFoundError(id);
746
- const sets = ["updated_at = ?"];
691
+ throw new MemoryNotFoundError(id);
692
+ if (existing.version !== input.version) {
693
+ throw new VersionConflictError(id, input.version, existing.version);
694
+ }
695
+ try {
696
+ d.run(`INSERT OR IGNORE INTO memory_versions (id, memory_id, version, value, importance, scope, category, tags, summary, pinned, status, created_at)
697
+ VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)`, [
698
+ uuid(),
699
+ existing.id,
700
+ existing.version,
701
+ existing.value,
702
+ existing.importance,
703
+ existing.scope,
704
+ existing.category,
705
+ JSON.stringify(existing.tags),
706
+ existing.summary,
707
+ existing.pinned ? 1 : 0,
708
+ existing.status,
709
+ existing.updated_at
710
+ ]);
711
+ } catch {}
712
+ const sets = ["version = version + 1", "updated_at = ?"];
747
713
  const params = [now()];
748
- if (input.name !== undefined) {
749
- sets.push("name = ?");
750
- params.push(input.name);
714
+ if (input.value !== undefined) {
715
+ sets.push("value = ?");
716
+ params.push(redactSecrets(input.value));
751
717
  }
752
- if (input.type !== undefined) {
753
- sets.push("type = ?");
754
- params.push(input.type);
718
+ if (input.category !== undefined) {
719
+ sets.push("category = ?");
720
+ params.push(input.category);
755
721
  }
756
- if (input.description !== undefined) {
757
- sets.push("description = ?");
758
- params.push(input.description);
722
+ if (input.scope !== undefined) {
723
+ sets.push("scope = ?");
724
+ params.push(input.scope);
725
+ }
726
+ if (input.summary !== undefined) {
727
+ sets.push("summary = ?");
728
+ params.push(input.summary);
729
+ }
730
+ if (input.importance !== undefined) {
731
+ sets.push("importance = ?");
732
+ params.push(input.importance);
733
+ }
734
+ if (input.pinned !== undefined) {
735
+ sets.push("pinned = ?");
736
+ params.push(input.pinned ? 1 : 0);
737
+ }
738
+ if (input.status !== undefined) {
739
+ sets.push("status = ?");
740
+ params.push(input.status);
759
741
  }
760
742
  if (input.metadata !== undefined) {
761
743
  sets.push("metadata = ?");
762
744
  params.push(JSON.stringify(input.metadata));
763
745
  }
746
+ if (input.expires_at !== undefined) {
747
+ sets.push("expires_at = ?");
748
+ params.push(input.expires_at);
749
+ }
750
+ if (input.tags !== undefined) {
751
+ sets.push("tags = ?");
752
+ params.push(JSON.stringify(input.tags));
753
+ d.run("DELETE FROM memory_tags WHERE memory_id = ?", [id]);
754
+ const insertTag = d.prepare("INSERT OR IGNORE INTO memory_tags (memory_id, tag) VALUES (?, ?)");
755
+ for (const tag of input.tags) {
756
+ insertTag.run(id, tag);
757
+ }
758
+ }
764
759
  params.push(id);
765
- d.run(`UPDATE entities SET ${sets.join(", ")} WHERE id = ?`, params);
766
- return getEntity(id, d);
760
+ d.run(`UPDATE memories SET ${sets.join(", ")} WHERE id = ?`, params);
761
+ const updated = getMemory(id, d);
762
+ try {
763
+ if (input.value !== undefined) {
764
+ const oldLinks = getEntityMemoryLinks(undefined, updated.id, d);
765
+ for (const link of oldLinks) {
766
+ unlinkEntityFromMemory(link.entity_id, updated.id, d);
767
+ }
768
+ runEntityExtraction(updated, existing.project_id || undefined, d);
769
+ }
770
+ } catch {}
771
+ return updated;
767
772
  }
768
- function deleteEntity(id, db) {
773
+ function deleteMemory(id, db) {
769
774
  const d = db || getDatabase();
770
- const result = d.run("DELETE FROM entities WHERE id = ?", [id]);
771
- if (result.changes === 0)
772
- throw new EntityNotFoundError(id);
775
+ const result = d.run("DELETE FROM memories WHERE id = ?", [id]);
776
+ return result.changes > 0;
773
777
  }
774
- function mergeEntities(sourceId, targetId, db) {
778
+ function touchMemory(id, db) {
775
779
  const d = db || getDatabase();
776
- getEntity(sourceId, d);
777
- getEntity(targetId, d);
778
- d.run(`UPDATE OR IGNORE relations SET source_entity_id = ? WHERE source_entity_id = ?`, [targetId, sourceId]);
779
- d.run(`UPDATE OR IGNORE relations SET target_entity_id = ? WHERE target_entity_id = ?`, [targetId, sourceId]);
780
- d.run("DELETE FROM relations WHERE source_entity_id = ? OR target_entity_id = ?", [
781
- sourceId,
782
- sourceId
783
- ]);
784
- d.run(`UPDATE OR IGNORE entity_memories SET entity_id = ? WHERE entity_id = ?`, [targetId, sourceId]);
785
- d.run("DELETE FROM entity_memories WHERE entity_id = ?", [sourceId]);
786
- d.run("DELETE FROM entities WHERE id = ?", [sourceId]);
787
- d.run("UPDATE entities SET updated_at = ? WHERE id = ?", [now(), targetId]);
788
- return getEntity(targetId, d);
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
+ }
789
813
  }
790
814
 
791
- // src/db/entity-memories.ts
792
- function parseEntityMemoryRow(row) {
815
+ // src/db/agents.ts
816
+ var CONFLICT_WINDOW_MS = 30 * 60 * 1000;
817
+ function parseAgentRow(row) {
793
818
  return {
794
- entity_id: row["entity_id"],
795
- memory_id: row["memory_id"],
796
- role: row["role"],
797
- created_at: row["created_at"]
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"]
798
828
  };
799
829
  }
800
- function linkEntityToMemory(entityId, memoryId, role = "context", db) {
830
+ function registerAgent(name, sessionId, description, role, projectId, db) {
801
831
  const d = db || getDatabase();
802
832
  const timestamp = now();
803
- d.run(`INSERT OR IGNORE INTO entity_memories (entity_id, memory_id, role, created_at)
804
- VALUES (?, ?, ?, ?)`, [entityId, memoryId, role, timestamp]);
805
- const row = d.query("SELECT * FROM entity_memories WHERE entity_id = ? AND memory_id = ?").get(entityId, memoryId);
806
- return parseEntityMemoryRow(row);
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);
807
871
  }
808
- function unlinkEntityFromMemory(entityId, memoryId, db) {
872
+ function getAgent(idOrName, db) {
809
873
  const d = db || getDatabase();
810
- d.run("DELETE FROM entity_memories WHERE entity_id = ? AND memory_id = ?", [entityId, memoryId]);
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;
811
884
  }
812
- function getMemoriesForEntity(entityId, db) {
885
+ function listAgents(db) {
813
886
  const d = db || getDatabase();
814
- const rows = d.query(`SELECT m.* FROM memories m
815
- INNER JOIN entity_memories em ON em.memory_id = m.id
816
- WHERE em.entity_id = ?
817
- ORDER BY m.importance DESC, m.created_at DESC`).all(entityId);
818
- return rows.map(parseMemoryRow);
887
+ const rows = d.query("SELECT * FROM agents ORDER BY last_seen_at DESC").all();
888
+ return rows.map(parseAgentRow);
819
889
  }
820
- function getEntityMemoryLinks(entityId, memoryId, db) {
890
+ function listAgentsByProject(projectId, db) {
821
891
  const d = db || getDatabase();
822
- const conditions = [];
823
- const params = [];
824
- if (entityId) {
825
- conditions.push("entity_id = ?");
826
- params.push(entityId);
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);
894
+ }
895
+ function updateAgent(id, updates, db) {
896
+ const d = db || getDatabase();
897
+ const agent = getAgent(id, d);
898
+ if (!agent)
899
+ return null;
900
+ const timestamp = now();
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
+ }
827
910
  }
828
- if (memoryId) {
829
- conditions.push("memory_id = ?");
830
- params.push(memoryId);
911
+ if (updates.description !== undefined) {
912
+ d.run("UPDATE agents SET description = ? WHERE id = ?", [updates.description, agent.id]);
831
913
  }
832
- let sql = "SELECT * FROM entity_memories";
833
- if (conditions.length > 0) {
834
- sql += ` WHERE ${conditions.join(" AND ")}`;
914
+ if (updates.role !== undefined) {
915
+ d.run("UPDATE agents SET role = ? WHERE id = ?", [updates.role, agent.id]);
835
916
  }
836
- sql += " ORDER BY created_at DESC";
837
- const rows = d.query(sql).all(...params);
838
- return rows.map(parseEntityMemoryRow);
917
+ if (updates.metadata !== undefined) {
918
+ d.run("UPDATE agents SET metadata = ? WHERE id = ?", [JSON.stringify(updates.metadata), agent.id]);
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);
839
925
  }
840
926
 
841
- // src/db/relations.ts
842
- function parseRelationRow(row) {
927
+ // src/db/locks.ts
928
+ function parseLockRow(row) {
843
929
  return {
844
930
  id: row["id"],
845
- source_entity_id: row["source_entity_id"],
846
- target_entity_id: row["target_entity_id"],
847
- relation_type: row["relation_type"],
848
- weight: row["weight"],
849
- metadata: JSON.parse(row["metadata"] || "{}"),
850
- created_at: row["created_at"]
931
+ resource_type: row["resource_type"],
932
+ resource_id: row["resource_id"],
933
+ agent_id: row["agent_id"],
934
+ lock_type: row["lock_type"],
935
+ locked_at: row["locked_at"],
936
+ expires_at: row["expires_at"]
851
937
  };
852
938
  }
853
- function parseEntityRow2(row) {
939
+ function acquireLock(agentId, resourceType, resourceId, lockType = "exclusive", ttlSeconds = 300, db) {
940
+ const d = db || getDatabase();
941
+ cleanExpiredLocks(d);
942
+ const ownLock = d.query("SELECT * FROM resource_locks WHERE resource_type = ? AND resource_id = ? AND agent_id = ? AND lock_type = ? AND expires_at > datetime('now')").get(resourceType, resourceId, agentId, lockType);
943
+ if (ownLock) {
944
+ const newExpiry = new Date(Date.now() + ttlSeconds * 1000).toISOString();
945
+ d.run("UPDATE resource_locks SET expires_at = ? WHERE id = ?", [
946
+ newExpiry,
947
+ ownLock["id"]
948
+ ]);
949
+ return parseLockRow({ ...ownLock, expires_at: newExpiry });
950
+ }
951
+ if (lockType === "exclusive") {
952
+ const existing = d.query("SELECT * FROM resource_locks WHERE resource_type = ? AND resource_id = ? AND lock_type = 'exclusive' AND agent_id != ? AND expires_at > datetime('now')").get(resourceType, resourceId, agentId);
953
+ if (existing) {
954
+ return null;
955
+ }
956
+ }
957
+ const id = shortUuid();
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]);
854
961
  return {
855
- id: row["id"],
856
- name: row["name"],
857
- type: row["type"],
858
- description: row["description"] || null,
859
- metadata: JSON.parse(row["metadata"] || "{}"),
860
- project_id: row["project_id"] || null,
861
- created_at: row["created_at"],
862
- updated_at: row["updated_at"]
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
863
969
  };
864
970
  }
865
- function createRelation(input, db) {
971
+ function releaseLock(lockId, agentId, db) {
866
972
  const d = db || getDatabase();
867
- const id = shortUuid();
868
- const timestamp = now();
869
- const weight = input.weight ?? 1;
870
- const metadata = JSON.stringify(input.metadata ?? {});
871
- d.run(`INSERT INTO relations (id, source_entity_id, target_entity_id, relation_type, weight, metadata, created_at)
872
- VALUES (?, ?, ?, ?, ?, ?, ?)
873
- ON CONFLICT(source_entity_id, target_entity_id, relation_type)
874
- DO UPDATE SET weight = excluded.weight, metadata = excluded.metadata`, [id, input.source_entity_id, input.target_entity_id, input.relation_type, weight, metadata, timestamp]);
875
- const row = d.query(`SELECT * FROM relations
876
- WHERE source_entity_id = ? AND target_entity_id = ? AND relation_type = ?`).get(input.source_entity_id, input.target_entity_id, input.relation_type);
877
- return parseRelationRow(row);
973
+ const result = d.run("DELETE FROM resource_locks WHERE id = ? AND agent_id = ?", [lockId, agentId]);
974
+ return result.changes > 0;
878
975
  }
879
- function getRelation(id, db) {
976
+ function releaseAllAgentLocks(agentId, db) {
880
977
  const d = db || getDatabase();
881
- const row = d.query("SELECT * FROM relations WHERE id = ?").get(id);
882
- if (!row)
883
- throw new Error(`Relation not found: ${id}`);
884
- return parseRelationRow(row);
978
+ const result = d.run("DELETE FROM resource_locks WHERE agent_id = ?", [agentId]);
979
+ return result.changes;
885
980
  }
886
- function listRelations(filter, db) {
981
+ function checkLock(resourceType, resourceId, lockType, db) {
887
982
  const d = db || getDatabase();
888
- const conditions = [];
889
- const params = [];
890
- if (filter.entity_id) {
891
- const dir = filter.direction || "both";
892
- if (dir === "outgoing") {
893
- conditions.push("source_entity_id = ?");
894
- params.push(filter.entity_id);
895
- } else if (dir === "incoming") {
896
- conditions.push("target_entity_id = ?");
897
- params.push(filter.entity_id);
898
- } else {
899
- conditions.push("(source_entity_id = ? OR target_entity_id = ?)");
900
- params.push(filter.entity_id, filter.entity_id);
901
- }
902
- }
903
- if (filter.relation_type) {
904
- conditions.push("relation_type = ?");
905
- params.push(filter.relation_type);
906
- }
907
- const where = conditions.length > 0 ? `WHERE ${conditions.join(" AND ")}` : "";
908
- const rows = d.query(`SELECT * FROM relations ${where} ORDER BY created_at DESC`).all(...params);
909
- return rows.map(parseRelationRow);
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);
910
987
  }
911
- function deleteRelation(id, db) {
988
+ function listAgentLocks(agentId, db) {
912
989
  const d = db || getDatabase();
913
- const result = d.run("DELETE FROM relations WHERE id = ?", [id]);
914
- if (result.changes === 0)
915
- throw new Error(`Relation not found: ${id}`);
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);
916
993
  }
917
- function getEntityGraph(entityId, depth = 2, db) {
994
+ function cleanExpiredLocks(db) {
918
995
  const d = db || getDatabase();
919
- const entityRows = d.query(`WITH RECURSIVE graph(id, depth) AS (
920
- VALUES(?, 0)
921
- UNION
922
- SELECT CASE WHEN r.source_entity_id = g.id THEN r.target_entity_id ELSE r.source_entity_id END, g.depth + 1
923
- FROM relations r JOIN graph g ON (r.source_entity_id = g.id OR r.target_entity_id = g.id)
924
- WHERE g.depth < ?
925
- )
926
- SELECT DISTINCT e.* FROM entities e JOIN graph g ON e.id = g.id`).all(entityId, depth);
927
- const entities = entityRows.map(parseEntityRow2);
928
- const entityIds = new Set(entities.map((e) => e.id));
929
- if (entityIds.size === 0) {
930
- return { entities: [], relations: [] };
931
- }
932
- const placeholders = Array.from(entityIds).map(() => "?").join(",");
933
- const relationRows = d.query(`SELECT * FROM relations
934
- WHERE source_entity_id IN (${placeholders})
935
- AND target_entity_id IN (${placeholders})`).all(...Array.from(entityIds), ...Array.from(entityIds));
936
- const relations = relationRows.map(parseRelationRow);
937
- return { entities, relations };
996
+ const result = d.run("DELETE FROM resource_locks WHERE expires_at <= datetime('now')");
997
+ return result.changes;
938
998
  }
939
- function findPath(fromEntityId, toEntityId, maxDepth = 5, db) {
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) {
940
1013
  const d = db || getDatabase();
941
- const rows = d.query(`WITH RECURSIVE path(id, trail, depth) AS (
942
- SELECT ?, ?, 0
943
- UNION
944
- SELECT
945
- CASE WHEN r.source_entity_id = p.id THEN r.target_entity_id ELSE r.source_entity_id END,
946
- p.trail || ',' || CASE WHEN r.source_entity_id = p.id THEN r.target_entity_id ELSE r.source_entity_id END,
947
- p.depth + 1
948
- FROM relations r JOIN path p ON (r.source_entity_id = p.id OR r.target_entity_id = p.id)
949
- WHERE p.depth < ?
950
- AND INSTR(p.trail, CASE WHEN r.source_entity_id = p.id THEN r.target_entity_id ELSE r.source_entity_id END) = 0
951
- )
952
- SELECT trail FROM path WHERE id = ? ORDER BY depth ASC LIMIT 1`).get(fromEntityId, fromEntityId, maxDepth, toEntityId);
953
- if (!rows)
954
- return null;
955
- const ids = rows.trail.split(",");
956
- const entities = [];
957
- for (const id of ids) {
958
- const row = d.query("SELECT * FROM entities WHERE id = ?").get(id);
959
- if (row)
960
- entities.push(parseEntityRow2(row));
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);
961
1023
  }
962
- return entities.length > 0 ? entities : null;
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);
963
1045
  }
964
1046
 
965
1047
  // src/lib/config.ts
966
1048
  import { existsSync as existsSync2, mkdirSync as mkdirSync2, readFileSync, readdirSync, writeFileSync, unlinkSync } from "fs";
967
1049
  import { homedir } from "os";
968
1050
  import { basename, dirname as dirname2, join as join2, resolve as resolve2 } from "path";
969
- var DEFAULT_CONFIG = {
970
- default_scope: "private",
971
- default_category: "knowledge",
972
- default_importance: 5,
973
- max_entries: 1000,
974
- max_entries_per_scope: {
975
- global: 500,
976
- shared: 300,
977
- private: 200
978
- },
979
- injection: {
980
- max_tokens: 500,
981
- min_importance: 5,
982
- categories: ["preference", "fact"],
983
- refresh_interval: 5
984
- },
985
- extraction: {
986
- enabled: true,
987
- min_confidence: 0.5
988
- },
989
- sync_agents: ["claude", "codex", "gemini"],
990
- auto_cleanup: {
991
- enabled: true,
992
- expired_check_interval: 3600,
993
- unused_archive_days: 7,
994
- stale_deprioritize_days: 14
995
- }
996
- };
997
- function deepMerge(target, source) {
998
- const result = { ...target };
999
- for (const key of Object.keys(source)) {
1000
- const sourceVal = source[key];
1001
- const targetVal = result[key];
1002
- if (sourceVal !== null && typeof sourceVal === "object" && !Array.isArray(sourceVal) && targetVal !== null && typeof targetVal === "object" && !Array.isArray(targetVal)) {
1003
- result[key] = deepMerge(targetVal, sourceVal);
1004
- } else {
1005
- result[key] = sourceVal;
1006
- }
1007
- }
1008
- return result;
1009
- }
1010
- var VALID_SCOPES = ["global", "shared", "private"];
1011
- var VALID_CATEGORIES = [
1012
- "preference",
1013
- "fact",
1014
- "knowledge",
1015
- "history"
1016
- ];
1017
- function isValidScope(value) {
1018
- return VALID_SCOPES.includes(value);
1019
- }
1020
- function isValidCategory(value) {
1021
- return VALID_CATEGORIES.includes(value);
1022
- }
1023
- function loadConfig() {
1024
- const configPath = join2(homedir(), ".mementos", "config.json");
1025
- let fileConfig = {};
1026
- if (existsSync2(configPath)) {
1027
- try {
1028
- const raw = readFileSync(configPath, "utf-8");
1029
- fileConfig = JSON.parse(raw);
1030
- } catch {}
1031
- }
1032
- const merged = deepMerge(DEFAULT_CONFIG, fileConfig);
1033
- const envScope = process.env["MEMENTOS_DEFAULT_SCOPE"];
1034
- if (envScope && isValidScope(envScope)) {
1035
- merged.default_scope = envScope;
1036
- }
1037
- const envCategory = process.env["MEMENTOS_DEFAULT_CATEGORY"];
1038
- if (envCategory && isValidCategory(envCategory)) {
1039
- merged.default_category = envCategory;
1040
- }
1041
- const envImportance = process.env["MEMENTOS_DEFAULT_IMPORTANCE"];
1042
- if (envImportance) {
1043
- const parsed = parseInt(envImportance, 10);
1044
- if (!Number.isNaN(parsed) && parsed >= 1 && parsed <= 10) {
1045
- merged.default_importance = parsed;
1046
- }
1047
- }
1048
- return merged;
1049
- }
1050
1051
  function findFileWalkingUp(filename) {
1051
1052
  let dir = process.cwd();
1052
1053
  while (true) {
@@ -1139,361 +1140,161 @@ function ensureDir2(dir) {
1139
1140
  }
1140
1141
  }
1141
1142
 
1142
- // src/db/memories.ts
1143
- function runEntityExtraction(memory, projectId, d) {
1144
- const config = loadConfig();
1145
- if (config.extraction?.enabled === false)
1146
- return;
1147
- const extracted = extractEntities(memory, d);
1148
- const minConfidence = config.extraction?.min_confidence ?? 0.5;
1149
- const entityIds = [];
1150
- for (const ext of extracted) {
1151
- if (ext.confidence >= minConfidence) {
1152
- const entity = createEntity({ name: ext.name, type: ext.type, project_id: projectId }, d);
1153
- linkEntityToMemory(entity.id, memory.id, "context", d);
1154
- entityIds.push(entity.id);
1155
- }
1156
- }
1157
- for (let i = 0;i < entityIds.length; i++) {
1158
- for (let j = i + 1;j < entityIds.length; j++) {
1159
- try {
1160
- createRelation({ source_entity_id: entityIds[i], target_entity_id: entityIds[j], relation_type: "related_to" }, d);
1161
- } catch {}
1162
- }
1163
- }
1164
- }
1165
- function parseMemoryRow(row) {
1143
+ // src/db/entities.ts
1144
+ function parseEntityRow(row) {
1166
1145
  return {
1167
1146
  id: row["id"],
1168
- key: row["key"],
1169
- value: row["value"],
1170
- category: row["category"],
1171
- scope: row["scope"],
1172
- summary: row["summary"] || null,
1173
- tags: JSON.parse(row["tags"] || "[]"),
1174
- importance: row["importance"],
1175
- source: row["source"],
1176
- status: row["status"],
1177
- pinned: !!row["pinned"],
1178
- agent_id: row["agent_id"] || null,
1179
- project_id: row["project_id"] || null,
1180
- session_id: row["session_id"] || null,
1147
+ name: row["name"],
1148
+ type: row["type"],
1149
+ description: row["description"] || null,
1181
1150
  metadata: JSON.parse(row["metadata"] || "{}"),
1182
- access_count: row["access_count"],
1183
- version: row["version"],
1184
- expires_at: row["expires_at"] || null,
1151
+ project_id: row["project_id"] || null,
1185
1152
  created_at: row["created_at"],
1186
- updated_at: row["updated_at"],
1187
- accessed_at: row["accessed_at"] || null
1153
+ updated_at: row["updated_at"]
1188
1154
  };
1189
1155
  }
1190
- function createMemory(input, dedupeMode = "merge", db) {
1156
+ function createEntity(input, db) {
1191
1157
  const d = db || getDatabase();
1192
1158
  const timestamp = now();
1193
- let expiresAt = input.expires_at || null;
1194
- if (input.ttl_ms && !expiresAt) {
1195
- expiresAt = new Date(Date.now() + input.ttl_ms).toISOString();
1196
- }
1197
- const id = uuid();
1198
- const tags = input.tags || [];
1199
- const tagsJson = JSON.stringify(tags);
1200
1159
  const metadataJson = JSON.stringify(input.metadata || {});
1201
- const safeValue = redactSecrets(input.value);
1202
- const safeSummary = input.summary ? redactSecrets(input.summary) : null;
1203
- if (dedupeMode === "merge") {
1204
- const existing = d.query(`SELECT id, version FROM memories
1205
- WHERE key = ? AND scope = ?
1206
- AND COALESCE(agent_id, '') = ?
1207
- AND COALESCE(project_id, '') = ?
1208
- AND COALESCE(session_id, '') = ?`).get(input.key, input.scope || "private", input.agent_id || "", input.project_id || "", input.session_id || "");
1209
- if (existing) {
1210
- d.run(`UPDATE memories SET
1211
- value = ?, category = ?, summary = ?, tags = ?,
1212
- importance = ?, metadata = ?, expires_at = ?,
1213
- pinned = COALESCE(pinned, 0),
1214
- version = version + 1, updated_at = ?
1215
- WHERE id = ?`, [
1216
- safeValue,
1217
- input.category || "knowledge",
1218
- safeSummary,
1219
- tagsJson,
1220
- input.importance ?? 5,
1221
- metadataJson,
1222
- expiresAt,
1223
- timestamp,
1224
- existing.id
1225
- ]);
1226
- d.run("DELETE FROM memory_tags WHERE memory_id = ?", [existing.id]);
1227
- const insertTag2 = d.prepare("INSERT OR IGNORE INTO memory_tags (memory_id, tag) VALUES (?, ?)");
1228
- for (const tag of tags) {
1229
- insertTag2.run(existing.id, tag);
1230
- }
1231
- const merged = getMemory(existing.id, d);
1232
- try {
1233
- const oldLinks = getEntityMemoryLinks(undefined, merged.id, d);
1234
- for (const link of oldLinks) {
1235
- unlinkEntityFromMemory(link.entity_id, merged.id, d);
1236
- }
1237
- runEntityExtraction(merged, input.project_id, d);
1238
- } catch {}
1239
- return merged;
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);
1240
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);
1241
1177
  }
1242
- 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)
1243
- VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, 'active', 0, ?, ?, ?, ?, 0, 1, ?, ?, ?)`, [
1178
+ const id = shortUuid();
1179
+ d.run(`INSERT INTO entities (id, name, type, description, metadata, project_id, created_at, updated_at)
1180
+ VALUES (?, ?, ?, ?, ?, ?, ?, ?)`, [
1244
1181
  id,
1245
- input.key,
1246
- input.value,
1247
- input.category || "knowledge",
1248
- input.scope || "private",
1249
- input.summary || null,
1250
- tagsJson,
1251
- input.importance ?? 5,
1252
- input.source || "agent",
1253
- input.agent_id || null,
1254
- input.project_id || null,
1255
- input.session_id || null,
1182
+ input.name,
1183
+ input.type,
1184
+ input.description || null,
1256
1185
  metadataJson,
1257
- expiresAt,
1186
+ input.project_id || null,
1258
1187
  timestamp,
1259
1188
  timestamp
1260
1189
  ]);
1261
- const insertTag = d.prepare("INSERT OR IGNORE INTO memory_tags (memory_id, tag) VALUES (?, ?)");
1262
- for (const tag of tags) {
1263
- insertTag.run(id, tag);
1264
- }
1265
- const memory = getMemory(id, d);
1266
- try {
1267
- runEntityExtraction(memory, input.project_id, d);
1268
- } catch {}
1269
- return memory;
1190
+ return getEntity(id, d);
1270
1191
  }
1271
- function getMemory(id, db) {
1192
+ function getEntity(id, db) {
1272
1193
  const d = db || getDatabase();
1273
- const row = d.query("SELECT * FROM memories WHERE id = ?").get(id);
1194
+ const row = d.query("SELECT * FROM entities WHERE id = ?").get(id);
1274
1195
  if (!row)
1275
- return null;
1276
- return parseMemoryRow(row);
1196
+ throw new EntityNotFoundError(id);
1197
+ return parseEntityRow(row);
1277
1198
  }
1278
- function listMemories(filter, db) {
1199
+ function getEntityByName(name, type, projectId, db) {
1279
1200
  const d = db || getDatabase();
1280
- const conditions = [];
1281
- const params = [];
1282
- if (filter) {
1283
- if (filter.scope) {
1284
- if (Array.isArray(filter.scope)) {
1285
- conditions.push(`scope IN (${filter.scope.map(() => "?").join(",")})`);
1286
- params.push(...filter.scope);
1287
- } else {
1288
- conditions.push("scope = ?");
1289
- params.push(filter.scope);
1290
- }
1291
- }
1292
- if (filter.category) {
1293
- if (Array.isArray(filter.category)) {
1294
- conditions.push(`category IN (${filter.category.map(() => "?").join(",")})`);
1295
- params.push(...filter.category);
1296
- } else {
1297
- conditions.push("category = ?");
1298
- params.push(filter.category);
1299
- }
1300
- }
1301
- if (filter.source) {
1302
- if (Array.isArray(filter.source)) {
1303
- conditions.push(`source IN (${filter.source.map(() => "?").join(",")})`);
1304
- params.push(...filter.source);
1305
- } else {
1306
- conditions.push("source = ?");
1307
- params.push(filter.source);
1308
- }
1309
- }
1310
- if (filter.status) {
1311
- if (Array.isArray(filter.status)) {
1312
- conditions.push(`status IN (${filter.status.map(() => "?").join(",")})`);
1313
- params.push(...filter.status);
1314
- } else {
1315
- conditions.push("status = ?");
1316
- params.push(filter.status);
1317
- }
1318
- } else {
1319
- conditions.push("status = 'active'");
1320
- }
1321
- if (filter.project_id) {
1322
- conditions.push("project_id = ?");
1323
- params.push(filter.project_id);
1324
- }
1325
- if (filter.agent_id) {
1326
- conditions.push("agent_id = ?");
1327
- params.push(filter.agent_id);
1328
- }
1329
- if (filter.session_id) {
1330
- conditions.push("session_id = ?");
1331
- params.push(filter.session_id);
1332
- }
1333
- if (filter.min_importance) {
1334
- conditions.push("importance >= ?");
1335
- params.push(filter.min_importance);
1336
- }
1337
- if (filter.pinned !== undefined) {
1338
- conditions.push("pinned = ?");
1339
- params.push(filter.pinned ? 1 : 0);
1340
- }
1341
- if (filter.tags && filter.tags.length > 0) {
1342
- for (const tag of filter.tags) {
1343
- conditions.push("id IN (SELECT memory_id FROM memory_tags WHERE tag = ?)");
1344
- params.push(tag);
1345
- }
1346
- }
1347
- if (filter.search) {
1348
- conditions.push("(key LIKE ? OR value LIKE ? OR summary LIKE ?)");
1349
- const term = `%${filter.search}%`;
1350
- params.push(term, term, term);
1351
- }
1352
- } else {
1353
- conditions.push("status = 'active'");
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);
1216
+ }
1217
+ function listEntities(filter = {}, db) {
1218
+ const d = db || getDatabase();
1219
+ const conditions = [];
1220
+ const params = [];
1221
+ if (filter.type) {
1222
+ conditions.push("type = ?");
1223
+ params.push(filter.type);
1354
1224
  }
1355
- let sql = "SELECT * FROM memories";
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";
1356
1235
  if (conditions.length > 0) {
1357
1236
  sql += ` WHERE ${conditions.join(" AND ")}`;
1358
1237
  }
1359
- sql += " ORDER BY importance DESC, created_at DESC";
1360
- if (filter?.limit) {
1238
+ sql += " ORDER BY updated_at DESC";
1239
+ if (filter.limit) {
1361
1240
  sql += " LIMIT ?";
1362
1241
  params.push(filter.limit);
1363
1242
  }
1364
- if (filter?.offset) {
1243
+ if (filter.offset) {
1365
1244
  sql += " OFFSET ?";
1366
1245
  params.push(filter.offset);
1367
1246
  }
1368
1247
  const rows = d.query(sql).all(...params);
1369
- return rows.map(parseMemoryRow);
1248
+ return rows.map(parseEntityRow);
1370
1249
  }
1371
- function updateMemory(id, input, db) {
1250
+ function updateEntity(id, input, db) {
1372
1251
  const d = db || getDatabase();
1373
- const existing = getMemory(id, d);
1252
+ const existing = d.query("SELECT id FROM entities WHERE id = ?").get(id);
1374
1253
  if (!existing)
1375
- throw new MemoryNotFoundError(id);
1376
- if (existing.version !== input.version) {
1377
- throw new VersionConflictError(id, input.version, existing.version);
1378
- }
1379
- try {
1380
- d.run(`INSERT OR IGNORE INTO memory_versions (id, memory_id, version, value, importance, scope, category, tags, summary, pinned, status, created_at)
1381
- VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)`, [
1382
- uuid(),
1383
- existing.id,
1384
- existing.version,
1385
- existing.value,
1386
- existing.importance,
1387
- existing.scope,
1388
- existing.category,
1389
- JSON.stringify(existing.tags),
1390
- existing.summary,
1391
- existing.pinned ? 1 : 0,
1392
- existing.status,
1393
- existing.updated_at
1394
- ]);
1395
- } catch {}
1396
- const sets = ["version = version + 1", "updated_at = ?"];
1254
+ throw new EntityNotFoundError(id);
1255
+ const sets = ["updated_at = ?"];
1397
1256
  const params = [now()];
1398
- if (input.value !== undefined) {
1399
- sets.push("value = ?");
1400
- params.push(redactSecrets(input.value));
1401
- }
1402
- if (input.category !== undefined) {
1403
- sets.push("category = ?");
1404
- params.push(input.category);
1405
- }
1406
- if (input.scope !== undefined) {
1407
- sets.push("scope = ?");
1408
- params.push(input.scope);
1409
- }
1410
- if (input.summary !== undefined) {
1411
- sets.push("summary = ?");
1412
- params.push(input.summary);
1413
- }
1414
- if (input.importance !== undefined) {
1415
- sets.push("importance = ?");
1416
- params.push(input.importance);
1257
+ if (input.name !== undefined) {
1258
+ sets.push("name = ?");
1259
+ params.push(input.name);
1417
1260
  }
1418
- if (input.pinned !== undefined) {
1419
- sets.push("pinned = ?");
1420
- params.push(input.pinned ? 1 : 0);
1261
+ if (input.type !== undefined) {
1262
+ sets.push("type = ?");
1263
+ params.push(input.type);
1421
1264
  }
1422
- if (input.status !== undefined) {
1423
- sets.push("status = ?");
1424
- params.push(input.status);
1265
+ if (input.description !== undefined) {
1266
+ sets.push("description = ?");
1267
+ params.push(input.description);
1425
1268
  }
1426
1269
  if (input.metadata !== undefined) {
1427
1270
  sets.push("metadata = ?");
1428
1271
  params.push(JSON.stringify(input.metadata));
1429
1272
  }
1430
- if (input.expires_at !== undefined) {
1431
- sets.push("expires_at = ?");
1432
- params.push(input.expires_at);
1433
- }
1434
- if (input.tags !== undefined) {
1435
- sets.push("tags = ?");
1436
- params.push(JSON.stringify(input.tags));
1437
- d.run("DELETE FROM memory_tags WHERE memory_id = ?", [id]);
1438
- const insertTag = d.prepare("INSERT OR IGNORE INTO memory_tags (memory_id, tag) VALUES (?, ?)");
1439
- for (const tag of input.tags) {
1440
- insertTag.run(id, tag);
1441
- }
1442
- }
1443
1273
  params.push(id);
1444
- d.run(`UPDATE memories SET ${sets.join(", ")} WHERE id = ?`, params);
1445
- const updated = getMemory(id, d);
1446
- try {
1447
- if (input.value !== undefined) {
1448
- const oldLinks = getEntityMemoryLinks(undefined, updated.id, d);
1449
- for (const link of oldLinks) {
1450
- unlinkEntityFromMemory(link.entity_id, updated.id, d);
1451
- }
1452
- runEntityExtraction(updated, existing.project_id || undefined, d);
1453
- }
1454
- } catch {}
1455
- return updated;
1456
- }
1457
- function deleteMemory(id, db) {
1458
- const d = db || getDatabase();
1459
- const result = d.run("DELETE FROM memories WHERE id = ?", [id]);
1460
- return result.changes > 0;
1461
- }
1462
- function touchMemory(id, db) {
1463
- const d = db || getDatabase();
1464
- d.run("UPDATE memories SET access_count = access_count + 1, accessed_at = ? WHERE id = ?", [now(), id]);
1274
+ d.run(`UPDATE entities SET ${sets.join(", ")} WHERE id = ?`, params);
1275
+ return getEntity(id, d);
1465
1276
  }
1466
- function cleanExpiredMemories(db) {
1277
+ function deleteEntity(id, db) {
1467
1278
  const d = db || getDatabase();
1468
- const timestamp = now();
1469
- const countRow = d.query("SELECT COUNT(*) as c FROM memories WHERE expires_at IS NOT NULL AND expires_at < ?").get(timestamp);
1470
- const count = countRow.c;
1471
- if (count > 0) {
1472
- d.run("DELETE FROM memories WHERE expires_at IS NOT NULL AND expires_at < ?", [timestamp]);
1473
- }
1474
- return count;
1279
+ const result = d.run("DELETE FROM entities WHERE id = ?", [id]);
1280
+ if (result.changes === 0)
1281
+ throw new EntityNotFoundError(id);
1475
1282
  }
1476
- function getMemoryVersions(memoryId, db) {
1283
+ function mergeEntities(sourceId, targetId, db) {
1477
1284
  const d = db || getDatabase();
1478
- try {
1479
- const rows = d.query("SELECT * FROM memory_versions WHERE memory_id = ? ORDER BY version ASC").all(memoryId);
1480
- return rows.map((row) => ({
1481
- id: row["id"],
1482
- memory_id: row["memory_id"],
1483
- version: row["version"],
1484
- value: row["value"],
1485
- importance: row["importance"],
1486
- scope: row["scope"],
1487
- category: row["category"],
1488
- tags: JSON.parse(row["tags"] || "[]"),
1489
- summary: row["summary"] || null,
1490
- pinned: !!row["pinned"],
1491
- status: row["status"],
1492
- created_at: row["created_at"]
1493
- }));
1494
- } catch {
1495
- return [];
1496
- }
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);
1497
1298
  }
1498
1299
 
1499
1300
  // src/lib/search.ts
@@ -1997,38 +1798,168 @@ function searchMemories(query, filter, db) {
1997
1798
  if (ftsResult !== null) {
1998
1799
  scored = ftsResult;
1999
1800
  } else {
2000
- scored = searchWithLike(d, query, queryLower, filter, graphBoostedIds);
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);
1898
+ } else {
1899
+ conditions.push("(source_entity_id = ? OR target_entity_id = ?)");
1900
+ params.push(filter.entity_id, filter.entity_id);
2001
1901
  }
2002
- } else {
2003
- scored = searchWithLike(d, query, queryLower, filter, graphBoostedIds);
2004
1902
  }
2005
- if (scored.length < 3) {
2006
- const fuzzyResults = searchWithFuzzy(d, query, filter, graphBoostedIds);
2007
- const seenIds = new Set(scored.map((r) => r.memory.id));
2008
- for (const fr of fuzzyResults) {
2009
- if (!seenIds.has(fr.memory.id)) {
2010
- scored.push(fr);
2011
- seenIds.add(fr.memory.id);
2012
- }
2013
- }
2014
- scored.sort((a, b) => {
2015
- if (b.score !== a.score)
2016
- return b.score - a.score;
2017
- return b.memory.importance - a.memory.importance;
2018
- });
1903
+ if (filter.relation_type) {
1904
+ conditions.push("relation_type = ?");
1905
+ params.push(filter.relation_type);
2019
1906
  }
2020
- const offset = filter?.offset ?? 0;
2021
- const limit = filter?.limit ?? scored.length;
2022
- const finalResults = scored.slice(offset, offset + limit);
2023
- logSearchQuery(query, scored.length, filter?.agent_id, filter?.project_id, d);
2024
- 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);
2025
1910
  }
2026
- function logSearchQuery(query, resultCount, agentId, projectId, db) {
2027
- try {
2028
- const d = db || getDatabase();
2029
- const id = crypto.randomUUID().slice(0, 8);
2030
- d.run("INSERT INTO search_history (id, query, result_count, agent_id, project_id) VALUES (?, ?, ?, ?, ?)", [id, query, resultCount, agentId || null, projectId || null]);
2031
- } 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;
2032
1963
  }
2033
1964
 
2034
1965
  // src/lib/duration.ts
@@ -2074,6 +2005,690 @@ var FORMAT_UNITS = [
2074
2005
  ["s", UNIT_MS["s"]]
2075
2006
  ];
2076
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
+
2077
2692
  // src/server/index.ts
2078
2693
  var DEFAULT_PORT = 19428;
2079
2694
  function parsePort() {
@@ -2604,7 +3219,7 @@ addRoute("POST", "/api/agents", async (req) => {
2604
3219
  if (!body || !body["name"]) {
2605
3220
  return errorResponse("Missing required field: name", 400);
2606
3221
  }
2607
- const agent = registerAgent(body["name"], body["description"], body["role"]);
3222
+ const agent = registerAgent(body["name"], body["session_id"], body["description"], body["role"], body["project_id"]);
2608
3223
  return json(agent, 201);
2609
3224
  });
2610
3225
  addRoute("GET", "/api/agents/:id", (_req, _url, params) => {
@@ -2639,6 +3254,47 @@ addRoute("PATCH", "/api/agents/:id", async (req, _url, params) => {
2639
3254
  return errorResponse(e instanceof Error ? e.message : "Update failed", 400);
2640
3255
  }
2641
3256
  });
3257
+ addRoute("POST", "/api/locks", async (req) => {
3258
+ const body = await readJson(req);
3259
+ if (!body?.agent_id || !body?.resource_type || !body?.resource_id) {
3260
+ return errorResponse("Missing required fields: agent_id, resource_type, resource_id", 400);
3261
+ }
3262
+ const lock = acquireLock(body["agent_id"], body["resource_type"], body["resource_id"], body["lock_type"] || "exclusive", body["ttl_seconds"] || 300);
3263
+ if (!lock) {
3264
+ const existing = checkLock(body["resource_type"], body["resource_id"], "exclusive");
3265
+ return errorResponse(`Lock conflict: resource ${body["resource_type"]}:${body["resource_id"]} is held by agent ${existing[0]?.agent_id ?? "unknown"}`, 409);
3266
+ }
3267
+ return json(lock, 201);
3268
+ });
3269
+ addRoute("GET", "/api/locks", (_req, url) => {
3270
+ const resourceType = url.searchParams.get("resource_type");
3271
+ const resourceId = url.searchParams.get("resource_id");
3272
+ const lockType = url.searchParams.get("lock_type");
3273
+ if (!resourceType || !resourceId) {
3274
+ return errorResponse("Missing required query params: resource_type, resource_id", 400);
3275
+ }
3276
+ return json(checkLock(resourceType, resourceId, lockType));
3277
+ });
3278
+ addRoute("DELETE", "/api/locks/:id", async (req, _url, params) => {
3279
+ const body = await readJson(req);
3280
+ if (!body?.agent_id)
3281
+ return errorResponse("Missing required field: agent_id", 400);
3282
+ const released = releaseLock(params["id"], body["agent_id"]);
3283
+ if (!released)
3284
+ return errorResponse("Lock not found or not owned by this agent", 404);
3285
+ return json({ released: true });
3286
+ });
3287
+ addRoute("GET", "/api/agents/:id/locks", (_req, _url, params) => {
3288
+ return json(listAgentLocks(params["id"]));
3289
+ });
3290
+ addRoute("DELETE", "/api/agents/:id/locks", (_req, _url, params) => {
3291
+ const count = releaseAllAgentLocks(params["id"]);
3292
+ return json({ released: count });
3293
+ });
3294
+ addRoute("POST", "/api/locks/clean", () => {
3295
+ const count = cleanExpiredLocks();
3296
+ return json({ cleaned: count });
3297
+ });
2642
3298
  addRoute("GET", "/api/projects", (_req, url) => {
2643
3299
  const q = getSearchParams(url);
2644
3300
  const projects = listProjects();
@@ -2992,6 +3648,58 @@ async function findFreePort(start) {
2992
3648
  }
2993
3649
  return start;
2994
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
+ });
2995
3703
  function startServer(port) {
2996
3704
  const hostname = process.env["MEMENTOS_HOST"] ?? "127.0.0.1";
2997
3705
  Bun.serve({